222

如何在 Java 中查找给定类的所有子类(或给定接口的所有实现者)?到目前为止,我有一种方法可以做到这一点,但我发现它效率很低(至少可以这么说)。方法是:

  1. 获取类路径上存在的所有类名的列表
  2. 加载每个类并测试它是否是所需类或接口的子类或实现者

在 Eclipse 中,有一个很好的特性,称为类型层次结构,它可以非常有效地显示这一点。如何以编程方式进行操作?

4

18 回答 18

138

使用纯 Java 扫描类并不容易。

Spring 框架提供了一个名为ClassPathScanningCandidateComponentProvider的类,可以满足您的需要。以下示例将在包 org.example.package 中找到 MyClass 的所有子类

ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(MyClass.class));

// scan in org.example.package
Set<BeanDefinition> components = provider.findCandidateComponents("org/example/package");
for (BeanDefinition component : components)
{
    Class cls = Class.forName(component.getBeanClassName());
    // use class cls found
}

这种方法的额外好处是使用字节码分析器来查找候选者,这意味着它不会加载它扫描的所有类。

于 2009-01-30T15:13:56.260 回答
81

除了您所描述的,没有其他方法可以做到这一点。想一想——如果不扫描类路径上的每个类,谁能知道哪些类扩展了 ClassX?

Eclipse 只能在看似“有效”的时间内告诉您有关超类和子类的信息,因为它已经在您按下“在类型层次结构中显示”按钮时加载了所有类型数据(因为它是不断地编译你的类,知道类路径上的一切,等等)。

于 2009-01-29T15:58:52.300 回答
49

仅使用内置的 Java 反射 API 无法做到这一点。

存在一个项目,对您的类路径进行必要的扫描和索引,以便您可以访问此信息...

思考

Java 运行时元数据分析,本着 Scannotations 的精神

Reflections 扫描您的类路径,索引元数据,允许您在运行时查询它,并且可以为项目中的许多模块保存和收集该信息。

使用反射,您可以查询元数据:

  • 获取某种类型的所有子类型
  • 获取所有带有一些注释的类型
  • 获取所有带有一些注解的类型,包括注解参数匹配
  • 获取所有带有一些注释的方法

(免责声明:我没有使用它,但该项目的描述似乎完全符合您的需求。)

于 2009-01-29T16:02:26.537 回答
13

试试ClassGraph。(免责声明,我是作者)。ClassGraph 支持在运行时或构建时扫描给定类的子类,但还支持更多。ClassGraph 可以在内存中为类路径上的所有类或选定包中的类构建整个类图(所有类、注释、方法、方法参数和字段)的抽象表示,但是您可以查询这个类图你要。ClassGraph 支持比任何其他扫描器更多的类路径规范机制和类加载器,并且还可以与新的 JPMS 模块系统无缝协作,因此如果您的代码基于 ClassGraph,您的代码将具有最大的可移植性。请参阅此处的 API。

于 2015-09-24T14:55:53.397 回答
11

不要忘记为一个类生成的Javadoc将包含一个已知子类的列表(对于接口,还有已知的实现类)。

于 2009-01-29T22:57:11.317 回答
9

我知道我参加这个聚会晚了几年,但我遇到了这个问题,试图解决同样的问题。如果您正在编写 Eclipse 插件(并因此利用它们的缓存等),您可以以编程方式使用 Eclipse 的内部搜索来查找实现接口的类。这是我的(非常粗略的)第一次剪辑:

  protected void listImplementingClasses( String iface ) throws CoreException
  {
    final IJavaProject project = <get your project here>;
    try
    {
      final IType ifaceType = project.findType( iface );
      final SearchPattern ifacePattern = SearchPattern.createPattern( ifaceType, IJavaSearchConstants.IMPLEMENTORS );
      final IJavaSearchScope scope = SearchEngine.createWorkspaceScope();
      final SearchEngine searchEngine = new SearchEngine();
      final LinkedList<SearchMatch> results = new LinkedList<SearchMatch>();
      searchEngine.search( ifacePattern, 
      new SearchParticipant[]{ SearchEngine.getDefaultSearchParticipant() }, scope, new SearchRequestor() {

        @Override
        public void acceptSearchMatch( SearchMatch match ) throws CoreException
        {
          results.add( match );
        }

      }, new IProgressMonitor() {

        @Override
        public void beginTask( String name, int totalWork )
        {
        }

        @Override
        public void done()
        {
          System.out.println( results );
        }

        @Override
        public void internalWorked( double work )
        {
        }

        @Override
        public boolean isCanceled()
        {
          return false;
        }

        @Override
        public void setCanceled( boolean value )
        {
        }

        @Override
        public void setTaskName( String name )
        {
        }

        @Override
        public void subTask( String name )
        {
        }

        @Override
        public void worked( int work )
        {
        }

      });

    } catch( JavaModelException e )
    {
      e.printStackTrace();
    }
  }

到目前为止,我看到的第一个问题是我只捕获直接实现接口的类,而不是它们的所有子类——但是一点递归不会伤害任何人。

于 2011-04-20T19:29:56.580 回答
9

几年前我就这样做了。最可靠的方法(即使用官方 Java API 且没有外部依赖项)是编写自定义 doclet 以生成可在运行时读取的列表。

您可以像这样从命令行运行它:

javadoc -d build -doclet com.example.ObjectListDoclet -sourcepath java/src -subpackages com.example

或者像这样从 ant 运行它:

<javadoc sourcepath="${src}" packagenames="*" >
  <doclet name="com.example.ObjectListDoclet" path="${build}"/>
</javadoc>

这是基本代码:

public final class ObjectListDoclet {
    public static final String TOP_CLASS_NAME =  "com.example.MyClass";        

    /** Doclet entry point. */
    public static boolean start(RootDoc root) throws Exception {
        try {
            ClassDoc topClassDoc = root.classNamed(TOP_CLASS_NAME);
            for (ClassDoc classDoc : root.classes()) {
                if (classDoc.subclassOf(topClassDoc)) {
                    System.out.println(classDoc);
                }
            }
            return true;
        }
        catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }
}

为简单起见,我删除了命令行参数解析,并且正在写入 System.out 而不是文件。

于 2012-05-14T21:08:28.590 回答
7

请记住其他答案中提到的限制,您还可以通过以下方式使用openpojoPojoClassFactory在 Maven 上可用):

for(PojoClass pojoClass : PojoClassFactory.enumerateClassesByExtendingType(packageRoot, Superclass.class, null)) {
    System.out.println(pojoClass.getClazz());
}

packageRoot您希望搜索的包的根字符串在哪里(例如"com.mycompany",甚至只是"com"),并且Superclass是您的超类型(这也适用于接口)。

于 2013-08-11T18:34:56.513 回答
4

我只是写了一个简单的演示来使用org.reflections.Reflections来获取抽象类的子类:

https://github.com/xmeng1/ReflectionsDemo

于 2017-02-02T17:32:36.130 回答
4

根据您的特定要求,在某些情况下,Java 的服务加载器机制可能会实现您所追求的。

简而言之,它允许开发人员通过在 JAR/WAR 文件META-INF/services目录中的文件中列出某个类来显式声明某个类是某个其他类的子类(或实现某个接口)。然后可以使用java.util.ServiceLoader类发现它,当给定一个Class对象时,该类将生成该类的所有声明的子类的实例(或者,如果Class代表一个接口,则所有实现该接口的类)。

这种方法的主要优点是不需要手动扫描整个类路径中的子类——所有发现逻辑都包含在ServiceLoader类中,它只加载META-INF/services目录中显式声明的类(不是类路径上的每个类) .

但是,也有一些缺点:

  • 它不会找到所有子类,只会找到那些明确声明的子类。因此,如果您需要真正找到所有子类,这种方法可能不够用。
  • 它需要开发者显式声明META-INF/services目录下的类。这对开发人员来说是一个额外的负担,并且容易出错。
  • ServiceLoader.iterator()生成子类实例,而不是它们的对象Class。这会导致两个问题:
    • 您对如何构造子类没有任何发言权 - 无参数构造函数用于创建实例。
    • 因此,子类必须具有默认构造函数,或者必须显式声明无参数构造函数。

显然,Java 9 将解决其中一些缺点(特别是关于子类实例化的缺点)。

一个例子

假设您有兴趣查找实现接口的类com.example.Example

package com.example;

public interface Example {
    public String getStr();
}

该类com.example.ExampleImpl实现了该接口:

package com.example;

public class ExampleImpl implements Example {
    public String getStr() {
        return "ExampleImpl's string.";
    }
}

您可以通过创建包含 text的文件来声明该类ExampleImpl是 的实现。ExampleMETA-INF/services/com.example.Examplecom.example.ExampleImpl

然后,您可以获得每个实现的Example实例(包括 的实例ExampleImpl),如下所示:

ServiceLoader<Example> loader = ServiceLoader.load(Example.class)
for (Example example : loader) {
    System.out.println(example.getStr());
}

// Prints "ExampleImpl's string.", plus whatever is returned
// by other declared implementations of com.example.Example.
于 2017-08-10T04:05:41.860 回答
3

还应该注意的是,这当然只会找到当前类路径中存在的所有子类。大概这对于您当前正在查看的内容是可以的,并且您可能确实考虑过这一点,但是如果您在任何时候将非final类释放到野外(对于不同级别的“野生”),那么完全可行的是其他人编写了他们自己的子类,您不会知道。

因此,如果您碰巧想要查看所有子类,因为您想要进行更改并且将要查看它如何影响子类的行为 - 那么请记住您看不到的子类。理想情况下,您的所有非私有方法和类本身都应该有详细的文档;根据本文档进行更改而不更改方法/非私有字段的语义,并且您的更改应该是向后兼容的,至少对于遵循您的超类定义的任何子类。

于 2009-01-29T16:31:31.610 回答
3

您看到您的实现与 Eclipse 之间存在差异的原因是因为您每次都扫描,而 Eclipse(和其他工具)只扫描一次(在大多数情况下在项目加载期间)并创建索引。下次您请求数据时,它不会再次扫描,而是查看索引。

于 2009-01-29T23:10:50.227 回答
3

我正在使用反射库,它会扫描您的所有子类的类路径:https ://github.com/ronmamo/reflections

这是如何完成的:

Reflections reflections = new Reflections("my.project");
Set<Class<? extends SomeType>> subTypes = reflections.getSubTypesOf(SomeType.class);
于 2017-08-03T08:48:39.707 回答
2

将它们添加到父类构造函数(或创建默认构造函数)内部的静态映射(this.getClass().getName()),但这将在运行时更新。如果延迟初始化是一个选项,您可以尝试这种方法。

于 2015-04-14T04:15:21.300 回答
1

我需要将其作为测试用例来执行,以查看是否已将新类添加到代码中。这就是我所做的

final static File rootFolder = new File(SuperClass.class.getProtectionDomain().getCodeSource().getLocation().getPath());
private static ArrayList<String> files = new ArrayList<String>();
listFilesForFolder(rootFolder); 

@Test(timeout = 1000)
public void testNumberOfSubclasses(){
    ArrayList<String> listSubclasses = new ArrayList<>(files);
    listSubclasses.removeIf(s -> !s.contains("Superclass.class"));
    for(String subclass : listSubclasses){
        System.out.println(subclass);
    }
    assertTrue("You did not create a new subclass!", listSubclasses.size() >1);     
}

public static void listFilesForFolder(final File folder) {
    for (final File fileEntry : folder.listFiles()) {
        if (fileEntry.isDirectory()) {
            listFilesForFolder(fileEntry);
        } else {
            files.add(fileEntry.getName().toString());
        }
    }
}
于 2018-04-20T16:51:31.237 回答
1

您可以使用 org.reflections 库,然后创建 Reflections 类的对象。使用此对象,您可以获得给定类的所有子类的列表。 https://www.javadoc.io/doc/org.reflections/reflections/0.9.10/org/reflections/Reflections.html

    Reflections reflections = new Reflections("my.project.prefix");
    System.out.println(reflections.getSubTypesOf(A.class)));
于 2020-03-05T11:07:57.773 回答
0

如果您打算加载同一包中给定类的所有子类,您可以这样做:

public static List<Class> loadAllSubClasses(Class pClazz) throws IOException, ClassNotFoundException {
    ClassLoader classLoader = pClazz.getClassLoader();
    assert classLoader != null;
    String packageName = pClazz.getPackage().getName();
    String dirPath = packageName.replace(".", "/");
    Enumeration<URL> srcList = classLoader.getResources(dirPath);

    List<Class> subClassList = new ArrayList<>();
    while (srcList.hasMoreElements()) {
        File dirFile = new File(srcList.nextElement().getFile());
        File[] files = dirFile.listFiles();
        if (files != null) {
            for (File file : files) {
                String subClassName = packageName + '.' + file.getName().substring(0, file.getName().length() - 6);
                if (! subClassName.equals(pClazz.getName())) {
                    subClassList.add(Class.forName(subClassName));
                }
            }
        }
    }

    return subClassList;
}
于 2020-12-11T08:59:36.680 回答
0

查找类路径中的所有类

public static List<String> getClasses() {
        URLClassLoader urlClassLoader = (URLClassLoader) Thread.currentThread().getContextClassLoader();
        List<String> classes = new ArrayList<>();
        for (URL url : urlClassLoader.getURLs()) {
            try {
                if (url.toURI().getScheme().equals("file")) {
                    File file = new File(url.toURI());
                    if (file.exists()) {
                        try {
                            if (file.isDirectory()) {
                                for (File listFile : FileUtils.listFiles(file, new String[]{"class"}, true)) {
                                    String classFile = listFile.getAbsolutePath().replace(file.getAbsolutePath(), "").replace(".class", "");
                                    if (classFile.startsWith(File.separator)) {
                                        classFile = classFile.substring(1);
                                    }
                                    classes.add(classFile.replace(File.separator, "."));
                                }
                            } else {
                                JarFile jarFile = new JarFile(file);
                                if (url.getFile().endsWith(".jar")) {
                                    Enumeration<JarEntry> entries = jarFile.entries();
                                    while (entries.hasMoreElements()) {
                                        JarEntry jarEntry = entries.nextElement();
                                        if (jarEntry.getName().endsWith(".class")) {
                                            classes.add(jarEntry.getName().replace(".class", "").replace("/", "."));
                                        }
                                    }
                                }
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } catch (URISyntaxException e) {
                e.printStackTrace();
            }
        }
        return classes;
    }
于 2021-04-22T04:47:52.540 回答