1. 引言

在Java世界中,注解是一种获取类和方法元数据的非常有用的方法。本教程将讨论如何在运行时扫描自定义注解。

2. 定义自定义注解

首先,我们定义一个示例注解和使用我们自定义注解的示例类:

@Target({ METHOD, TYPE })
@Retention(RUNTIME)
public @interface SampleAnnotation {
    String name();
}

@SampleAnnotation(name = "annotatedClass")
public class SampleAnnotatedClass {

    @SampleAnnotation(name = "annotatedMethod")
    public void annotatedMethod() {
        //Do something
    }

    public void notAnnotatedMethod() {
        //Do something
    }
}

现在,我们已经准备好解析这个自定义注解在类和方法上的“name”属性。

3. 使用Java反射扫描注解

借助Java的反射机制,我们可以扫描特定类或特定类中特定方法的注解。

为了实现这一目标,我们需要使用ClassLoader加载类。这种方法在我们知道要扫描哪些类时很有用:

Class<?> clazz = ClassLoader.getSystemClassLoader()
  .loadClass("com.baeldung.annotation.scanner.SampleAnnotatedClass");
SampleAnnotation classAnnotation = clazz.getAnnotation(SampleAnnotation.class);
Assert.assertEquals("SampleAnnotatedClass", classAnnotation.name());
Method[] methods = clazz.getMethods();
List<String> annotatedMethods = new ArrayList<>();
for (Method method : methods) {
    SampleAnnotation annotation = method.getAnnotation(SampleAnnotation.class);
    if (annotation != null) {
        annotatedMethods.add(annotation.name());
    }
}
Assert.assertEquals(1, annotatedMethods.size());
Assert.assertEquals("annotatedMethod", annotatedMethods.get(0));

4. 使用Spring上下文库扫描注解

另一种扫描注解的方式是使用Spring上下文库中的ClassPathScanningCandidateComponentProvider类。

首先,添加*spring-context*依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.22</version>
</dependency>

然后,看一个简单的例子:

ClassPathScanningCandidateComponentProvider provider =
  new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AnnotationTypeFilter(SampleAnnotation.class));

Set<BeanDefinition> beanDefs = provider
  .findCandidateComponents("com.baeldung.annotation.scanner");
List<String> annotatedBeans = new ArrayList<>();
for (BeanDefinition bd : beanDefs) {
    if (bd instanceof AnnotatedBeanDefinition) {
        Map<String, Object> annotAttributeMap = ((AnnotatedBeanDefinition) bd)
          .getMetadata()
          .getAnnotationAttributes(SampleAnnotation.class.getCanonicalName());
        annotatedBeans.add(annotAttributeMap.get("name").toString());
    }
}

Assert.assertEquals(1, annotatedBeans.size());
Assert.assertEquals("SampleAnnotatedClass", annotatedBeans.get(0));

与Java反射不同,我们无需知道具体类名即可扫描所有类。

5. 使用Spring核心库扫描注解

虽然Spring核心库不直接提供对代码中所有注解的全面扫描,但我们仍然可以通过使用该库的一些实用工具类来开发自己的全注解扫描器。

首先,添加*spring-core*依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.3.22</version>
</dependency>

下面是一个简单的示例:

Class<?> userClass = ClassUtils.getUserClass(SampleAnnotatedClass.class);

List<String> annotatedMethods = Arrays.stream(userClass.getMethods())
  .filter(method -> AnnotationUtils
  .getAnnotation(method, SampleAnnotation.class) != null)
  .map(method -> method.getAnnotation(SampleAnnotation.class)
  .name())
  .collect(Collectors.toList());

Assert.assertEquals(1, annotatedMethods.size());
Assert.assertEquals("annotatedMethod", annotatedMethods.get(0));

借助AnnotationUtilsClassUtils,可以查找带有特定注解的方法和类。

6. 使用Reflections库扫描注解

Reflections是一个据说遵循Scannotations库精神的库。它扫描并索引项目的类路径元数据。

添加*reflections*依赖:

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.10.2</version>
</dependency>

现在,我们可以使用该库搜索带有注解的类、方法、字段和类型:

Reflections reflections = new Reflections("com.baeldung.annotation.scanner");

Set<Method> methods = reflections
  .getMethodsAnnotatedWith(SampleAnnotation.class);
List<String> annotatedMethods = methods.stream()
  .map(method -> method.getAnnotation(SampleAnnotation.class)
  .name())
  .collect(Collectors.toList());

Assert.assertEquals(1, annotatedMethods.size());
Assert.assertEquals("annotatedMethod", annotatedMethods.get(0));

Set<Class<?>> types = reflections
  .getTypesAnnotatedWith(SampleAnnotation.class);
List<String> annotatedClasses = types.stream()
  .map(clazz -> clazz.getAnnotation(SampleAnnotation.class)
  .name())
  .collect(Collectors.toList());

Assert.assertEquals(1, annotatedClasses.size());
Assert.assertEquals("SampleAnnotatedClass", annotatedClasses.get(0));

如我们所见,Reflections库提供了灵活的扫描所有注解类和方法的方式,无需从SampleAnnotatedClass开始。

7. 使用Jandex库扫描注解

接下来,让我们看看名为Jandex的另一个库,它可以通过读取代码生成的Jandex文件在运行时扫描注解

该库引入了一个Maven插件来生成包含项目相关元信息的Jandex文件:

<plugin>
    <groupId>org.jboss.jandex</groupId>
    <artifactId>jandex-maven-plugin</artifactId>
    <version>1.2.3</version>
    <executions>
        <execution>
            <phase>compile</phase>
            <id>make-index</id>
            <goals>
                <goal>jandex</goal>
            </goals>
            <configuration>
                <fileSets>
                    <fileSet>
                        <directory>${project.build.outputDirectory}</directory>
                    </fileSet>
                </fileSets>
            </configuration>
        </execution>
    </executions>
</plugin>

运行maven-install命令后,文件会生成在classpath下的META-INF目录下,文件名为jandex.idx。如果需要,也可以使用Maven的rename插件修改文件名。

现在,我们准备好扫描任何类型的注解。首先,添加*jandex*依赖:

<dependency>
    <groupId>org.jboss</groupId>
    <artifactId>jandex</artifactId>
    <version>2.4.3.Final</version>
</dependency>

现在,到了扫描注解的时候了:

IndexReader reader = new IndexReader(appFile.getInputStream());
Index jandexFile = reader.read();
List<AnnotationInstance> appAnnotationList = jandexFile
  .getAnnotations(DotName
  .createSimple("com.baeldung.annotation.scanner.SampleAnnotation"));

List<String> annotatedMethods = new ArrayList<>();
List<String> annotatedClasses = new ArrayList<>();
for (AnnotationInstance annotationInstance : appAnnotationList) {
    if (annotationInstance.target().kind() == AnnotationTarget.Kind.METHOD) {
        annotatedMethods.add(annotationInstance.value("name")
          .value()
          .toString());
    }
    if (annotationInstance.target().kind() == AnnotationTarget.Kind.CLASS) {
        annotatedClasses.add(annotationInstance.value("name")
          .value()
          .toString());
    }
}

Assert.assertEquals(1, annotatedMethods.size()); 
Assert.assertEquals("annotatedMethod", annotatedMethods.get(0));
Assert.assertEquals(1, annotatedClasses.size());
Assert.assertEquals("SampleAnnotatedClass", annotatedClasses.get(0));

8. 总结

根据需求,我们有多种在运行时扫描注解的方式。每种方法都有其优缺点。在选择时,应考虑我们的实际需求。

这些示例的实现可在GitHub上找到。