Java создает аннотацию

Java
Аннотации — мощная часть Java, но большую часть времени мы склонны использовать аннотации, а не создавать их. Например, нетрудно найти аннотацию @Override, обработанную компилятором Java, в исходном коде Java,Весенний фреймворканнотацию @Autowired илиСпящий фреймворкИспользуется аннотация @Entity, но мы редко видим пользовательские аннотации. Хотя пользовательские аннотации — это аспект языка Java, который часто упускают из виду, они могут быть очень полезным ресурсом при разработке удобочитаемого кода, а также для понимания того, как распространенные фреймворки, такие как Spring или Hibernate, аккуратно достигают своих целей.
В этой статье мы рассмотрим основы аннотаций, включая то, что они собой представляют, как они используются в примерах и как с ними работать. Чтобы продемонстрировать, как аннотации работают на практике, мы создадим сериализатор Javascript Object Notation (JSON), который обрабатывает аннотированные объекты и создает строку JSON, представляющую каждый объект. Попутно мы рассмотрим многие распространенные блоки аннотаций, в том числе структуру отражения Java и вопросы видимости аннотаций. Заинтересованные читатели могутGitHubначальствооказатьсяИсходный код завершенного сериализатора JSON.

Что такое аннотации?

Аннотации — это декораторы, применяемые к структурам Java, такие как связывание метаданных с классами, методами или полями. Эти декораторы безобидны и не выполняют никакого кода сами по себе, но они могут использоваться средой выполнения, фреймворком или компилятором для каких-либо действий. Более формально, Спецификация языка Java (JLS)Раздел 9.7Даны следующие определения:
Аннотации — это теги, которые связывают информацию со структурой программы, но не действуют во время выполнения.
Важно отметить последнее предложение в этом определении: Аннотации не влияют на программу во время выполнения. Это не означает, что инфраструктура не меняет свое поведение во время выполнения в зависимости от наличия аннотации, но что программа, содержащая аннотацию, сама по себе не меняет своего поведения во время выполнения. Хотя это может показаться нюансом, его важно понять, чтобы понять полезность аннотаций.
Например, добавление аннотации @Autowired к полю экземпляра само по себе не меняет поведение программы во время выполнения: компилятор просто включает аннотацию во время выполнения, но аннотация не выполняет никакого кода или не вводит никакой логики для изменения нормальное поведение программы (ожидаемое поведение при игнорировании аннотаций). Как только мы представим среду Spring во время выполнения, мы сможем получить мощные возможности внедрения зависимостей (DI) при разрешении программ. Введя аннотацию, мы проинструктировали платформу Spring внедрить соответствующие зависимости в наши поля. Вскоре (когда мы создадим сериализатор JSON) мы увидим, что аннотации не делают этого сами по себе, а действуют как маркеры, чтобы сообщить платформе Spring, что мы хотим внедрить зависимости в аннотированные поля.

Удержание и цель

Для создания аннотаций требуется две части информации: (1) политика хранения и (2) цель. Политика хранения определяет, как долго должны храниться аннотации в течение срока службы программы. Например, аннотации могут быть сохранены во время компиляции или во время выполнения, в зависимости от политики сохранения, связанной с аннотацией. Начиная с Java 9, естьТри стандартные политики хранения, резюмируется следующим образом:

Стратегия

описывать

Source

Компилятор отбрасывает аннотации

Class

Аннотации записываются в файлы классов, созданные компилятором, но их не требуется сохранять виртуальной машиной Java (JVM), которая обрабатывает файлы классов во время выполнения.

Runtime

Аннотации записываются в файлы классов компилятором и сохраняются JVM во время выполнения.

Как мы увидим позже, параметр среды выполнения с сохранением аннотаций является одним из наиболее распространенных, поскольку он позволяет Java-программам рефлексивно обращаться к аннотациям и выполнять код на основе существующих аннотаций, а также получать доступ к данным, связанным с аннотациями. Обратите внимание, что аннотации имеют только связанную с ними политику хранения.
Цель аннотации указывает, к какой конструкции Java можно применить аннотацию. Например, некоторые аннотации могут быть действительны только для методов, тогда как другие могут быть действительны как для классов, так и для полей. Начиная с Java 9, есть11 стандартных целей для аннотаций, как показано в следующей таблице:

Цель

описывать

Annotation Type

Аннотировать другую аннотацию

Constructor

Аннотировать конструктор

Field

Аннотировать поле, например переменную экземпляра класса иликонстанта перечисления

Local variable

Аннотировать локальные переменные

Method

Аннотирование метода класса

Module

Модули аннотаций (новое в Java 9)

Package

Пакет аннотаций

Parameter

Параметры, аннотированные к методам или конструкторам

Type

Аннотировать тип, например класс, интерфейс, аннотированный тип или объявление перечисления.

Type Parameter

Параметры типа аннотации, например те, которые используются в качестве универсальных форм параметров.

Type Use

Использование типов аннотаций, например, при создании объекта типа с ключевым словом new, когда объект приводится к указанному типу, когда класс реализует интерфейс или при объявлении типа бросаемого объекта с помощью ключевого слова throws ( для получения дополнительной информации см. руководство по аннотациям типов и подключаемым системам типов Oracle)

Дополнительные сведения об этих целях см.Раздел 9.7.4 JLS. Обратите внимание, что аннотации могут быть связаны с одной или несколькими целями. Например, аннотации можно использовать в полях или конструкторах, если цели поля и конструктора связаны с аннотациями. С другой стороны, если аннотация связана только с целью метода, применение аннотации к любой конструкции, кроме метода, приведет к ошибке во время компиляции.

Параметры аннотации

Аннотации также могут иметь параметры. Эти параметры могут быть примитивными типами (например, int или double), строками, классами, перечислениями, аннотациями или массивами любого из первых пяти типов (см.Раздел 9.6.1 JLS). Связывание параметра с аннотацией позволяет аннотации предоставлять контекстную информацию или может параметризовать процессор аннотации. Например, в нашей реализации сериализатора JSON мы разрешим необязательный параметр аннотации, который указывает имя поля при сериализации (если имя не указано, по умолчанию используется имя переменной поля).

Как создавать аннотации?

Для нашего сериализатора JSON мы создадим аннотацию поля, которая позволит разработчикам помечать имена полей для преобразования при сериализации объектов. Например, если мы создаем класс автомобиля, мы можем аннотировать поля автомобиля (такие как марка и модель) нашими аннотациями. Когда мы сериализуем объект автомобиля, полученный JSON будет включать ключи марки и модели, где значения представляют значения полей марки и модели соответственно. Для простоты мы предполагаем, что эта аннотация используется только для полей типа String, гарантируя, что значение поля может быть напрямую сериализовано как строка.
Чтобы создать такую ​​аннотацию поля, мы объявляем новую аннотацию с помощью ключевого слова @interface:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
    public String value() default "";
}

Ядром нашего объявления является public @interface JsonField, объявляющий аннотацию с модификатором public, что позволяет использовать нашу аннотацию в любом пакете (при условии, что пакет правильно импортирован в другой модуль). Аннотация объявляет параметр типа String value, значением по умолчанию является пустая строка.

Обратите внимание, что значение имени переменной имеет особое значение: оно определяет одноэлементную аннотацию (Раздел 9.7.3 JLS) и разрешить нашим пользователям аннотаций указать один параметр для аннотации, не указывая имя параметра. Например, пользователь может использовать @JsonField("someFieldName") и не нужно объявлять аннотацию как @JsonField(value = "someFieldName") , хотя последнее все еще можно использовать (но не обязательно). Пустая строка, содержащая значение по умолчанию, позволяет опустить значение, значение, если значение не указано явно, что приводит к пустой строке. Например, если пользователь объявляет указанную выше аннотацию @JsonField в форме, параметру value присваивается пустая строка.
Политика хранения и цель, объявленные аннотацией, указываются с помощью аннотаций @Retention и @Target соответственно. использование политики храненияjava.lang.annotation.RetentionPolicyПеречисление указывает и содержит константы для трех стандартных политик хранения. Точно так же укажите цель какjava.lang.annotation.ElementTypeПеречисление, включая константы для каждого из 11 стандартных целевых типов.
Таким образом, мы создали общедоступную одноэлементную аннотацию под названием JsonField, которая зарезервирована JVM во время выполнения и может применяться только к полям. Эта аннотация имеет только один параметр, значение типа String, а значение по умолчанию — пустая строка. Создав аннотацию, мы теперь можем аннотировать поля, которые нужно сериализовать.

Как использовать аннотации?

Использование аннотаций требует только размещения аннотации перед соответствующей структурой (любой допустимой целью аннотации). Например, мы можем создать класс Car:

public class Car {
    @JsonField("manufacturer")
    private final String make;
    @JsonField
    private final String model;
    private final String year;

    public Car(String make, String model, String year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    public String getMake() {
        return make;
    }

    public String getModel() {
        return model;
    }

    public String getYear() {
        return year;
    }

    @Override
    public String toString() {
        return year + " " + make + " " + model;
    }
} 

 

Этот класс снабжен аннотацией @JsonField для двух основных целей: (1) иметь явное значение, (2) иметь значение по умолчанию. Мы также могли бы аннотировать поле с помощью @JsonField(value = "someName") , но этот стиль слишком многословен и не улучшает читаемость кода. Следовательно, если включение имени параметра аннотации в одноэлементную аннотацию не повышает удобочитаемость кода, его следует опустить. Для аннотаций с несколькими параметрами имя каждого параметра должно быть указано явно, чтобы различать параметры (если не указан только один параметр, и в этом случае параметр будет сопоставлен с параметром значения, если имя не указано явно).

Учитывая приведенное выше использование аннотации @JsonField, мы хотим сериализовать Car в строку JSON {"manufacturer":"someMake", "model":"someModel"} (обратите внимание, как мы увидим позже, мы игнорировать производителя и модель ключа в этом порядке строк JSON). Перед этим важно отметить, что добавление аннотации @JsonField не изменит поведение класса Car во время выполнения. Если класс скомпилирован, включение аннотации @JsonField не улучшит поведение класса по сравнению с отсутствием аннотации. Файл класса класса просто записывает эти аннотации и значения параметров. Изменение поведения системы во время выполнения требует от нас работы с этими аннотациями.

Как работать с аннотациями?

Обработка аннотаций осуществляется черезИнтерфейс прикладного программирования Java Reflection (API) готов. API отражения позволяет нам писать код для доступа к классам, методам, полям и т. д. объекта. Например, если мы создадим метод, который принимает объект Car, мы можем изучить класс объекта (т. е. Car) и обнаружить, что класс имеет три поля: (1) марка, (2) модель и (3) год выпуска. Кроме того, мы можем проверить эти поля, чтобы узнать, снабжено ли каждое поле определенной аннотацией.
Таким образом, мы можем выполнить итерацию по каждому полю связанного класса объекта параметра, переданного методу, и обнаружить, какие поля аннотированы с помощью @JsonField. Если поле помечено @JsonField, мы запишем имя поля и его значение. После обработки всех полей мы можем создать строки JSON, используя имена и значения этих полей.
Определение имени поля требует более сложной логики, чем определение значения. Если @JsonField содержит предоставленное значение параметра value (например, @JsonField("manufacturer"), использованное перед "manufacturer"), мы будем использовать предоставленное имя поля. Если значение параметра значения представляет собой пустую строку, мы знаем, что имя поля не было указано явно (поскольку это значение по умолчанию для параметра значения), в противном случае явно предоставляется пустая строка. В этих последних случаях мы будем использовать имя переменной поля в качестве имени поля (например, в объявлении модели private final String).
Объедините эту логику в класс JsonSerializer:

public class JsonSerializer {
    public String serialize(Object object) throws JsonSerializeException {
        try {
            Class<?> objectClass = requireNonNull(object).getClass();
            Map<String, String> jsonElements = new HashMap<>();
            for (Field field : objectClass.getDeclaredFields()) {
                field.setAccessible(true);
                if (field.isAnnotationPresent(JsonField.class)) {
                    jsonElements.put(getSerializedKey(field), (String) field.get(object));
                }
            }
            System.out.println(toJsonString(jsonElements));
            return toJsonString(jsonElements);
        } catch (IllegalAccessException e) {
            throw new JsonSerializeException(e.getMessage());
        }
    }

    private String toJsonString(Map<String, String> jsonMap) {
        String elementsString = jsonMap.entrySet().stream().map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"").collect(Collectors.joining(","));
        return "{" + elementsString + "}";
    }

    private static String getSerializedKey(Field field) {
        String annotationValue = field.getAnnotation(JsonField.class).value();
        if (annotationValue.isEmpty()) {
            return field.getName();
        } else {
            return annotationValue;
        }
    }
} 

Обратите внимание, что для краткости несколько функций объединены в этот класс. Рефакторинговую версию этого класса сериализатора см. в репозитории кодовой базы.эта ветка(https://github.com/albanoj2/dzone-json-serializer/tree/srp_generalization). Мы также создали исключение, чтобы указать, произошла ли ошибка во время обработки объекта методом сериализации:

public class JsonSerializeException extends Exception {
    private static final long serialVersionUID = -8845242379503538623L;

    public JsonSerializeException(String message) {
        super(message);
    }
} 

Хотя класс JsonSerializer выглядит сложным, он содержит три основные задачи: (1) найти все поля, аннотированные с помощью @JsonField, (2) записать имена всех полей, которые содержат аннотацию @JsonField (или явно заданные имена полей)) и значение, и (3) преобразовать записанное имя поля и пару "ключ-значение" в строку JSON.

requireNonNull(object).getClass() проверяет, что предоставленный объект не является нулевым (если это так, генерирует исключение NullPointerException) и получает объект, связанный с предоставленным объектомClassобъект. И используйте связанный класс этого объекта, чтобы получить связанные поля. Затем мы создаем сопоставление String to String, в котором хранятся пары «ключ-значение» имен и значений полей.
Когда структура данных установлена, пришло время перебрать каждое поле, объявленное в классе. Для каждого поля мы настраиваем подавление проверок доступа к языку Java при доступе к полю. Это очень важный шаг, потому что поля, которые мы аннотировали, являются приватными. В стандартном случае мы не сможем получить доступ к этим полям, а попытка получить значение приватного поля вызовет выброс IllegalAccessException. Чтобы получить доступ к этим закрытым полям, мы должны отключить стандартные проверки доступа Java для этого поля. setAccessible(boolean) определяется следующим образом:
Возвращаемое значение true указывает, что отражающий объект должен отключить проверки доступа к языку Java. false указывает, что отраженный объект должен применять проверки доступа к языку Java.
Обратите внимание, что с появлением модулей в Java 9 использование метода setAccessible требует, чтобы пакет, содержащий класс, обращающийся к его закрытым полям, был объявлен открытым в определении модуля. Для получения дополнительной информации см.это объяснение Михала ШевчикаиAccessing Private State of Java 9 Modules by Gunnar Morling.
Получив доступ к полю, мы проверяем, аннотировано ли поле с помощью @JsonField. Если это так, мы определяем имя поля (через явное имя или имя по умолчанию, указанное в аннотации @JsonField) и записываем имя и значение поля в карту, которую мы построили ранее. После обработки всех полей мы конвертируем карту имен полей в строку JSON.
После обработки всех записей мы объединяем все эти строки запятыми. Это создает строку "":"","":"",... . После объединения этой строки мы заключаем ее в фигурные скобки, чтобы создать допустимую строку JSON.
Чтобы протестировать этот сериализатор, мы можем выполнить следующий код:

Car car=new Car("Ford","F150","2018");
JsonSerializer serializer=new JsonSerializer();
serializer.serialize(car); 

вывод:

{"model":"F150","manufacturer":"Ford"}
Как и ожидалось, поля производителя и модели объекта Car были сериализованы с использованием имени поля в качестве ключа и значения поля в качестве значения. Обратите внимание, что порядок элементов JSON может быть обратным по сравнению с выводом, показанным выше. Это происходит потому, что для объявленного массива полей класса нет явного порядка, как вДокументация по getDeclaredFieldsкак указано в:
Элементы в возвращаемом массиве не отсортированы и не расположены в определенном порядке.
Из-за этого ограничения порядок элементов в строке JSON может отличаться. Чтобы сделать порядок элементов детерминированным, мы должны сами навязать порядок. Поскольку объекты JSON определяются как неупорядоченный набор пар ключ-значение, согласностандарт JSON, принудительная сортировка не требуется. Однако обратите внимание, что тестовый пример для метода сериализации должен выводить {"модель":"F150","производитель":"Ford"} или {"производитель":"Ford","model":"F150"}.

В заключение

Аннотации Java — очень мощная функция языка Java, но в основном мы используем стандартные аннотации (такие как @Override) или общие аннотации фреймворка (такие как @Autowired), а не разработчики. Хотя аннотации не следует использовать вместо этого в объектно-ориентированном стиле, они могут значительно упростить повторяющуюся логику. Например, мы можем аннотировать каждое сериализуемое поле вместо метода в интерфейсе, чтобы создать toJsonString, и все сериализуемые классы реализуют этот интерфейс. Он также отделяет логику сериализации от логики предметной области, удаляя беспорядок ручной сериализации из простой логики предметной области.
Хотя пользовательские аннотации редко используются в большинстве приложений Java, это функция, которую должен понимать любой пользователь языка Java среднего или продвинутого уровня. Знание этой функции не только расширяет базу знаний разработчика, но и помогает понять общие аннотации в самых популярных средах Java.
Общедоступный номер: Galaxy № 1
Контактный адрес электронной почты: public@space-explore.com
(Пожалуйста, не перепечатывайте без разрешения)