PostConstruct注解的方法多次被调用

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

PostConstruct注解的方法多次被调用

现象

某某公司希望在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);
    }
    
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值