nacos配置实时刷新@RefreshScope注解和定时任务@Scheduled注解同时使用导致失效问题

@RefreshScope@Scheduled的组合使用有时会导致@Scheduled任务失效,主要是由于它们在Spring中的工作机制不同。

@RefreshScope的工作原理

@RefreshScope是Spring Cloud中的一个注解,它允许在应用运行时刷新bean的属性,而不需要重启应用程序。具体来说,当配置变化时,@RefreshScope会重新创建bean实例,以便使新的配置生效。它依赖于Spring Cloud Context的刷新机制。

工作流程:

  1. Spring容器启动时,@RefreshScope会为标注的bean创建一个代理对象。
  2. 当配置变化触发刷新事件时,Spring Cloud Context会销毁旧的bean实例,并创建一个新的实例。
  3. 代理对象会在下次调用该bean时,委托给新的bean实例。

@Scheduled的工作原理

@Scheduled是Spring中的调度注解,用于声明方法在指定的时间间隔或指定的时间点运行。它由Spring的Task Scheduler管理。

工作流程:

  1. Spring容器启动时,会扫描所有标注了@Scheduled的方法,并将这些方法注册到Task Scheduler中。
  2. Task Scheduler根据方法上的调度参数,定期调用这些方法。

两者结合导致问题的原因

当一个@Scheduled方法所在的bean被标注为@RefreshScope时,以下问题可能会导致调度任务失效:

  1. Bean实例重建@RefreshScope会在配置变化时重新创建bean实例。这意味着原有的@Scheduled任务绑定到了旧的bean实例上。新创建的bean实例没有重新注册到Task Scheduler,导致调度任务失效。
  2. 代理对象@RefreshScope创建的代理对象在重新创建bean实例后,会指向新的实例。然而,Task Scheduler并不会感知到这种变化,它依然尝试调用旧实例中的方法,导致任务失效。

详细源码分析

@RefreshScope源码分析

 下面看@ReFreshScope注解源码

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

@RefreshScope的核心注解是@Scope("refresh"),它通过ContextRefresher来实现bean的刷新。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {

	/**
	 * Alias for {@link #scopeName}.
	 * @see #scopeName
	 */
	@AliasFor("scopeName")
	String value() default "";

	/**
	 * Specifies the name of the scope to use for the annotated component/bean.
	 * <p>Defaults to an empty string ({@code ""}) which implies
	 * {@link ConfigurableBeanFactory#SCOPE_SINGLETON SCOPE_SINGLETON}.
	 * @since 4.2
	 * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE
	 * @see ConfigurableBeanFactory#SCOPE_SINGLETON
	 * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
	 * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
	 * @see #value
	 */
	@AliasFor("value")
	String scopeName() default "";

	/**
	 * Specifies whether a component should be configured as a scoped proxy
	 * and if so, whether the proxy should be interface-based or subclass-based.
	 * <p>Defaults to {@link ScopedProxyMode#DEFAULT}, which typically indicates
	 * that no scoped proxy should be created unless a different default
	 * has been configured at the component-scan instruction level.
	 * <p>Analogous to {@code <aop:scoped-proxy/>} support in Spring XML.
	 * @see ScopedProxyMode
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

}

通过代码我们可以看到proxyMode 这个属性,其实就是@RefreshScope 实现的本质了。

我们需要关心的就是ScopedProxyMode.TARGET_CLASS 这个属性,此属性的功能就是在创建一个代理,在每次调用的时候都用它来调用GenericScope get 方法来获取对象。GenericScope是SpringCloud对Scope的一个实现,Scope的源码如下,我们主要关注get方法

看一下get方法的具体实现:

	@Override
	public Object get(String name, ObjectFactory<?> objectFactory) {
		BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
		this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
		try {
			return value.getBean();
		}
		catch (RuntimeException e) {
			this.errors.put(name, e);
			throw e;
		}
	}

GenericScope 实现了 Scope 最重要的 get(String name, ObjectFactory<?> objectFactory) 方法,在GenericScope 里面 包装了一个内部类 BeanLifecycleWrapperCache 来对加了 @RefreshScope 从而创建的对象进行缓存,使其在不刷新时获取的都是同一个对象。(这里你可以把 BeanLifecycleWrapperCache 想象成为一个大Map 缓存了所有@RefreshScope 标注的对象)

知道了对象是缓存的,所以在进行动态刷新的时候,只需要清除缓存,重新创建就好了。 

@Scheduled注解分析

@Scheduled的核心类是ScheduledAnnotationBeanPostProcessor,它在容器启动时扫描并注册调度任务。

解决方案

为了解决@RefreshScope导致的@Scheduled失效问题,可以采取以下几种策略:

  1. 手动重新注册任务:监听配置刷新事件,手动重新注册@Scheduled任务。
@Component
public class ScheduledTaskRegistrar implements ApplicationListener<EnvironmentChangeEvent> {
    //注释内容可以不使用,直接实现方法,内部为空即可
    //@Autowired
    //private ScheduledTaskRegistrar taskRegistrar;

    @Override
    public void onApplicationEvent(EnvironmentChangeEvent event) {
        //taskRegistrar.destroy();
        //taskRegistrar.afterPropertiesSet();
    }
}

2. 避免组合使用:尽量避免在同一个bean上同时使用@RefreshScope@Scheduled。因为配置刷新后会卸载类,并重新实例化类(如果类中存在计数等情况需要注意)

3. 使用自定义调度机制:创建一个自定义调度器,在配置刷新时重新初始化调度任务。

@Configuration
public class CustomSchedulerConfig {

    @Bean
    @RefreshScope
    public CustomScheduler customScheduler() {
        return new CustomScheduler();
    }
}

public class CustomScheduler {
    
    private ScheduledFuture<?> future;

    @Autowired
    private TaskScheduler taskScheduler;

    @PostConstruct
    public void start() {
        future = taskScheduler.scheduleAtFixedRate(this::task, 5000);
    }

    @PreDestroy
    public void stop() {
        if (future != null) {
            future.cancel(true);
        }
    }

    public void task() {
        // Task logic here
    }
}

通过上述分析和解决方案,可以更清楚地理解@RefreshScope@Scheduled的工作机制,并有效避免调度任务失效的问题。

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值