No tempo de execução, encontre todas as classes em um aplicativo Java que estendem uma classe base

147

Eu quero fazer algo assim:

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

Então, quero examinar todas as classes no universo do meu aplicativo e, quando encontrar uma que descende de Animal, quero criar um novo objeto desse tipo e adicioná-lo à lista. Isso me permite adicionar funcionalidades sem precisar atualizar uma lista de itens. Eu posso evitar o seguinte:

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

Com a abordagem acima, posso simplesmente criar uma nova classe que estenda Animal e ela será captada automaticamente.

ATUALIZAÇÃO: 16/10/2008 09:00 Hora padrão do Pacífico:

Esta pergunta gerou muitas ótimas respostas - obrigado. A partir das respostas e da minha pesquisa, descobri que o que realmente quero fazer não é possível no Java. Existem abordagens, como o mecanismo ServiceLoader do ddimitrov, que podem funcionar - mas são muito pesadas para o que eu quero e acredito que simplesmente movo o problema do código Java para um arquivo de configuração externo. Atualização 5/10/19 (11 anos depois!) Agora, existem várias bibliotecas que podem ajudar com isso, de acordo com a resposta do @ IvanNik org.reflections parece bom. Também ClassGraph de @Luke Hutchison resposta parece interessante. Existem várias outras possibilidades nas respostas também.

Outra maneira de declarar o que eu quero: uma função estática na minha classe Animal localiza e instancia todas as classes que herdam do Animal - sem nenhuma configuração / codificação adicional. Se eu tiver que configurar, é melhor instanciá-los na classe Animal de qualquer maneira. Entendo que, porque um programa Java é apenas uma federação frouxa de arquivos .class, é assim que é.

Curiosamente, parece que isso é bastante trivial em C #.

JohnnyLambada
fonte
1
Depois de procurar por algum tempo, parece que este é um problema difícil de decifrar em Java. Aqui está um tópico com algumas informações: forums.sun.com/thread.jspa?threadID=341935&start=15 As implementações nele são mais do que eu preciso, acho que vou continuar com a segunda implementação por enquanto.
JohnnyLambada
colocar isso como uma resposta e deixe leitores voto nele
VonC
3
O link para fazer isso em C # não mostra nada de especial. AC # Assembly é equivalente ao arquivo Java JAR. É uma coleção de arquivos de classe compilados (e recursos). O link mostra como obter as classes de uma única montagem. Você pode fazer isso quase tão facilmente com Java. O problema é que você precisa examinar todos os arquivos JAR e os verdadeiros arquivos soltos (diretórios); você precisaria fazer o mesmo com o .NET (pesquise um CAMINHO de algum tipo ou de vários tipos).
Kevin Brock

Respostas:

156

Eu uso org.reflections :

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

Outro exemplo:

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
fonte
6
Obrigado pelo link sobre org.reflections. Por engano, achei que isso seria tão fácil quanto no mundo .Net e essa resposta me salvou muito tempo.
Akmad # 1/12
1
Muito obrigado por este link útil para o ótimo pacote "org.reflections"! Com isso, finalmente encontrei uma solução viável e organizada para o meu problema.
Hartmut P.
3
Existe uma maneira de obter todas as reflexões, ou seja, sem notar um pacote específico, mas carregando todas as classes disponíveis?
Joey Baruch
Lembre-se de que encontrar as classes não resolverá seu problema de instancia-las (linha 5 animals.add( new c() );do seu código), porque enquanto Class.newInstance()existe, você não tem garantia de que a classe específica tenha um construtor sem parâmetros (o C # permite exigir isso em um genérico)
usr-local-ΕΨΗΕΛΩΝ
Veja também ClassGraph: github.com/classgraph/classgraph (Isenção de responsabilidade, eu sou o autor). Vou dar um exemplo de código em uma resposta separada.
9608 Luke Hutchison #
34

A maneira Java de fazer o que você deseja é usar o mecanismo ServiceLoader .

Além disso, muitas pessoas criam seus próprios arquivos tendo um local de caminho de classe conhecido (por exemplo, /META-INF/services/myplugin.properties) e, em seguida, usando ClassLoader.getResources () para enumerar todos os arquivos com esse nome em todos os jars. Isso permite que cada jar exporte seus próprios provedores e você pode instancia-los refletindo usando Class.forName ()

ddimitrov
fonte
1
Para gerar o arquivo de serviços META-INF facilmente com base em anotações nas classes, você pode verificar: metainf-services.kohsuke.org ou code.google.com/p/spi
elek
Bleah. Apenas adiciona um nível de complexidade, mas não elimina a necessidade de criar uma classe e depois registrá-la em um terreno distante.
Kevin cline
Por favor, veja resposta abaixo com 26 upvotes, muito melhor
SobiborTreblinka
4
De fato, se você precisar de um tipo de solução de trabalho, o Reflections / Scannotations é uma boa opção, mas ambos impõem uma dependência externa, são não determinísticos e não são suportados pelo Java Community Process. Além disso, gostaria de destacar que você nunca pode obter TODAS as classes de maneira confiável implementando uma interface, pois é trivial fabricar uma nova a partir do nada a qualquer momento. Apesar de todas as suas verrugas, o carregador de serviço do Java é fácil de entender e usar. Eu ficaria longe disso por diferentes razões, mas a varredura do caminho de classe é ainda pior: stackoverflow.com/a/7237152/18187
ddimitrov
11

Pense nisso de um ponto de vista orientado a aspectos; Na verdade, o que você quer fazer é conhecer todas as classes em tempo de execução que estenderam a classe Animal. (Acho que é uma descrição um pouco mais precisa do seu problema do que o seu título; caso contrário, não acho que você tenha uma pergunta em tempo de execução.)

Então, o que eu acho que você deseja é criar um construtor de sua classe base (Animal) que adicione à sua matriz estática (eu prefiro ArrayLists, eu mesmo, mas cada uma na sua) o tipo da classe atual que está sendo instanciada.

Então, aproximadamente;

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

Obviamente, você precisará de um construtor estático no Animal para inicializar o InstantiatedDerivedClass ... Acho que isso fará o que você provavelmente deseja. Observe que isso depende do caminho da execução; se você tiver uma classe Dog derivada de Animal que nunca é invocada, não a terá em sua lista de Classes de Animais.

Paul Sonier
fonte
6

Infelizmente, isso não é totalmente possível, pois o ClassLoader não informa quais classes estão disponíveis. Você pode, no entanto, chegar bem perto fazendo algo assim:

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 está correto (nos comentários) que isso funciona apenas para aplicativos Java independentes e não funciona em um servidor de aplicativos.

jonathan-stafford
fonte
Isso não funciona bem quando as classes são carregadas por outros meios, por exemplo, em um contêiner da Web ou J2EE.
10268 Johnstok
Você provavelmente poderia fazer um trabalho melhor, mesmo em um contêiner, se consultasse os caminhos (geralmente, URL[]mas nem sempre, para que não seja possível) da hierarquia do ClassLoader. Frequentemente, você deve ter permissão, pois normalmente você SecurityManagercarregaria dentro da JVM.
31511 Kevin Brock
Caiu como uma luva para mim! Eu temia que isso fosse muito pesado e lento, passando por todas as classes no caminho de classe, mas, na verdade, limitei os arquivos que verifiquei aos que continham "-ejb-" ou outros nomes de arquivos reconhecíveis, e ainda é 100 vezes mais rápido que iniciar um glassfish incorporado ou EJBContainer. Na minha situação específica, analisei as classes procurando anotações "Stateless", "Stateful" e "Singleton" e, se as vi, adicionei o nome JNDI Mapped ao meu MockInitialContextFactory. Obrigado novamente!
cs94njw
4

Você pode usar o ResolverUtil ( fonte bruta ) do Stripes Framework
se precisar de algo simples e rápido sem refatorar nenhum código existente.

Aqui está um exemplo simples que não carregou nenhuma das 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 {}

Isso também funciona em um servidor de aplicativos, já que foi onde ele foi projetado para funcionar;)

O código basicamente faz o seguinte:

  • itere sobre todos os recursos nos pacotes que você especificar
  • mantenha apenas os recursos que terminam em .class
  • Carregue essas classes usando ClassLoader#loadClass(String fullyQualifiedName)
  • Verifica se Animal.class.isAssignableFrom(loadedClass);
DJDaveMark
fonte
4

O mecanismo mais robusto para listar todas as subclasses de uma determinada classe é atualmente o ClassGraph , porque ele lida com a maior variedade possível de mecanismos de especificação de caminho de classe , incluindo o novo sistema de módulo JPMS. (Eu sou o autor.)

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
fonte
O resultado é do tipo List <Class <Animal>>
hiaclibe 26/07/19
2

O Java carrega dinamicamente as classes, portanto seu universo de classes seria apenas aquelas que já foram carregadas (e ainda não foram descarregadas). Talvez você possa fazer algo com um carregador de classes personalizado que possa verificar os supertipos de cada classe carregada. Eu não acho que exista uma API para consultar o conjunto de classes carregadas.

completamente
fonte
2

usa isto

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

Editar:

  • biblioteca para (ResolverUtil): Stripes
Ali Bagheri
fonte
2
Você precisará falar um pouco mais sobre o ResolverUtil. O que é isso? De que biblioteca vem?
precisa saber é o seguinte
1

Obrigado a todos que responderam a esta pergunta.

Parece que este é realmente um osso duro de roer. Acabei desistindo e criando uma matriz estática e getter na minha classe 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;
    }
}

Parece que o Java simplesmente não está configurado para auto-descoberta do jeito que o C # é. Suponho que o problema é que, como um aplicativo Java é apenas uma coleção de arquivos .class em um diretório / arquivo jar em algum lugar, o tempo de execução não conhece uma classe até que seja referenciada. Naquele momento, o carregador o carrega - o que estou tentando fazer é descobri-lo antes de fazer referência a ele, o que não é possível sem sair do sistema de arquivos e procurar.

Eu sempre gosto de código que pode se descobrir, em vez de eu ter que contar sobre si mesmo, mas infelizmente isso também funciona.

Obrigado novamente!

JohnnyLambada
fonte
1
Java está configurado para autodescoberta. Você só precisa fazer um pouco mais de trabalho. Veja minha resposta para detalhes.
Dave Jarvis
1

Usando o OpenPojo, você pode fazer o seguinte:

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
fonte
0

Esse é um problema difícil e você precisará descobrir essas informações usando a análise estática, que não está disponível facilmente no tempo de execução. Basicamente, obtenha o caminho de classe do seu aplicativo, verifique as classes disponíveis e leia as informações de bytecode de uma classe da qual a classe é herdada. Observe que uma classe Dog não pode herdar diretamente de Animal, mas pode herdar de Pet, que por sua vez é herdada de Animal, portanto, você precisará acompanhar essa hierarquia.

pdeva
fonte
0

Uma maneira é fazer com que as classes usem inicializadores estáticos ... Não acho que sejam herdados (não funcionará se forem):

public class Dog extends Animal{

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

Requer que você adicione esse código a todas as classes envolvidas. Mas evita ter um grande laço feio em algum lugar, testando todas as classes em busca de filhos de Animal.


fonte
3
Isso não funciona no meu caso. Como a classe não é referenciada em nenhum lugar, ela nunca é carregada; portanto, o inicializador estático nunca é chamado. Parece que um inicializador estático é chamado antes de tudo quando a classe é carregada - mas, no meu caso, a classe nunca é carregada.
JohnnyLambada
0

Resolvi esse problema de maneira elegante usando as Anotações no nível do pacote e, em seguida, fazendo com que a anotação tivesse como argumento uma lista de classes.

Encontre classes Java implementando uma interface

As implementações apenas precisam criar um package-info.java e inserir a anotação mágica na lista de classes que eles desejam suportar.

Adam Gent
fonte