Como você encontra todas as subclasses de uma determinada classe em Java?

207

Como alguém tenta encontrar todas as subclasses de uma determinada classe (ou todos os implementadores de uma determinada interface) em Java? A partir de agora, tenho um método para fazer isso, mas acho bastante ineficiente (para dizer o mínimo). O método é:

  1. Obtenha uma lista de todos os nomes de classe que existem no caminho da classe
  2. Carregue cada classe e teste para ver se é uma subclasse ou implementador da classe ou interface desejada

No Eclipse, há um recurso interessante chamado Hierarquia de Tipos que consegue mostrar isso com bastante eficiência. Como se faz e faz de forma programática?

Avrom
fonte
1
Embora a solução baseada nas reflexões e na primavera pareça interessante, eu precisava de uma solução simples que não tivesse dependências. Parece que meu código original (com alguns ajustes) foi o caminho a percorrer.
Avrom
1
Certamente você pode usar o método getSupeClass recursivamente?
Eu estava procurando especificamente todas as subclasses de uma determinada classe. O getSuperClass não informa quais subclasses uma classe possui, apenas obtém a superclasse imediata para uma subclasse específica. Além disso, o método isAssignableFrom on Class é mais adequado ao que você sugere (sem necessidade de recursão).
Avrom
Essa questão está vinculada a muitas outras duplicatas, mas não contém nenhuma resposta Java simples e útil. Suspiro ...
Eric Duminil 28/04

Respostas:

77

Não há outra maneira de fazer isso além do que você descreveu. Pense nisso - como alguém pode saber quais classes estendem o ClassX sem verificar cada classe no caminho de classe?

O Eclipse pode apenas falar sobre as super e subclasses no que parece ser uma quantidade "eficiente" de tempo, porque ele já possui todos os dados de tipo carregados no ponto em que você pressiona o botão "Exibir na Hierarquia de Tipos" (uma vez que é constantemente compilando suas aulas, sabe tudo sobre o caminho de classe, etc).

matt b
fonte
23
Agora existe uma biblioteca simples chamada org.reflections que ajuda com essa e outras tarefas comuns de reflexão. Com esta biblioteca, você pode simplesmente chamar o reflections.getSubTypesOf(aClazz)) link
Enwired
@matt b - se for necessário varrer todas as classes, isso significa que há uma degradação no desempenho quando você tem muitas classes em seu projeto, mesmo que apenas algumas delas estejam subclassificando sua classe?
LeTex
Exatamente. Apenas toca todas as classes. Você pode definir seu próprio scanner ou acelerar, excluindo determinados pacotes que você sabe que sua classe não seria estendida ou apenas abrir o arquivo da classe e procurar o nome da classe na seção constante da classe, evitando que o scanner de reflexão leia uma muito mais informações sobre classes que nem contêm a referência necessária à sua superclasse (direta) Indiretamente, você precisa fazer uma varredura adicional. Portanto, é o melhor que existe no momento.
Martin Kersten
A resposta da fforw funcionou para mim e deve ser marcada como a resposta correta. Claramente, isso é possível com a verificação do caminho de classe.
Farrukh Najmi 23/03
Você está errado, veja a resposta abaixo sobre a biblioteca
127

A varredura de classes não é fácil com Java puro.

A estrutura spring oferece uma classe chamada ClassPathScanningCandidateComponentProvider que pode fazer o que você precisa. O exemplo a seguir encontraria todas as subclasses de MyClass no pacote 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
}

Esse método tem o benefício adicional de usar um analisador de bytecode para encontrar os candidatos, o que significa que ele não carregará todas as classes que ele varre.

fforw
fonte
23
False deve ser passado como parâmetro ao criar ClassPathScanningCandidateComponentProvider para desativar os filtros padrão. Os filtros padrão corresponderão a outros tipos de classes, por exemplo, qualquer coisa anotada com @Component. Queremos apenas que o AssignableTypeFilter esteja ativo aqui.
MCDS 31/01
Você diz que não é fácil, mas como faríamos se quiséssemos fazê-lo com java puro?
Aequitas
49

Isso não é possível usando apenas a API Java Reflections integrada.

Existe um projeto que faz a digitalização e indexação necessárias do seu caminho de classe para que você possa acessar essas informações ...

Reflexões

Uma análise de metadados de tempo de execução Java, no espírito de Scannotations

O Reflections varre seu caminho de classe, indexa os metadados, permite consultá-lo em tempo de execução e pode salvar e coletar essas informações para muitos módulos em seu projeto.

Usando o Reflections, você pode consultar seus metadados para:

  • obter todos os subtipos de algum tipo
  • obter todos os tipos anotados com alguma anotação
  • obter todos os tipos anotados com algumas anotações, incluindo parâmetros de anotação correspondentes
  • obtenha todos os métodos anotados com alguns

(aviso de isenção de responsabilidade: eu não o usei, mas a descrição do projeto parece ser adequada às suas necessidades.)

Mark Renouf
fonte
1
Interessante. O projeto parece ter algumas dependências que sua documentação não parece mencionar. Ou seja, (os que eu encontrei até agora): javaassist, Log4J, XStream
Avrom
3
Eu incluí este projekt com maven e funcionou bem. Obtendo subclasses é realmente o primeiro exemplo de código-fonte e é duas linhas de longo :-)
KarlsFriend
Não é possível usar apenas a API Java Reflections embutida ou apenas muito inconveniente para fazer isso?
Flow
1
Tenha cuidado ao usar o Reflections e, em seguida, implante o WAR do seu aplicativo no GlassFish! Há um conflito na biblioteca do Guava e a implantação falhará com a falha de implantação do CDI: WELD-001408 - consulte GLASSFISH-20579 para obter mais detalhes. FastClasspathScanner é uma solução neste caso.
19446 lu_ko
Eu apenas tento este projeto e ele funciona. Eu apenas o uso para aprimorar o padrão de design da estratégia e obter toda a classe concreta da estratégia (subclasse). Compartilharemos a demonstração posteriormente.
Xin Meng
10

Não esqueça que o Javadoc gerado para uma classe incluirá uma lista de subclasses conhecidas (e para interfaces, classes de implementação conhecidas).

Roubar
fonte
3
isso é totalmente incorreto, a superclasse não deve depender de suas subclasses, nem mesmo em javadoc ou comentário.
hunter
@ caçador Eu discordo. É totalmente correto que o JavaDoc inclua uma lista de subclasses conhecidas . Obviamente, "conhecido" pode não incluir a classe que você está procurando, mas, em alguns casos de uso, será suficiente.
Qw3ry
E você pode perder algumas classes em qualquer caso: posso carregar um novo jar no caminho de classe (durante o tempo de execução) e todas as detecções que ocorreram antes falharão.
Qw3ry
10

Tente ClassGraph . (Isenção de responsabilidade, eu sou o autor). O ClassGraph suporta a varredura de subclasses de uma determinada classe, em tempo de execução ou em tempo de construção, mas também muito mais. O ClassGraph pode criar uma representação abstrata de todo o gráfico de classes (todas as classes, anotações, métodos, parâmetros de métodos e campos) na memória, para todas as classes no caminho de classe ou para classes em pacotes na lista de permissões, e você pode consultar esse gráfico de classe você quer. O ClassGraph suporta mais mecanismos de especificação de caminho de classe e carregadores de classes do que qualquer outro scanner, e também funciona perfeitamente com o novo sistema de módulo JPMS, portanto, se você basear seu código no ClassGraph, seu código será portátil no máximo. Veja a API aqui.

Luke Hutchison
fonte
9

Eu fiz isso há vários anos. A maneira mais confiável de fazer isso (por exemplo, com APIs Java oficiais e sem dependências externas) é escrever um doclet personalizado para produzir uma lista que possa ser lida em tempo de execução.

Você pode executá-lo na linha de comando da seguinte maneira:

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

ou execute-o a partir desta formiga:

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

Aqui está o código básico:

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

Para simplificar, removi a análise de argumentos da linha de comando e estou escrevendo no System.out em vez de em um arquivo.

David Leppik
fonte
Embora usar isso programaticamente possa ser complicado, devo dizer - uma abordagem inteligente !
Janaka Bandara
8

Sei que estou alguns anos atrasado para esta festa, mas me deparei com essa pergunta tentando resolver o mesmo problema. Você pode usar a pesquisa interna do Eclipse de forma programática, se estiver escrevendo um Plug-in do Eclipse (e, assim, tirar proveito do cache, etc), para encontrar classes que implementam uma interface. Aqui está o meu primeiro corte (muito difícil):

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

O primeiro problema que vejo até agora é que estou capturando apenas classes que implementam diretamente a interface, não todas as subclasses - mas uma pequena recursão nunca prejudica ninguém.

Curtis
fonte
3
OU, você não precisa fazer sua própria pesquisa. Você pode obter uma ITypeHierarchy diretamente do seu IType chamando .newTypeHierarchy (): dev.eclipse.org/newslists/news.eclipse.tools.jdt/msg05036.html
Curtis
7

Tendo em mente as limitações mencionadas nas outras respostas, você também pode usar o openpojo'sPojoClassFactory ( disponível no Maven ) da seguinte maneira:

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

Onde packageRootestá a String raiz dos pacotes que você deseja pesquisar (por exemplo, "com.mycompany"ou mesmo apenas "com"), e Superclassé o seu supertipo (isso também funciona nas interfaces).

mikołak
fonte
De longe, a solução mais rápida e elegante dentre as sugeridas.
KidCrippler
4

Acabei de escrever uma demo simples para usar as org.reflections.Reflectionssubclasses de get da classe abstract:

https://github.com/xmeng1/ReflectionsDemo

Xin Meng
fonte
seria mais produtivo se você postasse um exemplo de código aqui, em vez de um link com muitas classes para cavar.
JesseBoyd
4

Dependendo de seus requisitos específicos, em alguns casos, o mecanismo do carregador de serviço do Java pode alcançar o que você procura.

Em resumo, ele permite que os desenvolvedores declarem explicitamente que uma classe subclasse alguma outra classe (ou implementa alguma interface) listando-a em um arquivo no META-INF/servicesdiretório do arquivo JAR / WAR . Em seguida, ele pode ser descoberto usando a java.util.ServiceLoaderclasse que, quando recebe um Classobjeto, gera instâncias de todas as subclasses declaradas dessa classe (ou, se isso Classrepresenta uma interface, todas as classes que implementam essa interface).

A principal vantagem dessa abordagem é que não há necessidade de varrer manualmente todo o caminho de classe em busca de subclasses - toda a lógica de descoberta está contida na ServiceLoaderclasse e apenas carrega as classes explicitamente declaradas no META-INF/servicesdiretório (nem todas as classes no caminho de classe) .

Existem, no entanto, algumas desvantagens:

  • Ele não encontrará todas as subclasses, apenas aquelas que são explicitamente declaradas. Como tal, se você precisar realmente encontrar todas as subclasses, essa abordagem poderá ser insuficiente.
  • Exige que o desenvolvedor declare explicitamente a classe no META-INF/servicesdiretório Esse é um ônus adicional para o desenvolvedor e pode estar sujeito a erros.
  • O ServiceLoader.iterator()gera instâncias de subclasse, não seus Classobjetos. Isso causa dois problemas:
    • Você não tem nenhuma opinião sobre como as subclasses são construídas - o construtor no-arg é usado para criar as instâncias.
    • Como tal, as subclasses devem ter um construtor padrão ou explicitamente declarar um construtor sem argumento.

Aparentemente, o Java 9 abordará algumas dessas deficiências (em particular, as relacionadas à instanciação de subclasses).

Um exemplo

Suponha que você esteja interessado em encontrar classes que implementam uma interface com.example.Example:

package com.example;

public interface Example {
    public String getStr();
}

A classe com.example.ExampleImplimplementa essa interface:

package com.example;

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

Você declararia que a classe ExampleImplé uma implementação Examplecriando um arquivo que META-INF/services/com.example.Examplecontém o texto com.example.ExampleImpl.

Em seguida, você pode obter uma instância de cada implementação de Example(incluindo uma instância de ExampleImpl) da seguinte maneira:

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.
Mac
fonte
3

Deve-se notar também que, é claro, isso encontrará apenas todas as subclasses existentes no seu caminho de classe atual. Presumivelmente, isso é bom para o que você está vendo no momento, e é provável que você considere isso, mas se em algum momento você tiver liberado uma não- finalclasse para a natureza (para níveis variados de "natureza"), é inteiramente possível que alguém escreveu sua própria subclasse que você não conhecerá.

Portanto, se você estava querendo ver todas as subclasses porque deseja fazer uma alteração e vai ver como isso afeta o comportamento das subclasses - lembre-se das subclasses que não pode ver. Idealmente, todos os seus métodos não privados e a própria classe devem estar bem documentados; faça alterações de acordo com esta documentação sem alterar a semântica dos métodos / campos não privados e suas alterações deverão ser compatíveis com versões anteriores, para qualquer subclasse que siga pelo menos a sua definição de superclasse.

Andrzej Doyle
fonte
3

O motivo pelo qual você vê uma diferença entre sua implementação e o Eclipse é porque você varre cada vez, enquanto o Eclipse (e outras ferramentas) varre apenas uma vez (durante o carregamento do projeto na maioria das vezes) e cria um índice. Na próxima vez que você solicitar os dados, eles não serão verificados novamente, mas observe o índice.

OscarRyz
fonte
3

Estou usando uma biblioteca de reflexão, que verifica seu caminho de classe em todas as subclasses: https://github.com/ronmamo/reflections

É assim que isso seria feito:

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

Adicione-os a um mapa estático dentro (this.getClass (). GetName ()) do construtor de classes pai (ou crie um padrão), mas isso será atualizado em tempo de execução. Se a inicialização lenta for uma opção, você pode tentar esta abordagem.

Ravindranath Akila
fonte
0

Eu precisava fazer isso como um caso de teste, para ver se novas classes foram adicionadas ao código. Foi o que eu fiz

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
fonte