Компоновка аннотаций в Java

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Для тех кто торопится и пришел за готовым решением

Да, вы можете засунуть несколько аннотаций в одну и использовать только ее. И что самое интересное - да, вы можете передавать аттрибуты в аннотации которые вы объединили (передавать атрибуты в мета-аннотации).

Как это сделать? Очень просто!

  1. Создать аннотацию

  2. Аннотировать ее всеми аннотациями, которые необходимо скомпоновать

  3. Пометить аннотацией @AliasFor аттрибуты, которые необходимо передать в скомпановываемые аннотации

  4. ВЫ ПРЕКРАСНЫ!

@Target({METHOD})                      
@Retention(RUNTIME)      
@Operation(summary = "set user password")
@ApiResponses(value = {
         @ApiResponse(responseCode = "200", description = "OK",
                content = {@Content(mediaType = "application/json",
                        schema = @Schema(
                                description = "default response from server",                            
                                ref = "#/components/schemas/RegisterRsComplexRs"
                        )}),
         @ApiResponse(responseCode = "400", description = "name of error",
                content = {@Content(mediaType = "application/json",
                        schema = @Schema(description = "common error response",
                                         implementation = ErrorRs.class))}),
         @ApiResponse(responseCode = "401", description = "Unauthorized",
                content = @Content),
         @ApiResponse(responseCode = "403", description = "Forbidden",
                content = @Content)}) //---------->
//------------> Аннотации которые необходимо скомпоновать
// Наша кастомная аннотация
public @interface FullSwaggerDescription {

    // Аннотация @AliasFor позволяет передать значение параметра 
    // "myCustomAnnotationSummary" как атрибут аннотации @Operation
    @AliasFor(annotation = Operation.class, attribute = "summary")
    String myCustomAnnotationSummary();
}

Таким образом, это пиршество для глаз (это, кстати, всего один метод контроллера):

@Operation(summary = "set user password")
@ApiResponses(value = {
         @ApiResponse(responseCode = "200", description = "OK",
                content = {@Content(mediaType = "application/json",
                        schema = @Schema(
                                description = "default response from server",
                                //TODO check what is the correct answer
                                ref = "#/components/schemas/RegisterRsComplexRs"
                        )}),
         @ApiResponse(responseCode = "400", description = "name of error",
                content = {@Content(mediaType = "application/json",
                        schema = @Schema(description = "common error response",
                                         implementation = ErrorRs.class))}),
         @ApiResponse(responseCode = "401", description = "Unauthorized",
                content = @Content),
         @ApiResponse(responseCode = "403", description = "Forbidden",
                content = @Content)})
@PutMapping
public RegisterRs<ComplexRs> setPassword(@RequestBody PasswordSetRq passwordSetRq)
            throws BadRequestException {

    return accountService.setPassword(passwordSetRq);
}

Превращается вот в такую скромную, но удобную композицию:

@FullSwaggerDescription(myCustomAnnotationSummary = "set user password")
@PutMapping
public RegisterRs<ComplexRs> setPassword(@RequestBody PasswordSetRq passwordSetRq)
            throws BadRequestException {

    return accountService.setPassword(passwordSetRq);
}

Для примера используются аннотации Swagger'а для документации REST API Spring Boot приложения.


Небольшое вступление для тех, у кого есть свободные уши (глаза).

В процессе создания документации для Swagger'а я ужаснулся от того насколько нечитаемыми стали контроллеры (пример смотри выше).

В следствие беглого анализа проблемы стало очевидно, что описание большинства эндпоинтов мало чем отличается друг от друга. Но. Отличается! Возникла идея создать одну аннотацию (to rule them all), в которую можно было бы просто передавать нужные нам аттрибуты, и тем самым, менять структуру аннотации. Звучит как-то стремно, знаю, выше есть пример того, что я хотел получить в итоге.

А вот с реализацией внезапно и абсолютно неожиданно возникли проблемы. Даже ответ на простой вопрос - "как обьединять аннотации?" оказалось не просто найти. Интернет, похоже, умеет обьединять только "стандартные" @Overrideи @Deprecated...

Обьединение аннотаций:

Вот тут все просто - Собираем все нужные нам аннотации и аннотируем ими свою кастомную. Теперь на все места, где надо прописывать эти аннотации, нам достаточно поставить ее одну. Пример:

@Target({METHOD, TYPE})
@Retention(RUNTIME)
@ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content) //->
@ApiResponse(responseCode = "403", description = "Forbidden", content = @Content) //->
@ApiResponse(responseCode = "200") //->
//----> Аннотации которые необходимо объединить
public @interface AuthRequired {
}

Теперь вместо постоянной вставки трех строк @ApiResponce достаточно поставить одну аннотацию @AuthRequired!

Такое решение может показаться вполне приемлемым. Но оно все еще не идеально... Мы все еще не можем управлять аннотациями в случае, если нам необходимо что-то поменять (например описание ошибки).

@AliasFor и передача атрибутов в мета-аннотацию

Сразу еще один пример:

@Target({METHOD, TYPE})
@Retention(RUNTIME)
@ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content)
@ApiResponse(responseCode = "403", description = "Forbidden", content = @Content) 
@Operation
public @interface AuthRequired {
  
    @AliasFor(annotation = Operation.class, attribute = "summary")
    String myCustomAnnotationSummary();
}

Благодаря аннотации @AliasFor мы можем использовать аттрибут "myCustomAnnotationSummary" нашей кастомной аннотации , как аттрибут "summary" аннотации @Operation!

То есть, при использовании нашей аннотации @AuthRequired(myCustomAnnotationSummary = "Some text for representation") мы как бы применяем аннотацию @Operation(summary = "Some text for representation") в дополнение ко всем остальным скомпанованным аннотациям.

На самом деле возможности @AliasFor чуть более глубокие:

@Target({METHOD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Operation
public @interface OperationTestLayer1 {
  
    @AliasFor(annotation = Operation.class, attribute = "summary")
    String summary();
}
// Пока ничего нового

@Target({METHOD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@OperationTestLayer1(summary = "This text will be ignored") 
// Аттрибут переданный качестве аргумента конструктора промежуточной аннотации
// в таком случае будет проигнорирован!
public @interface OperationTestLayer2 {
  
    @AliasFor(annotation = OperationTestLayer1.class, attribute = "summary")
    String summary() default "Layer 2 default";
}
// А вот и оно!
// Мы можем передать аттрибут как бы "через" промежуточную аннотацию!

@Target({METHOD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@OperationTestLayer2
public @interface OperationTestLayer3 {

    @AliasFor(annotation = OperationTestLayer2.class, attribute = "summary")
    String summary() default "LAYYYYYEEEEEEERRRR 3!!!!!";
}
// И даже так! Промежуточных аннотаций может быть много! И все равно будет работать!

Таким образом мы можем передавать аттрибут в сколько угодно n*мета-аннотацию! В случае использования значения default - будет использовано значение непосредственно той аннотации, котрая будет использована в коде. Аттрибут переданный качестве аргумента конструктора промежуточной аннотации в таком случае будет проигнорирован!

К сожалению у такого подхода есть ограничение - невозможно передать аргумент в сущность внутри аннотации:

@Target({METHOD, TYPE})
@Retention(RUNTIME)
@ApiResponse(responseCode = "400")
@Content
@Operation
public @interface BadRequestResponseDescription {

    @AliasFor(annotation = Content.class, attribute = "schema")
    Content content() default @Schema(implementation = ErrorRs.class); 
  // Такая конструкция не сработает ни при каких обстоятельствах
  
    @AliasFor(annotation = ApiResponse.class, attribute = "content")
    Content content() default @Content(schema = @Schema(implementation = ErrorRs.class));
  // Для передачи в мета-аннотацию обьект должен быть строго того типа,
  // который указан в той аннотации использование которой мы подразумеваем
  }

В указанном выше примере передать @Schema(implementation = ErrorRs.class) непосредственно в аннотацию @ApiResponse не получится, несмотря на то, что мы указали аннотацию @Content. Для осуществления такого взаимодействия необходимо передать в@ApiResponse полную сущность, если можно так выразиться.

Заключение

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


Дорогой читатель! Искренне благодарю тебя за уделенное мне время. Всего тебе самого хорошего! X))

P.S.: Основной задачей этого очерка было продемонстрировать возможное решение проблемы и представить примеры применения аннотации @AliasFor которых, на мой взгляд, в сети маловато... На мой неопытный взгляд такой способ группировки аннотаций экономит целую кучу времени. Ну и самое главное - код становится значительно более читаем.

Источник: https://habr.com/ru/articles/778064/


Интересные статьи

Интересные статьи

Часто, при разработке сайтов на фреймворках, не придаётся особого внимания деталям, которые в данный фреймворк включены. И это нормально, ведь главная задача фреймворка - чтобы удобно было сайт делать...
Если обновляешься со старой Java на LTS-версию Java 17, как разобраться сразу во всех фичах за несколько лет? Чтобы помочь с этим, мы уже публиковали расшифровку доклада tagir_valeev с нашего IT-фес...
Привет. Меня зовут Ваня, и я Java-разработчик. Так получилось, что я много работаю с PostgreSQL – занимаюсь настройкой БД, оптимизацией структуры, производительностью и немного играю в DBA по вы...
Я начинающий JS front-end разработчик. Сейчас я учусь и стажируюсь в одной минской IT компании. Изучение основ web-ui проходит на примере JS библиотеки Webix и я хочу поделиться своим первым ...
Производительность — это один из важнейших вопросов, встающих перед разработчиками веб-страниц или веб-приложений. Никто не будет рад «падающему» от чрезмерной нагрузки приложению, или странице, ...