Injecting externalized value into Spring annotation
Asked Answered
O

6

34

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.

Odense answered 23/7, 2012 at 8:3 Comment(1)
related: #6789311Deeann
M
88

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
 }
Magdau answered 9/5, 2013 at 14:14 Comment(4)
this change beats the previous "workarounds" and is my preferred method now, even though it's not (yet) the accepted answer.Shrill
Nice, just what I was looking for. (Now I'm only wondering if Spring supports some syntax for reading a value from environment variable (e.g. Heroku config var) instead of property, or if specified env variables can be neatly mapped as properties.)Exhibitionism
@Exhibitionism you can, check PropertyPlaceholderConfigurer.setSystemPropertiesMode(int). In any case you can extends that and put properties as you need.Olwena
@IgnacioA.Poletti: Thanks, I'll look into that. SYSTEM_PROPERTIES_MODE_OVERRIDE looks like something I could use.Exhibitionism
O
4

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>
Odense answered 27/7, 2012 at 10:39 Comment(0)
D
3

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

Deeann answered 23/7, 2012 at 8:8 Comment(1)
Thank you for these links, however, Quartz integration is really unnecessary in this example; all I need is periodic task execution, without job prioritization or anything else fancy - but preferably with sub-minute resolution. That being said, @Scheduled "supports" placeholders, but partially - ScheduledAnnotationBeanPostProcessor resolves placeholder for cron annotation property (which is String), however, fixedDelay and fixedRate are of type long, so that won't work. Do you know of any trickery which could circumvent this (apart from writing my own annotation and postProcessor)?Odense
J
3

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}")
Jocko answered 23/7, 2012 at 8:8 Comment(1)
Well, the truth is - I'd really like to make this work with annotation and I see I'll end up writing my own annotation, so my question is: how to make ScheduledAnnotationBeanPostProcessor pick up this new annotation? Any suggestions?Odense
I
0

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();
    }   
}
Interference answered 2/11, 2018 at 8:31 Comment(0)
M
0

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

Megacycle answered 8/6, 2022 at 7:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.