Spring 4.3: Custom annotations

In this post I will show you how to bend the Spring Framework a little bit. In particular I will show you how you can make code like this:

1
2
3
4
5
6
7
8
9
10
11
@BusinessService
public class GreeterService {

  @LocalizedMessage("greeterservice.greeting")
  private Message greetingMsg;

  public String sayHello(@NotNull String caller) {
    return greetingMsg.format(caller);
  }

}

@BusinessService declares a perfectly valid Spring bean. It comes with the support for @NotNull parameter checks. @NotNull is from the bean validation API - no need to exercise too many keystrokes.

@LocalizedMessage is a perfectly valid injection capable annotation. Here we use that to inject a Message bean. This bean is context aware - it knows about the @LocalizedMessage annotation’s value attribute. With this information, Message is used from method sayHello to return Locale aware messages. ( AWESOME TIP UNLOCKED ).

Please note that:

Just because you can doesn't mean you should

Be careful about how creative you get when bending the Spring Framework. It could easily contribute to a codebase where only a few specialized authors understand what is really going on.

The next sections jumps right into the solution. I have prepared a working example at GitHub. Consult that to see the source code in it’s entirety and true surroundings. The example is based in Spring Boot 1.4.1 and Spring Framework 4.3.3.

Test cases

Here’s a few integration tests documenting how the service works:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@SpringBootTest
@RunWith(SpringRunner.class)
public class GreeterServiceIntegrationTests {

  @Autowired
  private GreeterService greeterService;

  @Test
  public void sayHello_whenInvoked_thenReturnsEnglishGreeting() {

    // Given
    String caller = "Duke";

    // When
    String greeting = greeterService.sayHello(caller);

    // Then
    assertThat(greeting).isEqualTo("Hello World, Duke");
  }

  @Test(expected = IllegalArgumentException.class)
  public void sayHello_whenInvokedWithNullArgument_thenThrowsIllegalArgumentException() {

    // Given
    String caller = null;

    // When
    greeterService.sayHello(caller);

    // Then
    // ( kapOOOf )
  }

  @Test
  public void sayHello_whenLocaleIsDanish_andInvoked_thenReturnsDanishGreeting() {

    // Given
    LocaleContextHolder.setLocale(new Locale("da", "DK"));
    String caller = "Duke";

    // When
    String greeting = greeterService.sayHello(caller);

    // Then
    assertThat(greeting).isEqualTo("Hej Verden, Duke");
  }

  @Before @After
  public void resetLocaleBeforeAndAfterEachTestCase() {
    LocaleContextHolder.setLocale(Locale.ENGLISH);
  }
}

From the second test case, notice that the service apparently knows how to validate the incoming parameter. From the first and the third test cases, notice that the service is locale aware.

The @BusinessService annotation

The @BusinessService annotation is a custom “stereotype” annotation. Spring already has a bunch of stereotype annotations, including: @Service, @Controller, @Repository, and so on. But you can also add your own - as in the case of @BusinessService:

1
2
3
4
@Component
@Retention(RetentionPolicy.RUNTIME)
public @interface BusinessService {
}

The only thing that makes this annotation special is the @Component annotation. With that in place you can now use the @BusinessService annotation to declare Spring beans.

Spring Framework even allows you to meta-annotate your custom stereotype annotations with other framework annotations. For example with @SessionScope and @Transactional. If you annotate beans with such a composed annotation, then they would both be HTTP session scoped as well as transactional of nature. Consult the reference documentation for further information on that subject [1].

Enforcing @NotNull functionality

With the custom stereotype annotation in place you can now use it for something. What that “something” is, is entirely up to your imagination. But for this example I would like to ensure that @BusinesService components can benefit from automatic not-null parameter validation, by declaring @NotNull as hints. I have used Spring AOP for that in my example - it is extremely simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Aspect
@Component
public class NotNullParameterAspect {

  @Before("@within(com.moelholm.spring43.customannotations.BusinessService)")
  public void before(JoinPoint caller) {

    Method method = getCurrentMethod(caller);

    Object[] parameters = caller.getArgs();

    Annotation[][] parameterAnnotations = method.getParameterAnnotations();

    // Throw exception if a parameter value is null AND
    // at the same time declares that it must be @NotNull
    for (int i = 0; i < parameters.length; i++) {
      Object parameterValue = parameters[i];
      Annotation[] annotationsOnParameter = parameterAnnotations[i];

      if (parameterValue == null && hasNotNullAnnotation(annotationsOnParameter)) {
        String msgTemplate = String.format("Parameter at index %s must not be null", i);
        throw new IllegalArgumentException(msgTemplate);
      }
    }

  }

  private boolean hasNotNullAnnotation(Annotation... annotations) {
    return Arrays.asList(annotations).stream().
              anyMatch(a -> a.annotationType() == NotNull.class);
  }

  private Method getCurrentMethod(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    return signature.getMethod();
  }
}

The @Before annotation tells spring that the method should be executed before any invocation on beans that are annotated with @BusinessService. The method body, the advice, throws an IllegalArgumentException if a parameter is null and @NotNull annotated at the same time.

It’s just an example here. You could do anything in such an advice. In fact there are other types as well, including: @After, @Around, etc. Imagine what you could do with them…

Spring AOP is super powerful. I barely scratched the surface here. So if this is new to you - then check out the appropriate section in the reference documentation [2]. It can be a bit heavy - so remember to bring some dark coffee.

The custom dependency injection annotation: @LocalizedMessage

In a typical Spring application you pick between @Autowired, @Resource, @Value, @Inject when injecting a bean into another bean. But it’s super easy to create your own:

1
2
3
4
5
6
7
@Autowired
@Retention(RetentionPolicy.RUNTIME)
public @interface LocalizedMessage {

  String value() default "";

}

Take note of the @Autowired annotation. Without it, we would additionally have to specify one of the standard annotations on the injection targets as well, ie: @Autowired @LocalizedMessage Message message. Also take note of the value attribute - this is used to declare the name of the resource bundle key of interest.

Implementing the @LocalizedMessage support

In order for the Message bean to be injected it must be… well a bean. Here is how that is declared:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
public class MessageConfig {

  @Bean
  public MessageSource messageSource() {

    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasename("messages");

    return messageSource;
  }

  @Bean
  @Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
  public Message message(InjectionPoint ip) {

    LocalizedMessage localizedMessage = AnnotationUtils
        .getAnnotation(ip.getAnnotatedElement(), LocalizedMessage.class);

    String resourceBundleKey = localizedMessage.value();

    return new Message(messageSource(), resourceBundleKey);
  }

}

Notice the message factory (/producer) method. It accesses the InjectionPoint class to fetch the resource bundle key of interest (fx. “greeterservice.greeting” declared in GreeterService). With that information and a valid MessageSource it then creates the Message bean.

Note that the scope of the message bean is prototype. It is very important for this case, as it ensures that each injection of Message is a new instance. Singleton scope here would have the effect that injections of Message beans would re-use the same instance (effectively tied to the same resource bundle message - despite the annotation key values at the injection targets).

In retrospective

The title claimed "Spring 4.3: Custom annotations". To be honest, Spring Framework have had support for custom stereotypes for a long time. Many, many years. So that’s not new. Neither is the Spring AOP support I’ve shown.

But take a look at the custom dependency injection part. The InjectionPoint class is what makes this possible - and that’s a new thing, since Spring Framework 4.3 [3]. But even this part could have been implemented back in the old days: using a BeanPostProcessor[4]. But it would be a bit messy - at least compared to a simple @Bean factory method.

References

[1] @Component and further stereotype annotations

[2] Aspect Oriented Programming with Spring

[3] Spring 4.3: Introducing the InjectionPoint

[4] BeanPostProcessor JavaDoc