SPEL 表达式在自定义注解中的应用

 在自定义注解中,如何解决值传递的问题是首要的问题。
 恰好在Spring中核心包中早已开始支持了文本表达式,也即是大名鼎鼎的SPEL表达式。与JSP的EL表达式一样,为了解决在非代码情况下获取Java Bean的值或者直接调用代码。
 而且SPEL表达式天然与Spring完美搭配,至于为什么,待我详细说明。


 首先我们是如何定义注解和获取注解的呢?(会了的可以跳过)

 示例代码:

public @interface ShowLog{
  //日志名称
  String name();
  //日志描述
  String desc();
}
.....
@ShowLog(name="#user.name",desc="yyy")
public void method(User user){
}

  1. 定义注解:注解的每个值本质其实是一个方法。例如ShowLog注解中的name和desc,写法其实和接口里面写方法声明一样。与接口不同的是,接口是使用者要主动实现接口中的方法。而注解是在获取时由动态代理实现。
  2. 获取注解:例如获取method方法的注解。method.getxxxAnnotation(AnnotationType)获取指定注解;也可以直接获取全部注解method.getxxxAnnotations(),在用instanceof 判断。获取注解的方法都是来自AnnotatedElement接口。在javadoc文档中是这样描述AnnotatedElement接口的。

Represents an annotated element of the program currently running in this VM. This interface allows annotations to be read reflectively. All annotations returned by methods in this interface are immutable and serializable.
表明在当前虚拟机运行的程序中被注解的元素(取决于注解目标,有可能是字段、方法、类、参数等)。此接口允许注解被反射获取。所有通过这个接口方法返回的注解都是不可变和序列化的。

 由上述可知:1. 注解通过反射获取;2. 通过反射返回的注解是不可变的(当然我们可以通过反射动态代理后的对象修改注解值,后面详细说明)。


 SPEL 表达式概要

详细语法可以参考并发编程网《Spring 5 官方文档》6.Spring 表达式语言
完整代码示例:

SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.OFF,
    this.getClass().getClassLoader());
SpelExpressionParser parser = new SpelExpressionParser(config);
StandardEvaluationContext evaluationContext = new StandardEvaluationContext(rootObject);
TemplateParserContext templateParserContext = new TemplateParserContext(prefix,suffix);
Expression expression = SpelPaserUtil.parser.parseExpression(expressionStr, parserContext);
BeanFactoryResolver  beanFactoryResolver = new BeanFactoryResolver(applicationContext);
evaluationContext.setBeanResolver(beanFactoryResolver);
expression.getValue(evaluationContext, rootObject, valueType);

主要解释一下其中的几个重要点的使用:
 1. 编译器模式

SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.OFF,
    this.getClass().getClassLoader());

 在SpelCompilerMode中有三个值OFF,IMMEDIATE,MIXED。先说下什么是编译器模式,编译器模式是为了加快解析速度而增加的一个功能,因为表达式是一串字符串,不具备表达类型的能力,如果加上表达类型的语法,就很死板和累赘了。这时,为了节省解析时间,在编译器模式下会把第一次的解析行为记录下来,这样后面解析就可以直接转换类型,而不用判断类型了。但是前提是你的解析结果尽量保持不变的情况下,这个编译模式会更快。如果经常改变,还是关闭吧。OFF对应关闭编译模式也称解释模式(时刻解释),IMMEDIATE打开编译模式,MIXED两者混合,如果编译模式下转换类型出错多次,就自动转换到解释模式。

 2. StandardEvaluationContext(表达式计算上下文)

StandardEvaluationContext evaluationContext = new StandardEvaluationContext(rootObject);

 表达式计算上下文,是在解析表达式时使用的环境,重点就是rootObject解析对象。例如有一个表达式:person.name,spel解析器默认不知道这个表达式是取哪个对象的值,如果不指定解析对象,就无法进行解析。解析对象可以是任何类型,常用的是POJO和Map。

 3. TemplateParserContext(表达式解析上下文)

TemplateParserContext templateParserContext = new TemplateParserContext(prefix,suffix);

 解析器上下文:不同的表达式书写方式和解析过程。解析器上下文实现ParserContext接口,该接口中有三个方法,分别是:getExpressionPrefix、getExpressionSuffix、isTemplate。对应着表达式前缀,后缀,是否是template表达式。具体什么含义呢?在用SPEL或者是其他的EL语言时,会碰到两种使用场景。一种是纯表达式,一种是文本夹杂表达式(也称之为“模板”)。
 例如,表达式一:user.name;表达式二:用户所在城市是#{user.city}。表达式一是纯表达式,直接解析返回一个结果。而表达式二是在文本中夹杂着SPEL表达式,也即是一个模板,这个时候如果还是使用默认的解析器上下文,默认的isTemplate方法返回值是false,就会将整个表达式二都当做SPEL语法,但是很显然整个表达式二并不是SPEL的语法,只有其中的“#{user.city}”才是SPEL语法,在默认解析器下必定无法解析。
 Spring中默认提供了TemplateParserContext 模板解析器,getExpressionPrefix方法返回值为"#{"、getExpressionSuffix方法返回值为"}"、isTemplate返回值为true。(完整代码请查看上面的示例代码)

 4. BeanFactoryResolver

BeanFactoryResolver  beanFactoryResolver = new BeanFactoryResolver(applicationContext);
evaluationContext.setBeanResolver(beanFactoryResolver);

BeanFactoryResolver是spring容器提供给SPEL访问容器中组件的解析器。允许SPEL表达式以语法@beanName访问容器中的bean

 5. elvis/三元操作符(弱三元操作符,截止版本为4.1.6)

SPEL中三元操作符依旧是 logic ? true ex : false ex;Elvis是简化的null三元判断,语法:user.name ?: 'zhangsan',当user的name为null时,返回zhangsan,否则返回user.name。但是为什么称SPEL表达式的三元操作符是弱三元呢?这个前提是使用SPEL的TemplateParserContext作为解析上下文,是因为在true ex 或者false ex部分不可继续使用模板作为返回值,或者一直嵌套递归处理,当然这个很少很少用到,一般也会重新分析逻辑,使用其他方法来完成。只是需要注意。其实这个也可以解决,只要自己解析一次三元操作符,把true ex 或者false ex 依旧使用TemplateParserContext解析就可以。代码参照我的另一篇文章(加强spel三元表达式工具类)。

 SPEL是一个非常强大的表达式,也可以看出与spring完美的结合。因此在自定义注解中,我们可以充分的利用SPEL表达式完成参数的记录,但请记住,千万不要使用SPEL表达式完成重要的业务代码,很容易被注入攻击。我常使用SPEL完成系统中的操作数据记录功能或者审计功能。利用AOP,和自定义注解,我可以直接从切点的参数中利用SPEL表达式获取我要的参数值,也不会因为注入攻击的可能而存在风险。
 例如最上面的示例代码中:@ShowLog(name="#user.name",desc="yyy"),name用SPEL表达式取参数user中的name值。
 本文完。若有错误,还请多加指正。

你可能感兴趣的:(SPEL 表达式在自定义注解中的应用)