Компоновка аннотаций в Spring
Для тех кто торопится и пришел за готовым решением
Да, вы можете засунуть несколько аннотаций в одну и использовать только ее. И что самое интересное - да, вы можете передавать аттрибуты в аннотации которые вы объединили (передавать атрибуты в мета-аннотации).
Как это сделать? Очень просто!
- Создать аннотацию
- Аннотировать ее всеми аннотациями, которые необходимо скомпоновать
- Пометить аннотацией
@AliasFor
аттрибуты, которые необходимо передать в группируемые аннотации - ВЫ ПРЕКРАСНЫ!
@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
которых, на мой взгляд, в сети маловато... На мой неопытный взгляд такой способ группировки аннотаций экономит целую кучу времени. Ну и самое главное - код становится значительно более читаем.