Аннотации в Java. Не путать с комментариями
Аннотации – это пометки, с помощью которых программист указывает компилятору Java и средствам разработки, что делать с участками кода помимо исполнения программы. Аннотировать можно переменные, параметры, классы, пакеты. Можно писать свои аннотации или использовать стандартные – встроенные в Джава.
Вы узнаете аннотацию по символу @ в начале имени: @Override – стандартная аннотация Javа, которая предупреждает, что ниже мы что-то переопределим:
class SomeClass < void method() < System.out.println("Работает метод родительского класса."); > > class AnotherClass extends SomeClass < // наследуем методы SomeClass в новом классе @Override void method() < // переопределяем метод System.out.println("Работает метод класса-потомка."); > >
Если в имени метода из AnotherClass будет опечатка, компилятор учтет @Override и выдаст ошибку. Без аннотации он не заметил бы подвоха и безропотно создал бы новый метод в дополнение к method из SomeClass.
Обратите внимание, сама аннотация никак не влияет на переопределение метода, но позволяет контролировать успешность переопределения при компиляции или сборке. Мы защитили участок кода от неприметной ошибки, на поиск которой в большой программе ушли бы часы. Это лишь одно из многих применений аннотаций.
Зачем нужны аннотации Java
- автоматически создавать конфигурационные XML-файлы и дополнительный Java-код на основе исходного аннотированного кода;
- документировать приложения и базы данных параллельно с их разработкой;
- проектировать классы без применения маркерных интерфейсов;
- быстрее подключать зависимости к программным компонентам;
- выявлять ошибки, незаметные компилятору;
- решать другие задачи по усмотрению программиста.
Поясним понятие «маркерный интерфейс». Интерфейсы без каких-либо методов действуют как маркеры. Они лишь говорят компилятору, что объекты классов, которые имплементируют такой интерфейс без методов, должны иметь отличительные черты, восприниматься иначе. Например, java.io.Serializable, java.lang.Cloneable, java.util.EventListener. Маркерные интерфейсы ещё известны как «теги» — они добавляют общий тег ко всем унаследованным классам и объединяют их в одну категорию.
При первом появлении в Java EE 5 аннотации были представлены как инструмент, который ускоряет разработку больших web-сервисов и клиентских приложений. Как это работает?
Обработка аннотации в Джава
На основе аннотаций компилятор может с помощью специальных обработчиков генерировать новый код и файлы конфигурации.
Обработчиками обычно выступают библиотеки и утилиты, которые можно брать у сторонних авторов (или создавать самостоятельно) и прикреплять к проекту в среде разработки. Способ подключения зависит от IDE или системы сборки. В Maven обработчики подключают с помощью модуля annotation-user или плагина maven-compiler-plugin.
Парсинг аннотаций происходит циклически. Компилятор ищет их в пользовательском коде и выбирает подходящие обработчики. Если вызванный обработчик на основе аннотации создаёт новые файлы с кодом, начинается следующий этап, где исходным материалом становится сгенерированный код. Так продолжается до тех пор, пока не будут созданы все необходимые файлы.
Пишем первую аннотацию на Java
Допустим, у нас есть веб-сервис, который поддерживает несколько версий одного функционала для соблюдения совместимости. И есть обработчик аннотаций, который позволяет компилятору выбирать нужные версии. На минутку забудем о существовании Git 🙂
Где хранить данные о версии и авторе функционала? Конечно же, в аннотации. Напишем её. Новую аннотацию объявляют с помощью ключевого слова @interface:
public class SomeClass < public @interface version < private float v(); // номер версии private String author() default “Аноним"; // автор > // остальное содержимое класса >
Это немного искусственный, но зато простой и наглядный пример аннотации на Java. Мы добавили два атрибута, которые выглядят как методы. Отличие в том, что при объявлении атрибутов никогда не используют оператор throws и не назначают параметров. Значениями могут выступать:
- примитивные типы Java,
- классы или снабженные параметрами обращения к классам,
- перечисления,
- другие аннотации,
- массивы из вышеперечисленных элементов.
Можно указывать значения по умолчанию, что мы и сделали выше с полем author. При постановке аннотации атрибуты с дефолтными значениями можно пропускать.
@version(v=1.0f); // автор остаётся “Анонимом”, а "f" после числа ставим для явного указания на тип float SomeClass < // . >
Чтобы нашу аннотацию использовали только по назначению, вернёмся к её объявлению и укажем, где и когда она должна работать:
@Target(ElementType.TYPE) /* Аннотация применима только к классам, а не к пакетам, отдельным методам, переменным и т.д. */ @Retention(RetentionPolicy.RUNTIME) /* Применяется во время выполнения программы. Если бы нам нужно было применять аннотацию к исходному коду на этапе компиляции программы, мы бы указали RetentionPolicy.SOURCE.*/ public @interface version < // . >
Остается ассоциировать аннотацию с нужным классом и запустить программу.
Используем аннотации Java для сравнения баз данных
Напишем что-нибудь более сложное и полезное. Нам нужно свести несколько БД в одну. Для начала сравним их, чтобы найти одинаковые поля и значения, устранить дубли и внести новую информацию из каждого источника. Могут возникнуть и сопутствующие задачи: одни значения потребуется отформатировать, другие — проигнорировать, всё вместе вывести в виде .xls-отчёта.
Мы наметили конфигурацию работы с данными, остаётся её реализовать. Вот здесь на сцену выходят аннотации. С их помощью мы можем задать (а если потребуется — и скорректировать) способы обработки каждого поля. При этом конфигурация будет выглядеть абсолютно прозрачно: читатель кода сразу поймёт, что к чему применено. И ход исполнения программы не будет затронут. Волшебно! Пишем:
// Сначала укажем, что аннотацию надо применять на уровне ПОЛЯ @Target(ElementType.FIELD) // Использовать аннотацию надо во время выполнения программы @Retention(RetentionPolicy.RUNTIME) public @interface FieldInspector < //создаём аннотацию, придумываем ей имя // Выбираем, сравнивать ли источники. По умолчанию — да. boolean compareSources() default true; /* Получаем формат значения в отчёте, а если не получаем — подставляем формат по умолчанию — native. */ СhooseTheFormat displayFormat() default СhooseTheFormat.NATIVE; // Получаем ID текущего поля для сопоставления в нескольких базах int id(); // Уточняем, какое имя использовать для поля в таблице. String field_name() default ""; // Какие источники сравнивать — формируем список. TheSource[] sourcesToCompare() default <>; >
Теперь ассоциируем аннотацию с полем. Допустим, у нас в БД книги:
@FieldInspector(id = 2, field_name = "TITLE") private String book_title; // переменная хранит название книг
и
Присвоили id, присвоили имя полю в отчёте. Сверка значений со всеми базами запустится по умолчанию, т.к мы не указали для compareSources значение false. А если мы заведомо знаем, что искать нужно не во всех БД, а в конкретных? Например, сюжет есть не у любой книги. Задаём источники вручную:
// проверять только пьесы и беллетристику @FieldInspector(id = 3, field_name = "PLOT" sourcesToCompare =TheSource.PLAYS, TheSource.FICTION>) private String plot;
Если у нас есть поле «Примечания», значение которого не нужно сверять, мы можем перед переменной для хранения примечаний поставить такую аннотацию:
@FieldInspector(id = 300, field_name = "NOTES", compareSources = false) private String notes;
Неплохо, но как сделать, чтобы правила обработки выбирались в зависимости от значения поля? Можно связать правила с классом обработчика, чтобы тот выбирал, как поступать с каждым полем. Для этого потребуется ещё одна короткая аннотация:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface RuleApplier < // массив строк, где хранятся параметры обработчика, по умолчанию — параметров нет String[] parameters() default <>; // класс, который отвечает за применение правил к каждому источнику Class?> processor() default MyRuleApplier.class; >
Проверим, как это работает. Давайте не сравнивать данные о литературе, которая поступила в библиотеку до 1990 года — допустим, эти данные у нас были только в одной базе:
@FieldInspector(id = 4, label = "THE YEAR OBTAINED", displayFormat = СhooseTheFormat.YEAR_FORMAT>) @RuleApplier(processor = IgnoreWhatWasObtainedBefore.class, parameters = < "1990" >) private int book_obtained;
Теперь реагировать на значения менее 1990 в поле «год поступления» мы доверяем конкретному пользовательскому классу. Это даёт нам больше гибкости в работе с данными, но помните, что аннотации — вещь статическая. Какое значение получил обработчик на входе, с тем и разбирается. Интерактива с динамической сменой значений пользователем не получится.
Аннотации в JAVA
Аннотации — это форма метаданных. Они предоставляют информацию о программе, при том сами частью программы не являются.
Применение
- Информация для компилятора. Могут использоваться компилятором для обнаружения ошибок и подавления предупреждений.
- Обработка во время компиляции и развертывания. Программа может создавать код, XML-файлы и т.п. на основе аннотаций.
- Обработка во время выполнения. Некоторые аннотации могут использоваться во время выполнения программы.
Синтаксис
Начинаются с @ , могут включать элементы, которым присваиваются значения:
@Author( name = "Benjamin Franklin" date = "3/27/2003" ) class MyClass
Если такой элемент один, его имя можно опустить:
@SupressWarnings("unchecked") void MyMethod()
Если таких элементов нет, можно опустить скобки. Можно использовать несколько аннотаций в одном объявлении:
@Author(name = "Jane Doe") @EBook class MyClass
Аннотации могут быть повторяющимися.
@Author(name = "Jane Doe") @Author(name = "John Smith") class MyClass
Где в коде можно использовать аннотации
Аннотации применяются с объявлениями классов, полей и других элементов программы.
Аннотации, использующиеся с типами, называются аннотациями типов. Примеры таких аннотаций:
- Создание экземпляра класса:
new @Interned MyObject();
- Приведение к типу:
myString = (@NonNull String) str;
- Имплементация:
class UnmodifiableList implements @Readonly List
- Объявление бросаемых исключений:
void monitorTemperature() throws @Critical TemperatureException
Создание аннотации
Синтаксис
Описание аннотации напоминает описание интерфейса. Оно начинается с @Interface , а его элементы похожи на методы, которые могут иметь дефолтные значения.
Пример
Допустим, в какой-то IT-компании тела всех классов начинаются с комментариев, содержащих важную информацию:
public class Generation3List extends Generation2List < // Author: John Doe // Date: 3/17/2002 // Current revision: 6 // Last modified: 4/12/2004 // By: Jane Doe // Reviewers: Alice, Bill, Cindy // class code goes here >
Описание аннотации, которая заменит комментарии:
@interface ClassPreamble < String author(); String date(); int currentRevision() default 1; String lastModified() default "N/A"; String lastModifiedBy() default "N/A"; // Можно использовать массив String[] reviewers(); >
Использование созданной аннотации:
@ClassPreamble ( author = "John Doe", date = "3/17/2002", currentRevision = 6, lastModified = "4/12/2004", lastModifiedBy = "Jane Doe", reviewers = ) public class Generation3List extends Generation2List
Замечание: для добавления аннотации в Javadocs нужно использовать @Documented:
import java.lang.annotation.*; @Documented @interface ClassPreamble < // Описание элементов аннотации >
Предопределенные аннотации
В Java есть аннотации, описанные заранее. Часть из них предоставляют информацию для компилятора, часть применяется к другим аннотациям.
Аннотации, использующиеся компилятором
Располагаются в пакете java.lang.
Помеченный этой аннотацией элемент устарел и больше не должен использоваться (это стоит отметить в Javadoc). При наличии такого элемента в программе компилятор сгенерирует предупреждение.
// Комментарий Javadoc: /** * @deprecated * объяснение, почему метод устарел. */ @Deprecated static void deprecatedMethod()
Информирует компилятор о том, что аннотируемый элемент должен переопределять элемент родительского класса. При некорректном переопределении компилятор сгенерирует ошибку.
@Override int overriddenMethod()
Подавляет генерируемые компилятором предупреждения.
Предупреждения делятся на непроверенные (unchecked) и устаревшие (deprecation). Первые возникают при использовании устаревшего кода, написанного до дженериков, вторые — при использовании кода, помеченного аннотацией @Deprecated.
Можно подавить как одну категорию, так и обе сразу:
@SuppressWarnings()
Применяется к методу или конструктору и утверждает, что код не выполняет потенциально небезопасных операций с параметрами varargs. При использовании аннотации подавляются unchecked предупреждения, связанные с varargs.
@SafeVarargs // На самом деле не безопасно! static void m(List. stringLists) < Object[] array = stringLists; ListtmpList = Arrays.asList(42); array[0] = tmpList; //Написано неверно, но скомпилируется без предупреждения String s = stringLists[0].get(0); //ClassCastException >
Используется при описании функционального интерфейса. Подчеркивает, что это именно функциональный интерфейс.
@FunctionalInterface public interface Predicate
Аннотации, применимые к другим аннотациям (мета-аннотации)
Располагаются в пакете java.lang.annotation.
Указывает, сколько хранится отмеченная аннотация.
- RetentionPolicy.SOURCE . Отмеченная аннотация сохраняется только на уровне исходного кода и игнорируется компилятором.
- RetentionPolicy.CLASS . Сохраняется компилятором во время компиляции, но игнорируется JVM.
- RetentionPolicy.RUNTIME . Сохраняется JVM для использования во время выполнения программы.
Указывает, что аннотация, должна быть задокументирована в Javadoc (по умолчанию аннотации не документируются).
Определяет права доступа аннотации (к каким элементам ее можно применять). В аннотации @Target указывается одно из следующих значений:
- ElementType.ANNOTATION_TYPE . Применяется к аннотации
- ElementType.CONSTRUCTOR . Применяется к конструктору.
- ElementType.FIELD . Применяется к полю или свойству.
- ElementType.LOCAL_VARIABLE . Применяется к локальной переменной.
- ElementType.METHOD . Применяется к методу.
- ElementType.PARAMETER . Применяется к параметру метода.
- ElementType.TYPE . Применяется к любому элементу класса.
Аннотация будет наследоваться дочерним классом (по умолчанию аннотации не наследуются). Применима только к описаниям классов.
Указывает, что аннотация повторяющаяся.
Повторяющиеся аннотации
Определение
Аннотации, которые могут применяться к одному и тому же элементу более одного раза.
Пример
Допустим, вам надо написать аннотацию, запускающую метод в заданное время или по определенному расписанию. В примере созданная аннотация @Schedule будет запускать метод каждый последний день месяца и каждую пятницу в 23:00.
@Schedule(dayOfMonth="last") @Schedule(dayOfWeek="Fri", hour="23") public void doPeriodicCleanup()
Создание повторяющейся аннотации
Для обеспечения обратной совместимости повторяющиеся аннотации хранятся в контейнере аннотаций, который автоматически генерируется java компилятором. Для генерации нужны следующие описания:
Аннотация должна быть помечена @Repeatable, в скобках указан тип контейнера аннотаций.
import java.lang.annotation.Repeatable; @Repeatable(Schedules.class) public @interface Schedule
- Описание контейнера аннотаций
Контейнер должен содержать массив повторяющихся аннотаций.
public @interface Schedules
Получение повторяющихся аннотаций
Reflection API предоставляет методы для получения аннотаций. При получении повторяющихся аннотаций поведение методов, которые возвращают одну аннотацию (например, AnnotatedElement.getAnnotation(Class) ) не меняется. Если нужно вернуть более одной, то необходимо сначала получить контейнер. Таким образом устаревший код продолжает работать. Также, для получения повторяющихся аннотаций можно использовать методы Java SE 8 (‘AnnotatedElement.getAnnotationsByType(Class)’).
Для получения информации о всех методах, обратитесь к документации класса AnnotatedElement.
Аннотации типов
Определение
Аннотации типов — аннотации, которые применяются вместе с типами. Везде, где вы видите тип, можно использовать эту аннотацию. Например, с оператором new, при приведении, при имплементации и при использовании throws.
Создание аннотации типов
Для создания аннотации типов в @Target указываются следующие значения, либо одно из них:
@Target () public @interface Test
TYPE_PARAMETER , означает, что аннотацию можно применять к переменным типа (например, MyClass ). TYPE_USE , разрешает применение с любыми типами.
Применение
Аннотации типов предназначены для улучшенного анализа программ и более строгой проверки типов. Например, @NonNull String str; . Java SE8 определяет аннотации типов, но не реализует. Вместо этого предлагается использовать сторонние фреймворки, реализующие их (Checker Framework).
Аннотации в Java. Как создать свою аннотацию
Объясняю на пальцах, что такое аннотации в Java, а также рассказываю как создать свою аннотацию и обработчик к ней в Java.
20 июня 2021 · 6 минуты на чтение
Аннотации – это своеобразные маркеры, с помощью которых программист указывает компилятору 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(< ElementType.PARAMETER, ElementType.LOCAL_VARIABLE >)
Помимо @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
Обработчик аннотации
Но магии в программировании нет, и аннотации сами по себе ничего не делают, нужно написать обработчик аннотации.
Проект на GitHub: creating-annotation
Самое большое ограничение аннотаций — это не возможность изменять существующие классы , можно только создавать новые. Исключением является проект lombok, который может изменять классы, например добавлять геттеры и сеттеры, конструкторы и так далее.
Давайте закрепим полученные знания на примере. Создадим аннотацию @FieldNames , которая будет генерировать новый класс содержащий строки названия полей. Проще на примере, есть у нас класс:
public class Simple
А наша аннотация должна сгенерировать нам класс в том же пакете с названием 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
Заключение
Мы разобрались что такое аннотация и как она выглядит. Также мы научились создавать свои собственные аннотации и обработчики к ним.
Как создать и использовать аннотации для валидации в Java
Узнайте, как создать и использовать аннотации для валидации данных в Java, упростив и улучшив этот процесс.
Алексей Кодов
Автор статьи
10 июля 2023 в 17:48
Аннотации в Java – это метаданные, которые добавляются к коду для предоставления дополнительной информации компилятору или другим инструментам во время выполнения. В данной статье мы рассмотрим, как создать и использовать аннотации для валидации данных в Java.
Создание аннотации
Для начала, создадим простую аннотацию @ValidName , которая будет проверять, является ли имя пользователя допустимым:
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.FIELD) public @interface ValidName
Здесь мы используем аннотации @Retention и @Target для указания того, как и где будет использоваться наша аннотация:
- @Retention(RetentionPolicy.RUNTIME) означает, что аннотация будет доступна во время выполнения программы.
- @Target(ElementType.FIELD) указывает, что аннотация может быть применена только к полям класса.
Теперь добавим аннотацию @ValidName к полю name в классе User :
public class User < @ValidName private String name; // . >
Использование аннотации для валидации
Для выполнения валидации на основе нашей аннотации, создадим метод validateUser , который будет проверять поля объекта User :
public class ValidationUtils < public static void validateUser(User user) < Field[] fields = user.getClass().getDeclaredFields(); for (Field field : fields) < if (field.isAnnotationPresent(ValidName.class)) < validateName(user, field); >> > private static void validateName(User user, Field field) < String name = (String) field.get(user); if (name == null || name.trim().isEmpty()) < ValidName annotation = field.getAnnotation(ValidName.class); String message = annotation.message(); throw new IllegalArgumentException(message); >> >
В данном примере метод validateUser проходит по всем полям объекта User , и если поле аннотировано нашей аннотацией @ValidName , вызывается метод validateName , который проверяет, является ли имя пользователя допустимым.
Java-разработчик: новая работа через 11 месяцев
Получится, даже если у вас нет опыта в IT
Теперь мы можем использовать метод validateUser для проверки объектов User :
public class Main < public static void main(String[] args) < User user = new User("John Doe"); try < ValidationUtils.validateUser(user); System.out.println("User is valid! ✅"); >catch (IllegalArgumentException ex) < System.out.println("User is not valid: " + ex.getMessage() + " ❌"); >> >
Если валидация проходит успешно, программа выведет сообщение «User is valid! ✅», иначе будет выведено сообщение об ошибке с указанием проблемы.
В заключение, аннотации в Java позволяют упростить и улучшить процесс валидации данных, делая его более удобным и наглядным. Этот подход может быть расширен и применен для валидации различных типов данных и условий.