Spring 获取所有带自定义注解的 Bean

1、概览

本文将带你了解如何在 Spring 中获取到所有带自定义注解的 Bean。

不同的 Spring 版本,实现方式也不同。

2、使用 Spring Boot 2.2 或更高版本

从 Spring Boot 2.2 起,可以使用 getBeansWithAnnotation 方法。

创建一个示例。首先,定义自定义注解。注意,要使用 @Retention(RetentionPolicy.RUNTIME) 元注解,以确保程序在运行时可以访问注解:

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

}

现在,用注解定义第一个 Bean(还要添加 @Component 注解):

@Component
@MyCustomAnnotation
public class MyComponent {

}

然后,定义另一个带有注解的 Bean。不过,这次通过 @Configuration 中的 @Bean 方法来创建:

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 5.2 之前的版本中,getBeansWithAnnotation 方法只能检测到类或接口级别上注解的 Bean,无法检测到在工厂方法级别上注解的 Bean。

如果使用旧版本的 Spring,刚才编写的测试就会失败(Spring Boot 2.2 的 Spring 已升级到 5.2):

  • MyComponent Bean 可以被正确检测到,因为注解是在类的级别上。
  • MyService Bean 检测不到,因为它是通过工厂方法创建的。

3.2、用 @Qualifier 装饰自定义注解

有一个相当简单的解决方法:只需用 @Qualifier 来装饰我们的注解即可。

如下:

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

}

现在,可以自动装配上述的两个自定义注解 bean:

@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 Qualifier。

3.3、通过 Factory 方法列出所有 Bean

我们可以使用反射来访问 Bean 的注解,这在所有情况下都起作用且不需要对自定义注解进行任何更改。

由于可以访问 Spring ApplicationContext,那么我们可以:

  • 访问 BeanFactory
  • 查找与每个 Bean 相关的 BeanDefinition
  • 检查 BeanDefinitionsource 是否为 AnnotatedTypeMetadata,这意味着我们可以访问 Bean 的注解
  • 如果 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;
    }
}

也可以使用 Stream 实现:

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、总结

本文介绍了如何在 Spring 中获取带有自定义注解的 Bean,如果你使用的是 Spring Boot 2.2 及其以上版本,可以直接使用 getBeansWithAnnotation 方法。

如果版本较低,那么可以考虑在自定义注解上添加 @Qualifier 或者是通过 BeanFactory 来反射查找带有自定义注解的 Bean。


参考:https://www.baeldung.com/spring-injecting-all-annotated-beans