Au moment de l'exécution, recherchez toutes les classes d'une application Java qui étendent une classe de base

147

Je veux faire quelque chose comme ça:

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

Donc, je veux regarder toutes les classes de l'univers de mon application, et quand j'en trouve une qui descend d'Animal, je veux créer un nouvel objet de ce type et l'ajouter à la liste. Cela me permet d'ajouter des fonctionnalités sans avoir à mettre à jour une liste de choses. Je peux éviter ce qui suit:

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

Avec l'approche ci-dessus, je peux simplement créer une nouvelle classe qui étend Animal et elle sera automatiquement récupérée.

MISE À JOUR: 16/10/2008 9h00, heure normale du Pacifique:

Cette question a généré beaucoup de bonnes réponses - merci. D'après les réponses et mes recherches, j'ai trouvé que ce que je veux vraiment faire n'est tout simplement pas possible sous Java. Il existe des approches, telles que le mécanisme ServiceLoader de ddimitrov qui peuvent fonctionner - mais elles sont très lourdes pour ce que je veux, et je crois que je déplace simplement le problème du code Java vers un fichier de configuration externe. Mise à jour 10/05/19 (11 ans plus tard!) Il existe maintenant plusieurs bibliothèques qui peuvent vous aider, selon la réponse de @ IvanNik org.reflections semble bonne. Aussi ClassGraph de @Luke de Hutchison réponse semble intéressante. Il y a aussi plusieurs autres possibilités dans les réponses.

Une autre façon de dire ce que je veux: une fonction statique dans ma classe Animal trouve et instancie toutes les classes qui héritent d'Animal - sans autre configuration / codage. Si je dois configurer, je pourrais tout aussi bien les instancier dans la classe Animal de toute façon. Je comprends cela parce qu'un programme Java est juste une fédération lâche de fichiers .class que c'est comme ça.

Fait intéressant, il semble que ce soit assez trivial en C #.

JohnnyLambada
la source
1
Après avoir cherché pendant un certain temps, il semble que ce soit une noix difficile à résoudre en Java. Voici un fil de discussion qui contient quelques informations: forums.sun.com/thread.jspa?threadID=341935&start=15 Les implémentations sont plus que ce dont j'ai besoin, je suppose que je vais simplement m'en tenir à la deuxième implémentation pour le moment.
JohnnyLambada
mettez cela comme une réponse et laissez les lecteurs voter dessus
VonC
3
Le lien pour faire cela en C # ne montre rien de spécial. AC # Assembly équivaut au fichier JAR Java. C'est une collection de fichiers de classe compilés (et de ressources). Le lien montre comment extraire les classes d'un seul assembly. Vous pouvez le faire presque aussi facilement avec Java. Le problème pour vous est que vous devez parcourir tous les fichiers JAR et les vrais fichiers libres (répertoires); vous auriez besoin de faire de même avec .NET (recherchez un PATH d'un ou de plusieurs types).
Kevin Brock

Réponses:

156

J'utilise org.reflections :

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

Un autre exemple:

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
la source
6
Merci pour le lien sur org.reflections. J'ai pensé à tort que ce serait aussi simple que dans le monde .Net et cette réponse m'a fait gagner beaucoup de temps.
akmad
1
Merci en effet pour ce lien utile vers le super package "org.reflections"! Avec cela, j'ai enfin trouvé une solution pratique et soignée à mon problème.
Hartmut P.
3
Existe-t-il un moyen d'obtenir toutes les réflexions, c'est-à-dire sans noter un package spécifique, mais en chargeant toutes les classes disponibles?
Joey Baruch
Rappelez-vous que trouver les classes ne résoudra pas votre problème de les instancier (ligne 5 animals.add( new c() );de votre code), car tant qu'il Class.newInstance()existe, vous n'avez aucune garantie que la classe spécifique a un constructeur sans paramètre (C # vous permet de l'exiger dans un générique)
usr-local-ΕΨΗΕΛΩΝ
Voir aussi ClassGraph: github.com/classgraph/classgraph (Disclaimer, je suis l'auteur). Je vais donner un exemple de code dans une réponse séparée.
Luke Hutchison
34

La manière Java de faire ce que vous voulez est d'utiliser le mécanisme ServiceLoader .

De plus, beaucoup de gens utilisent le leur en ayant un fichier dans un emplacement de chemin de classe bien connu (c'est-à-dire /META-INF/services/myplugin.properties), puis en utilisant ClassLoader.getResources () pour énumérer tous les fichiers avec ce nom de tous les jars. Cela permet à chaque jar d'exporter ses propres fournisseurs et vous pouvez les instancier par réflexion à l'aide de Class.forName ()

ddimitrov
la source
1
Pour générer le fichier des services META-INF facilement en fonction des annotations sur les classes, vous pouvez vérifier: metainf-services.kohsuke.org ou code.google.com/p/spi
Elek
Bleah. Ajoute simplement un niveau de complexité, mais n'élimine pas le besoin à la fois de créer une classe et de l'enregistrer ensuite dans un pays très lointain.
kevin cline
S'il vous plaît voir la réponse ci-dessous avec 26 votes positifs, beaucoup mieux
SobiborTreblinka
4
En effet, si vous avez besoin d'un type de solutions fonctionnelles, Reflections / Scannotations est une bonne option, mais les deux imposent une dépendance externe, ne sont pas déterministes et ne sont pas pris en charge par le Java Community Process. De plus, je voulais souligner que vous ne pouvez jamais obtenir de manière fiable TOUTES les classes implémentant une interface, car il est trivial d'en fabriquer une nouvelle à tout moment. Malgré toutes ses verrues, le chargeur de services de Java est facile à comprendre et à utiliser. Je resterais à l'écart pour différentes raisons, mais l'analyse du
chemin de classe
11

Pensez-y d'un point de vue orienté aspect; ce que vous voulez faire, vraiment, c'est connaître toutes les classes au moment de l'exécution qui ont étendu la classe Animal. (Je pense que c'est une description légèrement plus précise de votre problème que votre titre; sinon, je ne pense pas que vous ayez une question d'exécution.)

Donc, ce que je pense que vous voulez, c'est créer un constructeur de votre classe de base (Animal) qui ajoute à votre tableau statique (je préfère ArrayLists, moi-même, mais à chacun le leur) le type de la classe actuelle qui est instanciée.

Donc, à peu près;

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

Bien sûr, vous aurez besoin d'un constructeur statique sur Animal pour initialiser instanciéDerivedClass ... Je pense que cela fera ce que vous voulez probablement. Notez que cela dépend du chemin d'exécution; si vous avez une classe Dog qui dérive d'Animal qui n'est jamais invoquée, vous ne l'aurez pas dans votre liste de classes d'animaux.

Paul Sonier
la source
6

Malheureusement, ce n'est pas tout à fait possible car le ClassLoader ne vous dira pas quelles classes sont disponibles. Vous pouvez cependant vous en rapprocher assez en faisant quelque chose comme ceci:

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

Edit: johnstok a raison (dans les commentaires) que cela ne fonctionne que pour les applications Java autonomes et ne fonctionnera pas sous un serveur d'applications.

Jonathan-Stafford
la source
Cela ne fonctionne pas bien lorsque les classes sont chargées par d'autres moyens, par exemple, dans un conteneur Web ou J2EE.
johnstok
Vous pourriez probablement faire un meilleur travail, même sous un conteneur, si vous interrogiez les chemins (souvent URL[]mais pas toujours, donc cela peut ne pas être possible) à partir de la hiérarchie ClassLoader. Souvent, cependant, vous devez avoir une autorisation car généralement vous avez un SecurityManagerfichier chargé dans la JVM.
Kevin Brock
A fonctionné à merveille pour moi! Je craignais que ce ne soit trop lourd et trop lent, en parcourant toutes les classes du chemin de classe, mais en fait, j'ai limité les fichiers que j'ai vérifiés à ceux contenant "-ejb-" ou d'autres noms de fichiers reconnaissables, et c'est toujours 100 fois plus rapide que de démarrer un glassfish ou EJBContainer intégré. Dans ma situation particulière, j'ai ensuite analysé les classes à la recherche d'annotations "Stateless", "Stateful" et "Singleton", et si je les voyais, j'ai ajouté le nom JNDI Mapped à mon MockInitialContextFactory. Merci encore!
cs94njw
4

Vous pouvez utiliser ResolverUtil ( source brute ) de Stripes Framework
si vous avez besoin de quelque chose de simple et rapide sans refactoriser le code existant.

Voici un exemple simple n'ayant chargé aucune des classes:

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

Cela fonctionne également dans un serveur d'applications car c'est là qu'il a été conçu pour fonctionner;)

Le code fait essentiellement ce qui suit:

  • itérer sur toutes les ressources du ou des packages que vous spécifiez
  • ne conserver que les ressources se terminant par .class
  • Chargez ces classes en utilisant ClassLoader#loadClass(String fullyQualifiedName)
  • Vérifie si Animal.class.isAssignableFrom(loadedClass);
DJDaveMark
la source
4

Le mécanisme le plus robuste pour lister toutes les sous-classes d'une classe donnée est actuellement ClassGraph , car il gère le plus large éventail possible de mécanismes de spécification de chemin de classe , y compris le nouveau système de modules JPMS. (Je suis l'auteur.)

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
la source
Le résultat est de type List <Class <Animal>>
hiaclibe
2

Java charge dynamiquement les classes, de sorte que votre univers de classes ne serait que celles qui ont déjà été chargées (et pas encore déchargées). Vous pouvez peut-être faire quelque chose avec un chargeur de classe personnalisé qui pourrait vérifier les supertypes de chaque classe chargée. Je ne pense pas qu'il existe une API pour interroger l'ensemble des classes chargées.

complètement
la source
2

utilisez ceci

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

Éditer:

  • bibliothèque pour (ResolverUtil): Stripes
Ali Bagheri
la source
2
Vous aurez besoin d'en dire un peu plus sur ResolverUtil. Qu'Est-ce que c'est? De quelle bibliothèque vient-il?
JohnnyLambada
1

Merci à tous ceux qui ont répondu à cette question.

Il semble que ce soit en effet une noix difficile à casser. J'ai fini par abandonner et créer un tableau statique et un getter dans ma classe de 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;
    }
}

Il semble que Java n'est tout simplement pas configuré pour l'auto-découverte comme l'est C #. Je suppose que le problème est que, comme une application Java n'est qu'une collection de fichiers .class dans un répertoire / fichier jar quelque part, le moteur d'exécution ne connaît pas une classe tant qu'elle n'est pas référencée. À ce moment-là, le chargeur le charge - ce que j'essaie de faire est de le découvrir avant de le référencer, ce qui n'est pas possible sans aller dans le système de fichiers et regarder.

J'aime toujours le code qui peut se découvrir au lieu d'avoir à le dire sur lui-même, mais hélas cela fonctionne aussi.

Merci encore!

JohnnyLambada
la source
1
Java est configuré pour l'auto-découverte. Vous avez juste besoin de faire un peu plus de travail. Voir ma réponse pour plus de détails.
Dave Jarvis
1

En utilisant OpenPojo, vous pouvez effectuer les opérations suivantes:

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

Il s'agit d'un problème difficile et vous devrez trouver ces informations à l'aide de l'analyse statique, qui n'est pas facilement disponible au moment de l'exécution. En gros, récupérez le chemin de classe de votre application, parcourez les classes disponibles et lisez les informations de bytecode d'une classe dont elle hérite. Notez qu'une classe Dog ne peut pas hériter directement d'Animal mais peut hériter de Pet qui hérite à son tour d'Animal, vous devrez donc garder une trace de cette hiérarchie.

pdeva
la source
0

Une façon est de faire en sorte que les classes utilisent des initialiseurs statiques ... Je ne pense pas que ceux-ci soient hérités (cela ne fonctionnera pas s'ils le sont):

public class Dog extends Animal{

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

Il vous oblige à ajouter ce code à toutes les classes impliquées. Mais cela évite d'avoir une grosse boucle moche quelque part, testant chaque classe à la recherche d'enfants d'Animal.


la source
3
Cela ne fonctionne pas dans mon cas. Comme la classe n'est référencée nulle part, elle n'est jamais chargée, donc l'initialiseur statique n'est jamais appelé. Il semble qu'un initialiseur statique soit appelé avant tout le reste lorsque la classe est chargée - mais dans mon cas, la classe n'est jamais chargée.
JohnnyLambada
0

J'ai résolu ce problème assez élégamment en utilisant les annotations au niveau du package, puis en faisant en sorte que cette annotation ait comme argument une liste de classes.

Rechercher des classes Java implémentant une interface

Les implémentations doivent simplement créer un package-info.java et y mettre l'annotation magique avec la liste des classes qu'elles veulent prendre en charge.

Adam Gent
la source