美文网首页
spEL—基础语法+注解中动态调用Bean方法

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

作者: 小胖学编程 | 来源:发表于2020-11-13 13:45 被阅读0次

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

    相关文章

      网友评论

          本文标题:spEL—基础语法+注解中动态调用Bean方法

          本文链接:https://www.haomeiwen.com/subject/harvbktx.html