SpEL结合Nacos实现注解参数值动态配置

0. 前言

之前写一个注解的时候,想让这个注解传入的参数值变成动态配置的,类似Nacos动态配置bean的信息一样。但是Java中的注解参数值只能传一个常量值,并不能传一个bean的属性进去,类似下面这样这么写明显是不符合Java语法的。

既然注解的参数值必须要传一个常量,那可以传一个Spring的SpEL表达式,在切面类中解析这个表达式,动态的获取值,就可以起到动态配置的效果。

1. 动态配置原理

首先看下nacos动态配置的原理,nacos动态配置可以分为以下几步:

  1. SpringBoot启动时,当spring.cloud.nacos.config.enabled 配置项为true时,则加载Nacos中ClientWorker类定时向服务端发起长轮询来获取Nacos的配置信息。

  2. Nacos服务端收到客户端发起的长轮询时,会将这个请求放到一个定时队列,当满足以下任意一个条件时,服务端会向客户端发送返回

    1. 长轮询时间超过设置的超时时间。Nacos长轮询默认的超时时间为30s,但是服务端会在29.5s的时候就将长轮询结束返回给客户端。这里服务端提前500ms返回的原因是防止客户端超时。
    2. 查询的配置项发生过修改。 当客户端查询的配置项被修改时,服务端会马上将修改后的配置项返回给客户端。
  3. 客户端通过长轮询获得更新后的配置信息后,会和本地缓存的配置信息进行比较。当发现两者不一致时,客户端会发送刷新配置的事件给Spring容器。

  4. Spring容器收到刷新配置的事件后,会将被RefreshScope注解标注的Bean重新生成,根据最新的配置信息生成新的Bean。

从上面动态配置的过程中可以发现,Nacos动态配置的原理就是收到配置信息后,刷新对应的bean来实现动态配置。从这可以发现,如果我们将一个SpEL的bean属性表达式当成注解参数值传入,程序每次进入对应的切面类时都从这个SpEL表达式动态获取bean的某个属性,就可以实现动态配置了。

2. SpEL表达式

Spring表达式语言(简称" SpEL")是一种功能强大的表达式语言,支持在运行时查询和操作对象,并且支持调用方法。这里主要讲一下如何解析表达bean属性的SpEL表达式。

SpEl表达bean属性的SpEL表达式如下:

less 复制代码
#{@customProperty.getRolesConfig().getAppRoles()}

SpEL默认的表达式模板为#{}。如果表达式需要引用一个bean的属性,则Bean名称前面跟上一个@符号,后面则是bean的方法名或者属性名。

计算上述表达bean属性的SpEL表达式的过程如下:

  1. 首先通过new SpelExpressionParser()获得一个SpEL解析器PARSER,用以解析传入的SpEL表达式。然后再通过new TemplateParserContext()获得一个SpEL解析模板PARSER_TEMPLATE,用以表示传入SpEL表达式的格式。然后再通过PARSER.parseExpression(spEL, PARSER_TEMPLATE)获得一个Expression表达式对象。
java 复制代码
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private static final TemplateParserContext PARSER_TEMPLATE = new TemplateParserContext();
Expression exp = PARSER.parseExpression(spEL, PARSER_TEMPLATE);
  1. 获得一个Expression表达式对象后,就可以根据Spring的bean解析器来获得对应的bean属性了。首先通过new StandardEvaluationContext()获得SpEL解析的上下文对象CONTEXT,SpEL表达式的计算会在这个上下文中进行,并且这个CONTEXT对象可以设置一个BeanResolver用来专门解析bean属性,如下
java 复制代码
private final StandardEvaluationContext CONTEXT = new StandardEvaluationContext();
CONTEXT.setBeanResolver(new BeanFactoryResolver(applicationContext));

其中applicationContext对象需要切面类实现ApplicationContextAware接口来获得。

  1. 最后,直接通过Expression表达式对象的getValue方法就能获得对应的bean属性值,如下
ini 复制代码
Object value = exp.getValue(CONTEXT)

SpEL内部原理可以表示如下:

3. 总结

最后我们总结一下,实现注解参数值动态配置的步骤如下

  1. 引入Nacos配置相关依赖,设置spring.cloud.nacos.config.enabled配置项为true。
  2. 创建一个可以动态配置的类,在该类上加一个@RefreshScope注解和@ConfigurationProperties(prefix = "xxx"),并且再加上@Component注解将该类装配到Spring容器中。
less 复制代码
@Component
@ConfigurationProperties(prefix = "xxx")
@Data
@RefreshScope
  1. 创建一个注解,其参数值是String类型的,能够接受SpEL表达式。
java 复制代码
@HasRoleV2(dynamicRoles = "#{@customProperty.getRolesConfig().getAppRoles()}")
  1. 在注解的切面类上,解析这个SpEL表达式,动态获取bean的属性值。
java 复制代码
@Aspect
@Component
@Slf4j
public class XXXAspect implements ApplicationContextAware {

    @Before("@within(role)")
    public void roleCheckClass(HasRoleV2 role) {
        roleCheck(role);
    }

    @Before("@annotation(role)")
    public void roleCheckMethod(HasRoleV2 role) {
        roleCheck(role);
    }

    /**
     * 上下文对象实例
     */
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        this.CONTEXT.setBeanResolver(new BeanFactoryResolver(applicationContext));
    }

    //定义解析的模板
    private static final TemplateParserContext PARSER_CONTEXT = new TemplateParserContext();
    //定义解析器
    private static final SpelExpressionParser PARSER = new SpelExpressionParser();
    //获得上下文
    private final StandardEvaluationContext CONTEXT = new StandardEvaluationContext();

    public void roleCheck(HasRoleV2 role) {
        String[] requiredRoles;
        if (!StringUtils.isEmpty(role.dynamicRoles())) {
            // 解析SpEL表达式
            Expression exp = PARSER.parseExpression(role.dynamicRoles(), PARSER_CONTEXT);
            requiredRoles = (String[]) exp.getValue(CONTEXT);
        } else {
            requiredRoles = role.roles();
        }
        //
    }


}

Spring生态中很多常见的@Cacheable、@Value注解,以及Spring Security框架中的@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter等注解中都可以传入一个SpEL表达式。

相关推荐
深圳蔓延科技11 分钟前
Kafka的高性能之路
后端·kafka
Barcke12 分钟前
深入浅出 Spring WebFlux:从核心原理到深度实战
后端
JuiceFS12 分钟前
从 MLPerf Storage v2.0 看 AI 训练中的存储性能与扩展能力
运维·后端
大鸡腿同学14 分钟前
Think with a farmer's mindset
后端
Moonbit35 分钟前
用MoonBit开发一个C编译器
后端·编程语言·编译器
Reboot1 小时前
达梦数据库GROUP BY报错解决方法
后端
稻草人22221 小时前
java Excel 导出 ,如何实现八倍效率优化,以及代码分层,方法封装
后端·架构
渣哥1 小时前
原来 Java 里线程安全集合有这么多种
java
间彧2 小时前
Spring Boot集成Spring Security完整指南
java
掘金者阿豪2 小时前
打通KingbaseES与MyBatis:一篇详尽的Java数据持久化实践指南
前端·后端