Plug-in Java sem problemas

Neste artigo, gostaria de dizer como criar rápida e facilmente uma estrutura de aplicativo Java com suporte para carregamento dinâmico de plug-ins. O leitor provavelmente pensará imediatamente que essa tarefa foi resolvida por um longo tempo, e você pode simplesmente usar estruturas prontas ou escrever seu carregador de classes, mas nada disso será necessário na solução que proponho:

  • Não precisamos de bibliotecas ou estruturas especiais ( OSGi , Guice, etc.)
  • Não usaremos a análise de bytecode com ASM e bibliotecas semelhantes.
  • Não escreveremos nosso carregador de classes.
  • Não usaremos reflexão e anotações.
  • Não há necessidade de mexer com o caminho de classe para encontrar plugins. Não vamos tocar no caminho da classe.
  • Além disso, não usaremos XML, YAML ou qualquer outra linguagem declarativa para descrever pontos de extensão (pontos de extensão em plugins).

No entanto, ainda existe um requisito - essa solução funcionará apenas no Java 9 ou superior. Porque será baseado em módulos e serviços .

Então, vamos começar. Formulamos o problema mais especificamente:
Você precisa implementar uma estrutura mínima de aplicativo, que na inicialização carregará plugins de usuário da pasta plugins .

Ou seja, o aplicativo montado deve se parecer com isso:

 plugin-app/ plugins/ plugin1.jar plugin2.jar ... core.jar … 

Vamos começar com o módulo core . Este módulo é o núcleo da nossa aplicação, ou seja, é a nossa estrutura.

Para quem valoriza o tempo, o projeto finalizado está disponível no GitHub. Instruções de montagem.
Link

 git clone https://github.com/orionll/plugin-app cd plugin-app mvn verify cd core/target java --module-path core-1.0-SNAPSHOT.jar --module core 

Crie os 4 arquivos Java a seguir no módulo:

 core/ src/main/java/ org/example/pluginapp/core/ IService.java BasicService.java Main.java module-info.java 

O primeiro arquivo, IService.java , é o arquivo que descreve nosso ponto de extensão. Outros plugins poderão contribuir para esse ponto de expansão ("contribuir"). Esse é o princípio padrão para a criação de aplicativos de plug-in, chamado de princípio de inversão de dependência (Inversão de Dependência). Este princípio é baseado no fato de que o kernel não depende de classes específicas, mas de interfaces.

Dei ao ponto de extensão o nome abstrato IService , pois agora estou demonstrando um conceito exclusivamente. Na realidade, pode ser qualquer ponto de extensão específico, por exemplo, se você estiver escrevendo um editor gráfico, pode ser o efeito do processamento de imagens, por exemplo, IEffectProvider , IEffectContribution ou qualquer outra coisa, dependendo de como você prefere nomear os pontos de extensão. Ao mesmo tempo, o próprio aplicativo conterá um conjunto básico de efeitos, e os desenvolvedores de terceiros poderão criar efeitos adicionais mais sofisticados e entregá-los na forma de plug-ins. O usuário só precisa colocar esses efeitos na pasta de plugins - plugins e reiniciar o aplicativo.

O arquivo IService.java é o seguinte:

 public interface IService { void doJob(); static List<IService> getServices(ModuleLayer layer) { return ServiceLoader .load(layer, IService.class) .stream() .map(Provider::get) .collect(Collectors.toList()); } } 

Assim, o IService é apenas uma interface que faz algum trabalho abstrato doJob() (repito, os detalhes não são importantes, na realidade, será algo concreto).

Preste atenção também ao segundo método getServices() . Este método retorna todas as implementações da interface IService encontradas nesta camada de módulo e seus pais. Falaremos sobre isso com mais detalhes posteriormente.

O segundo arquivo, BasicService.java , é a implementação básica da interface IService . Ele sempre estará presente, mesmo se não houver plugins no aplicativo. Em outras palavras, o core não é apenas o núcleo, mas também ao mesmo tempo um plugin para si mesmo, que sempre será carregado. O arquivo BasicService.java fica assim:

 public class BasicService implements IService { @Override public void doJob() { System.out.println("Basic service"); } } 

Para simplificar, doJob() apenas imprime a string "Basic service" e é isso.

Assim, no momento, temos a seguinte imagem:



O terceiro arquivo, Main.java , é onde o método main() é implementado. Há um pouco de mágica nele, para entender o que você precisa saber o que é uma camada de módulo.

Sobre as camadas do módulo


Quando o Java inicia o aplicativo, todos os módulos da plataforma + módulos listados no argumento --module-path (e também classpath , se houver) se enquadram na camada de Boot . No nosso caso, se compilarmos o módulo core.jar e executarmos o java --module-path core.jar --module core na linha de comando, pelo menos os módulos java.base e core estarão na camada de Boot :



A camada de Boot está sempre presente em qualquer aplicativo Java e essa é a menor configuração possível. A maioria dos aplicativos existe em uma única camada de módulos. No entanto, no nosso caso, queremos fazer o carregamento dinâmico de plug- plugins pasta de plugins - plugins . Poderíamos forçar o usuário a corrigir a linha de inicialização do aplicativo, para que ele próprio adicione os plugins necessários ao --module-path , mas essa não será a melhor solução. Especialmente aquelas pessoas que não são programadores e não entendem por que precisam subir em algum lugar e consertar algo para uma coisa tão simples.

Felizmente, existe uma solução: Java permite que você crie suas próprias camadas de módulo em tempo de execução, que carregarão os módulos do local que precisamos. Para nossos propósitos, uma nova camada de plug-ins será suficiente, que terá uma camada de Boot como pai (qualquer camada deve ter um pai):



O fato de a camada de plug-in ter a camada de Boot como pai significa que os módulos da camada de plug-in podem fazer referência aos módulos da camada de Boot , mas não vice-versa.

Portanto, sabendo agora o que é uma camada de módulo, você pode finalmente examinar o conteúdo do arquivo Main.java :

 public final class Main { public static void main(String[] args) { Path pluginsDir = Paths.get("plugins"); //      plugins ModuleFinder pluginsFinder = ModuleFinder.of(pluginsDir); //  ModuleFinder      plugins       List<String> plugins = pluginsFinder .findAll() .stream() .map(ModuleReference::descriptor) .map(ModuleDescriptor::name) .collect(Collectors.toList()); //  ,      (   ) Configuration pluginsConfiguration = ModuleLayer .boot() .configuration() .resolve(pluginsFinder, ModuleFinder.of(), plugins); //      ModuleLayer layer = ModuleLayer .boot() .defineModulesWithOneLoader(pluginsConfiguration, ClassLoader.getSystemClassLoader()); //     IService       Boot List<IService> services = IService.getServices(layer); for (IService service : services) { service.doJob(); } } } 

Se esta é a primeira vez que você está vendo esse código, pode parecer muito complicado, mas essa é uma sensação falsa devido ao grande número de novas classes desconhecidas. Se você entender um pouco sobre o significado das classes ModuleFinder , Configuration e ModuleLayer , tudo se encaixará. Além disso, existem apenas algumas dezenas de linhas! Essa é toda a lógica que é escrita uma vez.

Descritor de módulo


Há mais um (quarto) arquivo que não consideramos: module-info.java . Este é o arquivo mais curto que contém a declaração do nosso módulo e uma descrição dos serviços (pontos de extensão):

 module core { exports org.example.pluginapp.core; uses IService; provides IService with BasicService; } 

O significado das linhas deste arquivo deve ser óbvio:

  • Primeiro, o módulo exporta o pacote org.example.pluginapp.core que os plugins possam herdar da interface IService (caso contrário, o IService não estaria acessível fora do módulo core ).
  • Em segundo lugar, ele anuncia que está usando o IService .
  • Terceiro, ele diz que fornece uma implementação do serviço IService por meio da classe BasicService .

Como a declaração do módulo é escrita em Java, obtemos vantagens muito importantes: verificações do compilador e garantias estáticas . Por exemplo, se cometêssemos um erro no nome dos tipos ou indicássemos um pacote inexistente, teríamos percebido imediatamente. No caso de alguns OSGi, não teríamos nenhuma verificação no tempo de compilação, pois a declaração dos pontos de extensão seria gravada em XML.

Então, o quadro está pronto. Vamos tentar executá-lo:

 > java --module-path core.jar --module core Basic service 

O que aconteceu

  1. Java tentou encontrar os módulos na pasta plugins e não encontrou nenhum.
  2. Uma camada vazia foi criada.
  3. O ServiceLoader começou a procurar todas as implementações do IService .
  4. Na camada vazia, ele não encontrou nenhuma implementação de serviço, pois não há módulos lá.
  5. Após essa camada, ele continuou pesquisando na camada pai (ou seja, a camada Boot ) e encontrou uma implementação do BasicService no módulo core .
  6. Todas as implementações encontradas tiveram o método doJob() chamado. Como apenas uma implementação foi encontrada, apenas o "Basic service" foi impresso.

Escrevendo um plugin


Tendo escrito o núcleo do nosso aplicativo, agora é a hora de tentar escrever plugins para ele. Vamos escrever dois plugins plugin1 e plugin2 : deixe a primeira impressão "Service 1" , a segunda - "Service 2" . Para fazer isso, você precisa fornecer mais duas implementações IService no plugin1 e plugin2 respectivamente:



Crie o primeiro plugin com dois arquivos:

 plugin1/ src/main/java/ org/example/pluginapp/plugin1/ Service1.java module-info.java 

Arquivo Service1.java :

 public class Service1 implements IService { @Override public void doJob() { System.out.println("Service 1"); } } 

Arquivo module-info.java :

 module plugin1 { requires core; provides IService with Service1; } 

Observe que o plugin1 é dependente do core . Este é o princípio de inversão de dependência que mencionei anteriormente: o kernel não depende de plugins, mas vice-versa.

O segundo plugin é completamente semelhante ao primeiro, então não o darei aqui.

Agora vamos coletar os plugins, colocá-los na pasta plugins e executar o aplicativo:

 > java --module-path core.jar --module core Service 1 Service 2 Basic service 

Hooray, os plugins foram apanhados! Como isso aconteceu:

  1. Java encontrou dois módulos na pasta plugins .
  2. Uma camada foi criada com dois módulos plugins1 e plugins2 .
  3. O ServiceLoader começou a procurar todas as implementações do IService .
  4. Na camada de plug-in, ele encontrou duas implementações do serviço IService .
  5. Depois disso, ele continuou pesquisando na camada pai (ou seja, na camada Boot ) e encontrou uma implementação do BasicService no módulo core .
  6. Todas as implementações encontradas tiveram o método doJob() chamado.

Observe que é precisamente porque a pesquisa por provedores de serviços começa com as camadas filho e, em seguida, vai para as camadas pai, então "Service 1" e "Service 2" impressos primeiro e, em seguida, "Basic Service" . Se você deseja que os serviços sejam classificados para que os serviços básicos sejam os primeiros e depois os plug-ins, você pode ajustar o método IService.getServices() adicionando a classificação lá (pode ser necessário adicionar o método int getOrdering() à interface IService ).

Sumário


Então, mostrei como você pode organizar rápida e eficientemente um aplicativo Java de plug-in que possui as seguintes propriedades:

  • Simplicidade: para pontos de extensão e suas ligações, apenas os recursos Java básicos (interfaces, classes e ServiceLoader ) são usados, sem estruturas, reflexão, anotações e carregadores de classe.
  • Declarabilidade: os pontos de extensão são descritos nos descritores dos módulos. Basta olhar para module-info.java e entender quais pontos de extensão existem e quais plugins contribuem para esses pontos.
  • Garantias estáticas: em caso de erros nos descritores do módulo, o programa não será compilado. Além disso, como bônus, se você usar o IntelliJ IDEA, receberá avisos adicionais (por exemplo, se você esqueceu de usar uses e usar ServiceLoader.load() )
  • Segurança: o sistema Java modular verifica na inicialização se a configuração dos módulos está correta e se recusa a executar o programa em caso de erros.

Repito, mostrei apenas a ideia. Em um aplicativo de plug-in real, haveria dezenas a centenas de módulos e centenas a milhares de pontos de extensão.

Decidi abordar esse tópico porque, nos últimos 7 anos, tenho escrito um aplicativo modular usando o Eclipse RCP, no qual o notório OSGi é usado como um sistema de plug-in e os descritores de plug-in são escritos em XML. Temos mais de uma centena de plugins e ainda estamos no Java 8. Mas mesmo se atualizarmos para uma nova versão do Java, é improvável que usemos módulos Java, pois eles estão fortemente vinculados ao OSGi.

Mas se você estiver escrevendo um aplicativo de plug-in do zero, os módulos Java são uma das opções possíveis para sua implementação. Lembre-se de que os módulos são apenas uma ferramenta, não uma meta.

Brevemente sobre mim


Faço programação há mais de 10 anos (8 deles em Java), respondo ao StackOverflow e gerencio meu próprio canal no Telegram dedicado a Java.

Source: https://habr.com/ru/post/pt479478/


All Articles