¿Cómo encuentra todas las subclases de una clase dada en Java?

207

¿Cómo se puede tratar de encontrar todas las subclases de una clase determinada (o todos los implementadores de una interfaz determinada) en Java? A partir de ahora, tengo un método para hacer esto, pero lo encuentro bastante ineficiente (por decir lo menos). El metodo es:

  1. Obtenga una lista de todos los nombres de clase que existen en la ruta de clase
  2. Cargue cada clase y pruebe para ver si es una subclase o implementador de la clase o interfaz deseada

En Eclipse, hay una buena característica llamada Jerarquía de tipos que logra mostrar esto de manera bastante eficiente. ¿Cómo se hace y se hace programáticamente?

Avrom
fuente
1
Aunque la solución basada en Reflections y Spring parece interesante, necesitaba una solución simple que no tuviera dependencias. Parece que mi código original (con algunos ajustes) fue el camino a seguir.
Avrom
1
¿Seguramente puedes usar el método getSupeClass de forma recursiva?
Estaba buscando específicamente todas las subclases de una clase determinada. getSuperClass no le dirá qué subclases tiene una clase, solo obtendrá la superclase inmediata para una subclase específica. Además, el método isAssignableFrom on Class se adapta mejor a lo que sugiere (sin necesidad de recurrencia).
Avrom
Esta pregunta está vinculada desde muchos otros duplicados, pero no contiene ninguna respuesta Java simple y útil. Suspiro ...
Eric Duminil

Respuestas:

77

No hay otra forma de hacerlo que no sea lo que describiste. Piénselo: ¿cómo puede alguien saber qué clases extienden ClassX sin escanear cada clase en el classpath?

Eclipse solo puede informarle sobre las superclases y subclases en lo que parece ser una cantidad de tiempo "eficiente" porque ya tiene todos los datos de tipo cargados en el punto donde presiona el botón "Mostrar en la jerarquía de tipos" (ya que es compila constantemente tus clases, sabe todo sobre el classpath, etc.

mate b
fuente
23
Ahora hay una biblioteca simple llamada org.reflections que ayuda con esta y otras tareas comunes de reflexión. Con esta biblioteca solo puede llamar a reflections.getSubTypesOf(aClazz)) link
Enwired
@matt b: si tiene que escanear todas las clases, ¿significa que hay una degradación del rendimiento cuando tiene muchas clases en su proyecto, aunque solo unas pocas subclasifiquen su clase?
LeTex
Exactamente. Simplemente toca todas las clases. Puede definir su propio escáner, puede acelerarlo, como excluir ciertos paquetes que sabe que su clase no se ampliaría o simplemente abrir el archivo de clase y verificar si encuentra el nombre de la clase en la sección constante de la clase, evitando que el escáner de reflexión lea un mucha más información sobre clases que ni siquiera contienen la referencia requerida a su superclase (directa) Indirectamente, necesita escanear más. Así que es lo mejor que hay en este momento.
Martin Kersten
La respuesta de fforw funcionó para mí y debe marcarse como la respuesta correcta. Claramente esto es posible con el escaneo classpath.
Farrukh Najmi
Estás equivocado, mira la respuesta a continuación sobre la biblioteca
127

Escanear en busca de clases no es fácil con Java puro.

Spring Framework ofrece una clase llamada ClassPathScanningCandidateComponentProvider que puede hacer lo que necesita. El siguiente ejemplo encontraría todas las subclases de MyClass en el paquete org.example.package

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
}

Este método tiene la ventaja adicional de usar un analizador de código de bytes para encontrar los candidatos, lo que significa que no cargará todas las clases que escanea.

adelante
fuente
23
False se debe pasar como parámetro al crear ClassPathScanningCandidateComponentProvider para deshabilitar los filtros predeterminados. Los filtros predeterminados coincidirán con otros tipos de clases, por ejemplo, cualquier cosa anotada con @Component. Solo queremos que AssignableTypeFilter esté activo aquí.
MCDS
Dices que no es fácil, pero ¿cómo lo haríamos si quisiéramos hacerlo con Java puro?
Aequitas
49

Esto no es posible hacerlo utilizando solo la API Java Reflections incorporada.

Existe un proyecto que realiza el escaneo e indexación necesarios de su classpath para que pueda acceder a esta información ...

Reflexiones

Un análisis de metadatos en tiempo de ejecución de Java, en el espíritu de Scannotations

Reflections escanea su classpath, indexa los metadatos, le permite consultarlo en tiempo de ejecución y puede guardar y recopilar esa información para muchos módulos dentro de su proyecto.

Usando Reflections puede consultar sus metadatos para:

  • obtener todos los subtipos de algún tipo
  • obtener todos los tipos anotados con alguna anotación
  • obtener todos los tipos anotados con alguna anotación, incluida la coincidencia de parámetros de anotación
  • obtener todos los métodos anotados con algunos

(descargo de responsabilidad: no lo he usado, pero la descripción del proyecto parece ajustarse exactamente a sus necesidades).

Mark Renouf
fuente
1
Interesante. El proyecto parece tener algunas dependencias que su documentación no parece mencionar. A saber (los que he encontrado hasta ahora): javaassist, log4J, XStream
Avrom
3
Incluí este proyecto con Maven y funcionó bien. Obtener subclases es en realidad el primer ejemplo de código fuente y tiene dos líneas de largo :-)
KarlsFriend
¿No es posible usar solo la API Java Reflections incorporada o simplemente es muy inconveniente hacerlo?
Flujo
1
¡Tenga cuidado cuando vaya a usar Reflections y luego implemente su aplicación WAR en GlassFish! Existe un conflicto en la biblioteca de Guava y la implementación fallará con un error Error de implementación de CDI: WELD-001408 : consulte GLASSFISH-20579 para obtener más detalles. FastClasspathScanner es una solución en este caso.
lu_ko
Solo intento este proyecto y funciona. Solo lo uso para mejorar el patrón de diseño de la estrategia y obtener toda la clase concreta de estrategia (subclase). Compartiré la demostración más adelante.
Xin Meng
10

No olvide que el Javadoc generado para una clase incluirá una lista de subclases conocidas (y para interfaces, clases de implementación conocidas).

Robar
fuente
3
esto es totalmente incorrecto, la superclase no debería depender de sus subclases, ni siquiera en javadoc o comentario.
cazador
@hunter no estoy de acuerdo. Es totalmente correcto que JavaDoc incluya una lista de subclases conocidas . Por supuesto, "conocido" puede no incluir la clase que está buscando, pero para algunos casos de uso será suficiente.
Qw3ry
Y puede perder algunas clases en cualquier caso: puedo cargar un nuevo jar en el classpath (durante el tiempo de ejecución) y cada detección que ocurrió antes fallará.
Qw3ry
10

Prueba ClassGraph . (Descargo de responsabilidad, yo soy el autor). ClassGraph admite la exploración de subclases de una clase determinada, ya sea en tiempo de ejecución o en tiempo de compilación, pero también mucho más. ClassGraph puede construir una representación abstracta de todo el gráfico de clase (todas las clases, anotaciones, métodos, parámetros de método y campos) en la memoria, para todas las clases en el classpath, o para las clases en paquetes incluidos en la lista blanca, y puede consultar ese gráfico de clase sin embargo usted quiere. ClassGraph admite más mecanismos de especificación de classpath y cargadores de clases que cualquier otro escáner, y también funciona a la perfección con el nuevo sistema de módulos JPMS, por lo que si basa su código en ClassGraph, su código será máximamente portátil. Vea la API aquí.

Luke Hutchison
fuente
9

Lo hice hace varios años. La forma más confiable de hacerlo (es decir, con las API oficiales de Java y sin dependencias externas) es escribir un doclet personalizado para producir una lista que se pueda leer en tiempo de ejecución.

Puede ejecutarlo desde la línea de comando de esta manera:

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

o ejecutarlo desde una hormiga como esta:

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

Aquí está el código básico:

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;
        }
    }
}

Para simplificar, he eliminado el análisis de argumentos de la línea de comandos y estoy escribiendo en System.out en lugar de un archivo.

David Leppik
fuente
Si bien usar esto mediante programación puede ser complicado, debo decir: ¡un enfoque inteligente !
Janaka Bandara
8

Sé que llegué unos años tarde a esta fiesta, pero me encontré con esta pregunta tratando de resolver el mismo problema. Puede utilizar la búsqueda interna de Eclipse mediante programación, si está escribiendo un complemento de Eclipse (y, por lo tanto, aprovecha su almacenamiento en caché, etc.), para encontrar clases que implementen una interfaz. Aquí está mi primer corte (muy áspero):

  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();
    }
  }

El primer problema que veo hasta ahora es que solo estoy captando clases que implementan directamente la interfaz, no todas sus subclases, pero una pequeña recursión nunca perjudica a nadie.

Curtis
fuente
3
O, resulta que no tienes que hacer tu propia búsqueda. Puede obtener una ITypeHierarchy directamente desde su IType llamando a .newTypeHierarchy () en ella: dev.eclipse.org/newslists/news.eclipse.tools.jdt/msg05036.html
Curtis
7

Teniendo en cuenta las limitaciones mencionadas en las otras respuestas, también puede usar openpojo'sPojoClassFactory ( disponible en Maven ) de la siguiente manera:

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

¿Dónde packageRootestá la cadena raíz de los paquetes en los que desea buscar (por ejemplo, "com.mycompany"o incluso solo "com"), y Superclasses su supertipo (esto también funciona en las interfaces)?

mikołak
fuente
Con mucho, la solución más rápida y elegante de las sugeridas.
KidCrippler
4

Solo escribo una demostración simple para usar org.reflections.Reflectionspara obtener subclases de clase abstracta:

https://github.com/xmeng1/ReflectionsDemo

Xin Meng
fuente
Sin embargo, sería más productivo publicar una muestra de código aquí en lugar de un enlace con muchas clases para excavar.
JesseBoyd
4

Dependiendo de sus requisitos particulares, en algunos casos, el mecanismo del cargador de servicios de Java puede lograr lo que busca.

En resumen, permite a los desarrolladores declarar explícitamente que una clase subclasifica alguna otra clase (o implementa alguna interfaz) al incluirla en un archivo en el META-INF/servicesdirectorio del archivo JAR / WAR . Luego se puede descubrir usando la java.util.ServiceLoaderclase que, cuando se le da un Classobjeto, generará instancias de todas las subclases declaradas de esa clase (o, si Classrepresenta una interfaz, todas las clases que implementan esa interfaz).

La principal ventaja de este enfoque es que no es necesario escanear manualmente todo el classpath en busca de subclases: toda la lógica de descubrimiento está contenida dentro de la ServiceLoaderclase, y solo carga las clases declaradas explícitamente en el META-INF/servicesdirectorio (no todas las clases en el classpath) .

Sin embargo, hay algunas desventajas:

  • No encontrará todas las subclases, solo aquellas que se declaren explícitamente. Como tal, si realmente necesita encontrar todas las subclases, este enfoque puede ser insuficiente.
  • Requiere que el desarrollador declare explícitamente la clase en el META-INF/servicesdirectorio. Esta es una carga adicional para el desarrollador y puede ser propensa a errores.
  • El ServiceLoader.iterator()genera instancias de subclase, no sus Classobjetos. Esto causa dos problemas:
    • No tiene nada que decir sobre cómo se construyen las subclases: el constructor sin argumentos se utiliza para crear las instancias.
    • Como tal, las subclases deben tener un constructor predeterminado o declarar explícitamente un constructor sin argumentos.

Aparentemente, Java 9 abordará algunas de estas deficiencias (en particular, las relacionadas con la creación de instancias de subclases).

Un ejemplo

Supongamos que está interesado en encontrar clases que implementen una interfaz com.example.Example:

package com.example;

public interface Example {
    public String getStr();
}

La clase com.example.ExampleImplimplementa esa interfaz:

package com.example;

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

Declararía que la clase ExampleImples una implementación Exampleal crear un archivo que META-INF/services/com.example.Examplecontenga el texto com.example.ExampleImpl.

Luego, puede obtener una instancia de cada implementación de Example(incluida una instancia de ExampleImpl) de la siguiente manera:

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.
Mac
fuente
3

También debe tenerse en cuenta que, por supuesto, esto solo encontrará todas las subclases que existen en su classpath actual. Presumiblemente, esto está bien para lo que está viendo actualmente, y es probable que lo haya considerado, pero si en algún momento ha lanzado unfinal clase en la naturaleza (para diferentes niveles de "salvaje"), entonces es completamente factible que alguien más ha escrito su propia subclase que no conocerá.

Por lo tanto, si desea ver todas las subclases porque desea hacer un cambio y verá cómo afecta el comportamiento de las subclases, tenga en cuenta las subclases que no puede ver. Idealmente todos sus métodos no privados, y la clase en sí misma debe estar bien documentada; realice cambios de acuerdo con esta documentación sin cambiar la semántica de los métodos / campos no privados y sus cambios deben ser compatibles con versiones anteriores, para cualquier subclase que siga su definición de la superclase al menos.

Andrzej Doyle
fuente
3

La razón por la que ve una diferencia entre su implementación y Eclipse es porque escanea cada vez, mientras que Eclipse (y otras herramientas) escanea solo una vez (durante la carga del proyecto la mayoría de las veces) y crea un índice. La próxima vez que solicite los datos, no volverá a escanear, pero mire el índice.

OscarRyz
fuente
3

Estoy usando una biblioteca de reflexión, que escanea su classpath para todas las subclases: https://github.com/ronmamo/reflections

Así es como se haría:

Reflections reflections = new Reflections("my.project");
Set<Class<? extends SomeType>> subTypes = reflections.getSubTypesOf(SomeType.class);
futchas
fuente
2

Agréguelos a un mapa estático dentro (this.getClass (). GetName ()) el constructor de clases padre (o cree uno predeterminado) pero esto se actualizará en tiempo de ejecución. Si la inicialización diferida es una opción, puede probar este enfoque.

Ravindranath Akila
fuente
0

Necesitaba hacer esto como un caso de prueba, para ver si se habían agregado nuevas clases al código. Esto es lo que hice

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());
        }
    }
}
Cutetare
fuente