Як ви знаходите всі підкласи даного класу на Java?


207

Як можна дійти і намагатися знайти всі підкласи даного класу (або всіх реалізаторів даного інтерфейсу) на Java? На даний момент у мене є метод зробити це, але я вважаю це досить неефективним (якщо не менше). Метод:

  1. Отримайте список усіх імен класів, які існують на шляху до класу
  2. Завантажте кожен клас і протестуйте, чи є це підкласом або реалізатором потрібного класу чи інтерфейсу

У Eclipse є приємна особливість під назвою Ієрархія типу, яка вдається показати це досить ефективно. Як це робити і робити це програмно?


1
Хоча рішення, що базується на Роздумах та Весні, виглядають цікавими, мені потрібно було просте рішення, яке не мало залежності. Здається, мій оригінальний код (з деякими налаштуваннями) був шлях.
Авром

1
Невже ви можете використовувати метод getSupeClass рекурсивно?

Я спеціально шукав усі підкласи даного класу. getSuperClass не скаже вам, які підкласи має клас, лише отримайте негайний суперклас для конкретного підкласу. Крім того, метод isAssignableFrom для Class краще підходить для запропонованого вами (немає необхідності в рекурсії).
Авром

Це питання пов'язане з багатьма іншими дублікатами, але воно не містить корисної, простої відповіді на Java.
Зітхніть

Відповіді:


77

Немає іншого способу зробити це, крім описаного вами. Подумайте над цим - як хтось може знати, які класи поширюють ClassX, не скануючи кожен клас на класі?

Eclipse може розповісти вам про суперкласи та підкласи лише за те, що, здається, "ефективний" час, тому що в ньому вже є всі типи даних, завантажені в точці, де ви натискаєте кнопку "Відображення в ієрархії типів" (оскільки це постійно збирає свої заняття, знає про все на уроці тощо).


23
Зараз існує проста бібліотека під назвою org.reflections, яка допомагає з цим та іншими поширеними завданнями на роздуми. З цією бібліотекою ви можете просто зателефонувати за reflections.getSubTypesOf(aClazz)) посиланням
Enwired

@matt b - якщо йому доведеться сканувати всі класи, чи це означає, що є зниження продуктивності, коли у вашому проекті багато класів, хоча лише деякі з них підкласують ваш клас?
LeTex

Саме так. Це просто торкається всіх класів. Ви можете визначити свій власний сканер, ви можете прискорити його, як виключаючи певні пакунки, за якими ви знаєте, що ваш клас не буде розширений, або просто відкрийте файл класу та перевірте, чи не знайдете ім'я класу в постійному розділі класу, уникаючи того, щоб сканер відображення прочитав a набагато більше інформації про класи, які навіть не містять необхідного посилання на ваш (прямий) суперклас. Побічно потрібно сканувати далі. Тож найкраще, що є на даний момент.
Мартін Керстен

Відповідь fforw працювала на мене і повинна бути позначена як правильна відповідь. Зрозуміло, що це можливо при скануванні на класовому шляху.
Farrukh Najmi

Ви помиляєтесь, див. Відповідь нижче про бібліотеку

127

Сканування класів непросте для чистої Java.

Весняна рамка пропонує клас під назвою ClassPathScanningCandidateComponentProvider, який може робити все, що вам потрібно. Наступний приклад знайде всі підкласи MyClass в пакеті 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
}

Цей метод має додаткову перевагу використання аналізатора байт-коду для пошуку кандидатів, а значить, він не завантажує всі класи, які він сканує.


23
False має бути передано як параметр під час створення ClassPathScanningCandidateComponentProvider для відключення фільтрів за замовчуванням. Фільтри за замовчуванням відповідатимуть іншим типам класів, наприклад, будь-чому, що позначається @Component. Ми хочемо лише, щоб AssignableTypeFilter був активним тут.
MCDS

Ви кажете, що це непросто, але як би ми це зробили, якби хотіли це зробити з чистою Java?
Aequitas

49

Це неможливо зробити, використовуючи лише вбудований API Java Reflections.

Існує проект, який виконує необхідне сканування та індексацію вашого класного шляху, щоб ви могли отримати доступ до цієї інформації ...

Роздуми

Аналіз метаданих під час виконання Java в дусі Scannotation

Роздуми сканує ваш класний шлях, індексує метадані, дозволяє запитувати його під час виконання та може зберігати та збирати цю інформацію для багатьох модулів у вашому проекті.

Використовуючи Роздуми, ви можете запитувати свої метадані щодо:

  • отримати всі підтипи певного типу
  • отримуйте всі типи з анотацією
  • отримувати всі типи коментарів із деякою анотацією, включаючи відповідність параметрів анотації
  • отримати всі методи, анотовані з деякими

(відмова від відповідальності: я цього не використовував, але опис проекту, здається, відповідає точно вашим потребам.)


1
Цікаво. Схоже, проект має деякі залежності, про які, схоже, не згадується їх документація. А саме (ті, кого я знайшов поки що): javaassist, log4J, XStream
Avrom

3
Я включив цей проект з Maven, і він прекрасно працював. Отримання підкласів насправді є першим прикладом вихідного коду і має два рядки :-)
KarlsFriend

Неможливо використовувати лише вбудований API Java Reflections або просто незручно це зробити?
Потік

1
Будьте уважні, коли ви збираєтесь використовувати Reflections, а потім розгорніть свій додаток WAR для GlassFish! У бібліотеці Guava виникає конфлікт, і розгортання не вдасться при помилці розгортання CDI: WELD-001408 - див. GLASSFISH-20579 для отримання більш детальної інформації. FastClasspathScanner - це рішення в цьому випадку.
lu_ko

Я просто спробую цей проект, і він працює. Я просто використовую його для вдосконалення структури дизайну стратегії та отримання всього конкретного класу (підкласу) стратегії. Я поділюся на демонстрацію останнього.
Xin Meng

10

Не забувайте, що згенерований Javadoc для класу буде містити список відомих підкласів (а також для інтерфейсів, відомих класів реалізації).


3
це абсолютно невірно, суперклас не повинен залежати від їх підкласів, навіть у javadoc чи коментарях.
мисливець

@hunter Я не згоден. Цілком правильно, що JavaDoc включає список відомих підкласів. Звичайно, "відомий" може не включати шуканий клас, але для деяких випадків використання цього буде достатньо.
Qw3ry

І ви можете пропустити деякі заняття в будь-якому випадку: я можу завантажити нову банку в classpath (під час виконання), і кожне виявлення, що відбулося раніше, не вдасться.
Qw3ry

10

Спробуйте ClassGraph . (Відмова, я автор). ClassGraph підтримує сканування підкласів даного класу, під час виконання або в час збирання, але також набагато більше. ClassGraph може створити абстрактне представлення всього графа класу (усіх класів, анотацій, методів, параметрів методу та полів) у пам'яті, для всіх класів на classpath або для класів у пакетах із списками білого списку, і ви можете запитати цей графік класу, проте ти хочеш. ClassGraph підтримує більше механізмів специфікації classload і завантажувачів, ніж будь-який інший сканер, а також працює безперебійно з новою системою модулів JPMS, тому якщо ви базуєте свій код на ClassGraph, ваш код буде максимально портативним. Дивіться API тут.


9

Я робив це кілька років тому. Найнадійніший спосіб зробити це (тобто з офіційними API API Java та без зовнішніх залежностей) - це написання користувальницького доклета для створення списку, який можна прочитати під час виконання.

Ви можете запустити його з командного рядка так:

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

або запустити його від мурашки так:

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

Ось основний код:

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

Для простоти я видалив розбір аргументів командного рядка і пишу в System.out, а не у файл.


Хоча програмно використовувати це може бути складним, я мушу сказати - розумний підхід!
Джанака Бандара

8

Я знаю, що я запізнився на кілька років до цієї партії, але я натрапив на це питання, намагаючись вирішити ту саму проблему. Ви можете використовувати внутрішній пошук Eclipse програмно, якщо ви пишете плагін Eclipse (і таким чином скористаєтеся їх кешуванням тощо), щоб знайти класи, які реалізують інтерфейс. Ось мій (дуже грубий) перший крій:

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

Перша проблема, яку я бачу до цього часу, полягає в тому, що я ловлю лише класи, які безпосередньо реалізують інтерфейс, а не всі їх підкласи - але невелика рекурсія ніколи нікому не шкодила.


3
АБО, виявляється, вам не потрібно робити власний пошук. Ви можете просто отримати ITypeHierarchy безпосередньо зі свого IType, зателефонувавши на .newTypeHierarchy () на ньому: dev.eclipse.org/newslists/news.eclipse.tools.jdt/msg05036.html
Кертіс,

7

Маючи на увазі обмеження, згадані в інших відповідях, ви також можете використовувати openpojoPojoClassFactory ( доступні в Maven ) таким чином:

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

Де packageRootзнаходиться коренева рядок пакетів, в яких ви хочете шукати (наприклад, "com.mycompany"або навіть просто "com"), і Superclassваш супертип (це працює і на інтерфейсах).


На сьогоднішній день найшвидше та найелегантніше рішення із запропонованих.
KidCrippler

4

Я просто пишу просту демонстрацію, щоб використовувати org.reflections.Reflectionsпідкласи абстрактного класу:

https://github.com/xmeng1/ReflectionsDemo


було б більш продуктивним, якби ви розмістили тут зразок коду, а не посилання з великою кількістю класів для копання.
JesseBoyd

4

Залежно від ваших конкретних вимог, в деяких випадках механізм завантажувача служб Java може досягти того, що вам потрібно.

Коротше кажучи, це дозволяє розробникам чітко заявляти, що клас підкласирує якийсь інший клас (або реалізує якийсь інтерфейс), перераховуючи його у файл у каталозі файлу JAR / WAR META-INF/services. Потім його можна виявити за допомогою java.util.ServiceLoaderкласу, який при наданні Classоб'єкта генерує екземпляри всіх оголошених підкласів цього класу (або, якщо Classявляє собою інтерфейс, усіх класів, що реалізують цей інтерфейс).

Основна перевага цього підходу полягає в тому, що немає необхідності вручну сканувати весь класний шлях на підкласи - вся логіка виявлення міститься в ServiceLoaderкласі, і він завантажує лише класи, явно оголошені в META-INF/servicesкаталозі (не кожен клас на classpath) .

Однак є деякі недоліки:

  • Він не знайде всіх підкласів, лише тих, які явно оголошені. Таким чином, якщо вам потрібно справді знайти всі підкласи, такий підхід може бути недостатнім.
  • Він вимагає від розробника явно оголосити клас під META-INF/servicesкаталогом. Це додаткове навантаження на розробника і може спричинити помилки.
  • ServiceLoader.iterator()Створює екземпляри підкласу, а не їхня Classоб'єкти. Це викликає дві проблеми:
    • Ви не можете сказати, як побудовані підкласи - конструктор no-arg використовується для створення екземплярів.
    • Таким чином, підкласи повинні мати конструктор за замовчуванням або повинен експліцитно оголошувати конструктор без аргументів.

Мабуть, Java 9 вирішить деякі з цих недоліків (зокрема, ті, що стосуються інстанції підкласів).

Приклад

Припустимо, вам цікаво знайти класи, які реалізують інтерфейс com.example.Example:

package com.example;

public interface Example {
    public String getStr();
}

Клас com.example.ExampleImplреалізує цей інтерфейс:

package com.example;

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

Ви б оголосили, що клас ExampleImplє реалізацією Example, створивши файл, META-INF/services/com.example.Exampleщо містить текст com.example.ExampleImpl.

Тоді ви можете отримати екземпляр кожної реалізації Example(включаючи екземпляр ExampleImpl) наступним чином:

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

Слід також зазначити, що це, звичайно, знайде лише ті підкласи, які існують на вашому поточному класі. Імовірно, це нормально для того, на що ви зараз дивитесь, і, швидше за все, ви це врахували, але якщо ви в будь-який момент випустили неклас finalу дику природу (для різних рівнів "диких"), то цілком можливо, що хтось ще написав свій підклас, про який ви не будете знати.

Таким чином, якщо вам траплялося бачити всі підкласи, тому що ви хочете внести зміни і збираєтесь побачити, як це впливає на поведінку підкласів - тоді майте на увазі підкласи, які ви не бачите. В ідеалі всі ваші неприватні методи, а сам клас повинні бути добре задокументовані; вносити зміни відповідно до цієї документації, не змінюючи семантику методів / неприватних полів, і ваші зміни повинні бути зворотно сумісними для будь-якого підкласу, який принаймні відповідав вашому визначенню надкласу.


3

Причина, коли ви бачите різницю між вашою реалізацією та Eclipse, полягає в тому, що ви скануєте кожен раз, тоді як Eclipse (та інші інструменти) сканують лише один раз (під час завантаження проекту більшість разів) та створюють індекс. Наступного разу, коли ви запитаєте дані, вони не скануються знову, але подивіться на індекс.


3

Я використовую ліфтинг для відображення, який сканує ваш класний шлях для всіх підкласів: https://github.com/ronmamo/reflections

Ось як це робиться:

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

2

Додайте їх до статичної карти всередині (this.getClass (). GetName ()) конструктора батьківських класів (або створіть за замовчуванням), але це буде оновлено під час виконання. Якщо лінива ініціалізація - це варіант, ви можете спробувати цей підхід.


1

Ви можете використовувати бібліотеку org.reflections, а потім створити об'єкт класу Reflections. Використовуючи цей об'єкт, ви можете отримати список усіх підкласів даного класу. https://www.javadoc.io/doc/org.reflections/reflections/0.9.10/org/reflections/Reflections.html

    Reflections reflections = new Reflections("my.project.prefix");
    System.out.println(reflections.getSubTypesOf(A.class)));

0

Мені потрібно було це зробити як тестовий випадок, щоб перевірити, чи були додані нові класи до коду. Це я і зробив

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());
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.