En tiempo de ejecución, encuentre todas las clases en una aplicación Java que extienda una clase base

147

Quiero hacer algo como esto:

List<Animal> animals = new ArrayList<Animal>();

for( Class c: list_of_all_classes_available_to_my_app() )
   if (c is Animal)
      animals.add( new c() );

Entonces, quiero ver todas las clases en el universo de mi aplicación, y cuando encuentro una que desciende de Animal, quiero crear un nuevo objeto de ese tipo y agregarlo a la lista. Esto me permite agregar funcionalidad sin tener que actualizar una lista de cosas. Puedo evitar lo siguiente:

List<Animal> animals = new ArrayList<Animal>();
animals.add( new Dog() );
animals.add( new Cat() );
animals.add( new Donkey() );
...

Con el enfoque anterior, simplemente puedo crear una nueva clase que extienda Animal y se recogerá automáticamente.

ACTUALIZACIÓN: 16/10/2008 9:00 am Hora estándar del Pacífico:

Esta pregunta ha generado muchas respuestas excelentes, gracias. A partir de las respuestas y mi investigación, descubrí que lo que realmente quiero hacer no es posible con Java. Hay enfoques, como el mecanismo ServiceLoader de ddimitrov que pueden funcionar, pero son muy pesados ​​para lo que quiero, y creo que simplemente muevo el problema del código Java a un archivo de configuración externo. Actualización 5/10/19 (¡11 años después!) Ahora hay varias bibliotecas que pueden ayudar con esto según la respuesta de @ IvanNik org.reflections se ve bien. También ClassGraph de la respuesta de @Luke Hutchison parece interesante. Hay varias posibilidades más en las respuestas también.

Otra forma de decir lo que quiero: una función estática en mi clase Animal encuentra e instancia todas las clases que heredan de Animal, sin ninguna configuración / codificación adicional. Si tengo que configurar, igual podría instanciarlos en la clase Animal de todos modos. Entiendo que debido a que un programa Java es solo una federación suelta de archivos .class, así es como es.

Curiosamente, parece que esto es bastante trivial en C #.

JohnnyLambada
fuente
1
Después de buscar por un tiempo, parece que esta es una nuez difícil de descifrar en Java. Aquí hay un hilo que tiene algo de información: foros.sun.com/thread.jspa?threadID=341935&start=15 Las implementaciones son más de lo que necesito, supongo que me quedaré con la segunda implementación por ahora.
JohnnyLambada
ponlo como respuesta y deja que los lectores lo voten
VonC
3
El enlace para hacer esto en C # no muestra nada especial. AC # Assembly es equivalente al archivo Java JAR. Es una colección de archivos de clase compilados (y recursos). El enlace muestra cómo sacar las clases de un solo ensamblado. Puedes hacerlo casi tan fácilmente con Java. El problema para usted es que necesita revisar todos los archivos JAR y los verdaderos archivos sueltos (directorios); necesitaría hacer lo mismo con .NET (buscar una RUTA de algún tipo o varios tipos).
Kevin Brock

Respuestas:

156

Yo uso org.reflections :

Reflections reflections = new Reflections("com.mycompany");    
Set<Class<? extends MyInterface>> classes = reflections.getSubTypesOf(MyInterface.class);

Otro ejemplo:

public static void main(String[] args) throws IllegalAccessException, InstantiationException {
    Reflections reflections = new Reflections("java.util");
    Set<Class<? extends List>> classes = reflections.getSubTypesOf(java.util.List.class);
    for (Class<? extends List> aClass : classes) {
        System.out.println(aClass.getName());
        if(aClass == ArrayList.class) {
            List list = aClass.newInstance();
            list.add("test");
            System.out.println(list.getClass().getName() + ": " + list.size());
        }
    }
}
IvanNik
fuente
66
Gracias por el enlace sobre org.reflections. Pensé erróneamente que esto sería tan fácil como lo es en el mundo .Net y esta respuesta me ha ahorrado mucho tiempo.
akmad
1
¡Gracias de hecho por este útil enlace al gran paquete "org.reflections"! Con eso finalmente encontré una solución práctica y ordenada para mi problema.
Hartmut P.
3
¿Hay alguna manera de obtener todas las reflexiones, es decir, sin señalar un paquete específico, pero cargando todas las clases disponibles?
Joey Baruch
Recuerde que encontrar las clases no resolverá su problema de instanciarlas (línea 5 animals.add( new c() );de su código), porque mientras Class.newInstance()exista, no tiene garantía de que la clase específica tenga un constructor sin parámetros (C # le permite requerir eso en un genérico)
usr-local-ΕΨΗΕΛΩΝ
Consulte también ClassGraph: github.com/classgraph/classgraph (Descargo de responsabilidad, soy el autor). Daré un código de ejemplo en una respuesta separada.
Luke Hutchison el
34

La forma Java de hacer lo que quiere es usar el mecanismo ServiceLoader .

Además, muchas personas obtienen sus propios archivos al tener un archivo en una ubicación conocida de classpath (es decir, /META-INF/services/myplugin.properties) y luego usan ClassLoader.getResources () para enumerar todos los archivos con este nombre de todos los archivos jar . Esto permite que cada jarra exporte sus propios proveedores y puede instanciarlos mediante reflexión usando Class.forName ()

ddimitrov
fuente
1
Para generar el archivo de servicios META-INF fácilmente en base a las anotaciones en las clases, se puede comprobar: metainf-services.kohsuke.org o code.google.com/p/spi
elek
Bleah Solo agrega un nivel de complejidad, pero no elimina la necesidad de crear una clase y luego registrarla en una tierra muy lejana.
Kevin Cline
Vea la respuesta a continuación con 26 votos a favor, mucho mejor
SobiborTreblinka
44
De hecho, si necesita un tipo de soluciones de trabajo, Reflections / Scannotations es una buena opción, pero ambas imponen una dependencia externa, no son deterministas y no son compatibles con el Proceso de la Comunidad Java. Además, quería resaltar que nunca se puede obtener de forma confiable TODAS las clases implementando una interfaz, ya que es trivial fabricar una nueva de la nada en cualquier momento. Para todas sus verrugas, el cargador de servicios de Java es fácil de entender y usar. Me mantendría alejado de él por diferentes razones, pero el escaneo de classpath es aún peor: stackoverflow.com/a/7237152/18187
ddimitrov
11

Piense en esto desde un punto de vista orientado a aspectos; lo que quieres hacer, realmente, es conocer todas las clases en tiempo de ejecución que HAN extendido la clase Animal. (Creo que es una descripción un poco más precisa de su problema que su título; de lo contrario, no creo que tenga una pregunta en tiempo de ejecución).

Entonces, lo que creo que desea es crear un constructor de su clase base (Animal) que agregue a su matriz estática (prefiero ArrayLists, yo mismo, pero a cada uno lo suyo) el tipo de la Clase actual que se está instanciando.

Entonces, más o menos;

public abstract class Animal
    {
    private static ArrayList<Class> instantiatedDerivedTypes;
    public Animal() {
        Class derivedClass = this.getClass();
        if (!instantiatedDerivedClass.contains(derivedClass)) {
            instantiatedDerivedClass.Add(derivedClass);
        }
    }

Por supuesto, necesitará un constructor estático en Animal para inicializar instanciadaDerivedClass ... Creo que esto hará lo que probablemente quiera. Tenga en cuenta que esto depende de la ruta de ejecución; Si tiene una clase de Perro que se deriva de Animal que nunca se invoca, no la tendrá en su lista de Clase de Animal.

Paul Sonier
fuente
6

Desafortunadamente, esto no es totalmente posible ya que ClassLoader no le dirá qué clases están disponibles. Sin embargo, puede acercarse bastante haciendo algo como esto:

for (String classpathEntry : System.getProperty("java.class.path").split(System.getProperty("path.separator"))) {
    if (classpathEntry.endsWith(".jar")) {
        File jar = new File(classpathEntry);

        JarInputStream is = new JarInputStream(new FileInputStream(jar));

        JarEntry entry;
        while( (entry = is.getNextJarEntry()) != null) {
            if(entry.getName().endsWith(".class")) {
                // Class.forName(entry.getName()) and check
                //   for implementation of the interface
            }
        }
    }
}

Editar: johnstok es correcto (en los comentarios) que esto solo funciona para aplicaciones Java independientes y no funcionará bajo un servidor de aplicaciones.

jonathan-stafford
fuente
Esto no funciona bien cuando las clases se cargan por otros medios, por ejemplo, en un contenedor web o J2EE.
johnstok
Probablemente podría hacer un mejor trabajo, incluso debajo de un contenedor, si consulta las rutas (a menudo URL[]pero no siempre, por lo que puede que no sea posible) desde la jerarquía de ClassLoader. A menudo, aunque debe tener permiso, ya que generalmente tendría una SecurityManagercarga dentro de la JVM.
Kevin Brock
¡Funcionó como un encanto para mí! Temía que esto fuera demasiado pesado y lento, pasando por todas las clases en el classpath, pero en realidad limité los archivos que verifiqué a aquellos que contenían "-ejb-" u otros nombres de archivos reconocibles, y todavía es 100 veces más rápido que iniciar un glassfish incrustado o EJBContainer. En mi situación particular, luego analicé las clases buscando anotaciones "Sin estado", "Con estado" y "Singleton", y si las vi, agregué el nombre Mapeado JNDI a mi MockInitialContextFactory. ¡Gracias de nuevo!
cs94njw
4

Puede usar ResolverUtil ( fuente sin procesar ) del Stripes Framework
si necesita algo simple y rápido sin refactorizar ningún código existente.

Aquí hay un ejemplo simple que no ha cargado ninguna de las clases:

package test;

import java.util.Set;
import net.sourceforge.stripes.util.ResolverUtil;

public class BaseClassTest {
    public static void main(String[] args) throws Exception {
        ResolverUtil<Animal> resolver = new ResolverUtil<Animal>();
        resolver.findImplementations(Animal.class, "test");
        Set<Class<? extends Animal>> classes = resolver.getClasses();

        for (Class<? extends Animal> clazz : classes) {
            System.out.println(clazz);
        }
    }
}

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
class Donkey extends Animal {}

Esto también funciona en un servidor de aplicaciones, ya que es donde fue diseñado para funcionar;)

El código básicamente hace lo siguiente:

  • iterar sobre todos los recursos en los paquetes que especifique
  • mantener solo los recursos que terminan en .class
  • Cargue esas clases usando ClassLoader#loadClass(String fullyQualifiedName)
  • Comprueba si Animal.class.isAssignableFrom(loadedClass);
DJDaveMark
fuente
4

El mecanismo más robusto para enumerar todas las subclases de una clase dada es actualmente ClassGraph , porque maneja la gama más amplia posible de mecanismos de especificación de classpath , incluido el nuevo sistema de módulos JPMS. (Soy el autor)

List<Class<Animal>> animals;
try (ScanResult scanResult = new ClassGraph().whitelistPackages("com.zoo.animals")
        .enableClassInfo().scan()) {
    animals = scanResult
        .getSubclasses(Animal.class.getName())
        .loadClasses(Animal.class);
}
Luke Hutchison
fuente
El resultado es de tipo Lista <Clase <Animal>>
hiaclibe
2

Java carga clases dinámicamente, por lo que su universo de clases sería solo aquellas que ya se han cargado (y aún no se han descargado). Quizás pueda hacer algo con un cargador de clases personalizado que pueda verificar los supertipos de cada clase cargada. No creo que haya una API para consultar el conjunto de clases cargadas.

a fondo
fuente
2

utilizar este

public static Set<Class> getExtendedClasses(Class superClass)
{
    try
    {
        ResolverUtil resolver = new ResolverUtil();
        resolver.findImplementations(superClass, superClass.getPackage().getName());
        return resolver.getClasses();  
    }
    catch(Exception e)
    {Log.d("Log:", " Err: getExtendedClasses() ");}

    return null;
}

getExtendedClasses(Animals.class);

Editar:

  • biblioteca para (ResolverUtil): Stripes
Ali Bagheri
fuente
2
Tendrá que decir un poco más sobre ResolverUtil. ¿Qué es? ¿De qué biblioteca viene?
JohnnyLambada
1

Gracias a todos los que respondieron esta pregunta.

Parece que esta es una nuez difícil de roer. Terminé rindiéndome y creando una matriz estática y getter en mi clase base.

public abstract class Animal{
    private static Animal[] animals= null;
    public static Animal[] getAnimals(){
        if (animals==null){
            animals = new Animal[]{
                new Dog(),
                new Cat(),
                new Lion()
            };
        }
        return animals;
    }
}

Parece que Java simplemente no está configurado para la autodescubrimiento como C #. Supongo que el problema es que, dado que una aplicación Java es solo una colección de archivos .class en un archivo directorio / jar en alguna parte, el tiempo de ejecución no conoce una clase hasta que se hace referencia a ella. En ese momento, el cargador lo carga; lo que intento hacer es descubrirlo antes de hacer referencia a él, lo que no es posible sin salir al sistema de archivos y buscarlo.

Siempre me gusta el código que puede descubrirse a sí mismo en lugar de que tenga que contarlo sobre sí mismo, pero desafortunadamente esto también funciona.

¡Gracias de nuevo!

JohnnyLambada
fuente
1
Java está configurado para el autodescubrimiento. Solo necesitas hacer un poco más de trabajo. Vea mi respuesta para más detalles.
Dave Jarvis el
1

Usando OpenPojo puedes hacer lo siguiente:

String package = "com.mycompany";
List<Animal> animals = new ArrayList<Animal>();

for(PojoClass pojoClass : PojoClassFactory.enumerateClassesByExtendingType(package, Animal.class, null) {
  animals.add((Animal) InstanceFactory.getInstance(pojoClass));
}
Osman Shoukry
fuente
0

Este es un problema difícil y necesitará encontrar esta información mediante análisis estático, no está disponible fácilmente en tiempo de ejecución. Básicamente, obtenga el classpath de su aplicación, escanee las clases disponibles y lea la información de código de bytes de una clase de la que hereda. Tenga en cuenta que una clase Dog no puede heredar directamente de Animal, pero puede heredar de Pet, que a su vez hereda de Animal, por lo que deberá realizar un seguimiento de esa jerarquía.

Pdeva
fuente
0

Una forma es hacer que las clases usen inicializadores estáticos ... No creo que se hereden (no funcionará si lo son):

public class Dog extends Animal{

static
{
   Animal a = new Dog();
   //add a to the List
}

Requiere que agregue este código a todas las clases involucradas. Pero evita tener un gran círculo feo en alguna parte, probando cada clase buscando niños de Animal.


fuente
3
Esto no funciona en mi caso. Como la clase no está referenciada en ningún lado, nunca se carga, por lo que nunca se llama al inicializador estático. Parece que se llama a un inicializador estático antes que todo lo demás cuando se carga la clase, pero en mi caso la clase nunca se carga.
JohnnyLambada
0

Resolví este problema con bastante elegancia usando las anotaciones de nivel de paquete y luego haciendo que esa anotación tenga como argumento una lista de clases.

Encuentra clases de Java implementando una interfaz

Las implementaciones solo tienen que crear un paquete-info.java y poner la anotación mágica con la lista de clases que desean admitir.

Adam Gent
fuente