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

11e51d511b68dc10acff2ef50ce11eb2

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

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

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

  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 setPassword(@RequestBody PasswordSetRq passwordSetRq)
            throws BadRequestException {

    return accountService.setPassword(passwordSetRq);
}

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

@FullSwaggerDescription(myCustomAnnotationSummary = "set user password")
@PutMapping
public RegisterRs 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 которых, на мой взгляд, в сети маловато… На мой неопытный взгляд такой способ группировки аннотаций экономит целую кучу времени. Ну и самое главное — код становится значительно более читаем.

© Habrahabr.ru