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

相关推荐
计算机安禾12 分钟前
【c++面向对象编程】第24篇:类型转换运算符:自定义隐式转换与explicit
java·c++·算法
zyk_computer1 小时前
AI 时代,或许 Rust 比 Python 更合适
人工智能·后端·python·ai·rust·ai编程·vibe coding
weixin199701080161 小时前
【保姆级教程】淘宝/天猫商品详情 API(item_get)接入指南:Python/Java/PHP 调用示例与 JSON 返回值解析
java·python·php
环流_1 小时前
redis核心数据类型在java中的操作
java·数据库·redis
雨辰AI1 小时前
SpringBoot3 项目国产化改造完整流程|从 MySQL 到人大金仓落地
java·数据库·后端·mysql·政务
带刺的坐椅1 小时前
Java 流程编排新范式 Solon Flow:一个引擎,七种节点,覆盖规则/任务/工作流/AI 编排全场景
java·spring·ai·solon·flow
知彼解己2 小时前
Arthas:Java生产环境问题排查利器,从入门到实战
java
GreenTea2 小时前
【Rust 2026教程:从零构建 Mini-OLAP 引擎】第 6 章 Benchmark 与优化路线图
后端
Rust语言中文社区2 小时前
【Rust日报】2026-05-14 Pyrefly v1.0 正式发布:快速的 Python 类型检查器和语言服务器
开发语言·后端·python·rust
吴声子夜歌3 小时前
Java——定时任务
java