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值。
 本文完。若有错误,还请多加指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容