Аннотации в JAVA: обзор синтаксиса и создание собственных
Статья ориентирована больше на новичков, или тех, кто еще не работал с данным механизмом в языке. Я постараюсь рассказать, что это такое, зачем они нужны, и как можно самому создавать удобные для себя аннотации.
Аннотации представляют из себя дескрипторы, включаемые в текст программы, и используются для хранения метаданных программного кода, необходимых на разных этапах жизненного цикла программы.
Информация, хранимая в аннотациях, может использоваться соответствующими обработчиками для создания необходимых вспомогательных файлов или для маркировки классов, полей и т.д.
Синтаксис
Аннотация задается описанием соответствующего интерфейса.
Например так:
import java.lang.annotation.*; @Target(value=ElementType.FIELD) @Retention(value= RetentionPolicy.RUNTIME) public @interface Name
Как видно из примера выше, аннотация определяется описанием с ключевым словом interface и может включать в себя несколько полей, которые можно задать как обязательными, так и не обязательными. В последнем случае подставляется default значение поля.
Также из примера видно, что саму аннотацию можно пометить несколькими аннотациями.
Разберемся для начала, чем можно пометить собственную аннотацию, и зачем.
Аннотация @Retention позволяет указать жизненный цикл аннотации: будет она присутствовать только в исходном коде, в скомпилированном файле, или она будет также видна и в процессе выполнения. Выбор нужного типа зависит от того, как вы хотите использовать аннотацию, например, генерировать что-то побочное из исходных кодов, или в процессе выполнения стучаться к классу через reflection.
Аннотация @Target указывает, что именно мы можем пометить этой аннотацией, это может быть поле, метод, тип и т.д.
Аннотация @Documentedуказывает, что помеченная таким образом аннотация должна быть добавлена в javadoc поля/метода и т.д.
Например, класс, помеченный аннотацией без @Documented, будет выглядеть так:
public class TestClass extends java.lang.Object
А если в описание аннотации добавить @Documented, получим:
@ControlledObject(name="name") public class TestClass extends java.lang.Object
Аннотация @Inherited помечает аннотацию, которая будет унаследована потомком класса, отмеченного такой аннотацией.
Сделаем для примера пару аннотаций и пометим ими класс.
@Inherited @interface PublicAnnotate < >@interface PrivateAnnotate < >@PublicAnnotate @PrivateAnnotate class ParentClass < >class ChildClass extends ParentClass
Класс ChildClass унаследует от родительского класса только аннотацию PublicAnnotate.
Пример своей аннотации.
Попробуем теперь написать рабочий пример с использованием аннотаций.
Представим себе, что у нас есть какой-то самодельный проект, который на вход получает класс, специально заанотированный, чтобы проект мог управлять жизненным циклом объектов этого класса, и пусть там будут аннотации StartObject, StopObject для описания методов класса, и ControlledObject для описания самого класса. Последней аннотации дадим еще поле name, путь там хранится якобы имя для поиска.
Аннотации будут выглядеть так:
@Target(value=ElementType.METHOD) @Retention(value= RetentionPolicy.RUNTIME) public @interface StartObject < >@Target(value=ElementType.METHOD) @Retention(value= RetentionPolicy.RUNTIME) public @interface StopObject < >@Target(value=ElementType.TYPE) @Retention(value= RetentionPolicy.RUNTIME) public @interface ControlledObject
Напишем модуль, проверяющий подходит ли класс для загрузки в наш гипотетический проект или нет.
Сперва определим сам проверяемый класс.
@ControlledObject(name="biscuits") public class Cookies < @StartObject public void createCookie()< //бизнес логика >@StopObject public void stopCookie() < //бизнес логика >>
Для того, чтобы работать с классом, сначала необходимо загрузить класс в контекст приложения. Используем:
Class cl = Class.forName(«org.annotate.test.classes.Cookies»);
Далее, через механизм reflection мы получаем доступ к полям и аннотациям класса.
Проверим наличие аннотированных методов в классе и аннотации на самом классе:
if(!cl.isAnnotationPresent(ControlledObject.class)) < System.err.println("no annotation"); >else < System.out.println("class annotated ; name - " + cl.getAnnotation(ControlledObject.class)); >boolean hasStart=false; boolean hasStop=false; Method[] method = cl.getMethods(); for(Method md: method) < if(md.isAnnotationPresent(StartObject.class)) if(md.isAnnotationPresent(StopObject.class)) if(hasStart && hasStop) > System.out.println("Start annotaton - " + hasStart + "; Stop annotation - " + hasStop );
Запустив, на выходе мы получим:
Start annotaton — true; Stop annotation — true.
Если попробовать убрать одну из аннотаций, то вывод сообщит о несоответствии требованиям.
Итог
Надеюсь, мне удалось показать, что аннотации предоставляют широкие возможности для работы с кодом программ, и имеет смысл не только пользоваться стандартными аннотациями языка, но и облегчать себе жизнь, создавая аннотации под свои нужды.
Подробнее ознакомиться с данным механизмом могу порекомендовать в книге «Java 2. Библиотека профессионала, том 2. Тонкости программирования» Кей С. Хорстманна, Гари Корнелла, или на сайте oracle, где так же есть подробные туториалы.
Аннотации в Java. Как создать свою аннотацию
Объясняю на пальцах, что такое аннотации в Java, а также рассказываю как создать свою аннотацию и обработчик к ней в Java.
Аннотации – это своеобразные маркеры, с помощью которых программист указывает компилятору Java и средствам разработки, что делать с участками кода помимо исполнения. Аннотировать можно переменные, параметры, классы, пакеты. Можно писать свои аннотации или использовать стандартные – встроенные в Java.
Вы узнаете аннотацию по символу @ в начале имени. Самая часто встречаемая аннотация, которую встречал любой программист это @Override . Эта аннотация сообщает компилятору, что мы переопределили метод. Поэтому, когда метод суперкласса будет удален или изменен, компилятор выдаст сообщение об ошибке. Рассмотрим небольшой пример:
class SomeClass < void method() < System.out.println("Работает метод родительского класса."); >> class AnotherClass extends SomeClass < // наследуем методы SomeClass в новом классе @Override void method() < // переопределяем метод System.out.println("Работает метод класса-потомка."); >>
Если в имени метода из класса AnotherClass будет опечатка, компилятор учтет @Override и выдаст ошибку. Без аннотации он не заметил бы подвоха и создал бы новый метод в дополнение к method из SomeClass .
Обратите внимание, сама аннотация никак не влияет на переопределение метода, но позволяет контролировать успешность переопределения при компиляции или сборке. Мы защитили участок кода от неприметной ошибки, на поиск которой в большой программе ушли бы часы. Это лишь одно из многих применений аннотаций.
Структура аннотации
Создание аннотаций очень похоже на создание интерфейса, только вот само ключевое слово interface пишется со знаком @ .
public @interface MyAnnotation
Параметры задаются как методы у интерфейсов, только без аргументов. А ключевое слово default — говорит про то, что метод по умолчанию будет возвращать определённое значение.
Так как мы не сконфигурировали аннотацию, то она может применяться к чему угодно: к классам, методам, атрибутам и т. п. Для того чтобы ограничить использование аннотации, её нужно разметить аннотациями 😄
@Target(ElementType.TYPE) public @interface MyAnnotation
Аннотация @Target позволяет ограничить область применения:
- @Target(ElementType.PACKAGE) – только для пакетов;
- @Target(ElementType.TYPE) – только для классов;
- @Target(ElementType.CONSTRUCTOR) – только для конструкторов;
- @Target(ElementType.METHOD) – только для методов;
- @Target(ElementType.FIELD) – только для атрибутов(переменных) класса;
- @Target(ElementType.PARAMETER) – только для параметров метода;
- @Target(ElementType.LOCAL_VARIABLE) – только для локальных переменных;
- @Target(ElementType.ANNOTATION_TYPE) — означает аннотацию конфигурацию. Таким образом, аннотация может использоваться только для аннотирования других аннотаций. Как @Target и @Retention .
Если нужно, что бы ваша аннотация использовалась больше чем для одного типа, укажите @Target следующим образом:
Помимо @Target есть еще несколько аннотаций, для настройки:
@Retention определяет в каком жизненном цикле кода аннотация будет доступна.
- SOURCE — аннотация доступна только в исходном коде и стирается во время создания .class файла;
- CLASS — аннотация хранится и в .class файле, но недоступна во время выполнения программы;
- RUNTIME — аннотация хранится в .class файле и доступна во время выполнения программы.
@Inherited позволяет реализовать наследование аннотаций родительского класса классом-наследником
@Inherited public @interface MyAnnotation < >@MyAnnotation public class MySuperClass < . >public class MySubClass extends MySuperClass
В этом примере класс MySubClass наследует аннотацию @MyAnnotation , потому что MySubClass наследуется от MySuperClass , а MySuperClass имеет @MyAnnotation .
@Documented — аннотация будет помещена в сгенерированную документацию javadoc
Обработчик аннотации
Но магии в программировании нет, и аннотации сами по себе ничего не делают, нужно написать обработчик аннотации.
Самое большое ограничение аннотаций — это не возможность изменять существующие классы , можно только создавать новые. Исключением является проект lombok, который может изменять классы, например добавлять геттеры и сеттеры, конструкторы и так далее.
Давайте закрепим полученные знания на примере. Создадим аннотацию @FieldNames , которая будет генерировать новый класс содержащий строки названия полей. Проще на примере, есть у нас класс:
А наша аннотация должна сгенерировать нам класс в том же пакете с названием SimpleFields :
public class SimpleFields
Для этого создаем аннотацию @FieldNames :
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface FieldNames
Параметр postfix будет отвечать за окончание названия сгенерированного класса. По умолчанию будет к названию класса добавляется Fields .
Самое простое позади, теперь создадим обработчик FieldNameProcessor , который наследуется от AbstractProcessor .
@SupportedAnnotationTypes("org.sadtech.example.annotation.FieldNames") @SupportedSourceVersion(SourceVersion.RELEASE_11) @AutoService(Processor.class) public class FieldNameProcessor extends AbstractProcessor < @Override public boolean process(Set set, RoundEnvironment roundEnvironment) < return false; >>
Аннотация @SupportedAnnotationTypes отвечает за указание аннотации для которой этот обработчик создается.
Аннотация @AutoService упрощает создание манифеста. Но для нее нужно добавить новую зависимость
public class TestEntityFields
Заключение
Мы разобрались что такое аннотация и как она выглядит. Также мы научились создавать свои собственные аннотации и обработчики к ним.