Suchen Sie zur Laufzeit alle Klassen in einer Java-Anwendung, die eine Basisklasse erweitern

147

Ich möchte so etwas machen:

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

Ich möchte mir also alle Klassen im Universum meiner Anwendung ansehen. Wenn ich eine finde, die von Animal abstammt, möchte ich ein neues Objekt dieses Typs erstellen und zur Liste hinzufügen. Auf diese Weise kann ich Funktionen hinzufügen, ohne eine Liste von Dingen aktualisieren zu müssen. Ich kann Folgendes vermeiden:

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

Mit dem obigen Ansatz kann ich einfach eine neue Klasse erstellen, die Animal erweitert, und sie wird automatisch aufgenommen.

UPDATE: 16.10.2008 9:00 Uhr pazifische Standardzeit:

Diese Frage hat viele gute Antworten hervorgebracht - danke. Aus den Antworten und meinen Recherchen habe ich herausgefunden, dass das, was ich wirklich tun möchte, unter Java einfach nicht möglich ist. Es gibt Ansätze wie den ServiceLoader-Mechanismus von ddimitrov, die funktionieren können - aber sie sind sehr schwer für das, was ich will, und ich glaube, ich verschiebe das Problem einfach von Java-Code in eine externe Konfigurationsdatei. Update 10.05.19 (11 Jahre später!) Es gibt jetzt mehrere Bibliotheken, die laut @ IvanNiks Antwort helfen können . Org.reflections sieht gut aus. Auch ClassGraph aus der Antwort von @Luke Hutchison sieht interessant aus. Es gibt auch mehrere weitere Möglichkeiten in den Antworten.

Eine andere Möglichkeit, anzugeben, was ich möchte: Eine statische Funktion in meiner Animal-Klasse findet und instanziiert alle Klassen, die von Animal erben - ohne weitere Konfiguration / Codierung. Wenn ich konfigurieren muss, kann ich sie genauso gut in der Animal-Klasse instanziieren. Ich verstehe das, weil ein Java-Programm nur ein loser Verbund von .class-Dateien ist, so ist es eben.

Interessanterweise scheint dies in C # ziemlich trivial zu sein.

Johnny Lambada
quelle
1
Nach einer Weile der Suche scheint es, dass dies eine schwierige Nuss ist, die man in Java knacken kann. Hier ist ein Thread mit einigen Informationen: forums.sun.com/thread.jspa?threadID=341935&start=15 Die darin enthaltenen Implementierungen sind mehr als ich brauche. Ich denke, ich bleibe vorerst bei der zweiten Implementierung.
Johnny Lambada
Geben
3
Der Link dazu in C # zeigt nichts Besonderes. AC # Assembly entspricht der Java JAR-Datei. Es ist eine Sammlung kompilierter Klassendateien (und Ressourcen). Der Link zeigt, wie Sie die Klassen aus einer einzelnen Assembly herausholen. Sie können es fast genauso einfach mit Java tun. Das Problem für Sie ist, dass Sie alle JAR-Dateien und echten losen Dateien (Verzeichnisse) durchsuchen müssen. Sie müssten dasselbe mit .NET tun (suchen Sie einen Pfad irgendeiner Art oder verschiedene Arten).
Kevin Brock

Antworten:

156

Ich benutze org.reflections :

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

Ein anderes Beispiel:

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
quelle
6
Danke für den Link zu org.reflections. Ich habe fälschlicherweise gedacht, dass dies so einfach ist wie in der .Net-Welt, und diese Antwort hat mir viel Zeit gespart.
Akmad
1
Vielen Dank für diesen hilfreichen Link zum tollen Paket "org.reflections"! Damit habe ich endlich eine praktikable und ordentliche Lösung für mein Problem gefunden.
Hartmut P.
3
Gibt es eine Möglichkeit, alle Reflexionen zu erhalten, dh ohne ein bestimmtes Paket zu notieren, aber alle verfügbaren Klassen zu laden?
Joey Baruch
Denken Sie daran , dass die Klassen zu finden , wird nicht Ihr Problem lösen sie instanziieren (Zeile 5 animals.add( new c() );des Codes), weil während Class.newInstance()vorhanden ist , haben Sie keine Garantie , dass die spezifische Klasse einen parameterlosen Konstruktor hat (C #, dass in einem allgemeinen erfordern erlaubt)
usr-local-ΕΨΗΕΛΩΝ
Siehe auch ClassGraph: github.com/classgraph/classgraph (Haftungsausschluss, ich bin der Autor). Ich werde ein Codebeispiel in einer separaten Antwort geben.
Luke Hutchison
34

Die Java-Methode, um das zu tun, was Sie möchten, ist die Verwendung des ServiceLoader- Mechanismus.

Viele Benutzer rollen auch ihre eigenen, indem sie eine Datei an einem bekannten Klassenpfad (z. B. /META-INF/services/myplugin.properties) haben und dann mit ClassLoader.getResources () alle Dateien mit diesem Namen aus allen Jars auflisten . Auf diese Weise kann jedes Glas seine eigenen Anbieter exportieren und Sie können sie durch Reflektion mit Class.forName () instanziieren.

ddimitrov
quelle
1
Um die META-INF-Servicedatei einfach anhand von Anmerkungen zu den Klassen zu generieren , können Sie Folgendes überprüfen: metainf-services.kohsuke.org oder code.google.com/p/spi
elek
Bleah. Fügt nur ein gewisses Maß an Komplexität hinzu, beseitigt jedoch nicht die Notwendigkeit, eine Klasse zu erstellen und sie dann in einem weit entfernten Land zu registrieren.
Kevin Cline
Bitte sehen Sie die Antwort unten mit 26 positiven Stimmen, viel besser
SobiborTreblinka
4
In der Tat ist Reflections / Scannotations eine gute Option, wenn Sie eine Art funktionierende Lösung benötigen. Beide stellen jedoch eine externe Abhängigkeit dar, sind nicht deterministisch und werden vom Java Community-Prozess nicht unterstützt. Darüber hinaus wollte ich hervorheben, dass Sie niemals zuverlässig ALLE Klassen erhalten können, die eine Schnittstelle implementieren, da es trivial ist, jederzeit eine neue aus dem Nichts herzustellen. Trotz aller Warzen ist der Service Loader von Java leicht zu verstehen und zu verwenden. Ich würde mich aus verschiedenen Gründen davon fernhalten, aber dann ist das Scannen von Klassenpfaden noch schlimmer: stackoverflow.com/a/7237152/18187
ddimitrov
11

Denken Sie darüber aus einer aspektorientierten Perspektive nach. Was Sie wirklich tun möchten, ist, alle Klassen zur Laufzeit zu kennen, die die Animal-Klasse erweitert haben. (Ich denke, das ist eine etwas genauere Beschreibung Ihres Problems als Ihr Titel; ansonsten glaube ich nicht, dass Sie eine Laufzeitfrage haben.)

Ich denke, Sie möchten einen Konstruktor Ihrer Basisklasse (Animal) erstellen, der Ihrem statischen Array (ich bevorzuge ArrayLists selbst, aber jedem eigenen) den Typ der aktuellen Klasse hinzufügt, die instanziiert wird.

Also ungefähr;

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

Natürlich benötigen Sie einen statischen Konstruktor für Animal, um instanziierteDerivedClass zu initialisieren ... Ich denke, dies wird das tun, was Sie wahrscheinlich wollen. Beachten Sie, dass dies vom Ausführungspfad abhängig ist. Wenn Sie eine Hundeklasse haben, die von einem Tier abgeleitet ist, das niemals aufgerufen wird, haben Sie sie nicht in Ihrer Tierklassenliste.

Paul Sonier
quelle
6

Leider ist dies nicht ganz möglich, da der ClassLoader Ihnen nicht sagt, welche Klassen verfügbar sind. Sie können jedoch ziemlich nahe kommen, wenn Sie so etwas tun:

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

Bearbeiten: johnstok ist richtig (in den Kommentaren), dass dies nur für eigenständige Java-Anwendungen funktioniert und nicht unter einem Anwendungsserver.

Jonathan-Stafford
quelle
Dies funktioniert nicht gut, wenn Klassen auf andere Weise geladen werden, z. B. in einem Web- oder J2EE-Container.
Johnstok
Sie könnten wahrscheinlich sogar unter einem Container einen besseren Job machen, wenn Sie die Pfade (oft, URL[]aber nicht immer, so dass dies möglicherweise nicht möglich ist) aus der ClassLoader-Hierarchie abfragen. Oft müssen Sie jedoch über eine Berechtigung verfügen, da in der Regel eine SecurityManagerJVM geladen wird.
Kevin Brock
Arbeitete wie ein Zauber für mich! Ich befürchtete, dass dies zu schwer und zu langsam sein würde, wenn ich alle Klassen im Klassenpfad durchlaufen würde, aber tatsächlich beschränkte ich die Dateien, die ich überprüfte, auf diejenigen, die "-ejb-" oder andere erkennbare Dateinamen enthielten, und es ist immer noch 100-mal schneller als das Starten ein eingebetteter Glasfisch oder EJBContainer. In meiner speziellen Situation analysierte ich dann die Klassen nach "Stateless" -, "Stateful" - und "Singleton" -Anmerkungen, und wenn ich sie sah, fügte ich meiner MockInitialContextFactory den Namen JNDI Mapped hinzu. Danke noch einmal!
cs94njw
4

Sie können ResolverUtil ( Rohquelle ) aus dem Stripes Framework verwenden,
wenn Sie etwas Einfaches und Schnelles benötigen, ohne vorhandenen Code umzugestalten.

Hier ist ein einfaches Beispiel, in dem keine der Klassen geladen wurde:

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

Dies funktioniert auch auf einem Anwendungsserver, da er dort entwickelt wurde;)

Der Code macht im Wesentlichen Folgendes:

  • Durchlaufen Sie alle Ressourcen in den von Ihnen angegebenen Paketen
  • Behalten Sie nur die Ressourcen bei, die auf .class enden
  • Laden Sie diese Klassen mit ClassLoader#loadClass(String fullyQualifiedName)
  • Überprüft, ob Animal.class.isAssignableFrom(loadedClass);
DJDaveMark
quelle
4

Der robusteste Mechanismus zum Auflisten aller Unterklassen einer bestimmten Klasse ist derzeit ClassGraph , da er die größtmögliche Anzahl von Klassenpfadspezifikationsmechanismen verarbeitet , einschließlich des neuen JPMS-Modulsystems. (Ich bin der 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
quelle
Das Ergebnis ist vom Typ List <Class <Animal >>
Hiaclibe
2

Java lädt Klassen dynamisch, sodass Ihr Klassenuniversum nur diejenigen sind, die bereits geladen (und noch nicht entladen) wurden. Vielleicht können Sie mit einem benutzerdefinierten Klassenladeprogramm etwas tun, das die Supertypen jeder geladenen Klasse überprüft. Ich glaube nicht, dass es eine API gibt, um die geladenen Klassen abzufragen.

gründlich
quelle
2

benutze das

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

Bearbeiten:

  • Bibliothek für (ResolverUtil): Stripes
Ali Bagheri
quelle
2
Sie müssen etwas mehr über ResolverUtil sagen. Was ist es? Aus welcher Bibliothek kommt es?
Johnny Lambada
1

Vielen Dank an alle, die diese Frage beantwortet haben.

Es scheint, dass dies in der Tat eine harte Nuss ist. Am Ende gab ich es auf und erstellte ein statisches Array und einen Getter in meiner Basisklasse.

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

Es scheint, dass Java nicht so auf Selbsterkennbarkeit eingerichtet ist wie C #. Ich nehme an, das Problem ist, dass die Laufzeit nichts über eine Klasse weiß, bis eine Java-App nur eine Sammlung von .class-Dateien ist, die sich irgendwo in einem Verzeichnis / einer JAR-Datei befinden. Zu diesem Zeitpunkt lädt der Loader es - ich versuche es zu entdecken, bevor ich darauf verweise, was nicht möglich ist, ohne zum Dateisystem zu gehen und nachzuschauen.

Ich mag immer Code, der sich selbst entdecken kann, anstatt dass ich ihn über sich selbst erzählen muss, aber leider funktioniert das auch.

Danke noch einmal!

Johnny Lambada
quelle
1
Java ist für die Selbstfindung eingerichtet. Sie müssen nur ein bisschen mehr arbeiten. Siehe meine Antwort für Details.
Dave Jarvis
1

Mit OpenPojo können Sie Folgendes tun:

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
quelle
0

Dies ist ein schwieriges Problem, und Sie müssen diese Informationen mithilfe einer statischen Analyse ermitteln, die zur Laufzeit nicht einfach verfügbar ist. Holen Sie sich im Grunde den Klassenpfad Ihrer App und durchsuchen Sie die verfügbaren Klassen und lesen Sie die Bytecode-Informationen einer Klasse, von welcher Klasse sie erbt. Beachten Sie, dass ein Klassenhund möglicherweise nicht direkt von Animal erbt, sondern möglicherweise von Pet, das wiederum von Animal erbt. Daher müssen Sie diese Hierarchie im Auge behalten.

pdeva
quelle
0

Eine Möglichkeit besteht darin, die Klassen dazu zu bringen, statische Initialisierer zu verwenden ... Ich glaube nicht, dass diese vererbt werden (es funktioniert nicht, wenn sie es sind):

public class Dog extends Animal{

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

Sie müssen diesen Code allen beteiligten Klassen hinzufügen. Aber es vermeidet, irgendwo eine große hässliche Schleife zu haben, die jede Klasse auf der Suche nach Kindern von Animal testet.


quelle
3
Das funktioniert in meinem Fall nicht. Da auf die Klasse nirgendwo verwiesen wird, wird sie niemals geladen, sodass der statische Initialisierer niemals aufgerufen wird. Es scheint, dass ein statischer Initialisierer vor allem anderen aufgerufen wird, wenn die Klasse geladen wird - aber in meinem Fall wird die Klasse nie geladen.
Johnny Lambada
0

Ich habe dieses Problem ziemlich elegant mithilfe von Annotationen auf Paketebene gelöst und diese Annotation dann als Argument für eine Liste von Klassen verwendet.

Suchen Sie nach Java-Klassen, die eine Schnittstelle implementieren

Implementierungen müssen lediglich eine package-info.java erstellen und die magische Annotation mit der Liste der Klassen einfügen, die sie unterstützen möchten.

Adam Gent
quelle