Come trovare tutte le sottoclassi di una determinata classe in Java?


207

Come si fa e si cerca di trovare tutte le sottoclassi di una determinata classe (o tutti gli implementatori di una determinata interfaccia) in Java? A partire da ora, ho un metodo per farlo, ma lo trovo abbastanza inefficiente (per non dire altro). Il metodo è:

  1. Ottieni un elenco di tutti i nomi di classe esistenti sul percorso della classe
  2. Carica ogni classe e verifica per vedere se si tratta di una sottoclasse o di un implementatore della classe o dell'interfaccia desiderata

In Eclipse, c'è una bella funzione chiamata Type Hierarchy che riesce a mostrarlo in modo abbastanza efficiente. Come si fa e lo si fa programmaticamente?


1
Sebbene la soluzione basata su Reflections e Spring sia interessante, avevo bisogno di una soluzione semplice che non avesse dipendenze. Sembra che il mio codice originale (con alcune modifiche) fosse la strada da percorrere.
Dal

1
Sicuramente puoi usare il metodo getSupeClass in modo ricorsivo?

In particolare stavo cercando tutte le sottoclassi di una determinata classe. getSuperClass non ti dirà quali sottoclassi ha una classe, ma solo la superclasse immediata per una sottoclasse specifica. Inoltre, il metodo isAssignableFrom on Class è più adatto a ciò che suggerisci (non è necessario ricorrere alla ricorsione).
Dal

Questa domanda è collegata da molti altri duplicati, ma non contiene alcuna risposta Java semplice e utile. Sospiro ...
Eric Duminil,

Risposte:


77

Non c'è altro modo per farlo se non quello che hai descritto. Pensaci: come si può sapere quali classi estendono ClassX senza eseguire la scansione di ogni classe sul percorso di classe?

Eclipse può solo parlarti del super e delle sottoclassi in quello che sembra essere un periodo di tempo "efficiente" perché ha già tutti i dati di tipo caricati nel punto in cui premi il pulsante "Visualizza nella gerarchia dei tipi" (dato che è compilare costantemente le tue classi, conoscere tutto sul percorso di classe, ecc.).


23
Ora esiste una semplice libreria chiamata org.reflections che aiuta con questa e altre attività di riflessione comuni. Con questa libreria puoi semplicemente chiamare reflections.getSubTypesOf(aClazz)) link
Enwired il

@matt b - se deve scansionare tutte le classi, significa che c'è un peggioramento delle prestazioni quando ci sono molte classi nel tuo progetto, anche se solo alcune di queste stanno classificando la tua sottoclasse?
LeTex,

Esattamente. Tocca solo tutte le classi. Puoi definire il tuo scanner e accelerarlo come escludendo determinati pacchetti che sai che la tua classe non verrebbe estesa o semplicemente aprire il file di classe e controllare per trovare il nome della classe nella sezione costante della classe evitando di lasciare che lo scanner di riflessione legga un molte più informazioni sulle classi che non contengono nemmeno il riferimento richiesto alla tua super classe (diretta) Indirettamente devi scansionare ulteriormente. Quindi è il migliore che ci sia al momento.
Martin Kersten,

La risposta di fforw ha funzionato per me e dovrebbe essere contrassegnata come la risposta corretta. Chiaramente questo è possibile con la scansione del percorso di classe.
Farrukh Najmi,

Ti sbagli, vedi sotto la risposta sulla libreria di

127

La scansione per le classi non è facile con Java puro.

Il framework di primavera offre una classe chiamata ClassPathScanningCandidateComponentProvider che può fare ciò di cui hai bisogno. L'esempio seguente troverà tutte le sottoclassi di MyClass nel pacchetto 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
}

Questo metodo ha il vantaggio aggiuntivo di utilizzare un analizzatore bytecode per trovare i candidati, il che significa che non caricherà tutte le classi che scansiona.


23
False deve essere passato come parametro durante la creazione di ClassPathScanningCandidateComponentProvider per disabilitare i filtri predefiniti. I filtri predefiniti corrisponderanno ad altri tipi di classi, ad es. Qualsiasi cosa annotata con @Component. Vogliamo solo che AssignableTypeFilter sia attivo qui.
MCDS

Dici che non è facile, ma come lo faremmo se volessimo farlo con Java puro?
Aequitas,

49

Non è possibile farlo utilizzando solo l'API Java Reflections integrata.

Esiste un progetto che esegue la scansione e l'indicizzazione necessarie del tuo percorso di classe in modo da poter accedere a queste informazioni ...

Riflessi

Un'analisi dei metadati di runtime Java, nello spirito di Scannotations

Reflections esegue la scansione del percorso di classe, indicizza i metadati, consente di interrogarlo in fase di esecuzione e può salvare e raccogliere tali informazioni per molti moduli all'interno del progetto.

Utilizzando Reflections è possibile eseguire una query sui metadati per:

  • ottenere tutti i sottotipi di qualche tipo
  • ottenere tutti i tipi annotati con alcune annotazioni
  • ottenere tutti i tipi annotati con alcune annotazioni, inclusa la corrispondenza dei parametri di annotazione
  • ottenere tutti i metodi annotati con alcuni

(disclaimer: non l'ho usato, ma la descrizione del progetto sembra adattarsi perfettamente alle tue esigenze.)


1
Interessante. Il progetto sembra avere alcune dipendenze che la loro documentazione non sembra menzionare. Vale a dire (quelli che ho trovato finora): javaassist, log4J, XStream
Avrom,

3
Ho incluso questo progetto con Maven e ha funzionato bene. Ottenere sottoclassi è in realtà il primo esempio di codice sorgente ed è lungo due righe :-)
KarlsFriend

Non è possibile utilizzare solo l'API Java Reflections integrata o semplicemente molto scomodo per farlo?
Flusso

1
Si prega di fare attenzione quando si intende utilizzare Reflections e quindi distribuire l'app WAR su GlassFish! Si è verificato un conflitto nella libreria Guava e la distribuzione non riuscirà con errore Errore distribuzione CDI: WELD-001408 - vedi GLASSFISH-20579 per maggiori dettagli. FastClasspathScanner è una soluzione in questo caso.
lu_ko,

Ho appena provato questo progetto e funziona. Lo uso solo per migliorare il modello di progettazione della strategia e ottenere tutta la classe di strategia (sottoclasse) che condividerò quest'ultima demo.
Xin Meng,

10

Non dimenticare che il Javadoc generato per una classe includerà un elenco di sottoclassi conosciute (e per interfacce, classi di implementazione note).


3
questo è totalmente errato, la superclasse non dovrebbe dipendere dalle loro sottoclassi, nemmeno in javadoc o commento.
cacciatore

@hunter non sono d'accordo. È assolutamente corretto che JavaDoc includa un elenco di sottoclassi note . Naturalmente, "noto" potrebbe non includere la classe che stai cercando, ma per alcuni casi d'uso sarà sufficiente.
Qw3ry

E potresti perdere alcune classi in ogni caso: posso caricare un nuovo vaso nel percorso di classe (durante il runtime) e ogni rilevazione avvenuta prima fallirà.
Qw3ry

10

Prova ClassGraph . (Dichiarazione di non responsabilità, sono l'autore). ClassGraph supporta la scansione di sottoclassi di una determinata classe, in fase di esecuzione o in fase di creazione, ma anche molto altro. ClassGraph può creare una rappresentazione astratta dell'intero grafico di classe (tutte le classi, annotazioni, metodi, parametri del metodo e campi) in memoria, per tutte le classi sul percorso di classe o per le classi in pacchetti autorizzati e puoi comunque interrogare quel grafico di classe tu vuoi. ClassGraph supporta più meccanismi di specifica del classpath e classloader rispetto a qualsiasi altro scanner, e funziona perfettamente anche con il nuovo sistema di moduli JPMS, quindi se si basa il codice su ClassGraph, il codice sarà al massimo portatile. Vedi l'API qui.


9

L'ho fatto diversi anni fa. Il modo più affidabile per farlo (cioè con API Java ufficiali e senza dipendenze esterne) è scrivere un doclet personalizzato per produrre un elenco che può essere letto in fase di esecuzione.

Puoi eseguirlo dalla riga di comando in questo modo:

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

o eseguilo da formica in questo modo:

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

Ecco il codice di base:

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

Per semplicità, ho rimosso l'analisi dell'argomento della riga di comando e sto scrivendo su System.out anziché su un file.


Mentre l'utilizzo a livello di codice potrebbe essere complicato, devo dire: un approccio intelligente !
Janaka Bandara,

8

So di essere in ritardo di qualche anno a questa festa, ma mi sono imbattuto in questa domanda cercando di risolvere lo stesso problema. Puoi utilizzare la ricerca interna di Eclipse a livello di codice, se stai scrivendo un plugin Eclipse (e quindi approfittare della loro memorizzazione nella cache, ecc.) Per trovare le classi che implementano un'interfaccia. Ecco il mio primo taglio (molto approssimativo):

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

Il primo problema che vedo finora è che sto solo prendendo lezioni che implementano direttamente l'interfaccia, non tutte le loro sottoclassi - ma una piccola ricorsione non fa mai male a nessuno.


3
OPPURE, risulta che non è necessario effettuare la propria ricerca. Puoi ottenere una gerarchia IType direttamente dal tuo IType chiamando .newTypeHierarchy () su di essa: dev.eclipse.org/newslists/news.eclipse.tools.jdt/msg05036.html
Curtis

7

Tenendo presente le limitazioni menzionate nelle altre risposte, è anche possibile utilizzare openpojoPojoClassFactory ( disponibile su Maven ) nel modo seguente:

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

Dov'è packageRootla stringa principale dei pacchetti in cui si desidera cercare (ad es. "com.mycompany"O anche solo"com" ), ed Superclassè il tuo supertipo (funziona anche su interfacce).


Di gran lunga la soluzione più veloce ed elegante tra quelle suggerite.
KidCrippler,


4

A seconda delle tue esigenze particolari, in alcuni casi il meccanismo del caricatore di servizi Java potrebbe raggiungere ciò che cerchi.

In breve, consente agli sviluppatori di dichiarare esplicitamente che una classe subclasse qualche altra classe (o implementa qualche interfaccia) elencandola in un file nella directory del file JAR / WAR META-INF/services. Può quindi essere scoperto usando la java.util.ServiceLoaderclasse che, quando viene dato un Classoggetto, genererà istanze di tutte le sottoclassi dichiarate di quella classe (o, se ilClass rappresenta un'interfaccia, tutte le classi che implementano quell'interfaccia).

Il vantaggio principale di questo approccio è che non è necessario scansionare manualmente l'intero percorso di classe per sottoclassi: tutta la logica di rilevamento è contenuta nella ServiceLoaderclasse e carica solo le classi dichiarate esplicitamente nella META-INF/servicesdirectory (non tutte le classi sul percorso di classe) .

Vi sono, tuttavia, alcuni svantaggi:

  • Non troverà tutte le sottoclassi, solo quelle dichiarate esplicitamente. Pertanto, se è necessario trovare davvero tutte le sottoclassi, questo approccio potrebbe non essere sufficiente.
  • Richiede allo sviluppatore di dichiarare esplicitamente la classe nella META-INF/servicesdirectory. Questo è un onere aggiuntivo per lo sviluppatore e può essere soggetto a errori.
  • Il ServiceLoader.iterator()genera istanze della sottoclasse, non la loroClass oggetti. Ciò causa due problemi:
    • Non si può dire nulla su come sono costruite le sottoclassi: il costruttore no-arg viene utilizzato per creare le istanze.
    • Come tale, le sottoclassi devono avere un costruttore predefinito o esplicitamente dichiarare un costruttore no-arg.

Apparentemente Java 9 affronterà alcune di queste carenze (in particolare quelle relative all'istanza delle sottoclassi).

Un esempio

Supponiamo che tu sia interessato a trovare classi che implementano un'interfaccia com.example.Example:

package com.example;

public interface Example {
    public String getStr();
}

La classe com.example.ExampleImplimplementa tale interfaccia:

package com.example;

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

Dichiareresti che la classe ExampleImplè un'implementazione di Examplecreando un file META-INF/services/com.example.Examplecontenente il testo com.example.ExampleImpl.

Quindi, è possibile ottenere un'istanza di ogni implementazione di Example(inclusa un'istanza di ExampleImpl) come segue:

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.

3

Va anche notato che questo naturalmente troverà solo tutte quelle sottoclassi che esistono sul tuo attuale percorso di classe. Presumibilmente questo è OK per quello che stai guardando attualmente, e probabilmente lo hai preso in considerazione, ma se in qualsiasi momento hai rilasciato una non finalclasse allo stato brado (per vari livelli di "selvaggio"), allora è del tutto fattibile che qualcun altro ha scritto la propria sottoclasse di cui non si saprà nulla.

Pertanto, se ti è capitato di voler vedere tutte le sottoclassi perché vuoi apportare una modifica e stai andando a vedere come influenza il comportamento delle sottoclassi, allora tieni a mente le sottoclassi che non riesci a vedere. Idealmente, tutti i tuoi metodi non privati ​​e la classe stessa dovrebbero essere ben documentati; apportare modifiche in base a questa documentazione senza cambiare la semantica dei metodi / campi non privati ​​e le modifiche devono essere retrocompatibili, per qualsiasi sottoclasse che abbia seguito almeno la definizione della superclasse.


3

Il motivo per cui vedi una differenza tra l'implementazione e Eclipse è perché esegui la scansione ogni volta, mentre Eclipse (e altri strumenti) esegue la scansione una sola volta (durante il caricamento del progetto la maggior parte delle volte) e crea un indice. La prossima volta che chiedi i dati, i dati non verranno nuovamente scansionati, ma guarda l'indice.


3

Sto usando una libreria di riflessione, che analizza il tuo percorso di classe per tutte le sottoclassi: https://github.com/ronmamo/reflections

Ecco come sarebbe fatto:

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

2

Aggiungili a una mappa statica all'interno (this.getClass (). GetName ()) del costruttore delle classi parent (o creane uno predefinito) ma questo verrà aggiornato in runtime. Se l'inizializzazione lazy è un'opzione, puoi provare questo approccio.



0

Avevo bisogno di fare questo come test case, per vedere se al codice erano state aggiunte nuove classi. Questo è quello che ho fatto

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