Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interface Annotation does not accept application.properties value

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

like image 833
Federico Gatti Avatar asked Sep 26 '18 08:09

Federico Gatti


People also ask

What is the use of @interface annotation?

The @interface element is used to declare an annotation. For example: @interface MyAnnotation{}

Which annotation is used to read the environment or application property value?

Spring @Value annotation is used to assign default values to variables and method arguments. We can read spring environment variables as well as system variables using @Value annotation. Spring @Value annotation also supports SpEL. Let's look at some of the examples of using @Value annotation.

Can we use annotation in interface?

Annotation gives you the ability to provide additional metadata alongside a Java entity (such as classes, interfaces, fields and methods). This additional metadata, called annotation, can be read and interrelated by the compiler or other utilities. They can also be stored in the class files.

How do I get parameters from application properties?

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.


2 Answers

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());
like image 129
Andrew Tobilko Avatar answered Oct 21 '22 07:10

Andrew Tobilko


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

like image 40
kj007 Avatar answered Oct 21 '22 07:10

kj007