I was curious about how such annotations work and wanted to document my understanding. The way these annotations are supported can be considered part of the SPI and so may break if the internal implementation changes in future.
Simple Enable* Annotations
One way to think about these custom annotations is that they add a set of new beans into the Spring's application context. Let us start by defining one such custom annotation:@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @interface EnableSomeBeans {}
and apply this annotation on a Spring @Configuration class:
@Configuration @EnableSomeBeans public static class SpringConfig {}
So now to bring in a set of beans when this annotation is applied is as simple as adding the set of beans to bring in using @Import annotation this way:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(SomeBeanConfiguration.class) @interface EnableSomeBeans {}
That is essentially it, if this imported @Configuration class defines any beans, they would now be part of the Application context:
@Configuration class SomeBeanConfiguration { @Bean public String aBean1() { return "aBean1"; } @Bean public String aBean2() { return "aBean2"; } }
Here is a gist with a working sample.
Enable* Annotations with Selectors
Enable annotations can be far more complex though, they can activate a different family of beans based on the context around them. An example of such an annotation is EnableCaching which activates configuration based on different caching implementations available in the classpath.
Writing such Enable* annotations is a little more involved than the simpler example earlier. As before start with a custom annotation:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(SomeBeanConfigurationSelector.class) public @interface EnableSomeBeansSelector { String criteria() default "default"; }
Note that in this case the custom annotation has a sample field called criteria, what I want to do is to activate two different set of beans based on this criteria. This can be achieved using a @Configuration selector which can return different @Configuration file based on the context(in this instance the value of the criteria field). This selector has a simple signature and this is a sample implementation:
import org.springframework.context.annotation.ImportSelector; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; public class SomeBeanConfigurationSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { AnnotationAttributes attributes = AnnotationAttributes.fromMap( importingClassMetadata.getAnnotationAttributes(EnableSomeBeansSelector.class.getName(), false)); String criteria = attributes.getString("criteria"); if (criteria.equals("default")) { return new String[]{"enableannot.selector.SomeBeanConfigurationDefault"}; }else { return new String[]{"enableannot.selector.SomeBeanConfigurationType1"}; } } } @Configuration class SomeBeanConfigurationType1 { @Bean public String aBean() { return "Type1"; } } @Configuration class SomeBeanConfigurationDefault { @Bean public String aBean() { return "Default"; } }
So if the criteria field is "default", the beans in "SomeBeanConfigurationDefault" gets added in, else the one in "SomeBeanConfigurationType1"
Here is a gist with a working sample.
Thanks for this summary. Check this link as well for even more details: http://blog.fawnanddoug.com/2012/08/how-those-spring-enable-annotations-work.html
ReplyDeleteUseful article
ReplyDelete