现象
某某公司希望在nacos中可以动态配置黑白名单,例如域名白名单,对在白名单的允许从本站跳转,不在白名单的不允许从本站跳转。
WhiteListConfig类@PostConstruct标记的init方法就是希望只在项目启动时执行,后续不执行。以后改动配置被触发的是onEventChange方法,从而近实时的加载白名单。
But,事与愿违。
在每次修改nacos配置后,onEventChange方法被调用,init方法同样会被调用。
代码示例如下:
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = WhiteListConfig.PREFIX)
public class WhiteListConfig {
public static final String PREFIX = "abc.link";
private List<String> whiteList;
@Autowired
private NacosConfigProperties nacosConfigProperties;
@PostConstruct
private void init() {
log.info("---->>>init white list, {}", this);
refreshWhiteList();
}
@EventListener(value = {EnvironmentChangeEvent.class})
public void onEventChange(EnvironmentChangeEvent event) {
Set<String> keys = CollectionUtil.emptyIfNull(event.getKeys());
boolean inKeySet = keys.stream().anyMatch(ele -> StrUtil.startWith(ele, PREFIX + ".white-list"));
if (!inKeySet) {
return;
}
refreshWhiteList();
}
private void refreshWhiteList() {
Map<String, String> properties = EnvironmentUtils.getSubProperties(nacosConfigProperties.getEnvironment(), PREFIX + ".white-list");
List<String> newWhiteList = MapUtil.emptyIfNull(properties).entrySet().stream()
.filter(ele -> StrUtil.isNotBlank(ele.getValue()))
.sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.collect(Collectors.toList());
ResponseUtil.refreshWhiteList(newWhiteList);
}
}
分析
1.第一次发现这个问题时怀疑WhiteListConfig 被多此扫描,核查配置后并没有发现有多次扫描包的问题,并且每次init方法被调用时日志中输出的实例是同一个,没有多次实例化。@PostConstruct是javax包里的,jsr-250规范,约定在对象实例化、属性设置后执行。
2.跟踪spring EnvironmentChangeEvent被消费的地方(只发关键代码):org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent
@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
// 所有在类上加@ConfigurationProperties的beans
private ConfigurationPropertiesBeans beans;
@ManagedOperation
public void rebind() {
this.errors.clear();
for (String name : this.beans.getBeanNames()) {
rebind(name);
}
}
@ManagedOperation
public boolean rebind(String name) {
if (!this.beans.getBeanNames().contains(name)) {
return false;
}
if (this.applicationContext != null) {
try {
Object bean = this.applicationContext.getBean(name);
if (AopUtils.isAopProxy(bean)) {
bean = ProxyUtils.getTargetObject(bean);
}
if (bean != null) {
// TODO: determine a more general approach to fix this.
// see https://github.com/spring-cloud/spring-cloud-commons/issues/571
if (getNeverRefreshable().contains(bean.getClass().getName())) {
return false; // ignore
}
this.applicationContext.getAutowireCapableBeanFactory()
.destroyBean(bean);
/***************PostConstruct的调用是在initializeBean()流程中执行的******************/
this.applicationContext.getAutowireCapableBeanFactory()
.initializeBean(bean, name);
return true;
}
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
catch (Exception e) {
this.errors.put(name, e);
throw new IllegalStateException("Cannot rebind to " + name, e);
}
}
return false;
}
*****省略*****
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.applicationContext.equals(event.getSource())
// Backwards compatible
|| event.getKeys().equals(event.getSource())) {
rebind();
}
}
}
ConfigurationPropertiesBeans 会管理在类上加@ConfigurationProperties的bean,每次消费EnvironmentChangeEvent时,spring会从applicationContext中根据ConfigurationPropertiesBeans 提供的bean名获取bean实例(即WhiteListConfig实例)。
WhiteListConfig实例默认是singleton,因此没有多次实例化。在*this.applicationContext.getAutowireCapableBeanFactory()
.initializeBean(bean, name)*这个流程里会处理PostConstruct逻辑。
解决方案
只在这里抛砖引玉,有更好的方案可以在讨论区聊聊。
方案一:类优化
既然@ConfigurationProperties的bean标注在类上会被ConfigurationPropertiesBeans管理,那标注在方法上不就行了吗。
实例代码如下:
@Slf4j
@Component
public class WhiteListConfig {
public static final String PREFIX = "abc.link";
@Getter
private List<String> whiteList;
@ConfigurationProperties(prefix = WhiteListConfig.PREFIX)
public void setWhiteList(List<String> whiteList) {
this.whiteList = whiteList;
}
@Autowired
private NacosConfigProperties nacosConfigProperties;
@PostConstruct
private void init() {
log.info("---->>>init white list, {}", this);
refreshWhiteList();
}
@EventListener(value = {EnvironmentChangeEvent.class})
public void onEventChange(EnvironmentChangeEvent event) {
Set<String> keys = CollectionUtil.emptyIfNull(event.getKeys());
boolean inKeySet = keys.stream().anyMatch(ele -> StrUtil.startWith(ele, PREFIX + ".white-list"));
if (!inKeySet) {
return;
}
refreshWhiteList();
}
private void refreshWhiteList() {
Map<String, String> properties = EnvironmentUtils.getSubProperties(nacosConfigProperties.getEnvironment(), PREFIX + ".white-list");
List<String> newWhiteList = MapUtil.emptyIfNull(properties).entrySet().stream()
.filter(ele -> StrUtil.isNotBlank(ele.getValue()))
.sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.collect(Collectors.toList());
ResponseUtil.refreshWhiteList(newWhiteList);
}
}
方案二:类拆分
在WhiteListConfig 里加上init/onEventChange方法是不是有点多余,不如将这些逻辑放在别的类中处理。
而且@PostConstruct注解在java 9后会被弃用,init逻辑放在ApplicationRunner/CommandLineRunner中处理更适合一些。
WhiteListConfig :
@Data
@Component
@ConfigurationProperties(prefix = WhiteListConfig.PREFIX)
public class WhiteListConfig {
public static final String PREFIX = "abc.link";
private List<String> whiteList;
}
MyApplicationRunner:
@Slf4j
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Autowired
private NacosConfigProperties nacosConfigProperties;
@Override
public void run(ApplicationArguments args) throws Exception {
initWhiteList();
}
private void initWhiteList() {
Map<String, String> properties = EnvironmentUtils.getSubProperties(nacosConfigProperties.getEnvironment(), WhiteListConfig.PREFIX + ".white-list");
List<String> newWhiteList = MapUtil.emptyIfNull(properties).entrySet().stream()
.filter(ele -> StrUtil.isNotBlank(ele.getValue()))
.sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.collect(Collectors.toList());
ResponseUtil.refreshWhiteList(newWhiteList);
}
}
MyEventListener:
@Component
public class MyEventListener {
@Autowired
private NacosConfigProperties nacosConfigProperties;
@EventListener(value = {EnvironmentChangeEvent.class})
public void onEventChange(EnvironmentChangeEvent event) {
Set<String> keys = CollectionUtil.emptyIfNull(event.getKeys());
boolean inKeySet = keys.stream().anyMatch(ele -> StrUtil.startWith(ele, PREFIX + ".white-list"));
if (!inKeySet) {
return;
}
refreshWhiteList();
}
private void refreshWhiteList() {
Map<String, String> properties = EnvironmentUtils.getSubProperties(nacosConfigProperties.getEnvironment(), WhiteListConfig.PREFIX + ".white-list");
List<String> newWhiteList = MapUtil.emptyIfNull(properties).entrySet().stream()
.filter(ele -> StrUtil.isNotBlank(ele.getValue()))
.sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.collect(Collectors.toList());
ResponseUtil.refreshWhiteList(newWhiteList);
}
}

本文介绍了在Spring Boot应用中遇到@PostConstruct注解的方法在每次Nacos配置变更时被多次调用的现象。分析了问题原因,发现并非由于类的多次实例化,而是配置变更事件触发了初始化逻辑。提出两种解决方案:一是通过将@PostConstruct注解移到方法上避免影响;二是将初始化逻辑移到ApplicationRunner或CommandLineRunner中,同时将类进行拆分以提高代码组织性。

4051

被折叠的 条评论
为什么被折叠?



