Annotation: примеры (JAVA)

Аннотации Java: принципы и набор примеров
Раздел: Аннотации (обработка)
Annotation

Общее описание аннотаций в Java

Аннотация в Java представляет собой мета-информацию, прикрепляемую к коду: к классам, методам, полям, параметрам, пакету и т.д. Аннотации не меняют поведение программы напрямую, но могут использоваться компилятором, средой выполнения или инструментами для генерации кода, проверки и конфигурации.

Синтаксис определения аннотации выглядит как интерфейс с префиксом @interface. Элементы аннотации объявляются как методы без параметров и могут иметь типы, ограниченные спецификацией: примитивы, java.lang.String, java.lang.Class, перечисления (enum), другие аннотации и массивы перечисленных типов. Каждый такой метод возвращает значение по умолчанию при указании default, либо становится обязательным для указания при использовании аннотации.

Основные мета-аннотации, управляющие поведением аннотаций:

  • @Retention - определяет, доступна ли аннотация только в исходниках (SOURCE), в байткоде (CLASS) или во время выполнения через рефлексию (RUNTIME).
  • @Target - указывает допустимые места использования: TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE и др.
  • @Inherited - разрешает наследование аннотации подклассами при применении к классам.
  • @Documented - включает аннотацию в Javadoc.
  • @Repeatable - позволяет применять одну аннотацию несколько раз к одному элементу, введено в Java 8.

Правила и ограничения для элементов аннотаций:

  • Имена элементов похожи на методы: возвращаемый тип обязан быть одним из разрешённых типов.
  • Элемент может иметь значение по умолчанию: String value() default "";. Если аннотация содержит единственный элемент с именем value, его можно указывать без имени при применении аннотации.
  • Массивы элементов объявляются как возвращаемые типы массива: String[] tags() default {};.
  • Аннотированные элементы не могут быть изменяемыми в рантайме через API аннотаций (в нормальной практике), так как аннотация - это интерфейс с фиксированными возвращаемыми значениями.

При обращении через рефлексию используется API java.lang.reflect.AnnotatedElement (методы getAnnotation, getAnnotations, isAnnotationPresent). Для создания процессинга во время компиляции применяется инструмент Annotation Processor (javax.annotation.processing API), который получает метаданные аннотаций и позволяет генерировать код или вырабатывать сообщения компилятора.

Возвращаемые значения: сама аннотация как объект при вызове getAnnotation(MyAnno.class) возвращает экземпляр-проекцию, у которого вызов методов-элементов даёт соответствующие значения (примитивы, строки, классы, перечисления, массивы). При отсутствии retention RUNTIME такие значения недоступны во время выполнения.

Короткие примеры применения

1) Простая аннотация с обязательным элементом и значением по умолчанию

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

@Retention(RetentionPolicy.RUNTIME)
@interface Author {
    String value(); // обязательный элемент с именем value
    String date() default ""; // необязательный элемент с дефолтом
}

@Author("Иван Иванов")
class Book { }

public class Main {
    public static void main(String[] args) {
        Author a = Book.class.getAnnotation(Author.class);
        System.out.println(a.value());
        System.out.println(a.date());
    }
}
Иван Иванов

2) Аннотация с массивом и enum

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface RolesAllowed {
    Role[] value();
}

enum Role { USER, ADMIN }

class Service {
    @RolesAllowed({Role.ADMIN, Role.USER})
    public void action() { }
}

// чтение
RolesAllowed r = Service.class.getMethod("action").getAnnotation(RolesAllowed.class);
System.out.println(java.util.Arrays.toString(r.value()));
[ADMIN, USER]

3) Repeatable-аннотация

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Tags.class)
@interface Tag { String value(); }

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Tags { Tag[] value(); }

@Tag("beta")
@Tag("api")
class ApiClass { }

Tag[] tags = ApiClass.class.getAnnotationsByType(Tag.class);
for (Tag t : tags) System.out.println(t.value());
beta
api

Похожие механизмы в Java

  • Маркерные интерфейсы: используются до появления аннотаций для маркировки классов. Не позволяют хранить параметры.
  • Конфигурационные файлы (properties, XML, YAML): гибкие для настроек, доступны вне кода, но лишены типовой строгости и связываемости с конкретным элементом кода.
  • Поля и конструкторы: можно явно передавать зависимости и параметры, что даёт явное управление временем жизни, но добавляет шаблонный код.
  • Аннотационные процессоры: не альтернатива, а расширение аннотаций для генерации кода во время компиляции.

Выбор между ними зависит от цели: если нужно хранить метаданные, связанные с типами и методами, предпочтительнее аннотации; если нужна изменяемая конфигурация или лёгкая правка без перекомпиляции - конфигурационные файлы.

Аналоги в других языках и отличия

C#: механизм атрибутов близок к Java-аннотациям. Атрибуты доступны через рефлексию, поддерживают параметры и позиционные/именованные аргументы.

using System;
[AttributeUsage(AttributeTargets.Class)]
public class AuthorAttribute : Attribute { public string Name { get; } public AuthorAttribute(string name) => Name = name; }

[Author("Иван")]
class Book { }

// Результат через рефлексию: получение атрибута и его свойства
AuthorAttribute с Name = "Иван"

Kotlin: использует аннотации совместимые с Java и добавляет некоторые улучшения; синтаксис короче.

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Info(val value: String)

@Info("demo")
class A
Аннотация доступна через Kotlin-рефлексию или Java-reflection

PHP: с версии 8 появились Attributes, похожие по духу.

#[Attribute]
class Route { public function __construct(public string $path) {} }

#[Route('/api')]
class Controller {}
Атрибуты доступны через ReflectionClass::getAttributes()

JavaScript/TypeScript: декораторы (experimental) применяются к классам и методам, дают возможность менять поведение во время объявления.

function log(target) { console.log('decorated', target.name); }
@log
class C {}
decorated C

Python: декораторы функций/классов реализуются как функции, принимающие и возвращающие объект; они не аннотации в строгом смысле, но выполняют похожие задачи.

def tag(name):
    def decorator(cls):
        cls.__tag__ = name
        return cls
    return decorator

@tag('beta')
class A: pass
print(A.__tag__)
beta

Go: нет аннотаций, есть теги структурных полей (struct tags), используемые в сериализации, валидации и т.д.

type User struct {
  Name string `json:"name" validate:"required"`
}
Теги доступны через reflect.StructTag

Lua: нет нативной системы аннотаций; метаданные обычно хранятся в таблицах или через метатаблицы.

Типичные ошибки при работе с аннотациями

  • Отсутствие правильного Retention. Частая ошибка - ожидание доступности аннотации через рефлексию при отсутствии RetentionPolicy.RUNTIME. Пример:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.SOURCE)
@interface Debug { }

@Debug
class A { }

public class Main { public static void main(String[] args) {
    System.out.println(A.class.isAnnotationPresent(Debug.class));
} }
false
  • Неправильный тип элемента аннотации. Нельзя использовать произвольный объект или коллекцию в качестве типа элемента: разрешены только примитивы, String, Class, enum, аннотации и массивы этих типов. Пример компилятора:
// Недопустимо
@interface Bad {
    java.util.List value();
}
Ошибка компиляции: element type not permitted in annotation
  • Попытка создать аннотацию через new. Аннотация не инстанцируется напрямую.
// Недопустимо
MyAnno a = new MyAnno();
Ошибка компиляции: cannot instantiate the type MyAnno
  • Применение аннотации вне указанного Target. Если указано @Target(ElementType.METHOD), использование на классе даст ошибку компилятора/предупреждение.
@Target(java.lang.annotation.ElementType.METHOD)
@interface M {}

@M
class C {}
Ошибка/предупреждение компилятора: annotation type not applicable to this kind of declaration

Рекомендация по диагностике ошибок: проверять Retention и Target, убедиться в валидности типов элементов, читать сообщения компилятора и использовать рефлексию только при RUNTIME retention.

Изменения и эволюция аннотаций

  • Java 5 - введение аннотаций как языка (JSR 175).
  • Java 6/7 - постепенная интеграция в инструменты и фреймворки, появление javax.annotation.processing для Annotation Processor.
  • Java 8 - добавлены @Repeatable, новые ElementType: TYPE_PARAMETER и TYPE_USE, расширена область использования аннотаций в типах.
  • Дальнейшие версии Java не меняли фундаментальную модель аннотаций, но повысили использование типовых аннотаций (type annotations) для проверки nullability и инструментов анализа.
  • В экосистеме Jakarta EE произошло переименование пакетов (javax -> jakarta), что влияет на аннотации платформы, но не на пользовательские аннотации Java.

Расширенные и нетривиальные примеры

1) Простая реализация собственного рантайм-ридера для DI-подобного поведения

Пример java
import java.lang.annotation.*;
import java.lang.reflect.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Inject { }

class Service { public String say() { return "ok"; } }

class Consumer {
    @Inject
    private Service svc;
}

public class Main {
    public static void main(String[] args) throws Exception {
        Consumer c = new Consumer();
        for (Field f : Consumer.class.getDeclaredFields()) {
            if (f.isAnnotationPresent(Inject.class)) {
                f.setAccessible(true);
                f.set(c, new Service()); // простая инъекция
            }
        }
        System.out.println(c.getClass().getDeclaredField("svc").get(c).getClass().getName());
    }
}
Service

2) Использование type-use аннотаций для null-check инструментов

Пример java
import java.lang.annotation.*;

@Target({ElementType.TYPE_USE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@interface NonNull {}

class A {
    @NonNull String s = "x";
}

// Инструмент статического анализа может проверять такие аннотации на уровне типов
Нет вывода в рантайме; инструмент анализа использует информацию

3) Пример простого Annotation Processor (очень упрощённая схема)

Пример java
// Annotation processor живёт в отдельном модуле и собирается как сервис
// @SupportedAnnotationTypes("com.example.AutoFactory")
// @SupportedSourceVersion(SourceVersion.RELEASE_8)
// public class AutoFactoryProcessor extends AbstractProcessor { ... }

// Внутри process(...) можно получить элементы с аннотацией и сгенерировать java-файл
При сборке генерируется новый класс в соответствующем package

4) Read-only view аннотации и отражение значений по умолчанию

Пример java
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@interface Config { String name() default "app"; int[] ports() default {80}; }

Config c = Config.class.getAnnotation(Config.class); // null для интерфейса
// получение дефолта через метод отражения:
try {
    java.lang.reflect.Method m = Config.class.getMethod("name");
    System.out.println(m.getDefaultValue());
} catch (Exception e) { e.printStackTrace(); }
app

5) Комбинация repeatable, target и пакетной аннотации для документации API

Пример java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PACKAGE)
@interface ApiVersion { String value(); }

// в файле package-info.java можно разместить несколько
/** @ApiVersion("1.0") */
package com.example.api;
Инструмент документации получает версии из package-info

6) Обработка аннотаций через AnnotationMirror (для продвинутых процессоров) - позволяет безопасно читать значения аннотации на этапе компиляции без загрузки классов.

джава Annotation function comments

En
Annotation The common interface extended by all annotation types