Is it possible to get a list of defined jobs in Spring Batch at runtime without using db? Maybe it's possible to get this metadata from jobRepository bean or some similar object?
It is possible to retrieve the list of all job names using JobExplorer.getJobNames().
You first have to define the jobExplorer bean using JobExplorerFactoryBean:
<bean id="jobExplorer" class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean">
<property name="dataSource" ref="dataSource"/>
</bean>
and then you can inject this bean when you need it.
To list jobs defined as beans, you can just let the spring context inject them for you all the bean types of type Job into a list as below:
#Autowired
private List<? extends Job> jobs;
..
//You can then launch you job given a name.
Alternative strategy to get list of job names that are configured as beans one can use the ListableJobLocator.
#Autowired
ListableJobLocator jobLocator;
....
jobLocator.getJobNames();
This does not require a job repository.
I use these code to list and execute jobs
private String jobName = "";
private JobLauncher jobLauncher = null;
private String selectedJob;
private String statusJob = "Exit Status : ";
private Job job;
ApplicationContext context;
private String[] lstJobs;
/**
* Execute
*/
public ExecuteJobBean() {
this.context = ApplicationContextProvider.getApplicationContext();
this.lstJobs = context.getBeanNamesForType(Job.class);
if (jobLauncher == null)
jobLauncher = (JobLauncher) context.getBean("jobLauncher");
}
/**
* Execute
*/
public void executeJob() {
setJob((Job) context.getBean(this.selectedJob));
try {
statusJob = "Exit Status : ";
JobParameters jobParameters = new JobParametersBuilder().addLong("time", System.currentTimeMillis()).toJobParameters();
JobExecution execution = jobLauncher.run(getJob(), jobParameters);
this.statusJob = execution.getStatus() + ", ";
} catch (Exception e) {
e.printStackTrace();
this.statusJob = "Error, " + e.getMessage();
}
this.statusJob += " Done!!";
}
Related
I have a startup singleton EJB than initializes a scheduler and stores an EJB in JobDataMap. The trigger goes to ERROR state when I store the EJB.
#Singleton
#Startup
#Lock(LockType.READ)
public class NotificationScheduler {
private Scheduler scheduler;
#Inject
private QuartzProperties quartzProperties;
#Inject
private NotificationMailProcess notificationMailProcess;
#PostConstruct
public void init() {
try {
Properties properties = new Properties();
properties.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, "NotificationScheduler");
properties.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, StdSchedulerFactory.AUTO_GENERATE_INSTANCE_ID);
properties.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, quartzProperties.getQuartzThreadPoolClass());
properties.put(StdSchedulerFactory.PROP_THREAD_POOL_PREFIX + ".threadCount", quartzProperties.getQuartzThreadPoolCount());
properties.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, quartzProperties.getQuartzJobStoreClass());
properties.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".driverDelegateClass",
quartzProperties.getQuartzJobStoreDriverDelegateClass());
properties.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".dataSource", quartzProperties.getQuartzJobStoreDataSource());
properties.put("org.quartz.dataSource." + quartzProperties.getQuartzJobStoreDataSource() + ".jndiURL",
quartzProperties.getQuartzJobStoreDataSource());
properties.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".nonManagedTXDataSource",
quartzProperties.getQuartzJobStoreNonManagedTXDataSource());
properties.put("org.quartz.dataSource." + quartzProperties.getQuartzJobStoreNonManagedTXDataSource() + ".jndiURL",
quartzProperties.getQuartzJobStoreNonManagedTXDataSource());
properties.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".isClustered", quartzProperties.getQuartzJobStoreIsClustered());
properties.put(StdSchedulerFactory.PROP_JOB_STORE_PREFIX + ".clusterCheckinInterval",
quartzProperties.getQuartzJobStoreClusterCheckinInterval());
StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
schedulerFactory.initialize(properties);
scheduler = schedulerFactory.getScheduler();
scheduler.start();
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("process", notificationMailProcess);
JobDetail notificationMailJobDetail = newJob(NotificationMailJob.class) .withIdentity(Constants.JOB_NAME,Constants.JOB_GROUP).usingJobData(jobDataMap).build();
String cronExpression = Constants.CONFIGURATION_DEFAULT_JOB_CRON);
Trigger trigger = newTrigger().withIdentity(Constants.JOB_TRIGGER, Constants.JOB_GROUP)
.withSchedule(cronSchedule(cronExpression)).build();
scheduler.scheduleJob(notificationMailJobDetail, new HashSet<>(Arrays.asList(trigger)), true);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
The NotificationMailProcess is a dummy EJB class that implements Serializable interface annotated with #Stateless.
Finally found that it was a serialization problem during persist of injected EJB. The solution was to alter the implementation and to get the EJB in Job using JNDI lookup.
I am trying to put some data into Quartz job data map and access them in the class which implements the Job class. But it gives me the Null Pointer exception. When the application is run without the code which access the Job Data Map, it runs fine.
I use a Cron trigger to execute a scheduled job. In this example case, I configured it to run in each 20 seconds.
#Bean
public Trigger simpleJobTrigger(#Qualifier("simpleJobDetail") JobDetail jobDetail) {
CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean();
factoryBean.setJobDetail(jobDetail);
factoryBean.setStartDelay(0L);
factoryBean.setName("test-trigger");
factoryBean.setStartTime(LocalDateTime.now().toDate());
factoryBean.setCronExpression("0/20 * * * * ?");
factoryBean.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW);
try {
factoryBean.afterPropertiesSet();
} catch (ParseException e) {
e.printStackTrace();
}
return factoryBean.getObject();
}
Following is my simpleJobDetail bean.
#Bean
public JobDetailFactoryBean simpleJobDetail() {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(Executor.class);
factoryBean.setDurability(true);
factoryBean.setName("test-job");
factoryBean.getJobDataMap().put("caller", "James");
return factoryBean;
}
This is my execute method.
public class Executor implements Job {
#Autowired
ScheduledTaskService scheduledTaskService;
#Override
public void execute(JobExecutionContext jobExecutionContext) {
JobDataMap jobDataMap = null;
try {
jobDataMap = jobExecutionContext.getTrigger().getJobDataMap();
String caller = jobDataMap.get("caller").toString();
System.out.println("This is called by the user "+caller);
} catch (Exception e) {
//e.printStackTrace();
System.out.println("UNABLE TO ACCESS THE JOB DATA MAP "+e);
}
scheduledTaskService.doThePayment();
}
}
When I run the application, it prints the log given in the catch clause.
UNABLE TO ACCESS THE JOB DATA MAP java.lang.NullPointerException
Why execute method fails to access the JobDataMap ? Is there any configuration or a property I should set? What is the reason for job map to be not available at this point?
How can I get this resolved?
Found the issue in my code.
When accessing the JobDataMap, I have accessed it in following way.
jobDataMap = jobExecutionContext.getTrigger().getJobDataMap();
instead, in my case, I should access it from JobDetails, not from the trigger.
jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
I have a simple annotation configuration for a Spring batch job as follows :
#Configuration
#EnableBatchProcessing
public abstract class AbstractFileLoader<T> {
private static final String FILE_PATTERN = "*.dat";
#Bean
#StepScope
#Value("#{stepExecutionContext['fileName']}")
public FlatFileItemReader<T> reader(String file) {
FlatFileItemReader<T> reader = new FlatFileItemReader<T>();
String path = file.substring(file.indexOf(":") + 1, file.length());
FileSystemResource resource = new FileSystemResource(path);
reader.setResource(resource);
DefaultLineMapper<T> lineMapper = new DefaultLineMapper<T>();
lineMapper.setFieldSetMapper(getFieldSetMapper());
DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer(",");
tokenizer.setNames(getColumnNames());
lineMapper.setLineTokenizer(tokenizer);
reader.setLineMapper(lineMapper);
reader.setLinesToSkip(1);
return reader;
}
#Bean
public ItemProcessor<T, T> processor() {
// TODO add transformations here
return null;
}
//Exception when using JobScope for the writer
#Bean
public ItemWriter<T> writer() {
ListItemWriter<T> writer = new ListItemWriter<T>();
return writer;
}
#Bean
public Job loaderJob(JobBuilderFactory jobs, Step s1,
JobExecutionListener listener) {
return jobs.get(getLoaderName()).incrementer(new RunIdIncrementer())
.listener(listener).start(s1).build();
}
#Bean
public Step readStep(StepBuilderFactory stepBuilderFactory,
ItemReader<T> reader, ItemWriter<T> writer,
ItemProcessor<T, T> processor, TaskExecutor taskExecutor,
ResourcePatternResolver resolver) {
final Step readerStep = stepBuilderFactory
.get(getLoaderName() + " ReadStep:slave").<T, T> chunk(25254)
.reader(reader).processor(processor).writer(writer)
.taskExecutor(taskExecutor).throttleLimit(16).build();
final Step partitionedStep = stepBuilderFactory
.get(getLoaderName() + " ReadStep:master")
.partitioner(readerStep)
.partitioner(getLoaderName() + " ReadStep:slave",
partitioner(resolver)).taskExecutor(taskExecutor)
.build();
return partitionedStep;
}
#Bean
public TaskExecutor taskExecutor() {
return new SimpleAsyncTaskExecutor();
}
#Bean
public Partitioner partitioner(
ResourcePatternResolver resourcePatternResolver) {
MultiResourcePartitioner partitioner = new MultiResourcePartitioner();
Resource[] resources;
try {
resources = resourcePatternResolver.getResources("file:"
+ getFilesPath() + FILE_PATTERN);
} catch (IOException e) {
throw new RuntimeException(
"I/O problems when resolving the input file pattern.", e);
}
partitioner.setResources(resources);
return partitioner;
}
#Bean
public JobExecutionListener listener(ItemWriter<T> writer) {
/* org.springframework.batch.core.scope.StepScope scope; */
return new JobCompletionNotificationListener<T>(writer);
}
public abstract FieldSetMapper<T> getFieldSetMapper();
public abstract String getFilesPath();
public abstract String getLoaderName();
public abstract String[] getColumnNames();
}
When I run the same instance of the job with two different job parameters, both instances run sequentially instead of running in parallel. I have a SimpleAysncTaskExecutor bean configured which I assume should cause the jobs to be triggered asynchronously.
Do I need to add any more configuration to this class to have the job instances execute in parallel?
You have to configure the jobLauncher that you're using to launch jobs to use your TaskExecutor (or a separate pool). The simplest way is to override the bean:
#Bean
JobLauncher jobLauncher(JobRepository jobRepository) {
new SimpleJobLauncher(
taskExecutor: taskExecutor(),
jobRepository: jobRepository)
}
Don't be confused by the warning that will be logged saying that a synchronous task executor will be used. This is due to an extra instance that is created owing to the very awkward way Spring Batch uses to configure the beans it provides in SimpleBatchConfiguration (long story short, if you want to get rid of the warning you'll need to provide a BatchConfigurer bean and specify how 4 other beans are to be created, even if you want to change just one).
Note that it being the same job is irrelevant here. The problem is that by default the job launcher will launch the job on the same thread.
Setup: Spring application deployed on Weblogic 12c, using JNDI lookup to get a datasource to the Oracle Database.
We have multiple services which will be polling the database regularly for new jobs. In order to prevent two services picking the same job we are using a native SELECT FOR UPDATE query in a CrudRepository. The application then takes the resulting job and updates it to PROCESSING instead of WAITING using the CrusRepository.save() method.
The problem is that I can't seem to get the save() to work within the FOR UPDATE transaction (at least this is my current working theory of what goes wrong), and as a result the entire polling freezes until the default 10 minute timeout occurs. I have tried putting #Transactional (with various propagation flags) basically everywhere, but I'm not able to get it to work (#EnableTransactionManagement is activated and working).
Obviously there must be some basic knowledge I'm missing. Is this even a possible setup? Unfortunately, just using #Transactional with a non-native CrudRepository SELECT query is not possible, as it apparently first makes a SELECT to see if the row is locked or not, and only then makes a new SELECT that locks it. Another service could very well pick up the same job in the meanwhile, which is why we need it to lock immediately.
Update in relation to #M. Deinum's comment.: I should perhaps also mention that it's a setup wherein the central component that's doing the polling is a library used by all the other services (therefore the library has #SpringBootApplication, as does each service using it, so double component scanning is certainly present). Furthermore, the service has two separate classes for polling depending on the type of service, with a lot of common code, shared in an AbstractTransactionHelper class. Below I've aggregated some code for the sake of brevity.
The library's main class:
#SpringBootApplication
#EnableTransactionManagement
#EnableJpaRepositories
public class JobsMain {
public static void initializeJobsMain(){
PersistenceProviderResolverHolder.setPersistenceProviderResolver(new PersistenceProviderResolver() {
#Override
public List<PersistenceProvider> getPersistenceProviders() {
return Collections.singletonList(new HibernatePersistenceProvider());
}
#Override
public void clearCachedProviders() {
//Not quite sure what this should do...
}
});
}
#Bean
public JtaTransactionManager transactionManager(){
return new WebLogicJtaTransactionManager();
}
public DataSource dataSource(){
final JndiDataSourceLookup dsLookup = new JndiDataSourceLookup();
dsLookup.setResourceRef(true);
DataSource dataSource = dsLookup.getDataSource("Jobs");
return dataSource;
}
}
The repository (we're returning a set with only one job as we had some other issues when returning a single object):
public interface JobRepository extends CrudRepository<Job, Integer> {
#Query(value = "SELECT * FROM JOB WHERE JOB.ID IN "
+ "(SELECT ID FROM "
+ "(SELECT * FROM JOB WHERE "
+ "JOB.STATUS = :status1 OR "
+ "JOB.STATUS = :status2 "
+ "ORDER BY JOB.PRIORITY ASC, JOB.CREATED ASC) "
+ "WHERE ROWNUM <= 1) "
+ "FOR UPDATE", nativeQuery = true)
public Set<Job> getNextJob(#Param("status1") String status1, #Param("status2") String status2);
The transaction handling class:
#Service
public class JobManagerTransactionHelper extends AbstractTransactionHelper{
#Transactional
#Override
public QdbJob getNextJobToProcess(){
Set<Job> jobs = null;
try {
jobs = jobRepo.getNextJob(Status.DONE.name(), Status.FAILED.name());
} catch (Exception ex) {
logger.error(ex);
}
return extractSingleJobFromSet(jobs);
}
Update 2: Some more code.
AbstractTransactionHelper:
#Service
public abstract class AbstractTransactionHelper {
#Autowired
QdbJobRepository jobRepo;
#Autowired
ArchivedJobRepository archive;
protected Job extractSingleJobFromSet(Set<Job> jobs){
Job job = null;
if(jobs != null && !jobs.isEmpty()){
for(job job : jobs){
if(this instanceof JobManagerTransactionHelper){
updateJob(job);
}
job = job;
}
}
return job;
}
protected void updateJob(Job job){
updateJob(job, Status.PROCESSING, null);
}
protected void updateJob(Job job, Status status, String serviceMessage){
if(job != null){
if(status != null){
job.setStatus(status);
}
if(serviceMessage != null){
job.setServiceMessage(serviceMessage);
}
saveJob(job);
}
}
protected void saveJob(Job job){
jobRepo.save(job);
archive.save(Job.convertJobToArchivedJob(job));
}
Update 4: Threading. newJob() is implemented by each service that uses the library.
#Service
public class JobManager{
#Autowired
private JobManagerTransactionHelper transactionHelper;
#Autowired
JobListener jobListener;
#Autowired
Config config;
protected final AtomicInteger atomicThreadCounter = new AtomicInteger(0);
protected boolean keepPolling;
protected Future<?> futurePoller;
protected ScheduledExecutorService pollService;
protected ThreadPoolExecutor threadPool;
public boolean start(){
if(!keepPolling){
ThreadFactory pollServiceThreadFactory = new ThreadFactoryBuilder()
.setNamePrefix(config.getService() + "ScheduledPollingPool-Thread").build();
ThreadFactory threadPoolThreadFactory = new ThreadFactoryBuilder()
.setNamePrefix(config.getService() + "ThreadPool-Thread").build();
keepPolling = true;
pollService = Executors.newSingleThreadScheduledExecutor(pollServiceThreadFactory);
threadPool = (ThreadPoolExecutor)Executors.newFixedThreadPool(getConfig().getThreadPoolSize(), threadPoolThreadFactory);
futurePoller = pollService.scheduleWithFixedDelay(getPollTask(), 0, getConfig().getPollingFrequency(), TimeUnit.MILLISECONDS);
return true;
}else{
return false;
}
}
protected Runnable getPollTask() {
return new Runnable(){
public void run(){
try{
while(atomicThreadCounter.get() < threadPool.getMaximumPoolSize() &&
threadPool.getActiveCount() < threadPool.getMaximumPoolSize() &&
keepPolling == true){
Job job = transactionHelper.getNextJobToProcess();
if(job != null){
threadPool.submit(getJobHandler(job));
atomicThreadCounter.incrementAndGet();//threadPool.getActiveCount() isn't updated fast enough the first loop
}else{
break;
}
}
}catch(Exception e){
logger.error(e);
}
}
};
}
protected Runnable getJobHandler(final Job job){
return new Runnable(){
public void run(){
try{
atomicThreadCounter.decrementAndGet();
jobListener.newJob(job);
}catch(Exception e){
logger.error(e);
}
}
};
}
As it turns out, the problem was the WeblogicJtaTransactionManager. My guess is that the FOR UPDATE resulted in a JPA transaction, but upon updating the object in the database, the WeblogicJtaTransactionManager was used, which failed to find an ongoing JTA transaction. Since we're deploying on Weblogic we wrongly assumed we had to use the WeblogicJtaTransactionManager.
Either way, exchanging the TransactionManager to a JpaTransactionManager (and explicitly setting the EntityManagerFactory and DataSource on it) basically solved all problems.
#Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(entityManagerFactory().getObject());
jpaTransactionManager.setDataSource(dataSource());
jpaTransactionManager.setJpaDialect(new HibernateJpaDialect());
return jpaTransactionManager;
}
Assuming you also have added an EntityManagerFactoryBean which is needed if you want to use multiple datasources in the same project (which we're doing, but not within single transactions, so no need for JTA).
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(vendorAdapter);
factoryBean.setPackagesToScan("my.model");
return factoryBean;
}
I've been thinking around the Java feature that evaluates annotation values in compile-time and it seems to really make difficult externalizing annotation values.
However, I am unsure whether it is actually impossible, so I'd appreciate any suggestions or definitive answers on this.
More to the point, I am trying to externalize an annotation value which controls delays between scheduled method calls in Spring, e.g.:
public class SomeClass {
private Properties props;
private static final long delay = 0;
#PostConstruct
public void initializeBean() {
Resource resource = new ClassPathResource("scheduling.properties");
props = PropertiesLoaderUtils.loadProperties(resource);
delay = props.getProperties("delayValue");
}
#Scheduled(fixedDelay = delay)
public void someMethod(){
// perform something
}
}
Suppose that scheduling.properties is on classpath and contains property key delayValue along with its corresponding long value.
Now, this code has obvious compilation errors since we're trying to assign a value to final variable, but that is mandatory, since we can't assign the variable to annotation value, unless it is static final.
Is there any way of getting around this? I've been thinking about Spring's custom annotations, but the root issue remains - how to assign the externalized value to annotation?
Any idea is welcome.
EDIT: A small update - Quartz integration is overkill for this example. We just need a periodic execution with sub-minute resolution and that's all.
The #Scheduled annotation in Spring v3.2.2 has added String parameters to the original 3 long parameters to handle this. fixedDelayString, fixedRateString and initialDelayString are now available too:
#Scheduled(fixedDelayString = "${my.delay.property}")
public void someMethod(){
// perform something
}
Thank you both for your answers, you have provided valuable info which led me to this solution, so I upvoted both answers.
I've opted to make a custom bean post processor and custom #Scheduled annotation.
The code is simple (essentially it is a trivial adaptation of existing Spring code) and I really wonder why they didn't do it like this from the get go. BeanPostProcessor's code count is effectively doubled since I chose to handle the old annotation and the new one.
If you have any suggestion on how to improve this code, I'll be glad to hear it out.
CustomScheduled class (annotation)
#Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface CustomScheduled {
String cron() default "";
String fixedDelay() default "";
String fixedRate() default "";
}
CustomScheduledAnnotationBeanPostProcessor class
public class CustomScheduledAnnotationBeanPostProcessor implements BeanPostProcessor, Ordered, EmbeddedValueResolverAware, ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, DisposableBean
{
private static final Logger LOG = LoggerFactory.getLogger(CustomScheduledAnnotationBeanPostProcessor.class);
// omitted code is the same as in ScheduledAnnotationBeanPostProcessor......
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
// processes both #Scheduled and #CustomScheduled annotations
public Object postProcessAfterInitialization(final Object bean, String beanName) throws BeansException {
final Class<?> targetClass = AopUtils.getTargetClass(bean);
ReflectionUtils.doWithMethods(targetClass, new MethodCallback() {
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Scheduled oldScheduledAnnotation = AnnotationUtils.getAnnotation(method, Scheduled.class);
if (oldScheduledAnnotation != null) {
LOG.info("#Scheduled found at method {}", method.getName());
Assert.isTrue(void.class.equals(method.getReturnType()), "Only void-returning methods may be annotated with #Scheduled.");
Assert.isTrue(method.getParameterTypes().length == 0, "Only no-arg methods may be annotated with #Scheduled.");
if (AopUtils.isJdkDynamicProxy(bean)) {
try {
// found a #Scheduled method on the target class for this JDK proxy -> is it
// also present on the proxy itself?
method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
} catch (SecurityException ex) {
ReflectionUtils.handleReflectionException(ex);
} catch (NoSuchMethodException ex) {
throw new IllegalStateException(String.format(
"#Scheduled method '%s' found on bean target class '%s', " +
"but not found in any interface(s) for bean JDK proxy. Either " +
"pull the method up to an interface or switch to subclass (CGLIB) " +
"proxies by setting proxy-target-class/proxyTargetClass " +
"attribute to 'true'", method.getName(), targetClass.getSimpleName()));
}
}
Runnable runnable = new ScheduledMethodRunnable(bean, method);
boolean processedSchedule = false;
String errorMessage = "Exactly one of 'cron', 'fixedDelay', or 'fixedRate' is required.";
String cron = oldScheduledAnnotation.cron();
if (!"".equals(cron)) {
processedSchedule = true;
if (embeddedValueResolver != null) {
cron = embeddedValueResolver.resolveStringValue(cron);
}
cronTasks.put(runnable, cron);
}
long fixedDelay = oldScheduledAnnotation.fixedDelay();
if (fixedDelay >= 0) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
fixedDelayTasks.put(runnable, fixedDelay);
}
long fixedRate = oldScheduledAnnotation.fixedRate();
if (fixedRate >= 0) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
fixedRateTasks.put(runnable, fixedRate);
}
Assert.isTrue(processedSchedule, errorMessage);
}
CustomScheduled newScheduledAnnotation = AnnotationUtils.getAnnotation(method, CustomScheduled.class);
if (newScheduledAnnotation != null) {
LOG.info("#CustomScheduled found at method {}", method.getName());
Assert.isTrue(void.class.equals(method.getReturnType()), "Only void-returning methods may be annotated with #CustomScheduled.");
Assert.isTrue(method.getParameterTypes().length == 0, "Only no-arg methods may be annotated with #CustomScheduled.");
if (AopUtils.isJdkDynamicProxy(bean)) {
try {
// found a #CustomScheduled method on the target class for this JDK proxy -> is it
// also present on the proxy itself?
method = bean.getClass().getMethod(method.getName(), method.getParameterTypes());
} catch (SecurityException ex) {
ReflectionUtils.handleReflectionException(ex);
} catch (NoSuchMethodException ex) {
throw new IllegalStateException(String.format("#CustomScheduled method '%s' found on bean target class '%s', "
+ "but not found in any interface(s) for bean JDK proxy. Either "
+ "pull the method up to an interface or switch to subclass (CGLIB) "
+ "proxies by setting proxy-target-class/proxyTargetClass " + "attribute to 'true'", method.getName(),
targetClass.getSimpleName()));
}
}
Runnable runnable = new ScheduledMethodRunnable(bean, method);
boolean processedSchedule = false;
String errorMessage = "Exactly one of 'cron', 'fixedDelay', or 'fixedRate' is required.";
boolean numberFormatException = false;
String numberFormatErrorMessage = "Delay value is not a number!";
String cron = newScheduledAnnotation.cron();
if (!"".equals(cron)) {
processedSchedule = true;
if (embeddedValueResolver != null) {
cron = embeddedValueResolver.resolveStringValue(cron);
}
cronTasks.put(runnable, cron);
LOG.info("Put cron in tasks map with value {}", cron);
}
// fixedDelay value resolving
Long fixedDelay = null;
String resolverDelayCandidate = newScheduledAnnotation.fixedDelay();
if (!"".equals(resolverDelayCandidate)) {
try {
if (embeddedValueResolver != null) {
resolverDelayCandidate = embeddedValueResolver.resolveStringValue(resolverDelayCandidate);
fixedDelay = Long.valueOf(resolverDelayCandidate);
} else {
fixedDelay = Long.valueOf(newScheduledAnnotation.fixedDelay());
}
} catch (NumberFormatException e) {
numberFormatException = true;
}
}
Assert.isTrue(!numberFormatException, numberFormatErrorMessage);
if (fixedDelay != null && fixedDelay >= 0) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
fixedDelayTasks.put(runnable, fixedDelay);
LOG.info("Put fixedDelay in tasks map with value {}", fixedDelay);
}
// fixedRate value resolving
Long fixedRate = null;
String resolverRateCandidate = newScheduledAnnotation.fixedRate();
if (!"".equals(resolverRateCandidate)) {
try {
if (embeddedValueResolver != null) {
fixedRate = Long.valueOf(embeddedValueResolver.resolveStringValue(resolverRateCandidate));
} else {
fixedRate = Long.valueOf(newScheduledAnnotation.fixedRate());
}
} catch (NumberFormatException e) {
numberFormatException = true;
}
}
Assert.isTrue(!numberFormatException, numberFormatErrorMessage);
if (fixedRate != null && fixedRate >= 0) {
Assert.isTrue(!processedSchedule, errorMessage);
processedSchedule = true;
fixedRateTasks.put(runnable, fixedRate);
LOG.info("Put fixedRate in tasks map with value {}", fixedRate);
}
Assert.isTrue(processedSchedule, errorMessage);
}
}
});
return bean;
}
}
spring-context.xml config file
<beans...>
<!-- Enables the use of a #CustomScheduled annotation-->
<bean class="org.package.CustomScheduledAnnotationBeanPostProcessor" />
</beans>
Some spring annotations support SpEL.
First:
<context:property-placeholder
location="file:${external.config.location}/application.properties" />
And then, for example:
#Value("${delayValue}")
private int delayValue;
I'm not sure if #Scheduled supports SPeL, though, but in general, that's the approach.
In regard to scheduling, check this post of mine and this related question
A better way to do this is to define the scheduling in xml using the task name space
<context:property-placeholder location="scheduling.properties"/>
<task:scheduled ref="someBean" method="someMethod" fixed-delay="${delayValue}"/>
If you for some reason want to do it with annotation, you need to create an annotation that has another optional attribute where you can specify the property name or better still a property placeholder expression or Spel expression.
#MyScheduled(fixedDelayString="${delay}")
If you want to make this work with annotation rather than bean configuration xml, you can use the following annotations: #Component, #PropertySource with PropertySourcesPlaceholderConfigurer Bean itself, like this:
#Component
#PropertySource({ "classpath:scheduling.properties" })
public class SomeClass {
#Scheduled(fixedDelay = "${delay}")
public void someMethod(){
// perform something
}
#Bean
public static PropertySourcesPlaceholderConfigurer placeHolderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
We can use a field value from other beans. Suppose we have a bean named someBean with a field someValue equal to 10. Then, 10 will be assigned to the field:
#Value("#{someBean.someValue}")
private Integer someBeanValue;
Reference: A Quick Guide to Spring #Value - Baeldung