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

от автора

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

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

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

  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")     Schema schema() 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 полную сущность, если можно так выразиться.

Заключение

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


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

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

UPD: Все вышеперечисленные методы доступны только в Spring!


ссылка на оригинал статьи https://habr.com/ru/articles/778064/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *