1. 简介
Spring表达式语言(简称“ SpEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了其他功能,最著名的是方法调用和基本的字符串模板功能。
SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,而是可以独立使用。为了自成一体,本章中的许多示例都将SpEL用作独立的表达语言。这需要创建一些自举基础结构类,例如解析器。大多数Spring用户不需要处理此基础结构,而只能编写表达式字符串进行评估。
2. spEL常用接口
expression:
parser:
2.1 ExpressionParser接口
表达式解析接口。默认实现
1 2 3 4 5 6 | public interface ExpressionParser { //该方法使用的ParserContext为null,即不使用模板 Expression parseExpression(String expressionString) throws ParseException; //expressionString:待解析的字符串。context解析的上下文对象 Expression parseExpression(String expressionString, ParserContext context) throws ParseException; } |
ParserContext:表示解析的模板。
即
案例一:不使用模板解析表达式
1 2 3 4 | ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("'Hello World'.concat('!')"); //现在的值message是“ Hello World!”。 String message = (String) exp.getValue(); |
案例二:使用模板解析表达式
1 2 3 4 5 6 7 | ExpressionParser parser = new SpelExpressionParser(); //定义模板。默认是以#{开头,以#结尾 TemplateParserContext PARSER_CONTEXT = new TemplateParserContext(); //传入解析模板 Expression exp = parser.parseExpression("#{'Hello World'.concat('!')}",PARSER_CONTEXT); String message = (String) exp.getValue(); System.out.println(message); |
案例三:表达式不符合规则
- 未使用模板,但是传入#{}的字符串
1 2 3 4 | ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("#{'Hello World'}"); String message = (String) exp.getValue(); System.out.println(message); |
会出现下面异常:
1 | Exception in thread "main" org.springframework.expression.spel.SpelParseException: Expression [#{'Hello World'}] @1: EL1043E: Unexpected token. Expected 'identifier' but was 'lcurly({)' |
- 使用#{}模板,但是传入%{}字符串
1 2 3 4 5 6 7 | ExpressionParser parser = new SpelExpressionParser(); //定义模板 TemplateParserContext PARSER_CONTEXT = new TemplateParserContext(); //传入解析模板 Expression exp = parser.parseExpression("%{'Hello World'.concat('!')}", PARSER_CONTEXT); String message = (String) exp.getValue(); System.out.println(message); |
未能进行解析:
1 | %{'Hello World'.concat('!')} |
2.2 EvaluationContext接口
evaluation:
表示上下文环境,默认实现是
- 使用setRootObject方法来设置根对象;
- 使用setVariable方法来注册自定义变量;
- 使用registerFunction来注册自定义函数等等;
2.3 Expression接口
根据上下文进行自我评估的表达式对象。有
1 2 3 4 5 | //获取上下文中表达式的值。 //context:评估的上下文对象。 //rootObject:会将#root放入到context中。 //desiredResultType:解析值的类型。 < T > T getValue(EvaluationContext context, Object rootObject, @Nullable Class < T > desiredResultType) throws EvaluationException; |
3. 常用API
3.1 SpringBean引用
SpEL支持使用@符号来引用Bean。在引用Bean时需要使用BeanResolver接口来查找Bean,Spring会提供
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | @Component @Slf4j @Aspect public class LogAnnoAspect implements BeanFactoryAware { //定义解析的模板 private static final TemplateParserContext PARSER_CONTEXT = new TemplateParserContext(); //定义解析器 private static final SpelExpressionParser PARSER = new SpelExpressionParser(); //定义评估的上下文对象 private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); //获取到Spring容器的beanFactory对象 private BeanFactory beanFactory; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; //填充evaluationContext对象的`BeanFactoryResolver`。 this.evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory)); } @Pointcut("@annotation(com.tellme.config.LogAnno)") public void permission() { } @Around(value = "permission();") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { Signature signature = joinPoint.getSignature(); //参数值 Object[] args = joinPoint.getArgs(); MethodSignature methodSignature = (MethodSignature) signature; Object target = joinPoint.getTarget(); //获取到当前执行的方法 Method method = target.getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getParameterTypes()); //获取方法的注解 Object proceed = joinPoint.proceed(); LogAnno logAnno = method.getAnnotation(LogAnno.class); /** * 1. resolve(logAnno.typeExpression()得到的值为:#{@ELService.x(#root)}。 * 2. parseExpression解析后得到实际字符串为@ELService.x(#root)表达式。 * 3. getValue去bean容器中执行ELService类的x方法。当然参数是context的#root对象。通过proceed传入。 * 4. 最终返回的类型为method.getReturnType()原方法的类型 */ return PARSER.parseExpression(resolve(logAnno.typeExpression()), PARSER_CONTEXT) .getValue(this.evaluationContext, proceed, method.getReturnType()); } /** * 作用是读取yml里面的值 * * @param value 例如:1. #{${ttt.xxx}}会读取yml的ttt.xxx: read配置值,替换为#{read} * 2.#{read}直接返回#{read} * @return #{read} */ private String resolve(String value) { if (this.beanFactory != null && this.beanFactory instanceof ConfigurableBeanFactory) { return ((ConfigurableBeanFactory) this.beanFactory).resolveEmbeddedValue(value); } return value; } } |
注解类:接受spEL表达式
1 2 3 4 5 6 7 | @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LogAnno { String typeExpression() default ""; } |
业务方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Service @Slf4j public class ELService{ @LogAnno(typeExpression = "#{@ELService.x(#root)}") public String ttt() { log.info("业务逻辑"); return "ttt返回值"; } //最终要执行该方法 public String x(String res) { log.info("获取的值:" + res); return res+"-x返回值"; } } |
3.2 context注册变量和方法
可以通过spEL表达式可灵活的获取context存储的数据(包括变量和方法),那么数据是如何存储的呢?
注册自定义函数(只能是静态方法)
目前只支持类静态方法注册为自定义函数;
SpEL使用
其实完全可以使用setVariable代替,两者其实本质是一样的;
推荐使用“registerFunction”方法注册自定义函数。
1 2 3 4 5 6 7 8 9 10 | public void t1() { StandardEvaluationContext context = new StandardEvaluationContext(); //获取Method对象 Method readBook = ReflectionUtils.findMethod(StuService.class, "readBook", String.class); //注册放到到自定义对象中 context.registerFunction("readBook", readBook); String value = PARSER.parseExpression("#readBook('钢铁')").getValue(context, String.class); log.info("解析:" + value); } |
注册变量
变量定义通过
除了引用自定义变量,SpEL还允许引用根对象及当前上下文对象,使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public static void t10() { ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = new StandardEvaluationContext(); context.setVariable("name", "tom"); context.setVariable("lesson", "spEL学习"); //获取name变量,lesson变量 String name = parser.parseExpression("#name").getValue(context, String.class); System.out.println(name); String lesson = parser.parseExpression("#lesson").getValue(context, String.class); System.out.println(lesson); //StandardEvaluationContext构造器传入root对象,可以通过#root来访问root对象 context = new StandardEvaluationContext("我是root对象"); String rootObj = parser.parseExpression("#root").getValue(context, String.class); System.out.println(rootObj); } |
3.3 context表达式赋值
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public static void t11(){ ExpressionParser parser = new SpelExpressionParser(); User user = new User(); user.setId(1); user.setName("tom"); //放入到根对象 EvaluationContext context = new StandardEvaluationContext(user); parser.parseExpression("#root.name").setValue(context,"lobai"); System.out.println(parser.parseExpression("#root").getValue(context,User.class)); } @Data public static class User{ //编号 private Integer id; //姓名 private String name; } |
3.4 链式调用的异常处理
使用spEL表达式,可能存在链式调用,会遇到空指针等系列问题,如何实现安全存储呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public static void t11() { ExpressionParser parser = new SpelExpressionParser(); User user = new User(); user.setId(1); //放入到根对象 EvaluationContext context = new StandardEvaluationContext(user); String value = parser.parseExpression("#root.name.toString()").getValue(context, String.class); System.out.println(value); } @Data public static class User { //编号 private Integer id; //姓名 private String name; } |
异常信息:
1 | Exception in thread "main" org.springframework.expression.spel.SpelEvaluationException: EL1011E: Method call: Attempted to call method toString() on null context object |
解决方案:
对象属性获取非常简单,即使用如“a.property.property”这种点缀式获取,SpEL对于属性名首字母是不区分大小写的;SpEL还引入了Groovy语言中的安全导航运算符“(对象|属性)?.属性”,用来避免“?.”前边的表达式为null时抛出空指针异常,而是返回null;修改对象属性值则可以通过赋值表达式或Expression接口的setValue方法修改。
使用如下的表达式:
1 | "#root.name?.toString()" |
文章参考
Spring5.1.5—spEL官网
玩转Spring中强大的spel表达式!