Spring framework(6):SpEL 表达式

Spring Expression Language (SpEL) 是一种强大的表达式语言,用于查询和操纵对象。本文介绍SpEL的基本使用、编译器、表达式语法等内容。

SpEL 概述

Spring 动态语言(SpEL)是一个支持运行时查询和操作对象的动态语言,其语法类似于 EL 表达式,具有如显示方式调用、字符串模板函数等强大特性,同时能够很好地与其他动态语言进行集成;

使用 SpEL 时需要导入 spring-expression 依赖;

基本使用

 
ExpressionParser parser = new SpelExpressionParser();   //创建 SpEL 表达式解析器
Expression exp = parser.parseExpression("'Hello World!'");   //对表达式进行解析
String result = (String) exp.getValue();    //对表达式进行求值
System.out.println(result);

使用 SpEL 编译器

默认情况下,SpEL 表达式只有在求值时才会进行表达式计算,所有表达式可以在运行时动态修改,但对于同一个表达式,如果每次求值都要进行动态解析,在一些调用频率比较高的场景下,对性能的影响比较大;
因此 Spring 提供了 SpelCompiler SpEL 编译器来解决这个问题,SpEL 编译器会将表达式编译成字节码,如果后续运行时表达式发生变化,才需要重新编译;
 
China china = new China();
//创建解析配置
SpelParserConfiguration config = new 
    SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,Compiler.class.getClassLoader()); 
//由解析配置创建解析器
SpelExpressionParser parser = new SpelExpressionParser(config);     
//创建取值上下文
EvaluationContext context  = new StandardEvaluationContext(china);  
//解析表达式
SpelExpression exp = parser.parseRaw("isCapital('Beijing') && isCapital('Shanghai')");  
//解析表达式
System.out.println(exp.getValue(context));   //第一次调用
System.out.println(exp.getValue(context));   //第二次调用
以上示例中,China是一个对象,含有一个返回boolean的isCaptal 成员方法;
在创建SpelParserConfiguration 配置对象时候,spelCompilerMode 参数含有以下3个枚举值可选:
  • SpelCompilerMode.OFF:默认编译模式,不启用编译;
  • SpelCompilerMode.MIXED:混合编译模式,前几次执行表达式取值采用解释型处理,当达到一个阈值(100)后,才启用编译处理;
  • SpelCompilerMode.IMMEDIAT:立即启用编译,实际上SpEL内部不会立即启用编译,而是在第二次执行表达式取值时才会启用编译;



SpEL 表达式语法

文本字符解析

SpEL 支持字符串、日期、数字(正数、负数、十六进制数)、布尔类型、null;
其中字符串需要使用类似 “'Hello world!'” "\Hello world!"\"" 的表述方式;
单引号可以使用 "\'" 来表示;
 
ExpressionParser parser = new SpelExpressionParser();
String helloworld = parser.parseExpression("\"Hello world\"").getValue(String.class);  //字符串解析
double doublenum = parser.parseExpression("3.141596E+10").getValue(Double.class);   //浮点数解析
int intnum = parser.parseExpression("0x7FFFFFFF").getValue(Integer.class);      //十六进制数解析
boolean trueValue = parser.parseExpression("true").getValue(Boolean.class);      //布尔值解析
Object nullValue = parser.parseExpression("null").getValue();                  //null值解析

操作符支持

SpEL 支持Java 标准的操作符,包括:
  • 关系操作符:>  , < ,  >= ,  <= ,  = ,正则表达式 , instanceof 运算符;
  • 逻辑运算符:&&,||,!,此外还也提供了 and 和 or 2个运算符分别等同于 && 和 ||;
  • 算数运算符:+,-,*,/,%,^;
  • 三元运算符:<表达式1>?<表达式2>:<表达式3>;

此外还支持 Groovy 的以下操作符:
  • 安全导航操作符:<对象>.?<变量属性/方法>
安全导航操作符用于避免空指针异常,通常在获取对象属性方法时验证该对象是否为空,需要加上一系列的验证代码,使用安全导航操作符可以节省这个过程;
 
//user是User类的实例,User类含有实例username
//原来代码:
if(user != null){
    String name = user.username;
}
//使用安全导航操作符的代码;
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context  = new StandardEvaluationContext(user);  
String name = parser.parseExpression("user.?username").getValue(String.class);
  • Elvis操作符:<变量>?<变量值>
在三元运算中,通常要重复2次变量,Elvis用于简化这个过程;
 
//三元元算符的表达
String name = "Al-assad";
String displayName = name!=null?name:"UnkonwnName";
//Elvis运算符的表达
String displayName = name?"UnknowName";

对象操作

对象属性解析,方法解析,属性赋值

1、SpEL 可以使用类似 ”xxx.yyy.zzz“ 的对象属性来访问对象属性;
2、SpEL可以直接访问上下文对象中的实例方法、静态方法;
3、SpEL 还可以对上下文对象的属性进行赋值;

以下示例中 User 类包含数据域 name,age,birthMessage(BirthMessage),包含成员方法 boolean validatePass(String passworld);
BirthMessage 类包含数据域 birthday(Date),city;
 
//测试对象
User user = new User("Al-assad",22, new BirthMessage(new SimpleDateFormat("yyyy-MM-dd").parse("1996-12-11"),"Chaozhou"));
//构造SpEL解析上下文
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext(user);  //设置上下文对象
/*1、获取对象属性*/
String name = parser.parseExpression("name").getValue(context,String.class);   //获取对象属性
int age = parser.parseExpression("age").getValue(context,Integer.class);
String city = parser.parseExpression("birthMessage.city").getValue(context,String.class);  //获取嵌套对象属性
System.out.println(name+" "+age+" "+city);
/*2、调用对象方法*/
boolean isValidate = parser.parseExpression("validatePass('1234455')").getValue(context,Boolean.class);
System.out.println(isValidate);
/*3、对象属性赋值*/
//① 通过setValue赋值
parser.parseExpression("name").setValue(context,"Vancy");
System.out.println(user.getName());
//② 通过表达式赋值
parser.parseExpression("name='Mofess'").getValue(context);
System.out.println(user.getName());

T 类型操作符-  T(全限定名)

T 类型操作符用于从类路径中加载指定类名称(全限定名)的 Class 对象,该功能类似于 ClassLoader#loadClass() ;
 
ExpressionParser parser = new SpelExpressionParser();  
//获取类型对象
Class stringclass = parser.parseExpression("T(java.lang.String)").getValue(Class.class);
System.out.println(stringclass.getName());
//调用类静态方法
double randomValue = parser.parseExpression("T(java.lang.Math).random()").getValue(Double.class);
System.out.println(randomValue);

构造器调用

在SpEL 中可以通过 new 运算符调用构造器构造一个对象实例,除了基本类型和字符串,其他类需要使用全限定名;
 
ExpressionParser parser = new SpelExpressionParser();  
//调用构造器构建一个实例
String str = parser.parseExpression("new String('Hello world')");
//调用构造器创建实例,并调用该实例的实例方法
int randomValue2 = parser.parseExpression("new java.util.Random().nextInt(10)").getValue(Integer.class);
System.out.println(randomValue2);

上下文变量设置和获取

在 EvaluationContext 上下文对象中的变量可以使用 ”#变量名“ 引用,可以使用 EvaluationContext#setVariable(var,value) 设置变量;
 
ExpressionParser parser = new SpelExpressionParser();  
EvaluationContext context = new StandardEvaluationContext(user);  
//为上下文添加新的变量值
context.setVariable("TempName","assad");
//在表达式中获取新设置的变量值
parser.parseExpression("name=#TempName").getValue(context);

数组、集合操作

数组、集合解析

SpEL 支持数组、集合类型(List、Map)的解析;
数组支持标准 Java 中的创建方法,如 "new int[]{1,2,3,4}"
List 的创建格式,"{1,2,3,4}" 、"{{'a','b'},{'c','d'}}"’ ;
Map 的创建格式,"{{username:'Al-assad',city:'Guangzhou'},{username:'Vancy',city:'Shanghai'}}" ;
 
//数组解析
int[] array = (int[])parser.parseExpression("new int[]{1,2,3,4}").getValue();
//List解析
List list = parser.parseExpression("{1,2,3,4}").getValue(List.class);
List lists = parser.parseExpression
    ("{{'Beijing','Shanghai','Guangzhou'},{'NewYork','Seattle'}}").getValue(List.class);  
//Map解析
Map userInfo = parser.parseExpression
    ("{{name:'Al-assad',city:'Guanzhou'},{name:'Vancy',city:'Shanghai'}}").getValue(Map.class);
String name = (String)userInfo.get("name"); 
System.out.println(name);

集合过滤

SpEL 支持动态语言的集合过滤,允许通过一个过滤条件获取元集合的子集,集合过滤的语法为:?[selectExpression]";
以下例子演示过滤获取一个List中大于5的元素;
 
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
context.setVariable("list",list);         //将集合设置为上下文属性
//过滤集合
List<Integer> listfilted = (List<Integer>)parser.parseExpression("#list.?[#this>5]").getValue(context);  
listfilted.forEach(System.out::println);
//output:6,7,8,9,10

集合转换

集合转行允许使用一个算子对集合内的元素进行运算,会得到一个新元素类型的集合,集合转换的语法为:![projectionExpression]"
 
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
context.setVariable("list",list);         
//集合转换:将一个List<Integer> 根据规定的条件,转化为以一个 List<Boolean>
List<Boolean> filterResult = (List<Boolean>)parser.parseExpression("#list.![#this>5]").getValue(context);
filterResult.forEach(System.out::println);
//output:false,false,false,false,false,true,true,true,true,true,




在 XML文件&注解中使用SpEL


在SpEL的实际使用场景中,经常会在Bean装配过程中,在XML配置文件或Bean注解中使用 SpEL,这两种方式都是使用统一的语法使用 SpEL 表达式:#{<expression string>}; 

在XML文件中使用SpEL

以下是在xml文件中使用SpEL的示例:
 
<beans ...>
    <bean id="numGuess" class="site.assad.SpELTest.NumberGuess"
        p:randomNumber="#{T(java.lang.Math).random()*100.0}"/>   <!--使用T元算符的SpEL调用类实例方法-->
    <bean id="shapeGuess" class="site.assad.SpELTest.ShapeGuess"
        p:initalShapeSeed="#{numGuess.randomNumber}}" />    <!--在 SpEL 中调用定义的bean的属性-->
</beans>

在注解中使用SpEL

@Value 注解可以标注在类的属性、方法和构造函数上,用于配置文件中加载一个参数值;

以下实例演示使用SpEL加载数据源连接参数:
数据源加载 Bean:DataSource.class
 
@Component
public class DateSource {
    @Value("#{dataSourceProperties['driverClassName']}")
    private String driverName;
    
    @Value("#{dataSourceProperties['url']}")
    private String url;
    
    @Value("#{dataSourceProperties['userName']}")
    private String userName;
    
    @Value("#{dataSourceProperties['password']}")
    private String password;
}
bean自动扫描配置:bean.xml
 
<beans ...>
    <!--引入用于使用SpEL表达式变量的util工具命名空间-->
    <util:properties id="dataSourceProperties" location="classpath:jdbc.properties" />
</beans>
储存数据源连接参数的配置文件:jdbc.properties
 
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql//127.0.0.1/testDb
userName=root
password=1234


实际上,SpEL还提供了一种用于简化@Value中参数获取的表达方式——属性占位符
bean自动扫描配置:bean.xml 更改为:
 
<beans ...>
    <!--引入用于使用SpEL表达式变量的util工具命名空间-->
    <util:properties id="dataSourceProperties" location="classpath:jdbc.properties" />
     <!--配置属性占位符工具-->
    <context:property-placeholder properties-ref="dataSourceProperties" />
</beans>
数据源加载 Bean:DataSource.class 可以如下:
 
@Component
public class DataSource {
    @Value("#{'driverClassName'}")
    private String driverName;
    @Value("#{'url'}")
    private String url;
    @Value("#{'userName'}")
    private String userName;
    @Value("#{'password'}")
    private String password;
}

### 在 Spring 自定义注解中通过 SpEL 表达式获取属性值的示例 #### 1. 自定义注解的定义 首先,需要定义一个自定义注解,该注解支持 SpEL 表达式的使用。 ```java import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ApiLog { String taskNo(); // 支持 SpEL 表达式 } ``` #### 2. 创建 AOP 切面解析 SpEL 表达式 为了在运行时解析 SpEL 表达式,可以创建一个 AOP 切面,并使用 `SpelExpressionParser` 来解析表达式。 ```java import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import java.lang.reflect.Method; @Aspect @Component public class ApiLogAspect { private final ExpressionParser parser = new SpelExpressionParser(); @Around("@annotation(apiLog)") public Object logApi(ProceedingJoinPoint joinPoint, ApiLog apiLog) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // 获取方法参数 Object[] args = joinPoint.getArgs(); // 创建上下文以解析 SpEL 表达式 StandardEvaluationContext context = new StandardEvaluationContext(); for (int i = 0; i < method.getParameters().length; i++) { context.setVariable(method.getParameters()[i].getName(), args[i]); } // 解析 SpEL 表达式 String taskNo = parser.parseExpression(apiLog.taskNo()).getValue(context, String.class); // 打印日志或执行其他逻辑 System.out.println("Task No: " + taskNo); return joinPoint.proceed(); } } ``` #### 3. 控制器中的使用示例 在控制器中,可以通过 `@ApiLog` 注解标记需要记录日志的方法,并使用 SpEL 表达式动态获取参数值。 ```java import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class ExampleController { @GetMapping("/test1") @ApiLog(taskNo = "#str") // 使用 SpEL 表达式引用方法参数 public String test1(@RequestParam String str) { return "Test1: " + str; } @GetMapping("/test2") @ApiLog(taskNo = "#requestDto.orderNo") // 使用 SpEL 表达式引用 POJO 参数的属性 public String test2(DailyPowerRequestDto requestDto) { return "Test2: " + requestDto.getOrderNo(); } } // 示例 POJO 类 class DailyPowerRequestDto { private String orderNo; public String getOrderNo() { return orderNo; } public void setOrderNo(String orderNo) { this.orderNo = orderNo; } } ``` #### 4. 运行机制解析 上述代码的工作流程如下: - 当请求到达 `/test1` 或 `/test2` 接口时,Spring AOP 切面会拦截这些方法调用。 - 在切面中,通过 `SpelExpressionParser` 解析注解中定义的 SpEL 表达式[^2]。 - 表达式可以引用方法参数或参数的属性,从而实现动态获取值的功能。 - 最终,解析后的值会被打印到控制台或用于其他逻辑处理。 #### 5. 注意事项 - 确保方法参数名称与 SpEL 表达式中引用的名称一致。如果编译器未启用参数名称保留功能,可以在项目中添加 `-parameters` 编译选项[^2]。 - 如果需要引用静态方法,可以在 SpEL 表达式中使用 `T()` 函数。 ### 示例完整代码 以下是完整的代码示例,包含自定义注解、AOP 切面和控制器的实现。 ```java // 自定义注解 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ApiLog { String taskNo(); } // AOP 切面 import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import java.lang.reflect.Method; @Aspect @Component public class ApiLogAspect { private final ExpressionParser parser = new SpelExpressionParser(); @Around("@annotation(apiLog)") public Object logApi(ProceedingJoinPoint joinPoint, ApiLog apiLog) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Object[] args = joinPoint.getArgs(); StandardEvaluationContext context = new StandardEvaluationContext(); for (int i = 0; i < method.getParameters().length; i++) { context.setVariable(method.getParameters()[i].getName(), args[i]); } String taskNo = parser.parseExpression(apiLog.taskNo()).getValue(context, String.class); System.out.println("Task No: " + taskNo); return joinPoint.proceed(); } } // 控制器 import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class ExampleController { @GetMapping("/test1") @ApiLog(taskNo = "#str") public String test1(@RequestParam String str) { return "Test1: " + str; } @GetMapping("/test2") @ApiLog(taskNo = "#requestDto.orderNo") public String test2(DailyPowerRequestDto requestDto) { return "Test2: " + requestDto.getOrderNo(); } } // 示例 POJO 类 class DailyPowerRequestDto { private String orderNo; public String getOrderNo() { return orderNo; } public void setOrderNo(String orderNo) { this.orderNo = orderNo; } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值