Interface Annotation does not accept application.properties value

5.2k views Asked by At

I have developed a simple Annotation Interface

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
    String foo() default "foo";
}

then I test it annotating a Class

@CustomAnnotation
public class AnnotatedClass {
}

and call it using a method

public void foo()  {
    CustomAnnotation customAnnotation = AnnotatedClass.class.getAnnotation(CustomAnnotation.class);
    logger.info(customAnnotation.foo());
}

and all works fine because it logs foo. I try also change the annotated class to @CustomAnnotation(foo = "123") and all works fine too, becuase it logs 123.

Now I want that the value passed to the annotation is retrieved by the application.properties, so I have changed my annotated class to

@CustomAnnotation(foo = "${my.value}")
public class AnnotatedClass {
}

but now the log returns the String ${my.vlaue} and not the value in application.properties.

I know that is possible use ${} instruction in annotation because I always use a @RestController like this @GetMapping(path = "${path.value:/}") and all works fine.


My solution on Github repository: https://github.com/federicogatti/annotatedexample

6

There are 6 answers

7
kj007 On BEST ANSWER

You can't do something like directly as an annotation attribute's value must be a constant expression.

What you can do is, you can pass foo value as string like @CustomAnnotation(foo = "my.value") and create advice AOP to get annotation string value and lookup in application properties.

create AOP with @Pointcut, @AfterReturn or provided others to match @annotation, method etc and write your logic to lookup property for corresponding string.

  1. Configure @EnableAspectJAutoProxy on main application or setting up by configuration class.

  2. Add aop dependency: spring-boot-starter-aop

  3. Create @Aspect with pointcut .

    @Aspect
    public class CustomAnnotationAOP {
    
    
    @Pointcut("@annotation(it.federicogatti.annotationexample.annotationexample.annotation.CustomAnnotation)")
     //define your method with logic to lookup application.properties
    

Look more in official guide : Aspect Oriented Programming with Spring

1
Ganesh chaitanya On

Make sure Annotated Class has @Component annotation along with @CustomAnnotation(foo = "${my.value}"), then Spring will recognize this class as Spring component and makes the necessary configurations to insert the value in.

1
phlogratos On

You can use ConfigurableBeanFactory.resolveEmbeddedValue to resolve ${my.value} into the value in application.properties.

@CustomAnnotation(foo="${my.value}")
@lombok.extern.slf4j.Slf4j
@Service
public class AnnotatedClass {

    @Autowired
    private ConfigurableBeanFactory beanFactory;

    public void foo()  {
        CustomAnnotation customAnnotation = AnnotatedClass.class.getAnnotation(CustomAnnotation.class);
        String fooValue = customAnnotation.foo().toString();
        String value = beanFactory.resolveEmbeddedValue(fooValue);
        log.info(value);
    }
}

If you also want to resolve expressions you should consider using EmbeddedValueResolver.

    EmbeddedValueResolver resolver = new EmbeddedValueResolver(beanFactory);
    final String value = resolver.resolveStringValue(fooValue);
12
Andrew Tobilko On

Spring Core-based approach

First off, I want to show you a standalone application that doesn't utilise Spring Boot auto-configurable facilities. I hope you will appreciate how much Spring does for us.

The idea is to have a ConfigurableBeanFactory set up with StringValueResolver which will be aware of our context (particularly, of the application.yaml properties).

class Application {

    public static void main(String[] args) {
        // read a placeholder from CustomAnnotation#foo
        // foo = "${my.value}"
        CustomAnnotation customAnnotation = AnnotatedClass.class.getAnnotation(CustomAnnotation.class);
        String foo = customAnnotation.foo();

        // create a placeholder configurer which also is a properties loader
        // load application.properties from the classpath
        PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
        configurer.setLocation(new ClassPathResource("application.properties"));

        // create a factory which is up to resolve embedded values
        // configure it with our placeholder configurer
        ConfigurableListableBeanFactory factory = new DefaultListableBeanFactory();
        configurer.postProcessBeanFactory(factory);

        // resolve the value and print it out
        String value = factory.resolveEmbeddedValue(foo);
        System.out.println(value);
    }

}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface CustomAnnotation {

    String foo() default "foo";

}

@CustomAnnotation(foo = "${my.value}")
class AnnotatedClass {}

Spring Boot-based approach

Now, I will demonstrate how to do it within your Spring Boot application.

We are going to inject ConfigurableBeanFactory (which has already been configured) and resolve the value similarly to the previous snippet.

@RestController
@RequestMapping("api")
public class MyController {

    // inject the factory by using the constructor
    private ConfigurableBeanFactory factory;

    public MyController(ConfigurableBeanFactory factory) {
        this.factory = factory;
    }

    @GetMapping(path = "/foo")
    public void foo() {
        CustomAnnotation customAnnotation = AnnotatedClass.class.getAnnotation(CustomAnnotation.class);
        String foo = customAnnotation.foo();

        // resolve the value and print it out
        String value = factory.resolveEmbeddedValue(foo);
        System.out.println(value);
    }

}

I don't like mixing up low-level Spring components, such as BeanFactory, in business logic code, so I strongly suggest we narrow the type to StringValueResolver and inject it instead.

@Bean
public StringValueResolver getStringValueResolver(ConfigurableBeanFactory factory) {
    return new EmbeddedValueResolver(factory);
}

The method to call is resolveStringValue:

// ...
String value = resolver.resolveStringValue(foo);
System.out.println(value);

Proxy-based approach

We could write a method that generates a proxy based on the interface type; its methods would return resolved values.

Here's a simplified version of the service.

@Service
class CustomAnnotationService {

    @Autowired
    private StringValueResolver resolver;

    public <T extends Annotation> T getAnnotationFromType(Class<T> annotation, Class<?> type) {
        return annotation.cast(Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class<?>[]{annotation},
                ((proxy, method, args) -> {
                    T originalAnnotation = type.getAnnotation(annotation);
                    Object originalValue = method.invoke(originalAnnotation);

                    return resolver.resolveStringValue(originalValue.toString());
                })));
    }

}

Inject the service and use it as follows:

CustomAnnotation customAnnotation = service.getAnnotationFromType(CustomAnnotation.class, AnnotatedClass.class);
System.out.println(customAnnotation.foo());
0
Mike On

You can look at Spring's RequestMappingHandlerMapping to see how they do it, which is using a EmbeddedValueResolver. You can inject the bean factory into any spring component and then use it to build your own resolver:

@Autowired
public void setBeanFactory(ConfigurableBeanFactory beanFactory)
{
   this.embeddedValueResolver = new EmbeddedValueResolver(beanFactory);

   CustomAnnotation customAnnotation = AnnotatedClass.class.getAnnotation(CustomAnnotation.class);
   String fooValue = customAnnotation.foo();
   System.out.println("fooValue = " + fooValue);
   String resolvedValue = embeddedValueResolver.resolveStringValue(fooValue);
   System.out.println("resolvedValue = " + resolvedValue);
}

Assuming you set foo.value=hello in your properties, the output would look something like:

fooValue = ${foo.value}
resolvedValue = hello

I tested this with Spring Boot 2.0.2 and it worked as expected.

Keep in mind this is a minimal example. You would want to handle the error cases of missing annotations on the class and missing resolved value (if the value isn't set and there's no default).

0
suraj singh On

To read property from application.propertie, one need to define PropertyPlaceholderConfigurer and map it with properties file.

XML based configuration:

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  <property name="ignoreUnresolvablePlaceholders" value="true"/>
  <property name="locations" value="classpath:application.properties" />
</bean>

For annotation based: one can use as below:

@Configuration
@PropertySource(  
value{"classpath:properties/application.properties"},ignoreResourceNotFound=true)
public class Config {

/**
 * Property placeholder configurer needed to process @Value annotations
 */
 @Bean
 public static PropertySourcesPlaceholderConfigurer propertyConfigurer() {
    return new PropertySourcesPlaceholderConfigurer();
 }
}