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