一、背景
最近在做一个智能体项目,javaer 直接采用了 springboot4.x + springai2.x,没有使用 springcloud 组件,感觉太重了。
现在遇到了运行时切换模型的需求,总不能修改配置文件然后重启吧,也太啰嗦了。
于是想要仅接入 nacos 作为配置中心,并且不想要 springcoud 那一套,感觉对于这个项目有点重量级。
研究了大半天,这里记录下。
二、引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-alibaba-nacos-config</artifactId>
<version>2025.1.0.0</version>
</dependency>
比较干净,没有 springcloud 那一套。
三、添加配置
spring:
config:
import: "nacos:my-app.yaml"
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: 6fef744c-b525-449e-9abf-d9d5b3af4da6
group: my-group
file-extension: yaml
springboot4.x 的配置方式有点不同,参考上面的配置就可以了。
四、自定义 RefreshScope 注解
引用 nacos 的目的是自动刷新配置,但是 @RefreshScope 注解在 springcloud 里面,个人觉得真是不合理,难道非微服务就没有刷新配置的场景吗?
没办法,想要轻量级那就自己造。
1、首先自定义注解
@Documented
@Target(ElementType.TYPE)
@Scope(DefaultRefreshScope.NAME)
@Retention(RetentionPolicy.RUNTIME)
public @interface RefreshScope {
@AliasFor(annotation = Scope.class)
String value() default DefaultRefreshScope.NAME;
@AliasFor(annotation = Scope.class)
String scopeName() default DefaultRefreshScope.NAME;
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
2、实现 Scope 接口
@Slf4j
@Component
public class DefaultRefreshScope implements Scope, BeanFactoryPostProcessor, ApplicationListener<EnvironmentRefreshedEvent> {
public static final String NAME = "refreshed";
private ConfigurableListableBeanFactory beanFactory;
private final ConcurrentMap<String, DecorateBean> cache;
public DefaultRefreshScope() {
this.cache = new ConcurrentHashMap<>();
}
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
return this.cache.computeIfAbsent(name, k -> new DecorateBean(objectFactory)).getProxy();
}
@Override
public Object remove(String name) {
try {
return Mapping.from(Mapping.from(this.cache.get(name)).then(DecorateBean::clear).get()).map(e -> e.target).get();
} catch (Throwable e) {
log.error("clear refresh bean error.", e);
return Mapping.from(this.cache.remove(name)).map(b -> b.target).get();
}
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope(NAME, this);
this.beanFactory = beanFactory;
}
@Override
public void onApplicationEvent(EnvironmentRefreshedEvent event) {
new LinkedList<>(this.cache.keySet()).forEach(e -> beanFactory.destroyScopedBean(e));
}
@RequiredArgsConstructor
private static class DecorateBean {
private volatile Object target;
private final ObjectFactory<?> factory;
public Object getProxy() {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTargetClass(factory.getObject().getClass());
proxyFactory.addAdvice(new MethodInterceptor() {
@Override
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
if (AopUtils.isEqualsMethod(method) || AopUtils.isToStringMethod(method) || AopUtils.isHashCodeMethod(method)) {
return invocation.proceed();
}
return ReflectionUtils.invokeMethod(method, getObject(), invocation.getArguments());
}
});
return proxyFactory.getProxy();
}
public Object getObject() {
if (target == null) {
synchronized (this) {
if (target == null) {
target = factory.getObject();
}
}
}
return target;
}
public void clear() {
this.target = null;
}
}
}
代码逻辑很简单,使用 Map 缓存 bean 实例,而 bean 实例创建的时候需要返回一个代理对象,这样我们才能拦截 bean 的方法调用。
然后监听 EnvironmentRefreshedEvent 事件,事件发生时,将缓存的 bean 实例销毁即可。
3、配置 nacos 监听器
当 nacos 配置变更时,会发布 NacosConfigRefreshEvent 事件,为了通用,我们监听这个事件,并将这个事件转换为 EnvironmentRefreshedEvent 发布。
除此之外,还有一个问题,那就是配置变更后,我们还需要将配置更新到 spring 的 ConfigurableEnvironment 里,否则重建 bean 的时候,拿到的配置还是旧的,还是无法实现配置刷新。
这里刷新 spring 的 ConfigurableEnvironment 的时候有两个坑点,这里特别记录下:
1、@ConfigurationProperties 注解,是由 ConfigurationPropertiesBindingPostProcessor 处理的,所以刷新环境时,需要调用 ConfigurationPropertiesBindingPostProcessor#afterPropertiesSet(),从而更新内部的 binder 字段,否则的话,配置还是无法自动刷新。
2、@Value 注解,是由 AutowiredAnnotationBeanPostProcessor 处理的,而实际上,是调用了 ConfigurableListableBeanFactory 内部的 embeddedValueResolvers 解析的,所以还需要更新下这个解析器,否则配置还是无法自动刷新。
这段逻辑代码如下:
@Slf4j
public class EnvironmentRefreshListener {
private static final String BEAN_NAME = "org.springframework.boot.context.internalConfigurationPropertiesBinder";
@Autowired
protected ConfigurableListableBeanFactory beanFactory;
@Autowired
protected ConfigurableApplicationContext applicationContext;
protected void refreshEnvironment(List<PropertySource<?>> refreshedPropertySources) throws Exception {
StandardEnvironment environment = new StandardEnvironment() {
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
for (PropertySource<?> refreshedPropertySource : refreshedPropertySources) {
propertySources.addLast(refreshedPropertySource);
}
}
};
// 合并环境
environment.merge(this.applicationContext.getEnvironment());
// 设置新环境
this.applicationContext.setEnvironment(environment);
// 删除旧的,下面的 configurer 会添加新的解析器
@SuppressWarnings("unchecked")
List<StringValueResolver> resolvers = (List<StringValueResolver>) ReflectUtil.getFieldValue(this.beanFactory, "embeddedValueResolvers");
resolvers.clear();
// 更新环境及配置
PropertySourcesPlaceholderConfigurer configurer = this.beanFactory.getBean(PropertySourcesPlaceholderConfigurer.class);
ReflectUtil.setFieldValue(configurer, "propertySources", null);
configurer.setEnvironment(environment);
configurer.postProcessBeanFactory(this.beanFactory);
// 销毁单例并触发初始化
ReflectUtil.invoke(this.beanFactory, "removeSingleton", BEAN_NAME);
this.beanFactory.getBean(ConfigurationPropertiesBindingPostProcessor.class).afterPropertiesSet();
}
}
好了,那么 nacos 监听器的代码就比较简单了:
@Slf4j
@Component
public class NacosConfigRefreshListener extends EnvironmentRefreshListener implements ApplicationListener<NacosConfigRefreshEvent> {
@Override
public void onApplicationEvent(NacosConfigRefreshEvent event) {
try {
String config = NacosConfigManager.getInstance().getConfigService().getConfig(event.getDataId(), event.getGroup(), 5000);
List<PropertySource<?>> refreshedPropertySources = new YamlPropertySourceLoader().load(
event.getGroup() + '@' + event.getDataId(),
new ByteArrayResource(config.getBytes(StandardCharsets.UTF_8))
);
this.refreshEnvironment(refreshedPropertySources);
this.applicationContext.publishEvent(new EnvironmentRefreshedEvent(applicationContext));
} catch (Exception e) {
log.error("nacos config refresh error", e);
}
}
}
到这里就结束了,给希望以轻量级方式接入 nacos 的开发者一个参考。