Skip to content

Latest commit

 

History

History
687 lines (493 loc) · 29 KB

annotations.md

File metadata and controls

687 lines (493 loc) · 29 KB

Аннотации

Введение

Аннотации в 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 ругается на это:

Intellij IDEA ругается на класс, реализующий аннотацию Wow


Вопрос:

Если аннотация - интерфейс, то кто её реализует?

Ответ:

В общем случае это должно быть не так важно, но если вдаться в детали реализации 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() {
    }
}

Если попробовать посмотреть на это с другой стороны, то аннотации добавляются к элементам класса, а не к их экземплярам


Вопрос:

Аннотации неизменяемы, однако их методы могут возвращать массивы, которые, в свою очередь, изменяемы. Что будет, если такой массив изменить?

Ответ:

Изменится возвращенный массив. При этом последующий вызов такого метода вернёт массив, в точности соответствующий исходному. Реализация для этого производит копирование.


Мета-аннотации, тесно связанные с языком Java

Аннотация, находящаяся над аннотацией, называется мета-аннотацией. Некоторые мета-аннотации особенно важны, т.к. они позволяют контролировать некоторые аспекты применения аннотаций.

Рассмотрим их чуть подробнее.

@Retention и доступность аннотаций

Обработка аннотаций может происходить:

  1. На этапе компиляции. Так делают: Dagger, Micronaut
  2. Во время исполнения. Так делают: Jackson, Gson, Retrofit

В связи с этим различием введено понятие retention. Он определяет, на каком этапе аннотация будет отброшена. Всего предусмотрено три таких этапа: SOURCE, CLASS, RUNTIME, они перечислены в RetentionPolicy.

Сравнение 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.

Что в class файле?

Для просмотра класс файла можно воспользоваться утилитой 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 - позволяет конкретно указать, где именно аннотация, аннотированная аннотацией @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. Наиболее распространенные из них - объявления:

  1. Типов (class, interface, enum, аннотации. ElementType.TYPE)
  2. Методов (ElementType.METHOD)
  3. Конструкторов (ElementType.CONSTRUCTOR)
  4. Полей (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

Аннотация @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.


Повторяемые аннотации и @Repeatable

Иногда может быть полезно применить одну и ту же аннотацию несколько раз, например:

@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

Чтобы это заработало необходимо:

  1. объявить другую аннотацию, которая:
    1. имеет метод, который:
      1. возвращает массив исходных аннотаций
      2. назван value
    2. не имеет других методов без указания default значений
  2. пометить исходную аннотацию как @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 и подобные - особые, т.к. они тесно связаны с самим языком

Обработка аннотаций в общем случае зависит от логики, которую необходимо добавить по предоставленной с помощью аннотаций информации.

Это может выглядеть следующим образом:

  1. Получить Class<?> объекта, который нужно обработать.
  2. Прочитать аннотации.
  3. Выполнить необходимую логику

Для чтения аннотаций через 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.