spEL—基础语法+注解中动态调用Bean方法

1. 简介

Spring表达式语言(简称“ SpEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了其他功能,最著名的是方法调用和基本的字符串模板功能。

SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,而是可以独立使用。为了自成一体,本章中的许多示例都将SpEL用作独立的表达语言。这需要创建一些自举基础结构类,例如解析器。大多数Spring用户不需要处理此基础结构,而只能编写表达式字符串进行评估。

2. spEL常用接口

expression:[ɪkˈspreʃn]表达式
parser:[ˈpɑːzə]解析器

2.1 ExpressionParser接口

表达式解析接口。默认实现org.springframework.expression.spel.standard.SpelExpressionParser使用parseExpression方法将表达式字符串解析为Experssion对象,常用的API:

public interface ExpressionParser {
//该方法使用的ParserContext为null,即不使用模板
Expression parseExpression(String expressionString) throws ParseException;
//expressionString:待解析的字符串。context解析的上下文对象
Expression parseExpression(String expressionString, ParserContext context) throws ParseException;
}

ParserContext:表示解析的模板。

ExpressionParser解析规则的字符串。例如org.springframework.expression.common.TemplateParserContext类,只是解析#{带解析的字符串}

案例一:不使用模板解析表达式

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); 
//现在的值message是“ Hello World!”。
String message = (String) exp.getValue();

案例二:使用模板解析表达式

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. 未使用模板,但是传入#{}的字符串
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("#{'Hello World'}");
String message = (String) exp.getValue();
System.out.println(message);

会出现下面异常:

Exception in thread "main" org.springframework.expression.spel.SpelParseException: Expression [#{'Hello World'}] @1: EL1043E: Unexpected token. Expected 'identifier' but was 'lcurly({)'
  1. 使用#{}模板,但是传入%{}字符串
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);

未能进行解析:

%{'Hello World'.concat('!')}

2.2 EvaluationContext接口

evaluation:[ɪˌvæljuˈeɪʃn]评估

表示上下文环境,默认实现是org.springframework.expression.spel.support包中的StandardEvaluationContext类,

  1. 使用setRootObject方法来设置根对象;
  2. 使用setVariable方法来注册自定义变量;
  3. 使用registerFunction来注册自定义函数等等;

2.3 Expression接口

根据上下文进行自我评估的表达式对象。有ExpressionParser解析字符串得到,通过getValue方法获取EvaluationContext上下文的变量。

//获取上下文中表达式的值。
//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会提供BeanFactoryResolver的实现。

@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表达式

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnno {

    String typeExpression() default "";
}

业务方法:

@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使用StandardEvaluationContextregisterFunction方法进行注册自定义函数。
其实完全可以使用setVariable代替,两者其实本质是一样的;
推荐使用“registerFunction”方法注册自定义函数。

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);
}

注册变量

变量定义通过EvaluationContext接口的setVariable(variableName, value)方法定义;在表达式中使用#variableName引用;
除了引用自定义变量,SpEL还允许引用根对象当前上下文对象,使用#root引用根对象。

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表达式赋值

使用Expression#setValue给表达式赋值。

    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表达式,可能存在链式调用,会遇到空指针等系列问题,如何实现安全存储呢?

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;
}

异常信息:

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方法修改。

使用如下的表达式:

"#root.name?.toString()"

文章参考

Spring5.1.5—spEL官网

玩转Spring中强大的spel表达式!

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

推荐阅读更多精彩内容