113

I am defining scheduled jobs with cron style patterns in Spring, using the @Scheduled annotation.

The cron pattern is stored in a config properties file. Actually there are two properties files: one default config, and one profile config that is environment dependent (e.g. dev, test, prod customer 1, prod customer 2 etc.) and overrides some of the default values.

I configured a property placeholder bean in my spring context which allows me to use ${} style placeholders to import values from my properties files.

The job beans looks like this:

@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
    }
}

Relevant parts of my context 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>

I really like this. It's quite simple and clean with minimal XML.

However I have one more requirement: some of these jobs can be totally disabled in some cases.

So, before I used Spring to manage them I created them manually and there is a boolean parameter along with the cron parameter in the config files, to specify if the job has to be enabled or not:

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

How can I use this parameter in Spring to conditionally create or just plainly ignore the bean, depending on this config parameter?

One obvious workaround would be to define a cron pattern that would never evaluate, so the job is never executed. But the bean would still be created and the config would be a bit obscure, so I feel there must be a better solution.

Luuklag
  • 3,897
  • 11
  • 38
  • 57
Pierre Henry
  • 16,658
  • 22
  • 85
  • 105
  • Did you ever find an answer for this? – Kias Jan 21 '14 at 01:35
  • 1
    @Chaos : Nope, still using extra boolean property for this. – Pierre Henry Jan 31 '14 at 10:37
  • 1
    "a cron pattern that would never evaluate" this does not work, the spring scheduler code requires a valid "next run date" otherwise it throws an exception. (might not be true for all versions) – David Balažic Jan 13 '17 at 15:42
  • I know this was written a long time ago but it still shows up when searching for a solution. You may want to consider using a different scheduling mechanism. You can roll your own using Spring and Quartz (or other mechanisms I've recently seen while searching for this) or use a solution like https://github.com/kagkarlsson/db-scheduler. I haven't tried this yet by I will be soon since I need a solution for a new project. – Dean Feb 19 '21 at 18:18

11 Answers11

77

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

xlm
  • 6,854
  • 14
  • 53
  • 55
Vpn_talent
  • 1,290
  • 12
  • 21
50
@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...

        }
    }
}
naXa stands with Ukraine
  • 35,493
  • 19
  • 190
  • 259
Prabhakaran Ramaswamy
  • 25,706
  • 10
  • 57
  • 64
  • 23
    Thanks, this is an option, but I'd rather not instantiate the bean at all than create the bean and have a method execute periodically, even though it does nothing. – Pierre Henry Aug 26 '13 at 07:10
  • 2
    I ended up implementing it like this, because it was simple although not perfect. And I learnt about the @Value annotation which is nice. – Pierre Henry Aug 27 '13 at 09:55
  • 15
    instead of "do your job here" I'd rather check for if (!enabled) { return;} I think it's cleaner that way. – Jeff Wang Feb 27 '15 at 22:21
  • A newer answer: https://stackoverflow.com/a/57655646/9640017 – vineetvdubey Nov 26 '21 at 12:43
40

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() {...}

}
Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277
Oleg
  • 575
  • 4
  • 13
  • 2
    Can you put `@ConditionalOnProperty` on single method? Or specify different properties for different `@Scheduled` methods? – izogfif Aug 10 '18 at 13:16
  • @izogfif As far a I understand it, it can only be placed on methods, which create a `@Bean`. `@ConditionalOnProperty`inherits from `Conditional`. [Link](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Conditional.html?is-external=true) – kamwo Apr 23 '19 at 10:46
35

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.

Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277
tmullin
  • 452
  • 1
  • 5
  • 9
  • 2
    Ideally this should work but it does not for my case with 1.3.2-RELEASE of Spring Boot. – dARKpRINCE Feb 10 '16 at 14:07
  • Also wasn't able to do this with the @ConditionalOnProperty in SpringBoot 1.4.0 – ETL Aug 13 '16 at 23:20
  • 6
    According to the `@Conditional` docs, the annotation only applies at the method level for methods annotated with `@Bean`, ie. a bean definition. – jordanpg Feb 22 '17 at 21:39
29

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

user3474985
  • 983
  • 8
  • 20
4

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

aweigold
  • 6,532
  • 2
  • 32
  • 46
  • While profiles would be fine in a simple dev/prod environments set, I feel they are not really adapted to multiple production environments (as in multiple customers with different features switched on/off) and would not mix well with our existing configuration management based on properties files and maven build-time selection of active profile. Right now all environment-dependent parameters are centralized in a single properties file. The Spring config is the same for all. That would not be the case if we use spring profiles. – Pierre Henry Aug 26 '13 at 07:56
  • You can conditionally activate spring profiles in a custom initializer (eg. by extending ApplicationContextInitializer and adding it to your web.xml). Then in your initializer, you can get the environment and activate profiles based on property settings, eg. String flag = environment.getProperty("my.custom.property"); if ( "true".equals(flag) ) { environment.activateProfile("MY_CUSTOM_PROFILE") }. We use this a lot for different features in different customer environments, and also to enable/disable certain features for tests (eg. enable zero-dep service). – jugglingcats Nov 26 '14 at 09:59
3

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 = "";
    }
}
labm0nkey
  • 626
  • 1
  • 5
  • 9
2
@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;
    }
}
Vinit Solanki
  • 1,863
  • 2
  • 15
  • 29
2

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.

Bin
  • 71
  • 3
0

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

Suraj Muraleedharan
  • 1,154
  • 2
  • 16
  • 30
0

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(ConditionalBeans.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.

Abhilash
  • 457
  • 5
  • 9