0. 前言
之前写一个注解的时候,想让这个注解传入的参数值变成动态配置的,类似Nacos动态配置bean的信息一样。但是Java中的注解参数值只能传一个常量值,并不能传一个bean的属性进去,类似下面这样这么写明显是不符合Java语法的。
既然注解的参数值必须要传一个常量,那可以传一个Spring的SpEL表达式,在切面类中解析这个表达式,动态的获取值,就可以起到动态配置的效果。
1. 动态配置原理
首先看下nacos动态配置的原理,nacos动态配置可以分为以下几步:
SpringBoot启动时,当spring.cloud.nacos.config.enabled
配置项为true时,则加载Nacos中ClientWorker类定时向服务端发起长轮询来获取Nacos的配置信息。
Nacos服务端收到客户端发起的长轮询时,会将这个请求放到一个定时队列,当满足以下任意一个条件时,服务端会向客户端发送返回
客户端通过长轮询获得更新后的配置信息后,会和本地缓存的配置信息进行比较。当发现两者不一致时,客户端会发送刷新配置的事件给Spring容器。
Spring容器收到刷新配置的事件后,会将被RefreshScope注解标注的Bean重新生成,根据最新的配置信息生成新的Bean。
从上面动态配置的过程中可以发现,Nacos动态配置的原理就是收到配置信息后,刷新对应的bean来实现动态配置。从这可以发现,如果我们将一个SpEL的bean属性表达式当成注解参数值传入,程序每次进入对应的切面类时都从这个SpEL表达式动态获取bean的某个属性,就可以实现动态配置了。
2. SpEL表达式
Spring表达式语言(简称“ SpEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象,并且支持调用方法。这里主要讲一下如何解析表达bean属性的SpEL表达式。
SpEl表达bean属性的SpEL表达式如下:
#{@customProperty.getRolesConfig().getAppRoles()}
SpEL默认的表达式模板为#{}。如果表达式需要引用一个bean的属性,则Bean名称前面跟上一个@符号,后面则是bean的方法名或者属性名。
计算上述表达bean属性的SpEL表达式的过程如下:
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private static final TemplateParserContext PARSER_TEMPLATE = new TemplateParserContext();
Expression exp = PARSER.parseExpression(spEL, PARSER_TEMPLATE);
private final StandardEvaluationContext CONTEXT = new StandardEvaluationContext();
CONTEXT.setBeanResolver(new BeanFactoryResolver(applicationContext));
其中applicationContext对象需要切面类实现ApplicationContextAware接口来获得。
Object value = exp.getValue(CONTEXT)
SpEL内部原理可以表示如下:
3. 总结
最后我们总结一下,实现注解参数值动态配置的步骤如下
spring.cloud.nacos.config.enabled
配置项为true。@Component
@ConfigurationProperties(prefix = "xxx")
@Data
@RefreshScope
@HasRoleV2(dynamicRoles = "#{@customProperty.getRolesConfig().getAppRoles()}")
@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表达式。