7

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

Federico Gatti
  • 535
  • 1
  • 9
  • 22
  • The fact that it works with `@GetMapping` doesn't mean it works for everything. The `@GetMapping` are processed by Spring and that knows that the `path` (and other) can contain SpEL expression or value expression. That is done at runtime when actually processing the annotations. Values in annotations have to be static! so if you don't do any custom processing of the annotation and only retrieve the value it will always read what you put in there. – M. Deinum Sep 26 '18 at 10:26
  • Ok, I want to define my _custom processing_ in order to obtain my target but I don't understand how – Federico Gatti Sep 26 '18 at 10:59
  • 1
    You are doing `logger.info(customAnnotation.foo())` however before you do that you will have to replace the value with the actual value. So you have to pass it through a `PropertyResolver` like the `Environment` by calling the `resolvePlaceholders` method and use the outcome of that method for logging. You will have to do that all by yourself, there is nothing Spring can do for you to handle this. – M. Deinum Sep 26 '18 at 11:01
  • Thank you for the answer, I have tried to search some reference using your suggestion but I not understand to implement it yet. Do you have some suggestion? – Federico Gatti Oct 03 '18 at 16:05
  • You can try to add the annotation `@PropertySource("classpath:application.properties")` to the class. – Alessandro Oct 04 '18 at 08:57

6 Answers6

4

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());
Andrew Tobilko
  • 48,120
  • 14
  • 91
  • 142
  • 1
    This is my target, but I want to insert this logic in the "annotation", maybe using a support class that is automatically loaded. Do you suggest to use the AOP (using @Aspect), like @kj007 suggets, in order to implement it? Or are there others better ways? – Federico Gatti Oct 08 '18 at 13:33
  • @FedericoGatti you can't insert this logic in the "annotation" because an annotation is nothing but meta-information that can be retrieved at some point – Andrew Tobilko Oct 08 '18 at 13:45
  • @FedericoGatti some classes usually are supplied with the annotation to provide the logic. Spring gives you the classes that can read `.properties` files and parse `${...}` expressions. – Andrew Tobilko Oct 08 '18 at 13:54
  • @FedericoGatti These classes are strongly integrated into the Spring ecosystem. So you need to either add your parts to fully-packed Spring Boot components (my second example) or write own process carefully picking up the required Spring components (my first example). – Andrew Tobilko Oct 08 '18 at 13:58
  • @FedericoGatti I don't know how we can apply AOP here. It sounds like over-engineering, but we could write a proxy (I can show how if you want me to) – Andrew Tobilko Oct 08 '18 at 21:42
  • I mixed up the 2 solutions https://github.com/federicogatti/annotatedexample see it if looks fine in your opinion. Now the annotation is ElementType.METHOD – Federico Gatti Oct 09 '18 at 07:52
  • @FedericoGatti it looks good, I didn't know that you could have the annotation on methods – Andrew Tobilko Oct 09 '18 at 08:16
  • @FedericoGatti though, the solution is all about interacting with methods annotated with your annotation, not changing the way how the annotation methods behave (as you wanted initially). – Andrew Tobilko Oct 09 '18 at 08:21
  • @FedericoGatti another question: if you use AOP, how would you access resolved values in the annotated method? – Andrew Tobilko Oct 09 '18 at 08:53
  • I think that you can do it using `@Around` instead `@Before` – Federico Gatti Oct 09 '18 at 09:41
  • @FedericoGatti `@Around` would affect **the return value** of the annotated method, it still wouldn't be available for use **inside** the method – Andrew Tobilko Oct 09 '18 at 10:01
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/181547/discussion-between-federico-gatti-and-andrew-tobilko). – Federico Gatti Oct 09 '18 at 10:46
2

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

kj007
  • 6,073
  • 4
  • 29
  • 47
  • AFAIK, `CustomAnnotation#foo` can't be proxied because it's not manageable by Spring. I am curious how you would write an advice – Andrew Tobilko Oct 08 '18 at 19:49
  • @EnableAspectJAutoProxy can enable Aspect – kj007 Oct 09 '18 at 02:07
  • `@EnableAspcetJAutoProxy` doesn't work in my example, but is enough using the `@Component` annotation in the Aspect in order to create the bean – Federico Gatti Oct 09 '18 at 07:55
  • @kj007 but `@Aspect` won't make a Spring bean – Andrew Tobilko Oct 09 '18 at 08:49
  • In configuration create a bean for aspect class – kj007 Oct 09 '18 at 08:56
  • @kj007 your instructions on how to create an `@Aspect` are irrelevant, the question here is how that `@Aspect` can help. You are missing an implementation of the pointcut and an example of its usage. – Andrew Tobilko Oct 09 '18 at 10:29
  • @AndrewTobilko AOP can help to lookup string into application properties and this is what I have already explained, as Federico asked me later how AOp can be created so I have given example, of course he need to look at his need by using annotation, after return, etc – kj007 Oct 09 '18 at 13:15
1

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.

Sudhir Ojha
  • 3,247
  • 3
  • 14
  • 24
Ganesh chaitanya
  • 638
  • 6
  • 18
0

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);
phlogratos
  • 13,234
  • 1
  • 32
  • 37
  • Hi, I have replicated your answer but I continue to receive the string **${my.value}** and not the content of the _application properties_. It seems because when it's called `logger.info(customAnnotation.foo())` the scope is not inside the annotated class, where is implemented the logic, but just enter inside the annotation interface and return the value. – Federico Gatti Oct 08 '18 at 11:03
0

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).

Mike
  • 4,722
  • 1
  • 27
  • 40
0

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();
 }
}