Wie finden Sie alle Unterklassen einer bestimmten Klasse in Java?

207

Wie geht man vor und versucht, alle Unterklassen einer bestimmten Klasse (oder alle Implementierer einer bestimmten Schnittstelle) in Java zu finden? Ab sofort habe ich eine Methode, um dies zu tun, aber ich finde es ziemlich ineffizient (gelinde gesagt). Die Methode ist:

  1. Ruft eine Liste aller Klassennamen ab, die im Klassenpfad vorhanden sind
  2. Laden Sie jede Klasse und testen Sie, ob es sich um eine Unterklasse oder einen Implementierer der gewünschten Klasse oder Schnittstelle handelt

In Eclipse gibt es eine nette Funktion namens Typhierarchie, die es schafft, dies recht effizient darzustellen. Wie geht man vor und macht es programmatisch?

Avrom
quelle
1
Obwohl die auf Reflections und Spring basierende Lösung interessant aussieht, brauchte ich eine einfache Lösung, die keine Abhängigkeiten hatte. Es scheint, dass mein ursprünglicher Code (mit einigen Verbesserungen) der richtige Weg war.
Avrom
1
Sicherlich können Sie die getSupeClass-Methode rekursiv verwenden?
Ich habe speziell nach allen Unterklassen einer bestimmten Klasse gesucht . getSuperClass sagt Ihnen nicht, welche Unterklassen eine Klasse hat, sondern ruft nur die unmittelbare Superklasse für eine bestimmte Unterklasse ab. Außerdem ist die Methode isAssignableFrom on Class besser für das geeignet, was Sie vorschlagen (keine Rekursion erforderlich).
Vom
Diese Frage ist mit vielen anderen Duplikaten verknüpft, enthält jedoch keine nützliche, einfache Java-Antwort. Seufz ...
Eric Duminil

Antworten:

77

Es gibt keinen anderen Weg, als den von Ihnen beschriebenen. Denken Sie darüber nach - wie kann jemand wissen, welche Klassen ClassX erweitern, ohne jede Klasse im Klassenpfad zu scannen?

Eclipse kann Sie nur in einer scheinbar "effizienten" Zeitspanne über die Super- und Unterklassen informieren, da bereits alle Typdaten an dem Punkt geladen sind, an dem Sie auf die Schaltfläche "In Typhierarchie anzeigen" klicken (da dies der Fall ist) Sie kompilieren ständig Ihre Klassen, wissen alles über den Klassenpfad usw.).

matt b
quelle
23
Es gibt jetzt eine einfache Bibliothek namens org.reflections , die bei dieser und anderen allgemeinen Reflexionsaufgaben hilft. Mit dieser Bibliothek können Sie einfach reflections.getSubTypesOf(aClazz)) Link
Enwired
@matt b - Wenn alle Klassen gescannt werden müssen, bedeutet dies, dass die Leistung beeinträchtigt wird, wenn Sie viele Klassen in Ihrem Projekt haben, obwohl nur wenige von ihnen Ihre Klasse unterklassifizieren?
LeTex
Genau. Es berührt nur alle Klassen. Sie können Ihren eigenen Scanner definieren, indem Sie ihn beschleunigen, indem Sie bestimmte Pakete ausschließen, von denen Sie wissen, dass Ihre Klasse nicht erweitert wird, oder einfach die Klassendatei öffnen und prüfen, ob der Klassenname im konstanten Abschnitt der Klasse gefunden wird, ohne dass der Reflexionsscanner a lesen muss Viel mehr Informationen zu Klassen, die nicht einmal den erforderlichen Verweis auf Ihre (direkte) Superklasse enthalten. Indirekt müssen Sie weiter scannen. Es ist also das Beste, was es im Moment gibt.
Martin Kersten
Die Antwort von fforw hat bei mir funktioniert und sollte als die richtige Antwort markiert werden. Dies ist natürlich beim Scannen von Klassenpfaden möglich.
Farrukh Najmi
Sie liegen falsch, siehe die Antwort unten über Burningwave Bibliothek
127

Das Scannen nach Klassen ist mit reinem Java nicht einfach.

Das Spring Framework bietet eine Klasse namens ClassPathScanningCandidateComponentProvider , die genau das tun kann, was Sie benötigen. Im folgenden Beispiel werden alle Unterklassen von MyClass im Paket org.example.package gefunden

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
}

Diese Methode hat den zusätzlichen Vorteil, dass ein Bytecode-Analysator verwendet wird, um die Kandidaten zu finden, was bedeutet, dass nicht alle gescannten Klassen geladen werden.

fforw
quelle
23
Beim Erstellen von ClassPathScanningCandidateComponentProvider sollte False als Parameter übergeben werden, um die Standardfilter zu deaktivieren. Die Standardfilter stimmen mit anderen Klassentypen überein, z. B. mit @Component. Wir möchten nur, dass der AssignableTypeFilter hier aktiv ist.
MCDS
Sie sagen, es ist nicht einfach, aber wie würden wir es machen, wenn wir es mit reinem Java machen wollten?
Aequitas
49

Dies ist nicht möglich, wenn nur die integrierte Java Reflections-API verwendet wird.

Es gibt ein Projekt, das das erforderliche Scannen und Indizieren Ihres Klassenpfads durchführt, damit Sie auf diese Informationen zugreifen können ...

Reflexionen

Eine Java-Laufzeit-Metadatenanalyse im Sinne von Scannotations

Reflections durchsucht Ihren Klassenpfad, indiziert die Metadaten, ermöglicht die Abfrage zur Laufzeit und kann diese Informationen für viele Module in Ihrem Projekt speichern und sammeln.

Mit Reflections können Sie Ihre Metadaten abfragen nach:

  • Holen Sie sich alle Untertypen eines Typs
  • Alle Typen mit Anmerkungen versehen
  • Lassen Sie alle Typen mit Anmerkungen versehen, einschließlich der Übereinstimmung der Anmerkungsparameter
  • Lassen Sie alle Methoden mit einigen kommentieren

(Haftungsausschluss: Ich habe es nicht verwendet, aber die Projektbeschreibung scheint genau Ihren Anforderungen zu entsprechen.)

Mark Renouf
quelle
1
Interessant. Das Projekt scheint einige Abhängigkeiten zu haben, die in der Dokumentation nicht erwähnt werden. Nämlich (die, die ich bisher gefunden habe): Javaassist, log4J, XStream
Avrom
3
Ich habe dieses Projekt mit maven aufgenommen und es hat gut funktioniert. Unterklassen zu bekommen ist eigentlich das erste Quellcodebeispiel und ist zwei Zeilen lang :-)
KarlsFriend
Ist es nicht möglich, nur die integrierte Java Reflections-API zu verwenden, oder ist dies nur sehr unpraktisch?
Flow
1
Bitte seien Sie vorsichtig, wenn Sie Reflections verwenden und dann Ihre App WAR auf GlassFish bereitstellen! Es liegt ein Konflikt in der Guava-Bibliothek vor und die Bereitstellung schlägt mit einem Fehler fehl. CDI-Bereitstellungsfehler: WELD-001408 - Weitere Informationen finden Sie unter GLASSFISH-20579 . FastClasspathScanner ist in diesem Fall eine Lösung.
lu_ko
Ich probiere einfach dieses Projekt aus und es funktioniert. Ich benutze es nur, um das Strategie-Design-Muster zu verbessern und alle Strategie-konkreten Klassen (Unterklassen) zu erhalten, die ich in der letzteren Demo teilen werde.
Xin Meng
10

Vergessen Sie nicht, dass das generierte Javadoc für eine Klasse eine Liste bekannter Unterklassen enthält (und für Schnittstellen bekannte implementierende Klassen).

rauben
quelle
3
Dies ist völlig falsch. Die Superklasse sollte nicht von ihren Unterklassen abhängen, auch nicht in Javadoc oder Kommentaren.
Jäger
@ Hunter Ich bin anderer Meinung. Es ist völlig richtig, dass JavaDoc eine Liste bekannter Unterklassen enthält. Natürlich enthält "bekannt" möglicherweise nicht die Klasse, nach der Sie suchen, aber für einige Anwendungsfälle ist dies ausreichend.
Qw3ry
Und Sie könnten auf jeden Fall einige Klassen verpassen: Ich kann ein neues Glas in den Klassenpfad laden (zur Laufzeit) und jede zuvor erfolgte Erkennung schlägt fehl.
Qw3ry
10

Versuchen Sie ClassGraph . (Haftungsausschluss, ich bin der Autor). ClassGraph unterstützt das Scannen nach Unterklassen einer bestimmten Klasse, entweder zur Laufzeit oder zur Erstellungszeit, aber auch viel mehr. ClassGraph kann eine abstrakte Darstellung des gesamten Klassendiagramms (alle Klassen, Anmerkungen, Methoden, Methodenparameter und Felder) im Speicher, für alle Klassen im Klassenpfad oder für Klassen in Paketen mit Whitelist erstellen, und Sie können dieses Klassendiagramm jedoch abfragen Sie wollen. ClassGraph unterstützt mehr Klassenpfadspezifikationsmechanismen und Klassenladeprogramme als jeder andere Scanner und funktioniert auch nahtlos mit dem neuen JPMS-Modulsystem. Wenn Sie Ihren Code also auf ClassGraph basieren, ist Ihr Code maximal portabel. Siehe die API hier.

Luke Hutchison
quelle
9

Ich habe das vor einigen Jahren gemacht. Der zuverlässigste Weg, dies zu tun (dh mit offiziellen Java-APIs und ohne externe Abhängigkeiten), besteht darin, ein benutzerdefiniertes Doclet zu schreiben, um eine Liste zu erstellen, die zur Laufzeit gelesen werden kann.

Sie können es wie folgt über die Befehlszeile ausführen:

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

oder führen Sie es von Ameise wie folgt aus:

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

Hier ist der Grundcode:

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

Der Einfachheit halber habe ich das Parsen von Befehlszeilenargumenten entfernt und schreibe eher in System.out als in eine Datei.

David Leppik
quelle
Während die programmatische Verwendung schwierig sein kann, muss ich sagen - ein kluger Ansatz!
Janaka Bandara
8

Ich weiß, dass ich ein paar Jahre zu spät zu dieser Party komme, aber ich bin auf diese Frage gestoßen, um das gleiche Problem zu lösen. Sie können die interne Suche von Eclipse programmgesteuert verwenden, wenn Sie ein Eclipse-Plugin schreiben (und somit dessen Caching usw. nutzen), um Klassen zu finden, die eine Schnittstelle implementieren. Hier ist mein (sehr grober) erster Schnitt:

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

Das erste Problem, das ich bisher sehe, ist, dass ich nur Klassen abfange, die die Schnittstelle direkt implementieren, nicht alle ihre Unterklassen - aber eine kleine Rekursion hat niemanden verletzt.

Curtis
quelle
3
ODER es stellt sich heraus, dass Sie keine eigene Suche durchführen müssen. Sie können eine ITypeHierarchy direkt von Ihrem IType abrufen, indem Sie .newTypeHierarchy () darauf aufrufen
Curtis
7

Unter Berücksichtigung der in den anderen Antworten genannten Einschränkungen können Sie openpojo'sPojoClassFactory ( verfügbar bei Maven ) auch folgendermaßen verwenden:

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

Wo packageRootbefindet sich die Stammzeichenfolge der Pakete, in denen Sie suchen möchten (z. B. "com.mycompany"oder auch nur "com"), und SuperclassIhr Supertyp (dies funktioniert auch auf Schnittstellen).

mikołak
quelle
Mit Abstand die schnellste und eleganteste Lösung unter den vorgeschlagenen.
KidCrippler
4

Ich schreibe nur eine einfache Demo, um die org.reflections.Reflectionszu verwenden, um Unterklassen der abstrakten Klasse zu erhalten:

https://github.com/xmeng1/ReflectionsDemo

Xin Meng
quelle
Es wäre produktiver, wenn Sie hier ein Codebeispiel veröffentlichen würden, anstatt einen Link mit vielen Klassen zu graben.
JesseBoyd
4

Abhängig von Ihren speziellen Anforderungen kann der Service Loader- Mechanismus von Java in einigen Fällen das erreichen, wonach Sie suchen.

Kurz gesagt, Entwickler können explizit deklarieren, dass eine Klasse eine andere Klasse unterklassifiziert (oder eine Schnittstelle implementiert), indem sie diese in einer Datei im META-INF/servicesVerzeichnis der JAR / WAR-Datei auflistet . Es kann dann mithilfe der java.util.ServiceLoaderKlasse ermittelt werden, die bei Angabe eines ClassObjekts Instanzen aller deklarierten Unterklassen dieser Klasse generiert (oder, wenn dies Classeine Schnittstelle darstellt, aller Klassen, die diese Schnittstelle implementieren).

Der Hauptvorteil dieses Ansatzes besteht darin, dass nicht der gesamte Klassenpfad manuell nach Unterklassen durchsucht werden muss. Die gesamte Erkennungslogik ist in der ServiceLoaderKlasse enthalten und es werden nur die Klassen geladen, die explizit im META-INF/servicesVerzeichnis deklariert sind (nicht jede Klasse im Klassenpfad). .

Es gibt jedoch einige Nachteile:

  • Es werden nicht alle Unterklassen gefunden, sondern nur diejenigen, die explizit deklariert sind. Wenn Sie also wirklich alle Unterklassen finden müssen, ist dieser Ansatz möglicherweise unzureichend.
  • Der Entwickler muss die Klasse unter dem META-INF/servicesVerzeichnis explizit deklarieren . Dies ist eine zusätzliche Belastung für den Entwickler und kann fehleranfällig sein.
  • Das ServiceLoader.iterator()generiert Unterklasseninstanzen, nicht deren ClassObjekte. Dies verursacht zwei Probleme:
    • Sie können nicht sagen, wie die Unterklassen aufgebaut sind - der Konstruktor no-arg wird zum Erstellen der Instanzen verwendet.
    • Daher müssen die Unterklassen einen Standardkonstruktor haben oder explizit einen Konstruktor ohne Argumente deklarieren.

Anscheinend wird Java 9 einige dieser Mängel beheben (insbesondere diejenigen, die die Instanziierung von Unterklassen betreffen).

Ein Beispiel

Angenommen, Sie möchten Klassen finden, die eine Schnittstelle implementieren com.example.Example:

package com.example;

public interface Example {
    public String getStr();
}

Die Klasse com.example.ExampleImplimplementiert diese Schnittstelle:

package com.example;

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

Sie würden deklarieren, dass die Klasse ExampleImpleine Implementierung ist, Exampleindem Sie eine Datei erstellen, META-INF/services/com.example.Exampledie den Text enthält com.example.ExampleImpl.

Dann könnten Sie eine Instanz jeder Implementierung von Example(einschließlich einer Instanz von ExampleImpl) wie folgt erhalten:

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

Es sollte auch beachtet werden, dass dies natürlich nur alle Unterklassen findet, die auf Ihrem aktuellen Klassenpfad existieren. Vermutlich ist dies in Ordnung für das, was Sie gerade betrachten, und es besteht die Möglichkeit, dass Sie dies in Betracht gezogen haben, aber wenn Sie zu irgendeinem Zeitpunkt eine Nicht-Veröffentlichung veröffentlicht habenfinal in die Wildnis entlassen haben (für unterschiedliche Ebenen von "Wild"), ist dies durchaus machbar jemand anderes hat eine eigene Unterklasse geschrieben, von der Sie nichts wissen.

Wenn Sie also zufällig alle Unterklassen sehen möchten, weil Sie eine Änderung vornehmen möchten und sehen möchten, wie sich dies auf das Verhalten der Unterklassen auswirkt, denken Sie an die Unterklassen, die Sie nicht sehen können. Im Idealfall sollten alle Ihre nicht privaten Methoden und die Klasse selbst gut dokumentiert sein. Nehmen Sie Änderungen gemäß dieser Dokumentation vor, ohne die Semantik von Methoden / nicht privaten Feldern zu ändern, und Ihre Änderungen sollten abwärtskompatibel sein, zumindest für jede Unterklasse, die Ihrer Definition der Oberklasse entspricht.

Andrzej Doyle
quelle
3

Der Grund, warum Sie einen Unterschied zwischen Ihrer Implementierung und Eclipse feststellen, besteht darin, dass Sie jedes Mal scannen, während Eclipse (und andere Tools) nur einmal scannen (meistens während des Projektladens) und einen Index erstellen. Wenn Sie das nächste Mal nach den Daten fragen, werden sie nicht erneut gescannt, sondern sehen sich den Index an.

OscarRyz
quelle
3

Ich verwende eine Reflection Lib, die Ihren Klassenpfad nach allen Unterklassen durchsucht: https://github.com/ronmamo/reflections

So würde es gemacht werden:

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

Fügen Sie sie einer statischen Zuordnung innerhalb (this.getClass (). GetName ()) des Konstruktors für übergeordnete Klassen hinzu (oder erstellen Sie eine Standardzuordnung), diese wird jedoch zur Laufzeit aktualisiert. Wenn eine verzögerte Initialisierung möglich ist, können Sie diesen Ansatz ausprobieren.

Ravindranath Akila
quelle
0

Ich musste dies als Testfall tun, um zu sehen, ob dem Code neue Klassen hinzugefügt wurden. Das habe ich getan

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
quelle