Во время выполнения найдите все классы в приложении Java, которые расширяют базовый класс

147

Я хочу сделать что-то вроде этого:

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

Итак, я хочу посмотреть на все классы во вселенной моего приложения, и когда я найду тот, который происходит от Animal, я хочу создать новый объект этого типа и добавить его в список. Это позволяет мне добавлять функциональность, не обновляя список вещей. Я могу избежать следующего:

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

С помощью описанного выше подхода я могу просто создать новый класс, расширяющий Animal, и он будет автоматически выбран.

ОБНОВЛЕНИЕ: 16.10.2008 9:00 по тихоокеанскому стандартному времени:

Этот вопрос вызвал много хороших ответов - спасибо. Из ответов и моих исследований я обнаружил, что то, что я действительно хочу сделать, просто невозможно в Java. Есть подходы, такие как механизм ServiceLoader ddimitrov, который может работать - но они очень тяжелы для того, что я хочу, и я считаю, что я просто перенесу проблему из кода Java во внешний файл конфигурации. Обновление 5/10/19 (11 лет спустя!) Есть теперь несколько библиотек , которые могут помочь с этим в соответствии с @ IvanNik в ответ org.reflections выглядит хорошо. Также ClassGraph от @Luke Хатчисон ответа выглядит интересно. В ответах есть еще несколько возможностей.

Другой способ заявить, что я хочу: статическая функция в моем классе Animal находит и создает все классы, которые наследуются от Animal - без какой-либо дальнейшей настройки / кодирования. Если мне нужно настроить, я все равно могу просто создать их экземпляр в классе Animal. Я понимаю это, потому что Java-программа - это просто свободная федерация файлов .class, и это именно так.

Интересно, что в C # это довольно тривиально .

JohnnyLambada
источник
1
После некоторого поиска кажется, что это сложный орешек в Java. Вот нить, в которой есть некоторая информация: forums.sun.com/thread.jspa?threadID=341935&start=15 Реализации в ней больше, чем мне нужно, думаю, сейчас я просто остановлюсь на второй реализации.
JohnnyLambada
Поместите это как ответ, и пусть читатели проголосуют за него
VonC
3
Ссылка на это в C # не показывает ничего особенного. Сборка AC # эквивалентна файлу Java JAR. Это коллекция скомпилированных файлов классов (и ресурсов). Ссылка показывает, как получить классы из одной сборки. Вы можете сделать это почти так же легко с Java. Проблема для вас в том, что вам нужно просмотреть все файлы JAR и настоящие свободные файлы (каталоги); вы должны сделать то же самое с .NET (поиск в PATH какого-либо рода или различных видов).
Кевин Брок

Ответы:

156

Я использую org.reflections :

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

Другой пример:

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
источник
6
Спасибо за ссылку об org.reflections. Я ошибочно полагал, что это будет так же просто, как и в мире .Net, и этот ответ сэкономил мне много времени.
акмад
1
Спасибо вам за эту полезную ссылку на отличный пакет "org.reflections"! С этим я наконец нашел практичное и аккуратное решение для моей проблемы.
Хартмут П.
3
Есть ли способ получить все отражения, то есть, не отмечая определенный пакет, но загружая все доступные классы?
Джои Барух,
Помните, что поиск классов не решит проблему создания их экземпляров (строка 5 animals.add( new c() );вашего кода), потому что, пока Class.newInstance()существует, у вас нет гарантии, что конкретный класс имеет конструктор без параметров (C # позволяет вам требовать этого в универсальном)
usr-local-ΕΨΗΕΛΩΝ
Смотрите также ClassGraph: github.com/classgraph/classgraph (Отказ от ответственности, я автор). Я приведу пример кода в отдельном ответе.
Люк Хатчисон
34

Java способ сделать то, что вы хотите, это использовать механизм ServiceLoader .

Также многие люди свертывают свои собственные, имея файл в хорошо известном месте пути к классам (например, /META-INF/services/myplugin.properties), а затем используя ClassLoader.getResources () для перечисления всех файлов с этим именем из всех jar- файлов. Это позволяет каждому банку экспортировать своих собственных провайдеров, и вы можете создавать их экземпляры путем отражения с помощью Class.forName ()

ддимитров
источник
1
Для создания файла META-INF услуги легко на основе аннотаций на классах, вы можете проверить: metainf-services.kohsuke.org или code.google.com/p/spi
Элек
Bleah. Просто добавляет уровень сложности, но не устраняет необходимость как создавать класс, так и затем регистрировать его в земле далеко-далеко.
Кевин Клайн
Пожалуйста, смотрите ответ ниже с 26 голосами против, намного лучше
СобиборТреблинка
4
Действительно, если вам нужны какие-то рабочие решения, Reflections / Scannotations - это хороший вариант, но оба они навязывают внешнюю зависимость, являются недетерминированными и не поддерживаются процессом сообщества Java. Кроме того, я хотел бы подчеркнуть, что вы никогда не сможете получить надежно ВСЕ классы, реализующие интерфейс, поскольку тривиально создать новый из ничего в любое время. Загрузчик служб Java прост в понимании и использовании. Я бы держался подальше от этого по разным причинам, но тогда сканирование пути к классам еще хуже: stackoverflow.com/a/7237152/18187
ddimitrov
11

Подумайте об этом с аспектно-ориентированной точки зрения; на самом деле вы хотите знать все классы во время выполнения, которые расширили класс Animal. (Я думаю, что это немного более точное описание вашей проблемы, чем ваше название; в противном случае, я не думаю, что у вас есть вопрос времени выполнения.)

Поэтому я думаю, что вы хотите создать конструктор вашего базового класса (Animal), который добавляет к вашему статическому массиву (я сам предпочитаю ArrayLists, но каждому из них) тип текущего класса, для которого создается экземпляр.

Итак, примерно;

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

Конечно, вам понадобится статический конструктор в Animal для инициализации instantiatedDerivedClass ... Я думаю, что это будет делать то, что вы, вероятно, хотите. Обратите внимание, что это зависит от пути выполнения; если у вас есть класс Dog, производный от Animal, который никогда не вызывается, его не будет в вашем списке классов Animal.

Пол Сонье
источник
6

К сожалению, это не совсем возможно, так как ClassLoader не скажет вам, какие классы доступны. Вы можете, однако, быть довольно близко, делая что-то вроде этого:

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

Изменить: johnstok правильно (в комментариях), что это работает только для автономных приложений Java, и не будет работать под сервером приложений.

Ионафан-Стаффорд
источник
Это не работает, когда классы загружаются другими способами, например, в веб-контейнере или контейнере J2EE.
Джонсток
Возможно, вы могли бы сделать лучше, даже под контейнером, если бы запрашивали пути (часто, URL[]но не всегда, так что это может быть невозможно) из иерархии ClassLoader. Часто, хотя у вас должно быть разрешение, поскольку обычно вы SecurityManagerзагружаете его в JVM.
Кевин Брок
Работал как шарм для меня! Я боялся, что это будет слишком тяжелый и медленный процесс, проходящий через все классы в classpath, но на самом деле я ограничил проверенные файлы файлами, содержащими «-ejb-» или другие распознаваемые имена файлов, и это все еще в 100 раз быстрее, чем запуск встроенная стеклянная рыба или EJBContainer. В моей конкретной ситуации я затем проанализировал классы в поисках аннотаций «Stateless», «Stateful» и «Singleton», и, если я их увидел, я добавил сопоставленное имя JNDI в мой MockInitialContextFactory. Еще раз спасибо!
cs94njw
4

Вы можете использовать ResolverUtil ( необработанный источник ) из Stripes Framework
если вам нужно что-то простое и быстрое без рефакторинга какого-либо существующего кода.

Вот простой пример, не загружающий ни один из классов:

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

Это также работает на сервере приложений, так как именно там он был разработан для работы;)

Код в основном делает следующее:

  • переберите все ресурсы в указанном вами пакете (ах)
  • оставьте только ресурсы, оканчивающиеся на .class
  • Загрузите эти классы, используя ClassLoader#loadClass(String fullyQualifiedName)
  • Проверяет, если Animal.class.isAssignableFrom(loadedClass);
DJDaveMark
источник
4

Наиболее надежным механизмом для перечисления всех подклассов данного класса в настоящее время является ClassGraph , поскольку он обрабатывает максимально широкий массив механизмов спецификации пути к классам , включая новую систему модулей JPMS. (Я автор.)

List<Class<Animal>> animals;
try (ScanResult scanResult = new ClassGraph().whitelistPackages("com.zoo.animals")
        .enableClassInfo().scan()) {
    animals = scanResult
        .getSubclasses(Animal.class.getName())
        .loadClasses(Animal.class);
}
Люк Хатчисон
источник
Результат имеет тип List <Class <Animal >>
hiaclibe
2

Java динамически загружает классы, поэтому ваша вселенная классов будет только теми, которые уже загружены (и еще не выгружены). Возможно, вы можете сделать что-то с пользовательским загрузчиком классов, который может проверять супертипы каждого загруженного класса. Я не думаю, что есть API для запроса набора загруженных классов.

тщательно
источник
2

использовать это

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

Редактировать:

Али Багери
источник
2
Вам нужно будет немного больше рассказать о ResolverUtil. Что это? Из какой он библиотеки?
JohnnyLambada
1

Спасибо всем, кто ответил на этот вопрос.

Кажется, это действительно крепкий орешек. В итоге я сдался и создал статический массив и геттер в своем базовом классе.

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

Кажется, что Java просто не настроен на самообнаружение, как C #. Я предполагаю, что проблема заключается в том, что, поскольку приложение Java представляет собой просто набор файлов .class в каком-либо каталоге / jar-файле, среда выполнения не знает о классе, пока на него не ссылаются. В это время загрузчик загружает его - я пытаюсь найти его, прежде чем ссылаться на него, что невозможно без обращения к файловой системе и поиска.

Мне всегда нравится код, который может обнаружить себя, а не рассказывать о себе, но, увы, это тоже работает.

Еще раз спасибо!

JohnnyLambada
источник
1
Java настроена на самораскрытие. Вам просто нужно сделать немного больше работы. Смотрите мой ответ для деталей.
Дэйв Джарвис
1

Используя OpenPojo вы можете сделать следующее:

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));
}
Осман Шоукри
источник
0

Это сложная проблема, и вам нужно будет найти эту информацию с помощью статического анализа, которая не может быть легко доступна во время выполнения. По сути, получите путь к классу вашего приложения, просмотрите доступные классы и прочитайте информацию о байт-коде класса, от которого он наследует класс. Обратите внимание, что класс Dog не может наследовать напрямую от Animal, но может наследовать от Pet, который в свою очередь наследует от Animal, поэтому вам нужно будет отслеживать эту иерархию.

pdeva
источник
0

Один из способов - заставить классы использовать статические инициализаторы ... Я не думаю, что они наследуются (это не сработает):

public class Dog extends Animal{

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

Требуется добавить этот код ко всем участвующим классам. Но это позволяет избежать больших уродливых циклов, где каждый класс ищет детей Animal.


источник
3
Это не работает в моем случае. Поскольку на класс нигде нет ссылок, он никогда не загружается, поэтому статический инициализатор никогда не вызывается. Кажется, что статический инициализатор вызывается раньше всего, когда класс загружается, но в моем случае класс никогда не загружается.
JohnnyLambada
0

Я решил эту проблему довольно элегантно, используя аннотации уровня пакета, а затем сделал эту аннотацию в качестве аргумента списком классов.

Найти классы Java, реализующие интерфейс

Реализации просто должны создать package-info.java и поместить волшебную аннотацию со списком классов, которые они хотят поддерживать.

Адам Гент
источник