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。

三. 都有哪些使用姿势,支持哪些操作解析,有何限制( ◠‿◠ )

  1. 直接在Spring中使用表达式获取符号#{}操作符来操作元素。如 第一部分展示的那样。
  2. 使用SpEL API来手动编写解析,使用内置的解析器和处理器。
    //直接解析字面量,无任何其他填充
    ExpressionParser parser = new SpelExpressionParser();
    Expression exp = parser.parseExpression("'Hello'");
    String message = (String) exp.getValue();//输出Hello字符串
    
    
  3. 支持的功能列表:
    功能 描述 用法举例
    字面量 支持以下字面量的表示方式:
    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 List list;
    内联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']}")
  4. 局限性:SpEL内置SpEL Compilation对表达式进行解释、编译,但不是所有类型的表达式都支持编译。主要关注的是可能在性能关键上下文中使用的常用表达式。无法编译的比如涉及赋值的表达式、自定义解析器或者访问器的表达式、使用重载运算符的表达式、使用数组构造语法的表达式、使用选择或投影的表达式。什么意思呢,其实就是说这些表达式不支持编译运行,这些表达式的性能就不如其他的可编译执行的表达式性能好。和我们JVM的编译执行、解释执行一个道理,各有千秋。
  5. 它是一个强大的表达式语言,用来做一些基于配置的动态判断,不复杂的逻辑判断等,不是让编程人员在SpEL里面去写业务逻辑或者复杂操作的,应当避免这种行为。
  6. 一般来说,应该避免在 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官网内容介绍


posted @ 2024-10-09 09:36  冰雪女娲  阅读(121)  评论(0)    收藏  举报