- Аннотации
Аннотации в Java предназначены для добавления метаинформации в код.
В качестве примера, рассмотрим аннотацию, с которой, наверняка, сталкивался каждый Java-разработчик - @Override
.
Она сообщает компилятору, что происходит переопределение метода супер-класса или интерфейса. Её отсутствие не вызовет ошибки компиляции, однако её присутствие у метода, не переопределяющего метод предка, ошибку компиляции всё-таки вызовет. Пример:
public class Person {
@Override
public boolean equals(Person person) {
// ^^^^^^
// Должен быть Object
return super.equals(obj);
}
}
Разработчик ошибся и в качестве типа параметра equals
указал Person
, хотя equals
принимает Object
.
Вопрос:
Где ещё аннотации могут быть полезны?
Ответ:
Аннотации могут выступать некоторым промежуточным звеном между определённым инструментом, библиотекой или фреймворком и прикладным разработчиком:
- С одной стороны: программист декларативно указывает, что он хочет получить в месте использования аннотации.
- С другой: обработчик считывает примененную аннотацию и выполняет что-то со своей стороны.
Это может быть:
- Статическая проверка кода на этапе компиляции, подобная
@Override
. - Проверка данных с помощью Bean Validation:
public class User { @Pattern("[a-z]{6,30}") private String username; }
- Библиотека проверит соответствие имени пользователя указанному регулярному выражению.
- Декларативное кэширование с помощью
@Cacheable
. - Явное указание имени json-свойства с помощью
@JsonProperty
из Jackson.
Для объявления аннотаций используется комбинация @
и ключевого слова interface
(а в Kotlin для этого есть ключевое слово - annotation
):
public @interface Wow {
}
В примере выше объявлена аннотация Wow
, использоваться она может примерно так:
@Wow
public class Something {
@Wow
private final Set<String> strings;
@Wow
public Something(@Wow Set<String> stringsSet) {
this.strings = stringsSet;
}
}
Здесь проаннотированы и класс, и поле, и конструктор, и его параметр.
Если доводить использование аннотаций до абсурда, то может получиться что-то такое:
Об этом написано в § 9.6 JLS - спецификации языка Java:
An annotation type declaration specifies a new annotation type, a special kind of interface type.
То есть, как ни странно, их можно реализовывать, используя ключевое слово implements
:
public class DefaultFoo implements Wow {
@Override
public Class<? extends Annotation> annotationType() {
return Wow.class;
}
}
при этом, компилятор попросит переопределить annotationType
- метод из интерфейса Annotation
, возвращающий тип аннотации (в данном случае необходимо вернуть Wow.class
).
Вопрос:
Имеет ли смысл реализовывать (implements
) аннотацию?
Ответ:
Скорее нет, чем да.
Intellij IDEA имеет инспекцию "Class extends annotation interface":
Reports any classes declared as implementing or extending an annotation interface. While it is legal to extend an annotation interface, it is often done by accident, and the result won't be usable as an annotation.
Реализовывать аннотации разрешено, но часто это происходит по ошибке, и IDEA ругается на это:
Вопрос:
Если аннотация - интерфейс, то кто её реализует?
Ответ:
В общем случае это должно быть не так важно, но если вдаться в детали реализации OpenJDK, то это делается динамически с помощью класса Proxy
:
@Wow
public class Main {
@Retention(RetentionPolicy.RUNTIME)
public @interface Wow {
}
public static void main(String[] args) {
Wow wow = Main.class.getAnnotation(Wow.class);
System.out.println(wow.getClass());
}
}
Вывод:
class com.sun.proxy.$Proxy1
У аннотаций могут быть методы.
Пример:
public @interface Scheduled {
int delayMillis();
int rateMillis();
}
Использоваться эти методы могут примерно следующим образом:
public class ScheduledUsage {
@Scheduled(delayMillis = 100, rateMillis = 1000)
public void scheduledMethod() {
}
}
У метода может быть значение по умолчанию, чтобы его задать используется ключевое слово default
(оно же может быть использовано для определения тела метода по умолчанию в интерфейсе, начиная с Java 8):
public @interface Scheduled {
int delayMillis();
int rateMillis() default 1000;
}
Тогда rateMillis
указывать будет необязательно.
Вместо этого будет использоваться значение по умолчанию - 1000
:
public class ScheduledUsage {
@Scheduled(delayMillis = 100)
public void scheduledMethod() {
}
}
Методы аннотаций могут иметь только один из следующих типов возвращаемого значения:
- Аннотация
- Примитив (
int
,long
,float
и т.д.) java.lang.Class
enum
java.lang.String
- Массивы того, что перечислено выше
Возвращаемые значения могут быть только константами, т.е. следующий пример не скомпилируется:
public class ScheduledUsage {
private final int delay;
private final int rate;
public ScheduledUsage(int delay, int rate) {
this.delay = delay;
this.rate = rate;
}
@Scheduled(delayMillis = delay, rateMillis = rate)
// java: element value must be a constant expression
public void scheduledMethod() {
}
}
Если попробовать посмотреть на это с другой стороны, то аннотации добавляются к элементам класса, а не к их экземплярам
Вопрос:
Аннотации неизменяемы, однако их методы могут возвращать массивы, которые, в свою очередь, изменяемы. Что будет, если такой массив изменить?
Ответ:
Изменится возвращенный массив. При этом последующий вызов такого метода вернёт массив, в точности соответствующий исходному. Реализация для этого производит копирование.
Аннотация, находящаяся над аннотацией, называется мета-аннотацией. Некоторые мета-аннотации особенно важны, т.к. они позволяют контролировать некоторые аспекты применения аннотаций.
Рассмотрим их чуть подробнее.
Обработка аннотаций может происходить:
- На этапе компиляции. Так делают: Dagger, Micronaut
- Во время исполнения. Так делают: Jackson, Gson, Retrofit
В связи с этим различием введено понятие retention.
Он определяет, на каком этапе аннотация будет отброшена.
Всего предусмотрено три таких этапа: SOURCE
, CLASS
, RUNTIME
, они перечислены в RetentionPolicy
.
RetentionPolicy | Описание | Остается в класс-файле | Доступна во время исполнения |
---|---|---|---|
SOURCE |
Отбрасываются после компиляции | Нет | Нет |
CLASS |
Отбрасывается на этапе загрузки класса | Да | Условно: если найти класс-файл и прочитать его |
RUNTIME |
Аннотация всегда доступна через reflection во время исполнения | Да | Да |
Значение по умолчанию - RetentionPolicy.CLASS
@Retention(RetentionPolicy.CLASS)
@interface RetainedInClass {
}
@Retention(RetentionPolicy.SOURCE)
@interface RetainedInSource {
}
@Retention(RetentionPolicy.RUNTIME)
@interface RetainedAtRuntime {
}
@RetainedInClass
@RetainedAtRuntime
@RetainedInSource
public class Annotations {
public static void main(String[] args) {
boolean retainedInClassIsVisible = Annotations.class.getAnnotation(RetainedInClass.class) != null;
boolean retainedInSourceIsVisible = Annotations.class.getAnnotation(RetainedInSource.class) != null;
boolean retainedAtRuntime = Annotations.class.getAnnotation(RetainedAtRuntime.class) != null;
System.out.println("RetainedInClass is visible? " + retainedInClassIsVisible);
System.out.println("RetainedInSource is visible? " + retainedInSourceIsVisible);
System.out.println("RetainedAtRuntime is visible? " + retainedAtRuntime);
}
}
- Аннотация
RetainedInClass
имеетretention = CLASS
,RetainedInSource
-SOURCE
,RetainedAtRuntime
- Над классом
Annotations
находятся все три - В методе
main
производится попытка получить аннотации через reflection api (см.Class#getAnnotation
)
Результат:
RetainedInClass is visible? false
RetainedInSource is visible? false
RetainedAtRuntime is visible? true
То есть, как и ожидалось, во время исполнения видна только аннотация с RetentionPolicy.RUNTIME
.
Для просмотра класс файла можно воспользоваться утилитой javap
, поставляемой вместе с jdk.
$ javap -v Annotations.class
-v
- выводить подробноAnnotations.class
- файл, полученный после компиляции
Часть вывода:
RuntimeVisibleAnnotations:
0: #32()
ru.misc.annotations.RetainedAtRuntime
RuntimeInvisibleAnnotations:
0: #34()
ru.misc.annotations.RetainedInClass
javap
говорит, что в .class
файле есть:
- Аннотация, видимая во время исполнения, -
RetainedAtRuntime
- Аннотация, которую во время исполнения не видно, это -
RetainedInClass
RetainedInSource
не упоминается в контексте использования аннотации в качестве аннотации
Аннотация @Target
- позволяет конкретно указать, где именно аннотация, аннотированная аннотацией @Target
, может быть использована.
Допустим, у нас есть аннотация, которую имеет смысл ставить только над полями - @MakesSenseOnlyAtFields
:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MakesSenseOnlyAtFields {
}
Указав @Target(ElementType.FIELD)
, мы сможем ограничить её использование, так чтобы следующий код проходил компиляцию:
public class User {
@MakesSenseOnlyAtFields
private final String username;
public User(String username) {
this.username = username;
}
}
А при попытке добавить её к классу:
@MakesSenseOnlyAtFields
public class User {
}
Мы получали ошибку:
java: annotation type not applicable to this kind of declaration
Исчерпывающий список мест применения, как и всегда в подобных случаях, приводится в JLS - спецификации языка Java, § 9.6.4.1. Наиболее распространенные из них - объявления:
- Типов (
class
,interface
,enum
, аннотации.ElementType.TYPE
) - Методов (
ElementType.METHOD
) - Конструкторов (
ElementType.CONSTRUCTOR
) - Полей (
ElementType.FIELD
)
Это соответствует перечислению ElementType
Вопрос:
Можно ли одновременно указать несколько @Target
'ов?
Ответ:
Да, конечно: value
у @Target
принимает массив ElementType
. Пример:
@Target({ElementType.METHOD, ElementType.FIELD})
@interface Lax {
}
public class Main {
@Lax
private int bar;
@Lax
public void foo() {
}
}
Аннотация @Inherited
показывает, что она должна быть унаследована.
Т.е. при запросе аннотации у class
'а будут проверены все супер-классы
Пример:
@Retention(RetentionPolicy.RUNTIME)
@Inherited // 1
public @interface Persistable {
}
@Persistable // 2
public abstract class AbstractEntity {
}
public class Task extends AbstractEntity { // 3
}
public class InheritedDemo {
public static void main(String[] args) {
Persistable persistable = Task.class.getAnnotation(Persistable.class); // 4
System.out.println(persistable);
}
}
1
: аннотация помечена как наследуемая2
:AbstractEntity
в свою очередь помечена как@Persistable
3
:Task extends AbstractEntity
без добавления аннотации4
: Запрос аннотации черезClass#getAnnotation
Вывод:
@ru.misc.annotations.Persistable()
Вывод без @Inherited
:
null
Вопрос:
А интерфейсы? Что будет, если такую аннотацию поставить над ним?
Ответ:
Она будет проигнорирована:
@Persistable
interface Identifiable {
}
public abstract class AbstractEntity implements Identifiable {
}
public class Main {
public static void main(String[] args) {
Persistable persistable = AbstractEntity.class.getAnnotation(Persistable.class); // 4
System.out.println(persistable);
}
}
Вывод:
null
Javadoc @Inherited
сообщает, что в этом нет смысла. Аннотация наследуется только от супер-класса:
Note that this meta-annotation type has no effect if the annotated type is used to annotate anything other than a class. Note also that this meta-annotation only causes annotations to be inherited from superclasses; annotations on implemented interfaces have no effect.
Иногда может быть полезно применить одну и ту же аннотацию несколько раз, например:
@Retention(RetentionPolicy.RUNTIME)
public @interface RunEveryDayAt {
int hours() default 0;
}
public class Cron {
@RunEveryDayAt(hours = 11)
@RunEveryDayAt(hours = 23)
public void compactSpace() {
}
}
Однако, при компиляции класса Cron
возникнет ошибка:
java: ru.misc.annotations.RunEveryDayAt is not a repeatable annotation type
Чтобы это заработало необходимо:
- объявить другую аннотацию, которая:
- имеет метод, который:
- возвращает массив исходных аннотаций
- назван
value
- не имеет других методов без указания
default
значений
- имеет метод, который:
- пометить исходную аннотацию как
@Repeatable
указав в ней аннотацию, полученную на предыдущем шаге
@Retention(RetentionPolicy.RUNTIME)
public @interface RunEveryDayAts {
RunEveryDayAt[] value() default {}; // 1
}
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(RunEveryDayAts.class) // 2
public @interface RunEveryDayAt {
int hours() default 0;
}
- Аннотация
RunEveryDayAts
- аннотация, полученная на первом шаге 1
: Методvalue
возвращает массив исходных аннотаций.2
: Исходная аннотация помечена какRepeatable
с указанием контейнерной аннотацииRunEveryDayAts
Тогда следующая программа:
public class Main {
public static void main(String[] args) throws NoSuchMethodException, SecurityException {
Method method = Cron.class.getMethod("compactSpace");
System.out.println(Arrays.toString(method.getAnnotationsByType(RunEveryDayAt.class)));
}
}
Выведет:
[@ru.misc.annotations.RunEveryDayAt(hours=11), @ru.misc.annotations.RunEveryDayAt(hours=23)]
getAnnotation(RunEveryDayAt.class)
в данном случае вернёт null
Аннотации сами по себе чаще всего ничего не значат, их должен кто-то обрабатывать.
- Это важно понимать, когда вы столкнётесь с
@Transactional
/@Cacheable
или@OneToMany
- Аннотации
@Target
,@Retention
и подобные - особые, т.к. они тесно связаны с самим языком
Обработка аннотаций в общем случае зависит от логики, которую необходимо добавить по предоставленной с помощью аннотаций информации.
Это может выглядеть следующим образом:
- Получить
Class<?>
объекта, который нужно обработать. - Прочитать аннотации.
- Выполнить необходимую логику
Для чтения аннотаций через reflection можно использовать методы интерфейса AnnotatedElement
.
Его реализуют Class
, Constructor
, Field
, Method
и другие.
Пример чтения аннотаций, находящихся над методами:
public class ScheduledScanner {
@Scheduled(delayMillis = 100, rateMillis = 100) // 1
public void scheduled1() {
}
@Scheduled(delayMillis = 200, rateMillis = 200) // 2
public void scheduled2() {
}
@Scheduled(delayMillis = 300, rateMillis = 300) // 3
public void scheduled3() {
}
private static List<Scheduled> getSchedules(Object o) { // 4
Class<?> clazz = o.getClass(); // 5
Method[] methods = clazz.getMethods(); // 6
return Arrays.stream(methods)
.map(method -> method.getAnnotation(Scheduled.class))// 7
.filter(Objects::nonNull) // 8
.collect(Collectors.toList());
}
public static void main(String[] args) {
ScheduledScanner scheduledScanner = new ScheduledScanner();
List<Scheduled> schedules = getSchedules(scheduledScanner);
schedules.forEach(scheduled -> System.out.println("@Scheduled (delay = " // 9
+ scheduled.delayMillis() + ", rate = " + scheduled.delayMillis() + ")"));
}
}
- Методы
1
,2
,3
помечены аннотацией@Scheduled
4
: метод принимаетObject
и возвращает список аннотаций5
:getClass
возвращает классo
6
: возвращает все методы7
:method.getAnnotation
вернет аннотацию илиnull
, если её нет8
: если был метод без аннотации, то полученныйnull
нужно пропустить9
: вывод полученных значений
Вывод:
@Scheduled (delay = 100, rate = 100)
@Scheduled (delay = 200, rate = 200)
@Scheduled (delay = 300, rate = 300)
Вместо вывода может быть добавлена любая другая логика, например: отправка выбранных методов на исполнение в ScheduledThreadPoolExecutor
.
См. также:
- Аннотация - элемент языка, позволяющий в определенном формате добавить мета-информацию в код.
- Виртуальная машина и язык Java предоставляют широкие возможности по работе с ними.
- Аннотации могут быть обработаны как на этапе компиляции, так и на во время исполнения.
- Awesome Annotation Processing - агрегатор ссылок по теме Annotation Processing API - программного интерфейса обработки аннотаций на этапе компиляции.
- Lesson: Annotations - руководство по аннотациям от Oracle.
- Lombok - Annotation Processor, способный избавить Java-разработчика от написания геттеров, сеттеров,
equals
'ов иhashCode
'ов. - How does lombok work? - вопрос на StackOverflow, где рассказывается, как работает Lombok.