在运行时扫描Java注解

297

如何搜索整个类路径以查找已注释的类?

我正在编写一个库,希望允许用户注释他们的类,因此当Web应用程序启动时,我需要扫描整个类路径以查找特定的注释。

我考虑使用Java EE 5 Web Services或EJB的新功能。您可以使用@WebService@EJB对类进行注释,系统会在加载时找到这些类,因此可以远程访问它们。

13个回答

246

使用org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider

API

一个组件提供者,从基本包扫描类路径。然后,应用排除和包含过滤器来查找候选类。

ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(<DO_YOU_WANT_TO_USE_DEFALT_FILTER>);

scanner.addIncludeFilter(new AnnotationTypeFilter(<TYPE_YOUR_ANNOTATION_HERE>.class));

for (BeanDefinition bd : scanner.findCandidateComponents(<TYPE_YOUR_BASE_PACKAGE_HERE>))
    System.out.println(bd.getBeanClassName());

6
谢谢您提供的信息。您是否知道如何扫描类路径以查找其字段具有自定义注释的类? - javatar
6
请使用Java的反射API。 <YOUR_CLASS>.class.getFields() 对于每个字段,请调用getAnnotation(<YOUR_ANNOTATION>)。 - Arthur Ronald
2
注意:如果您在Spring应用程序中执行此操作,Spring仍将根据@Conditional注释进行评估和操作。因此,如果一个类的@Conditional值返回false,则它不会被findCandidateComponents返回,即使它与扫描器的过滤器匹配。这让我今天感到困惑 - 我最终使用了下面Jonathan的解决方案。 - Adam Burley
1
@ArthurRonald 抱歉,Arthur。我的意思是 BeanDefinition 对象没有直接获取类的方法。最接近的方法似乎是 getBeanClassName,它返回一个完全限定的类名,但这个方法的确切行为不清楚。此外,也不清楚类是在哪个类加载器中找到的。 - Max
4
试试这个:Class<?> cl = Class.forName(beanDef.getBeanClassName());,来源是http://farenda.com/spring/find-annotated-classes/。 - James Watkins
显示剩余6条评论

159

还有另一种解决方案是ronmamoReflections

快速回顾:

  • 如果你正在使用Spring,那么Spring解决方案是最可靠的。否则它是一个庞大的依赖。
  • 直接使用ASM有点麻烦。
  • 直接使用Java Assist也很笨重。
  • Annovention超级轻量级且方便。尚未进行Maven集成。
  • Reflections索引了所有内容,因此非常快速。

59
new Reflections("my.package").getTypesAnnotatedWith(MyAnnotation.class) - zapp
5
我需要指定一个包名吗?通配符呢?为什么要用类路径中的所有类? - eastwater
1
请注意,Java 9支持仍然没有进展:https://github.com/ronmamo/reflections/issues/186 - Vadzim
org.reflections库在Java 13(可能更早版本也是如此)下无法正常工作。第一次调用时似乎没问题,但随后的实例化和使用会失败,并显示搜索URL未配置。 - Evvo
4
Google Reflections的链接无效。它是一个与Google无关的库。 - Lluis Martinez
@LluisMartinez 我同意。我已相应地编辑了答案。 - asherbret

45
你可以使用ClassGraph查找任何给定注释的类,以及搜索其他感兴趣的标准,例如实现给定接口的类。(免责声明,我是ClassGraph的作者。)ClassGraph可以在内存中构建整个类图的抽象表示形式(所有类、注释、方法、方法参数和字段),适用于类路径上的所有类或白名单包中的类,并且您可以随意查询该类图。ClassGraph支持比其他扫描器更多的类路径指定机制和类加载器,并且与新的JPMS模块系统完美协作,因此如果您的代码基于ClassGraph,您的代码将具有最大的可移植性。在此处查看API。

1
这需要Java 8才能运行吗? - David George
1
已更新为Java7,没有问题。只需删除注释并将函数转换为使用匿名内部类即可。我喜欢1个文件的风格。代码很干净,所以即使它不支持我想要的一些东西(同时使用类和注释),我认为添加这些内容也非常容易。做得好!如果有人无法完成修改v7的工作,他们应该考虑使用“Reflections”。此外,如果您正在使用guava / etc并想更改集合,那么这很容易。里面还有很棒的评论。 - Andrew
2
@Alexandros 感谢,你应该看看ClassGraph,它比FastClasspathScanner有显著的改进。 - Luke Hutchison
2
@AndrewBack ClassGraph(FastClasspathScanner的新版本)通过过滤器或集合操作完全支持布尔运算。请在此处查看代码示例:https://github.com/classgraph/classgraph/wiki/Code-examples - Luke Hutchison
1
@Luke Hutchison 已经在使用 ClassGraph。它帮助我迁移到了 Java 10,是一个非常有用的库。 - Alexandros
显示剩余6条评论

28
如果你想要一个真正轻量级(没有依赖,简单API,15 KB的JAR文件)和非常快速的解决方案,请看一下annotation-detector,它可以在这里找到。
免责声明:我是作者。

20
您可以使用Java Pluggable Annotation Processing API编写注解处理器,在编译过程中执行它,收集所有带注解的类并为运行时使用构建索引文件。这是发现带注解类的最快方法,因为您不需要在运行时扫描类路径,这通常是非常缓慢的操作。此外,这种方法适用于任何类加载器,而不仅仅是运行时扫描器通常支持的URLClassLoaders。上述机制已经在ClassIndex库中实现。要使用它,请使用@IndexAnnotated元注释注释自定义注释。这将在编译时创建一个索引文件:META-INF/annotations/com/test/YourCustomAnnotation,其中列出了所有带注解的类。您可以通过执行以下操作在运行时访问该索引:
ClassIndex.getAnnotated(com.test.YourCustomAnnotation.class)

在我的情况下它不起作用。我正在使用服务器运行,Java 11,Tomcat 9.0。 - Yogesh Kumar Gupta

16

有一个很棒的评论是由zapp提出的,它包含在所有这些答案中:

new Reflections("my.package").getTypesAnnotatedWith(MyAnnotation.class)

9

Spring有一个称为AnnotatedTypeScanner类的东西。
这个类内部使用了

ClassPathScanningCandidateComponentProvider

该类包含实际扫描类路径资源的代码。它通过在运行时使用可用的类元数据来实现这一点。

您可以简单地扩展此类或使用相同的类进行扫描。以下是构造函数定义。

/**
     * Creates a new {@link AnnotatedTypeScanner} for the given annotation types.
     * 
     * @param considerInterfaces whether to consider interfaces as well.
     * @param annotationTypes the annotations to scan for.
     */
    public AnnotatedTypeScanner(boolean considerInterfaces, Class<? extends Annotation>... annotationTypes) {

        this.annotationTypess = Arrays.asList(annotationTypes);
        this.considerInterfaces = considerInterfaces;
    }

5
这里有个问题需要回答。我的建议是,最好使用类库,例如ClassPathScanningCandidateComponentProvider 或者 Scannotations
但即便有人想动手尝试使用类加载器,我也编写了一些代码来打印一个包中类的注解信息。请参考以下内容:
public class ElementScanner {

public void scanElements(){
    try {
    //Get the package name from configuration file
    String packageName = readConfig();

    //Load the classLoader which loads this class.
    ClassLoader classLoader = getClass().getClassLoader();

    //Change the package structure to directory structure
    String packagePath  = packageName.replace('.', '/');
    URL urls = classLoader.getResource(packagePath);

    //Get all the class files in the specified URL Path.
    File folder = new File(urls.getPath());
    File[] classes = folder.listFiles();

    int size = classes.length;
    List<Class<?>> classList = new ArrayList<Class<?>>();

    for(int i=0;i<size;i++){
        int index = classes[i].getName().indexOf(".");
        String className = classes[i].getName().substring(0, index);
        String classNamePath = packageName+"."+className;
        Class<?> repoClass;
        repoClass = Class.forName(classNamePath);
        Annotation[] annotations = repoClass.getAnnotations();
        for(int j =0;j<annotations.length;j++){
            System.out.println("Annotation in class "+repoClass.getName()+ " is "+annotations[j].annotationType().getName());
        }
        classList.add(repoClass);
    }
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

/**
 * Unmarshall the configuration file
 * @return
 */
public String readConfig(){
    try{
        URL url = getClass().getClassLoader().getResource("WEB-INF/config.xml");
        JAXBContext jContext = JAXBContext.newInstance(RepositoryConfig.class);
         Unmarshaller um =  jContext.createUnmarshaller();
         RepositoryConfig rc = (RepositoryConfig) um.unmarshal(new File(url.getFile()));
         return rc.getRepository().getPackageName();
        }catch(Exception e){
            e.printStackTrace();
        }
    return null;

}
}

在配置文件中,您需要输入包名并将其反序列化为一个类。

2

4
好的想法,但是如果你将一个 null 值作为第二个参数(用于定义 Spring 将在继承层次结构中扫描使用注解的类的类),那么根据实现,你会始终得到一个 null 返回值。 - jonashackt

2
类加载器API没有“枚举”方法,因为类加载是一个“按需”活动--通常在类路径中有成千上万个类,其中只有一小部分会被需要(仅rt.jar就已经有48MB大小了!)。
因此,即使您可以枚举所有类,这也将非常耗费时间和内存。
简单的方法是在设置文件(xml或其他适合您喜欢的格式)中列出相关类;如果您想自动执行此操作,请限制自己只使用一个JAR或一个类目录。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接