Java Annotations were introduced with Java 5 back in 2004 as a way to add meta data into Java source code. Today many major frameworks like Spring (including Spring Boot) or Hibernate heavily rely on annotations.
In this post we will have a look at a very useful Spring feature which allows us to create our own annotations based on one or more Spring annotations.
Composing a custom annotation
Assume we have a set of Spring annotations we often use together. A common example is the combination of @Service and @Transactional:
@Service @Transactional(rollbackFor = Exception.class, timeout = 5) public class UserService { ... }
Instead of repeating both annotations over and over again, we can create our own annotation containing these two Spring annotations. Creating our own annotation is very easy and looks like this:
@Service @Transactional(rollbackFor = Exception.class, timeout = 5) @Retention(RetentionPolicy.RUNTIME) public @interface MyService {}
An annotation is defined with the @interface keyword (instead of class or interface). The standard Java Annotation @Retention is used to indicate that the annotation should be processable at runtime. We also added both Spring annotations to our annotation.
Now we can use our own @MyService annotations to annotate our services:
@MyService public class UserService { ... }
Spring now detects that @MyService is annotated with @Service and @Transactional and provides the same behaviour as the previous example with both annotations present at the UserService class.
Note that this is a feature of Spring's way of annotation processing and not a general Java feature. Annotations of other frameworks and libraries might not work if you add them to your own annotation.
Example use cases
Custom annotations can be used in various situations to improve the readability of our code. Here are two other examples that might come in handy.
Maybe we need a property value in various locations of our code. Properties are often injected using Spring's @Value annotation:
// injects configuration properties my.api.key @Value("${my.api.key}") private String apiKey;
In such a situation we can move the property expression out of our code into a separate annotation:
@Value("${my.api.key}") @Retention(RetentionPolicy.RUNTIME) public @interface ApiKey {}
Within our code we can now use @ApiKey instead of repeating the property expression everywhere:
@ApiKey private String apiKey;
Another example are integration tests. Within tests often various Spring annotations are used to define the test setup. These annotations can be grouped together using a custom annotation. For example, we can create a @MockMvcTest annotations that defines the Spring setup for mock mvc tests:
@SpringBootTest @AutoConfigureMockMvc(secure = false) @TestPropertySource(locations = "classpath:test.properties") @ExtendWith(SpringExtension.class) @Retention(RetentionPolicy.RUNTIME) public @interface MockMvcTest {}
The definition of our tests look a lot cleaner now. We just have to add @MockMvcTest to get the complete test setup:
@MockMvcTest public class MyTest { ... }
Note that our @MockMvcTest annotation also contains the @ExtendWith annotation of JUnit 5. Like Spring, JUnit 5 is also able to detect this annotation if it is added to your own custom annotation. Be aware that this will not work if you are still using JUnit 4. With JUnit 4 you have to use @RunWith instead of @ExtendWith. Unfortunatelly @RunWith only works when placed directly at the test class.
Examples in Spring
Spring uses this feature in various situations to define shortcuts for common annotations.
Here are a few examples:
- @GetMapping is the short version for @RequestMapping(method = {RequestMethod.GET}).
- @RestController is a composition of @Controller and @ResponseBody.
- Spring Boots @SpringBootApplication is a shortcut for @SpringBootConfiguration, @EnableAutoConfiguration and @ComponentScan
You can verify this yourself by looking into the definition of these annotations in Spring's source code.
Comments
Motti - Saturday, 29 February, 2020
Thanks.
Useful information
Martin Tlachac - Monday, 2 March, 2020
I understood that this work only for spring annotations + Junit5. How am I able to distinguish which annotation can I combine like this?
Leave a reply