Jak znaleźć wszystkie podklasy danej klasy w Javie?

207

Jak się porusza i próbuje znaleźć wszystkie podklasy danej klasy (lub wszystkich implementatorów danego interfejsu) w Javie? Na razie mam metodę, aby to zrobić, ale uważam, że jest to dość nieefektywne (co najmniej). Metoda jest następująca:

  1. Uzyskaj listę wszystkich nazw klas, które istnieją na ścieżce klasy
  2. Załaduj każdą klasę i sprawdź, czy jest to podklasa lub implementator pożądanej klasy lub interfejsu

W Eclipse istnieje przyjemna funkcja zwana Hierarchią Typów, która udowadnia to dość skutecznie. Jak się porusza i robi to programowo?

Avrom
źródło
1
Chociaż rozwiązanie oparte na Reflections i Spring wygląda interesująco, potrzebowałem prostego rozwiązania, które nie było zależne. Wygląda na to, że mój oryginalny kod (z pewnymi poprawkami) był właściwą drogą.
Avrom
1
Na pewno możesz rekursywnie korzystać z metody getSupeClass?
W szczególności szukałem wszystkich podklas danej klasy. getSuperClass nie powie ci, jakie podklasy ma klasa, a jedynie natychmiastową superklasę dla określonej podklasy. Ponadto metoda isAssignableFrom w klasie lepiej nadaje się do tego, co sugerujesz (nie ma potrzeby rekurencji).
Avrom
To pytanie jest powiązane z wieloma innymi duplikatami, ale nie zawiera żadnej użytecznej, prostej odpowiedzi Java. Westchnienie ...
Eric Duminil

Odpowiedzi:

77

Nie ma innego sposobu niż to, co opisałeś. Pomyśl o tym - skąd ktokolwiek może wiedzieć, jakie klasy rozszerzają ClassX bez skanowania każdej klasy na ścieżce klas?

Zaćmienie może powiedzieć ci tylko o superklasach i podklasach w czasie, który wydaje się być „efektywny”, ponieważ ma już załadowane wszystkie dane typu w punkcie, w którym naciskasz przycisk „Wyświetlaj w hierarchii typów” (ponieważ jest to ciągłe kompilowanie twoich zajęć, wie o wszystkim na ścieżce klas itp.).

matowy b
źródło
23
Dostępna jest teraz prosta biblioteka o nazwie org.reflections, która pomaga w wykonywaniu tego i innych typowych zadań związanych z refleksją. Dzięki tej bibliotece możesz po prostu zadzwonić pod reflections.getSubTypesOf(aClazz)) link
Enwired
@matt b - jeśli musi przeskanować wszystkie klasy, czy to oznacza, że ​​występuje spadek wydajności, gdy masz dużo klas w swoim projekcie, nawet jeśli tylko kilka z nich podklasuje twoją klasę?
LeTex,
Dokładnie. To po prostu dotyka wszystkich klas. Możesz zdefiniować własny skaner, możesz go przyspieszyć, wykluczając pewne pakiety, o których wiesz, że klasa nie zostanie przedłużona, lub po prostu otwórz plik klasy i sprawdź, czy nazwa klasy znajduje się w sekcji stałej klasy, unikając pozwalania skanerowi odbicia na odczyt dużo więcej informacji na temat klas, które nawet nie zawierają wymaganego odniesienia do twojej (bezpośredniej) superklasy Pośrednio musisz dalej skanować. To najlepsze, co jest obecnie.
Martin Kersten
Odpowiedź fforw działała dla mnie i powinna być oznaczona jako poprawna odpowiedź. Oczywiście jest to możliwe w przypadku skanowania ścieżki klas.
Farrukh Najmi
Mylisz się, zobacz odpowiedź poniżej dotyczącą biblioteki
127

Skanowanie w poszukiwaniu klas nie jest łatwe dzięki czystej Javie.

Struktura Spring oferuje klasę o nazwie ClassPathScanningCandidateComponentProvider, która może zrobić to, czego potrzebujesz. Poniższy przykład znalazłby wszystkie podklasy MyClass w pakiecie 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
}

Dodatkową zaletą tej metody jest użycie analizatora kodu bajtowego do znalezienia kandydatów, co oznacza, że nie załaduje wszystkich skanowanych klas.

fforw
źródło
23
False należy przekazać jako parametr podczas tworzenia ClassPathScanningCandidateComponentProvider, aby wyłączyć filtry domyślne. Domyślne filtry będą pasować do innych typów klas, np. Cokolwiek opatrzone adnotacją @Component. Chcemy tylko, aby AssignableTypeFilter był tutaj aktywny.
MCDS
Mówisz, że to nie jest łatwe, ale jak byśmy to zrobili, gdybyśmy chcieli to zrobić z czystą Javą?
Aequitas
49

Nie jest to możliwe przy użyciu tylko wbudowanego interfejsu API Java Reflections.

Istnieje projekt, który wykonuje niezbędne skanowanie i indeksowanie ścieżki klasy, aby uzyskać dostęp do tych informacji ...

Refleksje

Analiza metadanych środowiska wykonawczego Java w duchu Scannotations

Refleksje skanują ścieżkę klasy, indeksują metadane, pozwalają wyszukiwać je w środowisku wykonawczym i mogą zapisywać i gromadzić te informacje dla wielu modułów w projekcie.

Za pomocą Refleksji możesz wyszukiwać swoje metadane w celu:

  • pobierz wszystkie podtypy jakiegoś typu
  • otrzymaj wszystkie typy adnotacji z pewnymi adnotacjami
  • pobierz wszystkie typy z adnotacjami, w tym dopasowanie parametrów adnotacji
  • niektóre metody opatrzone są adnotacjami

(zrzeczenie się: Nie korzystałem z niego, ale opis projektu wydaje się dokładnie pasować do twoich potrzeb).

Mark Renouf
źródło
1
Ciekawy. Wydaje się, że projekt ma pewne zależności, o których ich dokumentacja nie wspomina. Mianowicie (te, które do tej pory znalazłem): javaassist, log4J, XStream
Avrom
3
Dołączyłem ten projekt do maven i działał dobrze. Pobieranie podklas jest właściwie pierwszym przykładem kodu źródłowego i ma dwa wiersze :-)
KarlsFriend
Czy nie jest możliwe używanie tylko wbudowanego interfejsu API Java Reflections lub po prostu bardzo niewygodne?
Flow
1
Zachowaj ostrożność, gdy zamierzasz korzystać z Reflections, a następnie wdrożyć swoją aplikację WAR na GlassFish! W bibliotece Guava wystąpił konflikt, a wdrożenie zakończy się niepowodzeniem z błędem Błąd wdrożenia CDI: WELD-001408 - więcej szczegółów można znaleźć w GLASSFISH-20579 . FastClasspathScanner jest rozwiązaniem w tym przypadku.
lu_ko
Właśnie próbuję tego projektu i działa. Po prostu używam go, aby ulepszyć wzorzec projektowania strategii i uzyskać całą konkretną klasę strategii (podklasę), którą udostępnię później.
Xin Meng,
10

Nie zapominaj, że wygenerowany Javadoc dla klasy będzie zawierał listę znanych podklas (a dla interfejsów - znanych klas implementujących).

Obrabować
źródło
3
jest to całkowicie niepoprawne, superklasa nie powinna zależeć od ich podklas, nawet w javadoc lub komentarzach.
myśliwy
@hunter Nie zgadzam się. Jest całkowicie poprawne, że JavaDoc zawiera listę znanych podklas. Oczywiście „znane” może nie obejmować poszukiwanej klasy, ale w niektórych przypadkach wystarczy.
Qw3ry
W każdym razie możesz przegapić niektóre klasy: mogę załadować nowy słoik do ścieżki klasy (w czasie wykonywania) i każde wykrycie, które miało miejsce wcześniej, zakończy się niepowodzeniem.
Qw3ry
10

Wypróbuj ClassGraph . (Zastrzeżenie, jestem autorem). ClassGraph obsługuje skanowanie podklas danej klasy, w czasie wykonywania lub w czasie kompilacji, ale także o wiele więcej. ClassGraph może zbudować abstrakcyjną reprezentację całego wykresu klas (wszystkich klas, adnotacji, metod, parametrów metod i pól) w pamięci, dla wszystkich klas na ścieżce klas lub dla klas w pakietach umieszczonych na białej liście, i możesz jednak zapytać o ten wykres klas chcesz. ClassGraph obsługuje więcej mechanizmów specyfikacji ścieżek klas i programów ładujących klasy niż jakikolwiek inny skaner, a także bezproblemowo współpracuje z nowym systemem modułów JPMS, więc jeśli oprzesz swój kod na ClassGraph, Twój kod będzie maksymalnie przenośny. Zobacz API tutaj.

Luke Hutchison
źródło
9

Zrobiłem to kilka lat temu. Najbardziej niezawodnym sposobem na to (tj. Z oficjalnymi interfejsami API Java i bez zewnętrznych zależności) jest napisanie niestandardowego dokumentu, aby utworzyć listę, którą można odczytać w czasie wykonywania.

Możesz uruchomić go z wiersza poleceń w następujący sposób:

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

lub uruchom go z ant w ten sposób:

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

Oto podstawowy kod:

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

Dla uproszczenia usunąłem analizę argumentów wiersza poleceń i piszę do System.out zamiast pliku.

David Leppik
źródło
Używanie tego programowo może być trudne, muszę powiedzieć - inteligentne podejście!
Janaka Bandara
8

Wiem, że spóźniłem się na tę imprezę kilka lat, ale natknąłem się na to pytanie, próbując rozwiązać ten sam problem. Możesz użyć wewnętrznego wyszukiwania Eclipse programowo, jeśli piszesz wtyczkę Eclipse (i dzięki temu korzystasz z ich buforowania itp.), Aby znaleźć klasy, które implementują interfejs. Oto moje (bardzo szorstkie) pierwsze cięcie:

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

Pierwszym problemem, jaki do tej pory widzę, jest to, że łapię tylko klasy bezpośrednio implementujące interfejs, a nie wszystkie ich podklasy - ale niewielka rekurencja nikomu nie zaszkodzi.

Curtis
źródło
3
LUB, okazuje się, że nie musisz samodzielnie wyszukiwać. Możesz po prostu pobrać ITypeHierarchy bezpośrednio ze swojego IType, wywołując na nim funkcję
Curtis
7

Pamiętając o ograniczeniach wymienionych w innych odpowiedziach, możesz również użyć openpojoPojoClassFactory ( dostępne w Maven ) w następujący sposób:

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

Gdzie packageRootjest główny ciąg pakietów, w których chcesz wyszukiwać (np. A "com.mycompany"nawet tylko "com"), a także Superclasstwój nadtyp (działa to również na interfejsach).

mikołak
źródło
Zdecydowanie najszybsze i najbardziej eleganckie rozwiązanie spośród tych sugerowanych.
KidCrippler
4

Właśnie piszę proste demo, aby użyć org.reflections.Reflectionspodklas klasy abstrakcyjnej:

https://github.com/xmeng1/ReflectionsDemo

Xin Meng
źródło
byłoby bardziej produktywne, gdybyś opublikował tutaj próbkę kodu zamiast linku z wieloma klasami do wykopania.
JesseBoyd
4

W zależności od konkretnych wymagań w niektórych przypadkach mechanizm modułu ładującego usługi Java może osiągnąć to, czego szukasz.

Krótko mówiąc, pozwala programistom jawnie zadeklarować, że klasa podklasuje jakąś inną klasę (lub implementuje jakiś interfejs), umieszczając ją w pliku w katalogu pliku JAR / WAR META-INF/services. Można go następnie odkryć za pomocą java.util.ServiceLoaderklasy, która po otrzymaniu Classobiektu wygeneruje instancje wszystkich zadeklarowanych podklas tej klasy (lub, jeśli Classreprezentuje interfejs, wszystkie klasy implementujące ten interfejs).

Główną zaletą tego podejścia jest to, że nie ma potrzeby ręcznego skanowania całej ścieżki klasy w poszukiwaniu podklas - cała logika wykrywania jest zawarta w ServiceLoaderklasie i ładuje tylko klasy jawnie zadeklarowane w META-INF/serviceskatalogu (nie każda klasa w ścieżce klasy) .

Istnieją jednak pewne wady:

  • Nie znajdzie wszystkich podklas, tylko te, które są wyraźnie zadeklarowane. Jako takie, jeśli naprawdę potrzebujesz znaleźć wszystkie podklasy, takie podejście może być niewystarczające.
  • Wymaga od programisty jawnego zadeklarowania klasy w META-INF/serviceskatalogu. Jest to dodatkowe obciążenie dla programisty i może być podatne na błędy.
  • ServiceLoader.iterator()Generuje podklasy przykłady, a nie ich Classobiektami. Powoduje to dwa problemy:
    • Nie masz nic do powiedzenia na temat budowy podklas - do tworzenia instancji służy konstruktor no-arg.
    • Jako takie, podklasy muszą mieć domyślny konstruktor, lub jawność deklarować konstruktor bez arg.

Najwyraźniej Java 9 rozwiąże niektóre z tych niedociągnięć (w szczególności te dotyczące tworzenia podklas).

Przykład

Załóżmy, że chcesz znaleźć klasy, które implementują interfejs com.example.Example:

package com.example;

public interface Example {
    public String getStr();
}

Klasa com.example.ExampleImplimplementuje ten interfejs:

package com.example;

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

Zadeklarowałbyś, że klasa ExampleImpljest implementacją Examplepoprzez utworzenie pliku META-INF/services/com.example.Examplezawierającego tekst com.example.ExampleImpl.

Następnie można uzyskać instancję każdej implementacji Example(w tym instancję ExampleImpl) w następujący sposób:

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.
Prochowiec
źródło
3

Należy również zauważyć, że to oczywiście znajdzie tylko te wszystkie podklasy, które istnieją w bieżącej ścieżce klasy. Przypuszczalnie jest to OK dla tego, na co obecnie patrzysz, i są szanse, że wziąłeś to pod uwagę, ale jeśli w dowolnym momencie wypuściłeś nieklasyczną finaldziką przyrodę (dla różnych poziomów „dzikiej”), to jest całkowicie możliwe, że ktoś inny napisał własną podklasę, o której nie będziesz wiedzieć.

Jeśli więc chciałeś zobaczyć wszystkie podklasy, ponieważ chcesz wprowadzić zmiany i zobaczysz, jak wpływa to na zachowanie podklas - pamiętaj o tych podklasach, których nie widzisz. Idealnie wszystkie twoje nieprywatne metody, a sama klasa powinna być dobrze udokumentowana; wprowadź zmiany zgodnie z tą dokumentacją bez zmiany semantyki metod / pól nieprywatnych, a twoje zmiany powinny być kompatybilne wstecz, dla każdej podklasy, która przynajmniej była zgodna z twoją definicją nadklasy.

Andrzej Doyle
źródło
3

Różnica między implementacją a środowiskiem Eclipse jest widoczna, ponieważ skanowanie odbywa się za każdym razem, podczas gdy środowisko Eclipse (i inne narzędzia) skanuje tylko raz (podczas ładowania projektu przez większość czasu) i tworzy indeks. Następnym razem, gdy poprosisz o dane, dane nie będą skanowane ponownie, ale spójrz na indeks.

OscarRyz
źródło
3

Używam biblioteki refleksyjnej, która skanuje ścieżkę klasy dla wszystkich podklas: https://github.com/ronmamo/reflections

Tak by to było zrobić:

Reflections reflections = new Reflections("my.project");
Set<Class<? extends SomeType>> subTypes = reflections.getSubTypesOf(SomeType.class);
futchas
źródło
2

Dodaj je do mapy statycznej wewnątrz (this.getClass (). GetName ()) konstruktora klas nadrzędnych (lub utwórz domyślną), ale zostanie to zaktualizowane w czasie wykonywania. Jeśli leniwa inicjalizacja jest opcją, możesz wypróbować to podejście.

Ravindranath Akila
źródło
0

Musiałem to zrobić jako przypadek testowy, aby sprawdzić, czy do kodu dodano nowe klasy. Oto co zrobiłem

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