Для тех кто торопится и пришел за готовым решением
Да, вы можете засунуть несколько аннотаций в одну и использовать только ее. И что самое интересное - да, вы можете передавать аттрибуты в аннотации которые вы объединили (передавать атрибуты в мета-аннотации).
Как это сделать? Очень просто!
Создать аннотацию
Аннотировать ее всеми аннотациями, которые необходимо скомпоновать
Пометить аннотацией
@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
которых, на мой взгляд, в сети маловато... На мой неопытный взгляд такой способ группировки аннотаций экономит целую кучу времени. Ну и самое главное - код становится значительно более читаем.
UPD: Все вышеперечисленные методы доступны только в Spring!
Комментарии (11)
igormich88
02.12.2023 19:21+2Я правильно понимаю статья про скорее про Spring, а не про чистую Java?
Shepardcmndr Автор
02.12.2023 19:21+1Да, вы абсолютно правы! Я сейчас же внесу ясность. Спасибо, что указали на неточность!
ruomserg
02.12.2023 19:21+2Кстати, да! Автор не указал (впрочем, в том контексте в котором он это делает - скорее всего даже не задумывался - потому что внутренние аннотации фреймворка только так и делают) что это исключительно spring-based штука, и работает оно только при доступе к аннотациям через сервисные классы спринга!
"...For alias semantics to be enforced, annotations must be loaded via the utility methods in
AnnotationUtils
. Behind the scenes, Spring will synthesize an annotation by wrapping it in a dynamic proxy that transparently enforces attribute alias semantics for annotation attributes..." (C) Spring docsShepardcmndr Автор
02.12.2023 19:21+2Да, вы абсолютно правы, я даже и не задумался в контексте происходящего! Поправил текст, что бы исключить недопонимание. Спасибо!)
souls_arch
02.12.2023 19:21-1Это чтоб враги (коллеги или любой, кто ваш код потом лопатить будет) не догадались? Так можно не только анотации прятать. Можно, чтобы вместо блока кода было написано - "...some code there". Jvm то плевать, а люди сразу поймут - мастер писал и спасибо скажут! Особенно поездато такой код читать с блокнота смартфона(ситуации разные бывают). Где справочка то уже не сработает.
Shepardcmndr Автор
02.12.2023 19:21Согласен, все это дело действительно сложно читать, а поддерживать… Уф!
Если необходимо уменьшить количество бойлерплейт-кода + улучшить читаемость (относительно аннотаций), то почему бы и нет?
Из опыта - не более 2*мета уровня вполне жизнеспособны и не превращаются в кашу. А если нужно укорачивать такие штуки как Swagger из статьи - то достаточно просто писать очевидные названия аннотаций.
igormich88
02.12.2023 19:21+3По моему если дать объединенной аннотации хорошее имя, то это будет более читабельно чем разбирать столбик из десятка аннотаций, но тут надо рассматривать конкретные примеры кода. Например пример из статьи с AuthRequired выглядит достаточно убедительно.
PS Спринг по моему в принципе про написание аннотаций вместо кода.
Rive
02.12.2023 19:21+1Немного похоже по духу на сложные комментарии JS Doc в Javascript, когда мероприятие "хочу подсветку типов полей и аргументов" зашло слишком далеко, и в ход пошла тяжёлая артиллерия типа наследования одних типов объектов от других и запрятывания переиспользуемых типов в отдельные файлы, к которым имеют доступ разные части проекта для более ясного представления о типах пакетов, которые они друг другу шлют.
Правда, в отличие от аннотаций Spring - это не оказывает ни малейшего прямого влияния на ход исполнения кода. По сути это просто ещё одна система тестов, которая сильно ускоряет добавление новых аргументов или полей объектов, если вдруг понадобилось это сделать: проблемы в коде будут подсвечены IDE.
Shepardcmndr Автор
02.12.2023 19:21О! Вы очень точно подобрали словосочетание «когда мероприятие зашло слишком далеко»))
И да, такая штука может оказывать влияние на ход исполнения кода. Тут просто стоит оставаться осторожным. И конечно не стоит прятать важную логику таким образом, но если это нечто описательное, не имеющее прямого влияния на логику, то почему бы нет?
tatyana_august
Спасибо.
Изучаю сейчас Java, не встречала, что можно аннотации объединять.
Shepardcmndr Автор
Вам спасибо) Мне очень приятно, что моя статья показалась вам интересной!