In fase di esecuzione, trova tutte le classi in un'applicazione Java che estendono una classe base


147

Voglio fare qualcosa del genere:

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

Quindi, voglio guardare tutte le classi nell'universo della mia applicazione e quando trovo una che discende da Animal, voglio creare un nuovo oggetto di quel tipo e aggiungerlo all'elenco. Questo mi permette di aggiungere funzionalità senza dover aggiornare un elenco di cose. Posso evitare quanto segue:

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

Con l'approccio sopra, posso semplicemente creare una nuova classe che estende Animal e verrà raccolto automaticamente.

AGGIORNAMENTO: 16/10/2008 9:00 Pacific Standard Time:

Questa domanda ha generato molte risposte fantastiche - grazie. Dalle risposte e dalle mie ricerche, ho scoperto che ciò che voglio davvero fare non è possibile sotto Java. Ci sono approcci, come il meccanismo ServiceLoader di ddimitrov che può funzionare, ma sono molto pesanti per quello che voglio e credo di spostare semplicemente il problema dal codice Java a un file di configurazione esterno. Aggiornamento 5/10/19 (11 anni dopo!) Ora ci sono diverse librerie che possono aiutarti in questo secondo la risposta di @ IvanNik org.reflections sembra buono. Anche ClassGraph della risposta di @Luke Hutchison sembra interessante. Ci sono anche molte altre possibilità nelle risposte.

Un altro modo per affermare quello che voglio: una funzione statica nella mia classe Animal trova e crea un'istanza di tutte le classi che ereditano da Animal, senza ulteriori configurazioni / codifiche. Se devo configurare, potrei anche semplicemente istanziarli nella classe Animal. Lo capisco perché un programma Java è solo una federazione libera di file .class che è così.

È interessante notare che questo sembra abbastanza banale in C #.


1
Dopo aver cercato per un po ', sembra che questo sia un dado difficile da decifrare in Java. Ecco un thread che ha alcune informazioni: forums.sun.com/thread.jspa?threadID=341935&start=15 Le implementazioni in esso contenute sono più di quelle di cui ho bisogno, immagino che per il momento continuerò con la seconda implementazione.
JohnnyLambada,

metti questa come risposta e lascia che i lettori
votino

3
Il link per farlo in C # non mostra nulla di speciale. AC # Assembly equivale al file JAR Java. È una raccolta di file di classe compilati (e risorse). Il collegamento mostra come estrarre le classi da un singolo assembly. Puoi farlo quasi altrettanto facilmente con Java. Il problema per te è che devi guardare attraverso tutti i file JAR e veri file sciolti (directory); dovresti fare lo stesso con .NET (cerca un PERCORSO di qualche tipo o vari tipi).
Kevin Brock,

Risposte:


156

Uso org.reflections :

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

Un altro esempio:

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

6
Grazie per il link su org.reflections. Ho erroneamente pensato che sarebbe stato facile come nel mondo .Net e questa risposta mi ha fatto risparmiare un sacco di tempo.
circa

1
Grazie davvero per questo utile link al fantastico pacchetto "org.reflections"! Con ciò ho finalmente trovato una soluzione praticabile e chiara per il mio problema.
Hartmut P.

3
C'è un modo per ottenere tutte le riflessioni, cioè senza notare un pacchetto specifico, ma caricando tutte le classi disponibili?
Joey Baruch,

Ricorda che trovare le classi non risolverà il tuo problema di istanziarle (riga 5 animals.add( new c() );del tuo codice), perché mentre Class.newInstance()esiste, non hai alcuna garanzia che la classe specifica abbia un costruttore senza parametri (C # ti consente di richiederlo in un generico)
usr-local-ΕΨΗΕΛΩΝ

Vedi anche ClassGraph: github.com/classgraph/classgraph (Disclaimer, sono l'autore). Darò un esempio di codice in una risposta separata.
Luke Hutchison,

34

Il modo Java per fare quello che vuoi è usare il meccanismo ServiceLoader .

Inoltre, molte persone riescono ad avere un file in una posizione del percorso di classe ben nota (ad esempio /META-INF/services/myplugin.properties) e quindi utilizzando ClassLoader.getResources () per enumerare tutti i file con questo nome da tutti i barattoli. Ciò consente a ciascun vaso di esportare i propri provider e puoi istanziarli riflettendo usando Class.forName ()


1
Per generare facilmente il file dei servizi META-INF in base alle annotazioni sulle classi, puoi controllare: metainf-services.kohsuke.org o code.google.com/p/spi
elek

Bleah. Aggiunge solo un livello di complessità, ma non elimina la necessità sia di creare una classe che di registrarla in una terra molto lontana.
Kevin Cline,

Si prega di vedere la risposta sotto con 26 voti, molto meglio
SobiborTreblinka,

4
In effetti, se hai bisogno di un tipo di soluzioni funzionanti, Reflections / Scannotations è una buona opzione, ma entrambi impongono una dipendenza esterna, non sono deterministici e non sono supportati dal Java Community Process. Inoltre, volevo sottolineare che non puoi mai ottenere TUTTE le classi in modo affidabile implementando un'interfaccia, poiché è banale produrne una nuova dal nulla in qualsiasi momento. Nonostante tutte le sue verruche, il caricatore di servizi Java è di facile comprensione e utilizzo. Vorrei stare lontano da esso per motivi diversi, ma poi la scansione percorso di classe è ancora peggio: stackoverflow.com/a/7237152/18187
ddimitrov

11

Pensaci da un punto di vista orientato all'aspetto; quello che vuoi fare, in realtà, è conoscere tutte le classi in fase di esecuzione che HANNO esteso la classe Animal. (Penso che sia una descrizione leggermente più accurata del tuo problema rispetto al tuo titolo; altrimenti, non penso che tu abbia una domanda di runtime.)

Quindi quello che penso tu voglia è creare un costruttore della tua classe di base (Animal) che aggiunga al tuo array statico (preferisco ArrayLists, io stesso, ma a ciascuno il loro) il tipo della Classe corrente che viene istanziata.

Quindi, approssimativamente;

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

Ovviamente, avrai bisogno di un costruttore statico su Animal per inizializzareDerivedClass ... Penso che questo farà quello che probabilmente vorrai. Si noti che questo dipende dal percorso di esecuzione; se hai una classe Cane che deriva da un animale che non viene mai invocato, non la avrai nell'elenco dei tuoi animali.


6

Sfortunatamente questo non è del tutto possibile poiché ClassLoader non ti dirà quali classi sono disponibili. Tuttavia, puoi avvicinarti abbastanza facendo qualcosa del genere:

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

Modifica: johnstok ha ragione (nei commenti) che funziona solo con applicazioni Java standalone e non funziona con un server applicazioni.


Questo non funziona bene quando le classi vengono caricate con altri mezzi, ad esempio in un contenitore Web o J2EE.
johnstok,

Probabilmente potresti fare un lavoro migliore, anche sotto un container, se richiedessi i percorsi (spesso URL[]ma non sempre, quindi potrebbe non essere possibile) dalla gerarchia ClassLoader. Spesso però devi avere i permessi poiché di solito avresti un SecurityManagercarico all'interno della JVM.
Kevin Brock,

Ha funzionato benissimo per me! Temevo che questo sarebbe troppo pesante e lento, passando attraverso tutte le classi nel percorso di classe, ma in realtà ho limitato i file che ho controllato a quelli contenenti "-ejb-" o altri nomi di file riconoscibili, ed è ancora 100 volte più veloce dell'avvio un pesce vetro incorporato o EJBContainer. Nella mia situazione particolare, ho quindi analizzato le classi alla ricerca di annotazioni "Stateless", "Stateful" e "Singleton" e, se le avessi viste, avrei aggiunto il nome Mappato JNDI al mio MockInitialContextFactory. Grazie ancora!
cs94njw,

4

È possibile utilizzare ResolverUtil ( sorgente non elaborata ) da Stripes Framework
se è necessario qualcosa di semplice e rapido senza refactoring di alcun codice esistente.

Ecco un semplice esempio che non ha caricato nessuna delle classi:

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

Funziona anche in un application server poiché è lì che è stato progettato per funzionare;)

Il codice sostanzialmente fa quanto segue:

  • iterare su tutte le risorse nei pacchetti specificati
  • mantenere solo le risorse che terminano in .class
  • Carica quelle classi usando ClassLoader#loadClass(String fullyQualifiedName)
  • Controlla se Animal.class.isAssignableFrom(loadedClass);

4

Il meccanismo più robusto per elencare tutte le sottoclassi di una determinata classe è attualmente ClassGraph , poiché gestisce la più ampia gamma possibile di meccanismi di specifica del percorso di classe , incluso il nuovo sistema di moduli JPMS. (Sono l'autore.)

List<Class<Animal>> animals;
try (ScanResult scanResult = new ClassGraph().whitelistPackages("com.zoo.animals")
        .enableClassInfo().scan()) {
    animals = scanResult
        .getSubclasses(Animal.class.getName())
        .loadClasses(Animal.class);
}

Il risultato è di tipo Elenco <Classe <Animale>>
hiaclibe il

2

Java carica dinamicamente le classi, quindi il tuo universo di classi sarebbe solo quello che è già stato caricato (e non ancora scaricato). Forse puoi fare qualcosa con un caricatore di classi personalizzato che potrebbe controllare i supertipi di ogni classe caricata. Non credo che ci sia un'API per interrogare l'insieme di classi caricate.


2

Usa questo

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

Modificare:

  • libreria per (ResolverUtil): Stripes

2
Dovrai aggiungere qualcosa in più su ResolverUtil. Che cos'è? Da quale biblioteca proviene?
JohnnyLambada,

1

Grazie a tutti coloro che hanno risposto a questa domanda.

Sembra che questo sia davvero un dado difficile da decifrare. Ho finito per rinunciare e creare un array statico e getter nella mia 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;
    }
}

Sembra che Java non sia impostato per l'auto-scoperta come C #. Suppongo che il problema sia che, dal momento che un'app Java è solo una raccolta di file .class in un file directory / jar da qualche parte, il runtime non conosce una classe fino a quando non viene referenziato. In quel momento il caricatore lo carica - quello che sto cercando di fare è scoprirlo prima di fare riferimento a ciò che non è possibile senza uscire dal file system e guardare.

Mi piace sempre il codice che può scoprire se stesso invece che doverlo raccontare su se stesso, ma purtroppo funziona anche questo.

Grazie ancora!


1
Java è impostato per l'auto-scoperta. Hai solo bisogno di fare un po 'più di lavoro. Vedi la mia risposta per i dettagli.
Dave Jarvis,

1

Utilizzando OpenPojo è possibile effettuare le seguenti operazioni:

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

0

Questo è un problema difficile e dovrai scoprire queste informazioni usando l'analisi statica, che non è facilmente disponibile in fase di esecuzione. Fondamentalmente ottieni il percorso di classe della tua app ed esegui la scansione delle classi disponibili e leggi le informazioni sul bytecode di una classe da cui eredita la classe. Nota che un cane di classe non può ereditare direttamente dall'animale ma può ereditare dall'animale che a sua volta eredita dall'animale, quindi dovrai tenere traccia di quella gerarchia.


0

Un modo è fare in modo che le classi utilizzino un inizializzatore statico ... Non penso che queste siano ereditate (non funzionerà se lo sono):

public class Dog extends Animal{

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

È necessario aggiungere questo codice a tutte le classi coinvolte. Ma evita di avere un grosso brutto giro da qualche parte, testando ogni classe alla ricerca di bambini di animali.


3
Questo non funziona nel mio caso. Poiché la classe non fa riferimento in nessun luogo, non viene mai caricata, quindi l'inizializzatore statico non viene mai chiamato. Sembra che un inizializzatore statico venga chiamato prima di tutto quando la classe viene caricata, ma nel mio caso la classe non viene mai caricata.
JohnnyLambada,

0

Ho risolto questo problema in modo abbastanza elegante usando le annotazioni a livello di pacchetto e quindi facendo di quell'annotazione come argomento un elenco di classi.

Trova le classi Java che implementano un'interfaccia

Le implementazioni devono solo creare un pacchetto-info.java e inserire l'annotazione magica con l'elenco delle classi che vogliono supportare.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.