Как аннотации и метааннотации в Java упрощают жизнь разработчику
Как аннотации и метааннотации в Java упрощают жизнь разработчику
В современных фреймворках Java создавать простой и декларативный код помогают аннотации. Это специальные маркеры, которые добавляют к исходному коду некую дополнительную информацию. Например, с помощью аннотаций можно добавлять статические метаданные к классам, методам и переменным, а также ко многим другим элементам исходного кода.
Сами по себе аннотации никак не влияют на исполнение программы. Но они помогают другим инструментам (например, компилятору, фреймворку или даже IDE) использовать эти аннотации и реализовывать некую дополнительную функциональность.
Например, аннотация `@Override` указывает компилятору, что нужно проверить наличие такого же метода в родительском классе; если метод отсутствует, компилятор выдаст ошибку компиляции.
Современные фреймворки предоставляют аннотации, с помощью которых разработчик может писать декларативный код. Это означает, что вместо реализации поведения разработчик описывает (декларирует) желаемое состояние, а за обеспечение этого состояния отвечает фреймворк. Например, разработчик может использовать аннотацию `@Test` над методом, который описывает тест-кейс, а фреймворк JUnit отвечает за обработку аннотации и запуск этого тест-кейса. Применение аннотаций позволяет разработчику быстрее и проще работать с фреймворком и в итоге экономит время для выполнения других задач.
Познакомиться с современными подходами и решениями в разработке и вывести карьеру на новый уровень поможет курс «Мидл Java-разработчик». Программа рассчитана на полгода, на учёбу нужно закладывать около 15 часов в неделю.
- `@Configuration` указывает на то, что класс является конфигурацией;
- `@EnableScheduling` включает автоконфигурацию, которая отвечает за настройку задач по расписанию;
- `@RequestMapping` помечает метод как обработчик HTTP-запросов.
Некоторые аннотации в Spring объединяют в себе несколько других аннотаций, чтобы они работали вместе. Например, аннотация `@SpringBootApplication` объединяет функциональность трёх других аннотаций:
- `@ComponentScan` активирует сканирования компонентов от главного класса приложения;
- `@EnableAutoConfiguration` включает автоконфигурации, подключаемые через зависимости проекта;
- `@SpringBootConfiguration` — аналог `@Configuration`, но используется только для главного класса проекта.
```java
/* ... */
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(/* ... */)
public @interface SpringBootApplication { /* ... */ }
```
☝️ Аннотации, которые объединяют другие аннотации, называются метааннотациями.
В Java нет механизма наследования для аннотации. Если просто поставить одну аннотацию над другой, то она не будет считываться автоматически. Разработчику нужно рекурсивно сканировать все родительские аннотации, чтобы найти нужную.
В Spring этот механизм сканирования реализован в виде специального компонента `AnnotationUtils`, который делегирует свою работу к `MergedAnnotations`. Простыми словами, внутри Spring реализована поддержка «наследования» для аннотаций на уровне работы с ними. Благодаря этому разработчики могут создавать собственные аннотации, которые будут автоматически интегрированы в экосистему Spring без дополнительной настройки.
Рассмотрим на примере. Представьте, что у вас есть несколько Spring-проектов, в каждом из которых нужно подключить функциональность задач по расписанию и поддержку `Security`-аннотаций. Эти функциональности включаются с помощью аннотаций `@EnableScheduling` и `@EnableMethodSecurity`, поэтому главный класс приложения может выглядеть так:
```java
@SpringBootApplication // Основная аннотация для SpringBoot-приложения
@EnableScheduling // Включаем отложенные задачи
@EnableMethodSecurity // Включаем поддержку Security-аннотаций
public class Application {
/* ... */
}
```
В зависимости от того, какая именно функциональность должна присутствовать в каждом из проектов, дублирующаяся часть может быть больше. Чтобы сократить и переиспользовать её, разработчик может создать свою собственную метааннотацию, которая объединит в себе всё остальное:
```java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootApplication
@EnableScheduling
@EnableMethodSecurity
// ... другие дублирующиеся аннотации
public @interface StandardSpringBootApplication {}
Теперь для главного класса можно указать лишь одну аннотацию:
```java
@StandardSpringBootApplication
// Тянет за собой
// - @SpringBootApplication
// - @EnableScheduling
// - @EnableMethodSecurity
public class Application {
/* ... */
}
Логика обработки аннотаций позволяет не только наследовать их, но и переопределять атрибуты родительских аннотаций. Для этого в Spring есть отдельная аннотация — `@AliasFor`.
Чтобы понять, как работает `@AliasFor`, вернёмся к примеру с `@StandardSpringBootApplication`.
Дело в том, что у родительской аннотации `@SpringBootConfiguration` был атрибут `scanBasePackages`, который теперь отсутствует. Если просто добавить такой же атрибут, то он не будет работать: у Spring недостаточно информации, чтобы понять, к чему этот атрибут относится.
Аннотация `@AliasFor` решает эту проблему. Она позволяет однозначно указать, к какой родительской аннотации относится атрибут:
```
/* ... */
@SpringBootApplication
public @interface StandardSpringBootApplication {
// Указываем, атрибут какой аннотации тут переопределяется
@AliasFor(annotation = SpringBootApplication.class, attribute = "scanBasePackages")
String[] scanBasePackages() default {};
}
```
Аннотация `@AliasFor` работает с любой глубиной наследования. Spring будет искать подходящую родительскую аннотацию рекурсивно вверх.
Например, можно указать целевую аннотацию `@ComponentScan` (она прямой родитель для `SpringBootApplication`):
```java
// ...
public @interface StandardSpringBootApplication {
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
}
```
```java
@SpringBootTest
@ActiveProfiles
@DataJpaTest
@RestClientTest
// ...
public @interface StandardSpringBootTest {
/**
* По умолчанию запускаем тесты с профилем test,
* но при необходимости его можно изменить,
*/
@AliasFor(annotation = ActiveProfiles.class, attribute = "profiles")
String[] profiles() default {"test"};
// ... можно реализовать и другие алиасы
}
@StandardSpringBootTest
class OrderControllerTest { /* ... */ }
@StandardSpringBootTest
class CustomerControllerTest { /* ... */ }
@StandardSpringBootTest
class PaymentControllerTest { /* ... */ }
```java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@RestController
@RequestMapping
@Validated
// ... аннотации для OpenAPI, Spring Security, ...
public @interface StandardController {
// ... алиасы для @RequestMapping
}
```
Читать также: