SpEL探秘
Spring中的动态表达式艺术:SpEL
由浅入深、从应用到实战,逐步学习理解和掌握Spring Expression Language。通过学习能够在实际开发过程中灵活应用、扩展以及排查和定位相关问题。
一. 先认眼熟,瞅瞅这玩意是个啥ʕ•̀ω•́ʔ✧
注意EpEL表达式以#开头。可以访问属性、调用属性支持的大部分方法、应用计算等
//自定义了PropertiesFactoryBean,装载了一批属性,等于重命名了
@Value("#{applicationProperties['ris.enable']?:false}")
private Boolean flagRis;
@Value("#{applicationProperties['ris.baseUrl']}")
private String baseUrl;
//做了一个算术运算
@Value("#{${router.times} + 2}")
private String routerTimes;
//取bean的某个属性,public的id,或者private的getId()
@Value("#{transportEntity.id}")
private String transportId;
//system打头的默认只能获取环境变量、系统属性。而environment都可以,即使自定义属性文件只要加载进来即可访问
@Value("#{environment['server.port']?:80}")
private int serverPort;
@Value("#{systemProperties['server.port']?:80}")
private int serverPort2;
@Value("#{systemEnvironment['server.port']?:80}")
private int serverPort3;
//以下两个的获取范围是environment
@Value("#{${server.port}?:80}")//如果没有server.port这个属性,会直接报错
private int serverPort4
@Value("${server.port:80}")
private int serverPort5;
//当某个表达式满足条件是创建bean
@Bean
@ConditionalOnExpression("#{${business.maxCount:0} gt 0}")
public BusinessFood businessFood(){
return new BusinessFood();
}
以下这种是属性占位符,以$开头,不能包含SpEL表达式。里面只能包含OGNL表达式访问属性,不能调用方法。
//这是属性表达式。助理属性表达式占位符中不能包含SpEL表达式,它只能获取属性配置
@Value("${doc.online.appId:}")
private String appId;
介绍
Spring 表达式语言(简称“SpEL”)是一种强大的表达式语言,支持运行时查询和操作对象图。语法类似于 Jakarta 表达式语言,但提供了其他功能,最明显的是方法调用和基本字符串模板功能。而且可以独立使用,不过需要创建一些引导基础结构类,例如解析器。但是使用Spring环境是开箱即可食用。
-
表达式语言支持很多功能:诸如访问 字面量、属性、数组、列表、map、变量、方法、正则表达式、类型表示、字符串操作、Bean访问等
-
关系运算符:and, or, not, &&, ||, !
-
逻辑运算符:<, >, ==, !=, <=, >=, lt, gt, eq, ne, le, ge
-
算数运算符:+, -, *, /, %, ^, div, mod、++、--
-
条件表达式:?:
二. 实际哪里会用到(•ө•)♡
-
配置文件中注入属性,比如使用Spring的xml配置、Springboot的@Value中访问
demo.name=myApp demo.version=1.0.0在配置文件中使用它,ExcelUtils是一个简单的工具类,里面有个静态的get方法.(注意调用非Bean的静态累的表达式是T(xxx).方法 )
<bean id="demoBean" class="com.demo.DemoBean"> <property name="name" value="#{T(com.demo.utils.ExcelUtils).get('demo.name')}" /> <property name="version" value="#{T(com.demo.utils.ExcelUtils).get('demo.version')}" /> </bean>在Springboot中使用它
@Value("#{T(com.demo.util.ExcelUtils).get('${demo.name}')}") private String dynamicVal; -
依赖注入和按条件配置,比如上面的
@ConditionalOnExpression("#{${business.maxCount:0} gt 0}")这一段,就表示${business.maxCount:0}这个属性的类型必须是int(必须是数值类型),gt表示同时它的值必须大于0,满足条件才会创建对应的Bean//表示读取系统属性中的os.name属性,也就是判断当前操作系统是否为Linux @ConditionalOnExpression("#{T(java.lang.System).getProperties()['os.name'].startsWith('Linux')}") @Component public class LinuxSpecificService implements Service { }换一个姿势,诸如Bean的时候按照表达式解析动态传入数据:
@Bean public ExampleBean exampleBean(@Value("#{T(java.lang.System).getProperties()['os.name'].startsWith('Linux')} ? 'linux' : 'other'}") String osType) { return new ExampleBean(osType); } -
SpEL在AOP中的应用:
@Aspect @Component public class LoggingAspect { //表示是所有包含Loggable这个注解的方法之前执行。 @Before("@annotation(com.demo.annotations.Loggable)") public void logBefore(JoinPoint joinPoint) { } } -
常见的Spring框架内置的支持SpEL的有:@Value、@ConditionalOnExpression、@ConditionalOnProperty<本身基于属性,但我们可以使用SpEL做增强判断等>、Spring Data JPA 的查询、Spring Security 中的表达式等。其他一些基于Spring的扩展也会支持SpEL。
三. 都有哪些使用姿势,支持哪些操作解析,有何限制( ◠‿◠ )
- 直接在Spring中使用表达式获取符号#{}操作符来操作元素。如 第一部分展示的那样。
- 使用SpEL API来手动编写解析,使用内置的解析器和处理器。
//直接解析字面量,无任何其他填充 ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("'Hello'"); String message = (String) exp.getValue();//输出Hello字符串 - 支持的功能列表:
功能 描述 用法举例 字面量 支持以下字面量的表示方式:
String:以单引号、双引号分割。
Number:支持使用负号、指数表示法和小数点。
Boolean:true、false
也支持空的表示null
(需要注意的是负数是在评估的时候去按照0-x解析的,也就是不存储负数,那么Integer.MIN_VALUE在表达式中就会报错)@Value("#{'Hello World'.concat("!!!")}")
private String literal_1;
@Value("#{1.5 + 2.22}")
private double literal_2;属性、数组、集合
map、索引对于访问属性,直接使用对象名.属性名访问即可。虽然首字母不区分大小写,但是建议小写,按照约定来。
数组和列表的内容是通过使用方括号表示法获得的.
map通方括号加单引号模式获取['xx'] 比如officers['president'].placeOfBirth.city@Value("#{app.name}")
private double appName;
@Value("#{myArray[0]}")
private double appName;内联列表 使用{}里面放元素即可,和我们自己声明数组并赋值的一样{1,2,3,4}、{'a','b'},{'x','y'}、{} @Value("#{{'a','b'}}")
private Listlist; 内联Map 和我们用toString把map对象打印出来的语法一样{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}} 定义Map接受即可 数组构造 和Java创建数组的语法一模一样new int[] @Value("#{new int[] {1, 2, 3}}")
private int[] arrayInt;方法调用 就和在Java代码中调用方法一样'hello'.substring(1, 3)、自定义类的方法等,上面的例子中已经有 @Value("#{strToTransport.convert('8:蝴蝶')}")
private TransportEntity transportEntity;类型运算 关系运算、逻辑运算、算数运算、条件表达式、正则运算。(还可以自定义实现OperatorOverloader来实现更加复杂运算) @Value("#{${server.port} instanceof T(Integer)}")
private boolean isInt;类类型表达 语法:T()、支持静态类和方法。其中java.lang包中的类访问不需要全限定名,其他的都要全限定名,比如T(java.math.RoundingMode).FLOOR @Value("#{T(cn.loo.demo.util.ExcelUtils).get('${app.transport.name}')}")
private String dynamicVal;
@Value("#{T(System).getProperties()['os.name'].startsWith('Linux')}")
private boolean isLinux;构造方法 构造方法方式和java语法一样,使用new关键字带上全限定名。java.lang包中的不需要全限定名 @Value("#{new cn.loo.demo.entity.TransportEntity(1001,'${app.transport.name}')}")
private TransportEntity transportEntity;定义变量 变量名必须符合一定规范。PS照着java变量命名规范来,错不了。
#this 变量始终是定义的,并引用当前评估对象(根据该对象解析未限定的引用)。#root 变量始终是定义的,并引用根上下文对象。尽管在计算表达式的组成部分时,#this 可能会有所不同,但 #root 始终引用根。TransportEntitytesla = new TransportEntity(1, "纸飞机");
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");
parser.parseExpression("name = #newName").getValue(context, tesla);
System.out.println(tesla.getName()); // "Mike Tesla"其他 function:通过自定义函数,然后绑定到评估上下文使用即可。
Bean引用:如果已使用Bean 解析器配置了评估上下文,则可以使用 @ 符号从表达式中查找 Bean
三目运算:false ? 'trueExp' : 'falseExp'
安全操作符号: 避免空指针异常,比如:placeOfBirth?.city。?.?、第一个:?.^、最后一个:?.$、?.! 比如:members?.?[nationality == 'Serbian']
模板组合等:@Value("'咪咪要吃小鱼干:'+ #{T(java.lang.Math).random()}"@Value("#{@transportConfig.id}")
private String variable;
@Value("#{systemProperties['pop3.port'] ?: 25}")
@Value("#{T(java.lang.Math).random() + environment['server.port']}") - 局限性:SpEL内置SpEL Compilation对表达式进行解释、编译,但不是所有类型的表达式都支持编译。主要关注的是可能在性能关键上下文中使用的常用表达式。无法编译的比如涉及赋值的表达式、自定义解析器或者访问器的表达式、使用重载运算符的表达式、使用数组构造语法的表达式、使用选择或投影的表达式。什么意思呢,其实就是说这些表达式不支持编译运行,这些表达式的性能就不如其他的可编译执行的表达式性能好。和我们JVM的编译执行、解释执行一个道理,各有千秋。
- 它是一个强大的表达式语言,用来做一些基于配置的动态判断,不复杂的逻辑判断等,不是让编程人员在SpEL里面去写业务逻辑或者复杂操作的,应当避免这种行为。
- 一般来说,应该避免在 SPEL 表达式中包含用户输入。如果用户输入必须包含在表达式中,那么应该在有限的上下文中进行计算,不允许任意调用方法。【避免使用表达式攻击系统】
四. 除了直接使用Spring支持的注解调用,还可以手动调用(´◔౪◔)ʃ
//定义复杂表达式要用到的对象
TransportEntity entity = new TransportEntity();
entity.setId(8);
entity.setTransportName("SpaceX");
//定义转换器
ExpressionParser expressionParser = new SpelExpressionParser();
//定义表达式,并解析表达式,返回可重复求值的表达式对象
Expression expression = expressionParser.parseExpression("transportName.concat(' lunch')");
//定义表达式评估上下文,也就是在此上下文执行设值.这里使用的标准表达式上下文,如果是纯运算等简单的,还可以直接使用SimpleEvaluationContext
EvaluationContext context = new StandardEvaluationContext(entity);
//通过提供的上下文,计算表达式并返回结果
String result = (String) expression.getValue(context);//输出SpaceX lunch
Expression expression1 = expressionParser.parseExpression("id >1?100:0");
System.out.println(expression1.getValue(context));//输出100
五. 噢哟,不错哦!它怎么做到的呢(✿╹◡╹)
- 解析的总体过程如下:

-
SimpleevalationContext 被设计为仅支持SpEL语言语法的一个子集。它排除了Java类型引用、构造函数和bean引用。它还要求显式地选择对表达式中的属性和方法的支持级别。默认情况下,create ()静态工厂方法只允许对属性进行读访问。
StandardEvaluationContext 是完整版的支持,公开全套SpEL语言特性和配置选项。可以使用它来指定默认的根对象,并配置每个可用的计算相关策略。(Bean的引用默认实现使用JavaBean约定) -
默认情况下,SpEL 使用 Spring 核心中提供的转换服务(org.springframework.core.Convert)。此转换服务附带了许多用于常见转换的内置转换器。此外,它还支持泛型。这意味着,在表达式中处理泛型类型时,SpEL 尝试转换来维护它遇到的任何对象的类型正确性
通过conversionService.canConvert(sourceTypeDesc, typeDescriptor)来判断是否能将源数据类型转换为目标类型,比如最常见的就是String转为int(使用的StringToNumberConverterFactory中的静态内部类StringToNumber)(使用NumberUtils.parseNumber方法)
内置的类型转换器都在spring-core核心包的org.springframework.core.convert包里面。表达式的定义和解析在spring-expression包中。
六. 类似的表达式语言还蛮多哟(๑◔‿◔๑)
Java 表达式语言(OGNL、MVEL 和 JBoss EL 等){不要忘记我们刚开始后端写jsp经常用到的EL表达式哦,由web容器实现并解析和展示,比如我们经常内置Tomcat就会看到tomcat-embed-el-xxx.jar,独立的Tomcat在lib目录里面的jasper-el.jar,就是用来把我们在jsp页面上写的表达式经过japer-el引擎转为字面量展示}
点这里看SpEL官网内容介绍

浙公网安备 33010602011771号