Comment trouvez-vous toutes les sous-classes d'une classe donnée en Java?

207

Comment procéder et essayer de trouver toutes les sous-classes d'une classe donnée (ou tous les implémenteurs d'une interface donnée) en Java? Pour l'instant, j'ai une méthode pour le faire, mais je la trouve assez inefficace (c'est le moins qu'on puisse dire). La méthode est la suivante:

  1. Obtenez une liste de tous les noms de classe qui existent sur le chemin de classe
  2. Chargez chaque classe et testez pour voir s'il s'agit d'une sous-classe ou d'un implémenteur de la classe ou de l'interface souhaitée

Dans Eclipse, il existe une fonctionnalité intéressante appelée la hiérarchie des types qui parvient à le montrer assez efficacement. Comment procéder et le faire par programme?

Avrom
la source
1
Bien que la solution basée sur les réflexions et le printemps semble intéressante, j'avais besoin d'une solution simple qui n'avait pas de dépendances. Il semble que mon code d'origine (avec quelques ajustements) était la voie à suivre.
Avrom
1
Vous pouvez sûrement utiliser la méthode getSupeClass de manière récursive?
Je cherchais spécifiquement toutes les sous - classes d'une classe donnée. getSuperClass ne vous dira pas quelles sous-classes ont une classe, obtenez seulement la super classe immédiate pour une sous-classe spécifique. De plus, la méthode isAssignableFrom on Class est mieux adaptée à ce que vous suggérez (pas besoin de récursivité).
Avrom
Cette question est liée à de nombreux autres doublons, mais elle ne contient aucune réponse Java simple et utile. Soupir ...
Eric Duminil

Réponses:

77

Il n'y a pas d'autre moyen de le faire que ce que vous avez décrit. Pensez-y - comment peut-on savoir quelles classes étendent ClassX sans analyser chaque classe sur le chemin de classe?

Eclipse ne peut vous parler que des super et des sous-classes dans ce qui semble être un temps "efficace" car il a déjà toutes les données de type chargées au point où vous appuyez sur le bouton "Afficher dans la hiérarchie des types" (car il s'agit de compilant constamment vos classes, sait tout sur le chemin de classe, etc.).

mat b
la source
23
Il existe maintenant une bibliothèque simple appelée org.reflections qui facilite cette tâche et d'autres tâches de réflexion courantes. Avec cette bibliothèque, vous pouvez simplement appeler le reflections.getSubTypesOf(aClazz)) lien
Enwired
@matt b - s'il doit analyser toutes les classes, cela signifie-t-il qu'il y a une dégradation des performances lorsque vous avez beaucoup de classes dans votre projet, même si seules quelques-unes sous-classent votre classe?
LeTex du
Exactement. Il touche juste toutes les classes. Vous pouvez définir votre propre scanner, vous pouvez l'accélérer comme exclure certains packages que vous savez que votre classe ne serait pas étendue ou simplement ouvrir le fichier de classe et vérifier pour trouver le nom de la classe dans la section constante de la classe en évitant de laisser le scanner de réflexion lire un beaucoup plus d'informations sur les classes qui ne contiennent même pas la référence requise à votre super classe (directe) Indirectement, vous devez analyser davantage. C'est donc le meilleur qu'il y ait en ce moment.
Martin Kersten
La réponse de fforw a fonctionné pour moi et doit être marquée comme la bonne réponse. De toute évidence, cela est possible avec l'analyse du chemin de classe.
Farrukh Najmi
Vous vous trompez, voyez la réponse ci-dessous à propos de la bibliothèque
127

La recherche de classes n'est pas facile avec Java pur.

Le framework Spring propose une classe appelée ClassPathScanningCandidateComponentProvider qui peut faire ce dont vous avez besoin. L'exemple suivant trouverait toutes les sous-classes de MyClass dans le package 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
}

Cette méthode a l'avantage supplémentaire d'utiliser un analyseur de bytecode pour trouver les candidats, ce qui signifie qu'elle ne chargera pas toutes les classes qu'elle analyse.

fforw
la source
23
False doit être transmis comme paramètre lors de la création de ClassPathScanningCandidateComponentProvider pour désactiver les filtres par défaut. Les filtres par défaut correspondent à d'autres types de classes, par exemple tout ce qui est annoté avec @Component. Nous voulons seulement que le AssignableTypeFilter soit actif ici.
MCDS
Vous dites que ce n'est pas facile, mais comment le ferions-nous si nous voulions le faire avec du java pur?
Aequitas
49

Il n'est pas possible de le faire en utilisant uniquement l'API Java Reflections intégrée.

Il existe un projet qui effectue l'analyse et l'indexation nécessaires de votre chemin de classe afin que vous puissiez accéder à ces informations ...

Réflexions

Une analyse des métadonnées d'exécution Java, dans l'esprit des scannotations

Reflections analyse votre chemin de classe, indexe les métadonnées, vous permet de l'interroger lors de l'exécution et peut enregistrer et collecter ces informations pour de nombreux modules au sein de votre projet.

À l'aide de Reflections, vous pouvez interroger vos métadonnées pour:

  • obtenir tous les sous-types d'un certain type
  • obtenir tous les types annotés avec une annotation
  • obtenir tous les types annotés avec une certaine annotation, y compris la correspondance des paramètres d'annotation
  • obtenir toutes les méthodes annotées avec certaines

(Avertissement: je ne l'ai pas utilisé, mais la description du projet semble correspondre exactement à vos besoins.)

Mark Renouf
la source
1
Intéressant. Le projet semble avoir quelques dépendances que leur documentation ne semble pas mentionner. À savoir (ceux que j'ai trouvés jusqu'à présent): javaassist, log4J, XStream
Avrom
3
J'ai inclus ce projet avec maven et cela a bien fonctionné. Obtenir des sous-classes est en fait le premier exemple de code source et
comprend
N'est-il pas possible d'utiliser uniquement l'API Java Reflections intégrée ou simplement très gênant de le faire?
Flow
1
Soyez prudent lorsque vous allez utiliser Reflections, puis déployez votre application WAR sur GlassFish! Il y a un conflit dans la bibliothèque de Guava et le déploiement échouera avec une erreur de déploiement du CDI: WELD-001408 - veuillez consulter GLASSFISH-20579 pour plus de détails. FastClasspathScanner est une solution dans ce cas.
lu_ko
Je viens d'essayer ce projet et ça marche. Je l'utilise juste pour améliorer le modèle de conception de stratégie et obtenir toute la classe concrète de stratégie (sous-classe) Je partagerai la dernière démo.
Xin Meng
10

N'oubliez pas que la Javadoc générée pour une classe inclura une liste de sous-classes connues (et pour les interfaces, les classes d'implémentation connues).

Rob
la source
3
c'est totalement incorrect, la super classe ne devrait pas dépendre de leurs sous-classes, pas même en javadoc ou en commentaire.
chasseur
@hunter, je ne suis pas d'accord. Il est tout à fait exact que JavaDoc inclut une liste de sous- classes connues . Bien sûr, "connu" peut ne pas inclure la classe que vous recherchez, mais pour certains cas d'utilisation, cela suffira.
Qw3ry
Et vous pourriez manquer certaines classes dans tous les cas: je peux charger un nouveau pot dans le chemin de classe (pendant l'exécution) et chaque détection qui s'est produite avant échouera.
Qw3ry
10

Essayez ClassGraph . (Avertissement, je suis l'auteur). ClassGraph prend en charge l'analyse des sous-classes d'une classe donnée, soit au moment de l'exécution, soit au moment de la construction, mais aussi beaucoup plus. ClassGraph peut créer une représentation abstraite du graphique de classe entier (toutes les classes, annotations, méthodes, paramètres de méthode et champs) en mémoire, pour toutes les classes sur le chemin de classe, ou pour les classes dans des packages en liste blanche, et vous pouvez cependant interroger ce graphique de classe tu veux. ClassGraph soutient plusieurs mécanismes de spécification classpath et classloaders que tout autre scanner, et fonctionne également de façon transparente avec le nouveau système de module JPMS, donc si vous basez votre code sur ClassGraph, votre code sera au maximum portable. Voir l'API ici.

Luke Hutchison
la source
9

Je l'ai fait il y a plusieurs années. La façon la plus fiable de le faire (c'est-à-dire avec les API Java officielles et sans dépendances externes) est d'écrire un doclet personnalisé pour produire une liste qui peut être lue au moment de l'exécution.

Vous pouvez l'exécuter à partir de la ligne de commande comme ceci:

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

ou exécutez-le à partir de fourmi comme ceci:

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

Voici le code de 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;
        }
    }
}

Pour simplifier, j'ai supprimé l'analyse des arguments de la ligne de commande et j'écris dans System.out plutôt que dans un fichier.

David Leppik
la source
Bien que l'utilisation de ce programme puisse être délicate, je dois dire - une approche intelligente !
Janaka Bandara
8

Je sais que j'ai quelques années de retard à cette fête, mais je suis tombé sur cette question en essayant de résoudre le même problème. Vous pouvez utiliser la recherche interne d'Eclipse par programmation, si vous écrivez un plugin Eclipse (et ainsi profiter de leur mise en cache, etc.), pour trouver des classes qui implémentent une interface. Voici ma première coupe (très grossière):

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

Le premier problème que je vois jusqu'à présent est que je n'attrape que les classes qui implémentent directement l'interface, pas toutes leurs sous-classes - mais une petite récursivité n'a jamais blessé personne.

Curtis
la source
3
OU, il s'avère que vous n'avez pas à faire votre propre recherche. Vous pouvez simplement obtenir un ITypeHierarchy directement à partir de votre IType en appelant .newTypeHierarchy () dessus: dev.eclipse.org/newslists/news.eclipse.tools.jdt/msg05036.html
Curtis
7

En gardant à l'esprit les limitations mentionnées dans les autres réponses, vous pouvez également utiliser openpojo'sPojoClassFactory ( disponible sur Maven ) de la manière suivante:

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

packageRootest la chaîne racine des packages dans lesquels vous souhaitez rechercher (par exemple "com.mycompany"ou même juste "com"), et Superclassest votre supertype (cela fonctionne également sur les interfaces).

mikołak
la source
De loin la solution la plus rapide et la plus élégante de celles proposées.
KidCrippler
4

Je viens d'écrire une démo simple pour utiliser le org.reflections.Reflectionspour obtenir des sous-classes de classe abstraite:

https://github.com/xmeng1/ReflectionsDemo

Xin Meng
la source
ce serait plus productif si vous publiez un exemple de code ici plutôt qu'un lien avec beaucoup de classes à creuser.
JesseBoyd
4

En fonction de vos besoins particuliers, dans certains cas, le mécanisme du chargeur de services Java peut atteindre ce que vous recherchez.

En bref, il permet aux développeurs de déclarer explicitement qu'une classe sous-classe une autre classe (ou implémente une interface) en la répertoriant dans un fichier dans le répertoire du fichier JAR / WAR META-INF/services. Il peut ensuite être découvert à l'aide de la java.util.ServiceLoaderclasse qui, lorsqu'elle reçoit un Classobjet, générera des instances de toutes les sous-classes déclarées de cette classe (ou, si Classreprésente une interface, toutes les classes implémentant cette interface).

Le principal avantage de cette approche est qu'il n'est pas nécessaire d'analyser manuellement le chemin de classe entier pour les sous-classes - toute la logique de découverte est contenue dans la ServiceLoaderclasse, et elle ne charge que les classes explicitement déclarées dans le META-INF/servicesrépertoire (pas toutes les classes sur le chemin de classe) .

Il existe cependant certains inconvénients:

  • Il ne trouvera pas toutes les sous-classes, seulement celles qui sont explicitement déclarées. En tant que tel, si vous devez vraiment trouver toutes les sous-classes, cette approche peut être insuffisante.
  • Il nécessite que le développeur déclare explicitement la classe sous le META-INF/servicesrépertoire. Ceci représente une charge supplémentaire pour le développeur et peut être sujet à erreurs.
  • le ServiceLoader.iterator() génère des instances de sous-classe, pas leurs Classobjets. Cela provoque deux problèmes:
    • Vous n'avez pas votre mot à dire sur la façon dont les sous-classes sont construites - le constructeur no-arg est utilisé pour créer les instances.
    • En tant que telles, les sous-classes doivent avoir un constructeur par défaut, ou doivent explicitement déclarer un constructeur sans argument.

Apparemment, Java 9 corrigera certaines de ces lacunes (en particulier celles concernant l'instanciation des sous-classes).

Un exemple

Supposons que vous souhaitiez trouver des classes qui implémentent une interface com.example.Example:

package com.example;

public interface Example {
    public String getStr();
}

La classe com.example.ExampleImplimplémente cette interface:

package com.example;

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

Vous déclareriez que la classe ExampleImplest une implémentation de Exampleen créant un fichier META-INF/services/com.example.Examplecontenant le texte com.example.ExampleImpl.

Ensuite, vous pouvez obtenir une instance de chaque implémentation de Example(y compris une instance de ExampleImpl) comme suit:

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
la source
3

Il convient également de noter que cela ne trouvera bien sûr que toutes les sous-classes qui existent sur votre chemin de classe actuel. Vraisemblablement, cela convient à ce que vous regardez actuellement, et il est probable que vous ayez envisagé cela, mais si vous avez à un moment donné libéré une non- finalclasse dans la nature (pour différents niveaux de "sauvage"), il est tout à fait possible que quelqu'un d'autre a écrit sa propre sous-classe que vous ne connaissez pas.

Ainsi, si vous vouliez voir toutes les sous-classes parce que vous voulez apporter un changement et que vous allez voir comment cela affecte le comportement des sous-classes - alors gardez à l'esprit les sous-classes que vous ne pouvez pas voir. Idéalement, toutes vos méthodes non privées, et la classe elle-même doit être bien documentée; apportez des modifications conformément à cette documentation sans modifier la sémantique des méthodes / champs non privés et vos modifications doivent être rétrocompatibles, pour toute sous-classe qui a suivi votre définition de la superclasse au moins.

Andrzej Doyle
la source
3

La raison pour laquelle vous voyez une différence entre votre implémentation et Eclipse est que vous effectuez une analyse à chaque fois, tandis qu'Eclipse (et d'autres outils) analysent une seule fois (lors du chargement du projet la plupart du temps) et créent un index. La prochaine fois que vous demanderez les données, elles ne seront plus analysées, mais regardez l'index.

OscarRyz
la source
3

J'utilise une bibliothèque de réflexion, qui analyse votre chemin de classe pour toutes les sous-classes: https://github.com/ronmamo/reflections

Voici comment cela se ferait:

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

Ajoutez-les à une carte statique à l'intérieur (this.getClass (). GetName ()) du constructeur des classes parent (ou créez-en un par défaut) mais cela sera mis à jour lors de l'exécution. Si l'initialisation paresseuse est une option, vous pouvez essayer cette approche.

Ravindranath Akila
la source
0

J'avais besoin de le faire comme cas de test, pour voir si de nouvelles classes avaient été ajoutées au code. C'est ce que j'ai fait

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
la source