如何在 Spring 中有条件地启用或禁用计划作业?

我在 Spring 中使用 @Scheduled注释使用 cron 样式模式定义计划作业。

Cron 模式存储在配置属性文件中。实际上有两个属性文件: 一个默认配置和一个与环境相关的配置文件(例如 dev、 test、 prod customer 1、 prod customer 2等) ,并覆盖一些默认值。

我在 Spring 上下文中配置了一个属性占位符 bean,它允许我使用 ${}样式的占位符从属性文件中导入值。

工作豆看起来像这样:

@Component
public class ImagesPurgeJob implements Job {


private Logger logger = Logger.getLogger(this.getClass());


@Override
@Transactional(readOnly=true)
@Scheduled(cron = "${jobs.mediafiles.imagesPurgeJob.schedule}")
public void execute() {
//Do something
//can use DAO or other autowired beans here
}
}

我的上下文的相关部分 XML:

<!-- Enable configuration of scheduled tasks via annotations -->
<task:annotation-driven/>


<!-- Load configuration files and allow '${}' style placeholders -->
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:config/default-config.properties</value>
<value>classpath:config/environment-config.properties</value>
</list>
</property>
<property name="ignoreUnresolvablePlaceholders" value="true"/>
<property name="ignoreResourceNotFound" value="false"/>
</bean>

我真的很喜欢这个,它非常简单和干净,只有最少的 XML。

然而,我还有一个要求: 在某些情况下,这些作业中的一些可以完全禁用。

因此,在我使用 Spring 来管理它们之前,我手动创建了它们,并且在配置文件中有一个布尔参数和 cron 参数,用于指定作业是否必须启用:

jobs.mediafiles.imagesPurgeJob.enable=true or false
jobs.mediafiles.imagesPurgeJob.schedule=0 0 0/12 * * ?

根据这个配置参数,我如何在 Spring 中使用这个参数来有条件地创建或者直接忽略 bean?

一个明显的变通方法是定义一个永远不会计算的 cron 模式,这样作业就永远不会执行。但是 bean 仍然会被创建,而且配置会有点模糊,所以我觉得必须有一个更好的解决方案。

141777 次浏览
@Component
public class ImagesPurgeJob implements Job {


private Logger logger = Logger.getLogger(this.getClass());


@Value("${jobs.mediafiles.imagesPurgeJob.enable}")
private boolean imagesPurgeJobEnable;


@Override
@Transactional(readOnly=true)
@Scheduled(cron = "${jobs.mediafiles.imagesPurgeJob.schedule}")
public void execute() {


//Do something
//can use DAO or other autowired beans here
if(imagesPurgeJobEnable){


Do your conditional job here...


}
}
}

Your question states to condition the actual creation of the bean. You can do this easily with this parameter by using @Profile if you are using at least Spring 3.1.

See the documentation here: http://static.springsource.org/spring/docs/3.1.x/javadoc-api/org/springframework/context/annotation/Profile.html

Spring Boot provides @ConditionalOnProperty, which would be perfect if you were using Spring Boot. This annotation is a specialization of @Conditional, introduced with Spring 4.0.0.

Assuming you're just using "regular" spring and not Spring Boot, you could create your own Condition implementation for use with @Conditional that would mimic Spring Boot's @ConditionalOnProperty.

You can group schedule methods by conditions into number of services and init them like this:

@Service
@ConditionalOnProperty("yourConditionPropery")
public class SchedulingService {


@Scheduled
public void task1() {...}


@Scheduled
public void task2() {...}


}
@Component
public class CurrencySyncServiceImpl implements CurrencySyncService {


private static Boolean isEnableSync;
/**
* Currency Sync FixedDelay in minutes
*/
private static Integer fixedDelay;


@Transactional
@Override
@Scheduled(fixedDelayString = "#{${currency.sync.fixedDelay}*60*1000}")
public void sync() {
if(CurrencySyncServiceImpl.isEnableSync) {
//Do something
//you can use DAO or other autowired beans here.
}
}


@Value("${currency.sync.fixedDelay}")
public void setFixedDelay(Integer fixedDelay) {
CurrencySyncServiceImpl.fixedDelay = fixedDelay;
}


@Value("${currency.sync.isEnable}")
public void setIsEnableSync(Boolean isEnableSync) {
CurrencySyncServiceImpl.isEnableSync = isEnableSync;
}
}

I know my answer is a hack, but giving a valid cron expression that never executes may fix the issue (in the environment specific configuration), Quartz: Cron expression that will never execute

If you are looking to toggle @EnableScheduling from a property you can do this in Spring Boot by moving the @EnableScheduling annotation to a configuration class and use @ConditionalOnProperty as follows:

@Configuration
@EnableScheduling
@ConditionalOnProperty(prefix = "com.example.scheduling", name="enabled", havingValue="true", matchIfMissing = true)
public class SchedulingConfiguration {


}

This will disable scheduling for the application. This may be useful in a situation where you want to be able to run the application once or scheduled depending on how it's being started.

From wilkinsona's comment on here: https://github.com/spring-projects/spring-boot/issues/12682

You can also create a Bean based on condition and that Bean can have a Scheduled method.

@Component
@Configuration
@EnableScheduling
public class CustomCronComponent {
@Bean
@ConditionalOnProperty(value = "my.cron.enabled", matchIfMissing = true, havingValue = "true")
public MyCronTask runMyCronTask() {
return new MyCronTask();
}
}

and

@Component
public class MyCronTask {
@Scheduled(cron = "${my.cron.expression}")
public void run() {
String a = "";
}
}

The most efficient way to disable @Scheduled in Spring is to set cron expression to -

@Scheduled(cron = "-")
public void autoEvictAllCache() {
LOGGER.info("Refresing the Cache Start :: " + new Date());
activeMQUtility.sendToTopicCacheEviction("ALL");
LOGGER.info("Refresing the Cache Complete :: " + new Date());
}

From the docs:

CRON_DISABLED

public static final String CRON_DISABLED
A special cron expression value that indicates a disabled trigger: "-". This is primarily meant for use with ${...} placeholders, allowing for external disabling of corresponding scheduled methods.

Since: 5.1 See Also: ScheduledTaskRegistrar.CRON_DISABLED

Please see my answer in another question. I think this is the best way to solve it. How to stop a scheduled task that was started using @Scheduled annotation?

Define a custom annotation like below.

@Documented
@Retention (RUNTIME)
@Target(ElementType.TYPE)
public @interface ScheduledSwitch {
// do nothing
}

Define a class implements org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.

public class ScheduledAnnotationBeanPostProcessorCustom
extends ScheduledAnnotationBeanPostProcessor {


@Value(value = "${prevent.scheduled.tasks:false}")
private boolean preventScheduledTasks;


private Map<Object, String> beans = new HashMap<>();


private final ReentrantLock lock = new ReentrantLock(true);


@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
ScheduledSwitch switch = AopProxyUtils.ultimateTargetClass(bean)
.getAnnotation(ScheduledSwitch.class);
if (null != switch) {
beans.put(bean, beanName);
if (preventScheduledTasks) {
return bean;
}
}
return super.postProcessAfterInitialization(bean, beanName);
}


public void stop() {
lock.lock();
try {
for (Map.Entry<Object, String> entry : beans.entrySet()) {
postProcessBeforeDestruction(entry.getKey(), entry.getValue());
}
} finally {
lock.unlock();
}
}


public void start() {
lock.lock();
try {
for (Map.Entry<Object, String> entry : beans.entrySet()) {
if (!requiresDestruction(entry.getKey())) {
super.postProcessAfterInitialization(
entry.getKey(), entry.getValue());
}
}
} finally {
lock.unlock();
}
}


}

Replace ScheduledAnnotationBeanPostProcessor bean by the custom bean in configuration.

@Configuration
public class ScheduledConfig {


@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationBeanPostProcessor() {
return new ScheduledAnnotationBeanPostProcessorCustom();
}


}

Add @ScheduledSwitch annotation to the beans that you want to prevent or stop @Scheduled tasks.

We can disable the bean creation of the class having that scheduled methods using @Conditional annotation. This is very similar to @ConditionalOnProperty. This is used to conditionally spin up a bean on to the spring context. If we set the value to false, then the bean will not be spun up and loaded to spring. Below is the code.

application.properties:


com.boot.enable.scheduling=enable

Condition:

public class ConditionalBeans implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return "enabled".equalsIgnoreCase(context.getEnvironment().getProperty("com.boot.enable.scheduling"));
}
}

My schedule class

@Service
@Conditional(ConditionalSchedules.class)
public class PrintPeriodicallyService {


@Scheduled(fixedRate = 3000)
public void runEvery3Seconds() {
System.out.println("Current time : " + new Date().getTime());
}
}

This approach has a lot of flexibility where the condition generation is totally under our control.