Przeglądając dokumentację Springa natrafiłem na adnotację o nazwie @AliasFor. Pamiętam, że miałem kiedyś przypadek, w którym taki mechanizm by mi się bardzo przydał. Niestety teraz nie mogę sobie przypomnieć jak on dokładnie wyglądał. Niemniej postanowiłem i tak przedstawić działanie adnotacji @AliasFor tutaj na blogu. Być może komuś się ona przyda albo, mi w przyszłości, gdy zapomniany przez mnie przypadek nie będzie już zapomniany.

Główną możliwością omawianej adnotacji jest, jak sama nazwa wskazuje, dodawanie aliasów. Dzięki niej więcej niż jeden atrybut w adnotacji może być wymiennie stosowany z innymi. Zobaczmy to za chwilę na przykładzie.

Załóżmy, że mamy adnotację BaseAnnotation, która ma 3 atrybuty: value, anotherValue i differentValue. Dwa z nich, value i anotherValue, mają zdefiniowane aliasy do siebie. Oznacza to, że jeśli przypiszemy wartość do atrybutu anotherValue to ta sama wartość zostanie również przypisana do atrybutu value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface BaseAnnotation {

  @AliasFor(attribute = "anotherValue")
  String value() default "";

  @AliasFor(attribute = "value")
  String anotherValue() default "";

  String differentValue() default "default";

}

Zanim sprawdzimy działanie tego mechanizmu w runtime to przejdźmy do omówienia kolejnej funkcjonalości adnotacji @AliasFor. I tak naprawdę kluczowej dla tego wpisu. To ona pozwoliłby mi na rozwiązanie zapomnianego przeze mnie problemu. Ta funkcjonalność pozwala na nadpisywanie atrybutów adnotacji, które opisują projektowaną przez nas adnotację. Są to tzw. meta-adnotacje. Jak to wygląda w praktyce? Spójrzmy na kawałek kodu poniżej.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@BaseAnnotation(
    value = "abc"
)
public @interface AdditionalAnnotation {

  @AliasFor(annotation = BaseAnnotation.class, attribute = "differentValue")
  String overriddenAnnotation() default "default";

}

Tworzymy nową adnotację @AdditionalAnnotation, którą oznaczamy wcześniej utworzoną adnotacją @BaseAnnotation nadając wartość abc dla atrybutu value. W @AdditionalAnnotation tworzymy nowy atrybut overriddenAnnotation, który dzięki @AliasFor powinien nadpisywać wartość atrybutu differentValue adnotacji @BaseAnnotation. Aby się o tym przekonać musimy stworzyć dodatkową klasę AnnotatedClass będącą beanem Springa oznaczoną nową adnotacją @AdditionalAnnotation.

1
2
3
4
5
6
7
8
import org.springframework.stereotype.Component;

@Component
@AdditionalAnnotation(
    overriddenAnnotation = "overridden"
)
public class AnnotatedClass {
}

Teraz przyszła pora na sprawdzenie naszego rozwiązania w runtime. Utwórzmy test, który postawi cały kontekst Springa. W nim pobierzemy adnotacje przypisane instacji klasy AnnotatedClass. Jednak nie zrobimy tego refleksją. Skorzystamy z gotowych narzędzi udostępnionych przez Springa - AnnotationUtils oraz AnnotatedElementUtils.

import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils;

@SpringBootTest class AnnotatedClassTest {

@Autowired AnnotatedClass annotatedClass;

@Test void checkAliasForAnnotation() { BaseAnnotation baseAnnotation = AnnotationUtils.getAnnotation(annotatedClass.getClass(), BaseAnnotation.class); AdditionalAnnotation additionalAnnotation = AnnotationUtils.getAnnotation(annotatedClass.getClass(), AdditionalAnnotation.class);

BaseAnnotation mergedAnnotation = AnnotatedElementUtils.getMergedAnnotation(annotatedClass.getClass(), BaseAnnotation.class);

System.out.println("=== AdditionalAnnotation ===");
System.out.println(additionalAnnotation.overriddenAnnotation());
System.out.println("=== BaseAnnotation ===");
System.out.println(baseAnnotation.value());
System.out.println(baseAnnotation.anotherValue());
System.out.println(baseAnnotation.differentValue());

System.out.println();
System.out.println("=== BaseAnnotation with merge ===");
System.out.println(mergedAnnotation.value());
System.out.println(mergedAnnotation.anotherValue());
System.out.println(mergedAnnotation.differentValue());   }

}

Dlaczego musieliśmy zrobić takie rozróżnienie? Jeśli zajrzymy do dokumentacji AnnotatedElementUtils to znajdziemy tam takie oto zdanie:

AnnotatedElementUtils defines the public API for Spring’s meta-annotation programming model with support for annotation attribute overrides. If you do not need support for annotation attribute overrides, consider using AnnotationUtils instead.

Właśnie to jest powód. Nie udało mi się dotrzeć do tego dlaczego w ten sposób zostało to zaprojektowane. Ale taka jest rzeczywistość i jeśli uruchomimy powyższy test to dostaniemy output pokazujący różnicę w działaniu tych klas utility.

1
2
3
4
5
6
7
8
9
10
11
=== AdditionalAnnotation ===
overridden
=== BaseAnnotation ===
abc
abc
default

=== BaseAnnotation with merge ===
abc
abc
overridden

Wykonanie metody getMergedAnnotation z arsenału AnnotatedElementUtils zadziałało tak jak tego oczekiwaliśmy. Właśnie takie wykorzystanie adnotacji @AliasFor wydaje mi się ciekawe z punktu widzenia dewelopera. Warto być go świadomym np. tworząc customowe adnotacje dla @Transactional czy @EventListener. W tym celu też można wzorować się na produkcyjnych przykładach znajdujących się w ekosystemie Springa - @RestController czy też @RequestScope.