Перейти к основному содержимому

Поиск всех компонентов с пользовательской аннотацией в Spring

· 4 мин. чтения

Задача: Сумма двух

Дано массив целых чисел и целая сумма. Нужно найти индексы двух чисел, сумма которых равна заданной ...

ANDROMEDA

1. Обзор

В этом руководстве мы объясним, как найти все bean-компоненты, аннотированные пользовательской аннотацией в Spring. Мы покажем разные методы в зависимости от используемой версии Spring.

2. С Spring Boot 2.2 или более поздней версии

Начиная с Spring Boot 2.2 мы можем использовать метод getBeansWithAnnotation .

Давайте построим пример. Во-первых, мы определим нашу пользовательскую аннотацию. Давайте аннотируем его с помощью @Retention (RetentionPolicy.RUNTIME) , чтобы программа могла получить доступ к аннотации во время выполнения:

@Retention( RetentionPolicy.RUNTIME )
public @interface MyCustomAnnotation {

}

Теперь давайте определим первый компонент, аннотированный нашей аннотацией. Мы также аннотируем его с помощью @Component :

@Component
@MyCustomAnnotation
public class MyComponent {

}

Затем давайте определим другой компонент, аннотированный нашей аннотацией. Однако на этот раз мы создадим его благодаря аннотированному методу @Bean в файле @Configuration :

public class MyService {

}

@Configuration
public class MyConfigurationBean {

@Bean
@MyCustomAnnotation
MyService myService() {
return new MyService();
}
}

Теперь давайте напишем тест, чтобы проверить, может ли метод getBeansWithAnnotation обнаружить оба наших bean-компонента:

@Test
void whenApplicationContextStarted_ThenShouldDetectAllAnnotatedBeans() {
try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext( MyComponent.class, MyConfigurationBean.class )) {
Map<String,Object> beans = applicationContext.getBeansWithAnnotation(MyCustomAnnotation.class);
assertEquals(2, beans.size());
assertTrue(beans.keySet().containsAll(List.of("myComponent", "myService")));
}
}

3. Со старой версией Spring

3.1. Исторический контекст

В версиях Spring Framework до 5.2 метод getBeansWithAnnotation обнаруживал только бины, аннотированные на уровне класса или интерфейса, но не мог обнаруживать бины, аннотированные на уровне заводского метода.

Зависимость Spring Framework была обновлена до 5.2 в Spring Boot 2.2, поэтому в более старых версиях Spring только что написанный тест не пройден:

  • bean - компонент MyComponent правильно обнаружен, поскольку аннотация находится на уровне класса
  • bean - компонент MyService не обнаружен, поскольку он создан с помощью фабричного метода

Давайте посмотрим, как мы можем обойти это поведение.

3.2. Украсьте нашу пользовательскую аннотацию @Qualifier

Есть довольно простой обходной путь: мы можем просто украсить нашу аннотацию @Qualifier .

Тогда наша аннотация будет выглядеть так:

@Retention( RetentionPolicy.RUNTIME )
@Qualifier
public @interface MyCustomAnnotation {

}

Теперь мы можем автоматически подключить оба аннотированных компонента. Давайте проверим это с помощью теста:

@Autowired
@MyCustomAnnotation
private List<Object> annotatedBeans;

@Test
void whenAutowiring_ThenShouldDetectAllAnnotatedBeans() {
assertEquals(2, annotatedBeans.size());
List<String> classNames = annotatedBeans.stream()
.map(Object::getClass)
.map(Class::getName)
.map(s -> s.substring(s.lastIndexOf(".") + 1))
.collect(Collectors.toList());
assertTrue(classNames.containsAll(List.of("MyComponent", "MyService")));
}

Этот обходной путь является самым простым, однако он может не соответствовать нашим потребностям, например, если у нас нет аннотации .

Отметим также, что украшение нашей пользовательской аннотации @Qualifier превратит ее в квалификатор Spring.

3.3. Список бинов, созданных фабричным методом

Теперь, когда мы поняли, что проблема в основном возникает с bean-компонентами, созданными фабричными методами, давайте сосредоточимся на том, как перечислить только их. Мы представим решение, которое работает во всех случаях, не предполагая каких-либо изменений в нашей пользовательской аннотации. Мы будем использовать отражение для доступа к аннотациям bean-компонентов.

Учитывая, что у нас есть доступ к Spring ApplicationContext , мы выполним ряд шагов:

  • Доступ к BeanFactory
  • Найдите BeanDefinition, связанный с каждым компонентом
  • Проверьте, является ли источником BeanDefinition AnnotatedTypeMetadata , что означает, что мы сможем получить доступ к аннотациям компонента.
  • Если у bean-компонента есть аннотации, проверьте, есть ли среди них нужная аннотация

Давайте создадим собственный служебный класс BeanUtils и реализуем эту логику внутри метода:

public class BeanUtils {

public static List<String> getBeansWithAnnotation(GenericApplicationContext applicationContext, Class<?> annotationClass) {
List<String> result = new ArrayList<String>();
ConfigurableListableBeanFactory factory = applicationContext.getBeanFactory();
for(String name : factory.getBeanDefinitionNames()) {
BeanDefinition bd = factory.getBeanDefinition(name);
if(bd.getSource() instanceof AnnotatedTypeMetadata) {
AnnotatedTypeMetadata metadata = (AnnotatedTypeMetadata) bd.getSource();
if (metadata.getAnnotationAttributes(annotationClass.getName()) != null) {
result.add(name);
}
}
}
return result;
}
}

В качестве альтернативы мы могли бы также написать ту же функцию, используя Streams :

public static List<String> getBeansWithAnnotation(GenericApplicationContext applicationContext, Class<?> annotationClass) {
ConfigurableListableBeanFactory factory = applicationContext.getBeanFactory();
return Arrays.stream(factory.getBeanDefinitionNames())
.filter(name -> isAnnotated(factory, name, annotationClass))
.collect(Collectors.toList());
}

private static boolean isAnnotated(ConfigurableListableBeanFactory factory, String beanName, Class<?> annotationClass) {
BeanDefinition beanDefinition = factory.getBeanDefinition(beanName);
if(beanDefinition.getSource() instanceof AnnotatedTypeMetadata) {
AnnotatedTypeMetadata metadata = (AnnotatedTypeMetadata) beanDefinition.getSource();
return metadata.getAnnotationAttributes(annotationClass.getName()) != null;
}
return false;
}

В этих методах мы использовали GenericApplicationContext , который является реализацией Spring ApplicationContext , не предполагающей определенного формата определения bean-компонента.

Чтобы иметь доступ к GenericApplicationContext , мы можем, например, внедрить его в компонент Spring:

@Component
public class AnnotatedBeansComponent {

@Autowired
GenericApplicationContext applicationContext;

public List<String> getBeansWithAnnotation(Class<?> annotationClass) {
return BeanUtils.getBeansWithAnnotation(applicationContext, annotationClass);
}
}

4. Вывод

В этой статье мы обсудили, как перечислить bean-компоненты, аннотированные заданной аннотацией. Мы видели, что начиная с Spring Boot 2.2 это делается естественным образом с помощью метода getBeansWithAnnotation .

С другой стороны, мы показали некоторые альтернативные методы для преодоления ограничений предыдущего поведения этого метода: либо просто добавив @Qualifier поверх нашей аннотации, либо просматривая bean-компоненты, используя отражение, чтобы проверить, имеют ли они аннотация или нет.

Как всегда, полный код доступен на GitHub .