W czasie wykonywania znajdź wszystkie klasy w aplikacji Java, które rozszerzają klasę bazową

147

Chcę zrobić coś takiego:

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

Chcę więc przyjrzeć się wszystkim klasom we wszechświecie mojej aplikacji, a kiedy znajdę taką, która pochodzi od Animal, chcę utworzyć nowy obiekt tego typu i dodać go do listy. Dzięki temu mogę dodawać funkcje bez konieczności aktualizowania listy rzeczy. Mogę uniknąć następujących rzeczy:

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

Dzięki powyższemu podejściu mogę po prostu utworzyć nową klasę, która rozszerza Animal i zostanie automatycznie pobrana.

AKTUALIZACJA: 16-10-2008 9:00 czasu pacyficznego:

To pytanie wywołało wiele świetnych odpowiedzi - dziękuję. Z odpowiedzi i moich badań wynika, że ​​to, co naprawdę chcę zrobić, jest po prostu niemożliwe w Javie. Istnieją podejścia, takie jak mechanizm ServiceLoader firmy ddimitrov, który może działać - ale są one bardzo ciężkie dla tego, czego chcę, i uważam, że po prostu przenoszę problem z kodu Java do zewnętrznego pliku konfiguracyjnego. Aktualizacja 10.05.19 (11 lat później!) Istnieje obecnie kilka bibliotek, które mogą pomóc w tym według @ IvanNik za odpowiedź org.reflections wygląda dobrze. Również ClassGraph z @Luke Hutchison za odpowiedź wygląda interesująco. W odpowiedziach jest również kilka innych możliwości.

Innym sposobem określenia tego, czego chcę: funkcja statyczna w mojej klasie Animal wyszukuje i tworzy instancje wszystkich klas, które dziedziczą po Animal - bez dalszej konfiguracji / kodowania. Jeśli muszę skonfigurować, równie dobrze mogę po prostu utworzyć ich instancję w klasie Animal. Rozumiem to, ponieważ program Java to po prostu luźna federacja plików .class, tak właśnie jest.

Co ciekawe, wydaje się, że jest to dość trywialne w C #.

JohnnyLambada
źródło
1
Po dłuższych poszukiwaniach wydaje się, że jest to trudny orzech do zgryzienia w Javie. Oto wątek, który zawiera pewne informacje: forums.sun.com/thread.jspa?threadID=341935&start=15 Implementacje w nim są więcej niż potrzebuję, myślę, że na razie pozostanę przy drugiej implementacji.
JohnnyLambada
umieść to jako odpowiedź i pozwól czytelnikom zagłosować na to
VonC
3
Link do tego w C # nie pokazuje nic specjalnego. AC # Assembly jest odpowiednikiem pliku Java JAR. Jest to zbiór skompilowanych plików klas (i zasobów). Link pokazuje, jak pobrać klasy z jednego zestawu. Z Javą możesz to zrobić prawie tak łatwo. Problem polega na tym, że musisz przejrzeć wszystkie pliki JAR i prawdziwe luźne pliki (katalogi); musiałbyś zrobić to samo z .NET (przeszukać jakąś lub różnego rodzaju ŚCIEŻKĘ).
Kevin Brock

Odpowiedzi:

156

Używam org. Odbić :

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

Inny przykład:

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
źródło
6
Dzięki za link o org.reflections. Błędnie pomyślałem, że będzie to tak proste, jak w świecie .Net i ta odpowiedź pozwoliła mi zaoszczędzić dużo czasu.
akmad
1
Bardzo dziękuję za ten pomocny link do wspaniałego pakietu „org.reflections”! Dzięki temu w końcu znalazłem praktyczne i schludne rozwiązanie mojego problemu.
Hartmut P.
3
Czy jest sposób, aby uzyskać wszystkie odbicia, tj. Bez zwracania uwagi na konkretny pakiet, ale załadowania wszystkich dostępnych klas?
Joey Baruch
Pamiętaj, że znalezienie klas nie rozwiąże problemu z ich utworzeniem (linia 5 animals.add( new c() );twojego kodu), ponieważ chociaż Class.newInstance()istnieje, nie masz gwarancji, że dana klasa ma konstruktora bez parametrów (C # pozwala wymagać tego w ogólnym)
usr-local-ΕΨΗΕΛΩΝ
Zobacz także ClassGraph : github.com/classgraph/classgraph (Zastrzeżenie, jestem autorem). Podam przykład kodu w osobnej odpowiedzi.
Luke Hutchison
34

Sposób w Javie, aby zrobić to, co chcesz, polega na użyciu mechanizmu ServiceLoader .

Również wiele osób tworzy własne, mając plik w dobrze znanej lokalizacji ścieżki klas (np. /META-INF/services/myplugin.properties), a następnie używając ClassLoader.getResources () do wyliczenia wszystkich plików o tej nazwie ze wszystkich plików jar. Dzięki temu każdy słoik może eksportować swoich własnych dostawców i możesz utworzyć ich wystąpienie przez odbicie za pomocą Class.forName ()

ddimitrov
źródło
1
Aby łatwo wygenerować plik usług META-INF w oparciu o adnotacje dotyczące klas, możesz sprawdzić: metainf-services.kohsuke.org lub code.google.com/p/spi
elek
Bleah. Po prostu dodaje poziom złożoności, ale nie eliminuje konieczności tworzenia klasy, a następnie rejestrowania jej w odległej krainie.
kevin cline
Proszę zobaczyć odpowiedź poniżej z 26 głosami za, znacznie lepiej
SobiborTreblinka
4
Rzeczywiście, jeśli potrzebujesz jakiegoś rodzaju działającego rozwiązania, Refleksje / Skanowanie jest dobrą opcją, ale obie narzucają zewnętrzną zależność, są niedeterministyczne i nie są obsługiwane przez proces społeczności Java. Ponadto chciałem podkreślić, że nigdy nie można uzyskać niezawodnie WSZYSTKICH klas implementujących interfejs, ponieważ wyprodukowanie nowego z powietrza w dowolnym momencie jest trywialne. Mimo wszystkich swoich wad program ładujący usługi Java jest łatwy do zrozumienia i użycia.
Trzymałbym
11

Pomyśl o tym z punktu widzenia aspektowego; tak naprawdę chcesz poznać wszystkie klasy w czasie wykonywania, które rozszerzyły klasę Animal. (Myślę, że to nieco dokładniejszy opis twojego problemu niż tytuł; w przeciwnym razie nie sądzę, że masz pytanie dotyczące czasu wykonania).

Więc myślę, że chcesz utworzyć konstruktor swojej klasy bazowej (Animal), który dodaje do twojej tablicy statycznej (ja wolę ArrayLists, siebie, ale do każdej z nich) typ bieżącej klasy, której instancja jest tworzona.

Więc z grubsza;

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

Oczywiście będziesz potrzebować statycznego konstruktora w Animal, aby zainicjować instancję instantiatedDerivedClass ... Myślę, że to zrobi to, czego prawdopodobnie chcesz. Zauważ, że jest to zależne od ścieżki wykonania; jeśli masz klasę Dog, która pochodzi od Animal, która nigdy nie jest wywoływana, nie będziesz jej mieć na swojej liście Animal Class.

Paul Sonier
źródło
6

Niestety nie jest to całkowicie możliwe, ponieważ ClassLoader nie powie Ci, jakie klasy są dostępne. Możesz jednak podejść dość blisko, robiąc coś takiego:

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

Edycja: johnstok ma rację (w komentarzach), że działa to tylko dla samodzielnych aplikacji Java i nie będzie działać na serwerze aplikacji.

jonathan-stafford
źródło
Nie działa to dobrze, gdy klasy są ładowane w inny sposób, na przykład w kontenerze WWW lub J2EE.
johnstok
Prawdopodobnie mógłbyś wykonać lepszą pracę, nawet pod kontenerem, gdybyś odpytywał ścieżki (często, URL[]ale nie zawsze, więc może to nie być możliwe) z hierarchii ClassLoader. Często jednak musisz mieć pozwolenie, ponieważ zwykle masz SecurityManagerzaładowany plik w JVM.
Kevin Brock
U mnie zadziałało jak urok! Obawiałem się, że przechodzenie przez wszystkie klasy w ścieżce klas będzie zbyt duże i powolne, ale w rzeczywistości ograniczyłem sprawdzane pliki do plików zawierających „-ejb-” lub inne rozpoznawalne nazwy plików i nadal jest to 100 razy szybsze niż uruchomienie wbudowany szklista ryba lub EJBContainer. W mojej konkretnej sytuacji przeanalizowałem następnie klasy wyszukujące adnotacje „Stateless”, „Stateful” i „Singleton” i jeśli je zobaczyłem, dodałem nazwę JNDI Mapped do mojej MockInitialContextFactory. Dzięki jeszcze raz!
cs94njw
4

Możesz użyć ResolverUtil ( surowe źródło ) z Stripes Framework,
jeśli potrzebujesz czegoś prostego i szybkiego bez refaktoryzacji istniejącego kodu.

Oto prosty przykład bez załadowania żadnej z klas:

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

Działa to również na serwerze aplikacji, ponieważ tam został zaprojektowany;)

Kod zasadniczo wykonuje następujące czynności:

  • iteruj po wszystkich zasobach w pakietach, które określisz
  • zachowaj tylko zasoby kończące się na .class
  • Załaduj te klasy za pomocą ClassLoader#loadClass(String fullyQualifiedName)
  • Sprawdza, czy Animal.class.isAssignableFrom(loadedClass);
DJDaveMark
źródło
4

Najbardziej niezawodnym mechanizmem wyświetlania wszystkich podklas danej klasy jest obecnie ClassGraph , ponieważ obsługuje on najszerszy możliwy zestaw mechanizmów specyfikacji ścieżek klas , w tym nowy system modułów JPMS. (Jestem autorem.)

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
źródło
Wynik jest typu List <Class <Animal>>
hiaclibe
2

Java dynamicznie ładuje klasy, więc Twój wszechświat klas obejmowałby tylko te, które zostały już załadowane (i jeszcze nie zostały usunięte). Być może możesz zrobić coś z niestandardowym programem ładującym klasy, który mógłby sprawdzić nadtypy każdej załadowanej klasy. Nie sądzę, aby istniał interfejs API do wysyłania zapytań do zestawu załadowanych klas.

dokładnie
źródło
2

Użyj tego

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

Edytować:

  • biblioteka dla (ResolverUtil): Stripes
Ali Bagheri
źródło
2
Musisz powiedzieć trochę więcej o ResolverUtil. Co to jest? Z jakiej biblioteki pochodzi?
JohnnyLambada
1

Dziękuję wszystkim, którzy odpowiedzieli na to pytanie.

Wygląda na to, że to naprawdę twardy orzech do zgryzienia. Skończyło się na tym, że poddałem się i utworzyłem statyczną tablicę i metodę pobierającą w mojej klasie bazowej.

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

Wygląda na to, że Java po prostu nie jest skonfigurowana do samodzielnego wykrywania, tak jak C #. Przypuszczam, że problem polega na tym, że ponieważ aplikacja Java jest po prostu zbiorem plików .class w jakimś katalogu / pliku jar, środowisko wykonawcze nie wie o klasie, dopóki nie zostanie przywołana. W tym czasie program ładujący go ładuje - próbuję go odkryć, zanim odwołam się do niego, co nie jest możliwe bez wejścia do systemu plików i sprawdzenia.

Zawsze lubię kod, który potrafi się odkryć, zamiast tego, żebym musiał o nim mówić, ale niestety to też działa.

Dzięki jeszcze raz!

JohnnyLambada
źródło
1
Java jest skonfigurowana do samodzielnego wykrywania. Musisz tylko trochę więcej popracować. Zobacz moją odpowiedź po szczegóły.
Dave Jarvis
1

Korzystając z OpenPojo , możesz wykonać następujące czynności:

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
źródło
0

Jest to trudny problem i będziesz musiał znaleźć te informacje za pomocą analizy statycznej, która nie jest łatwo dostępna w czasie wykonywania. Po prostu pobierz ścieżkę klas swojej aplikacji i przejrzyj dostępne klasy i przeczytaj informacje o kodzie bajtowym klasy, z której ona dziedziczy. Zwróć uwagę, że klasa Dog nie może bezpośrednio dziedziczyć po Animal, ale może dziedziczyć po Pet, który z kolei dziedziczy po Animal, więc będziesz musiał śledzić tę hierarchię.

pdeva
źródło
0

Jednym ze sposobów jest użycie statycznych inicjatorów w klasach ... Nie sądzę, żeby były dziedziczone (nie zadziała, jeśli są):

public class Dog extends Animal{

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

Wymaga dodania tego kodu do wszystkich zaangażowanych klas. Ale unika gdzieś dużej, brzydkiej pętli, testując każdą klasę szukającą dzieci Animal.


źródło
3
To nie działa w moim przypadku. Ponieważ klasa nie jest nigdzie przywoływana, nigdy nie jest ładowana, więc inicjator statyczny nigdy nie jest wywoływany. Wygląda na to, że statyczny inicjator jest wywoływany przed wszystkim innym, gdy klasa jest ładowana - ale w moim przypadku klasa nigdy nie jest ładowana.
JohnnyLambada
0

Rozwiązałem ten problem dość elegancko, używając adnotacji na poziomie pakietu, a następnie ustawiając tę ​​adnotację jako argument jako listę klas.

Znajdź klasy Java implementujące interfejs

Implementacje muszą po prostu utworzyć pakiet-info.java i umieścić magiczną adnotację wraz z listą klas, które chcą obsługiwać.

Adam Gent
źródło