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.
Method 3 : Using @Value annotation This method involves applying @Value annotation over bean properties whose values are to be injected. The string provided along with the annotation may either be the value of the bean field or it may refer to a property name from a properties file loaded earlier in Spring context.
The Javadoc of the @Value annotation.
Most people know that you can use @Autowired to tell Spring to inject one object into another when it loads your application context. A lesser known nugget of information is that you can also use the @Value annotation to inject values from a property file into a bean's attributes.
You can use @Value("${property-name}") from the application. properties if your class is annotated with @Configuration or @Component . You can make use of static method to get the value of the key passed as the parameter.
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With