Eu estava trabalhando em um projeto que precisava permitir que outros desenvolvedores criassem suas próprias implementações de uma funcionalidade, a serem adicionadas em tempo de execução, no estilo de plugins: basta colocar o JAR em uma pasta específica e o sistema passa a reconhecê-lo automaticamente. Ao começar a pesquisar como implementar essa ideia, imaginei que teria bastante trabalho, mas o processo acabou sendo muito mais simples do que eu esperava, graças ao Java ServiceLoader.

Vamos ver, na prática, como implementar esse recurso. Para isso, criaremos um projeto Maven responsável por carregar os plugins disponíveis e executar suas implementações.

mvn archetype:generate -DgroupId=com.example -DartifactId=service-loader-example -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false

Aproveite este momento para ajustar a versão do Java no pom.xml, garantindo que o projeto seja compilado com a versão adequada do JDK.

<properties>
     <maven.compiler.release>21</maven.compiler.release>
</properties>

Dentro do projeto, criamos uma interface (ou classe abstrata) que deverá ser implementada pelos plugins, conhecida como Service Interface. É esta interface que o Service Loader irá carregar. Para fins de exemplo, utilizaremos uma interface bastante simples.

public interface Plugin {

    String getName();

    void execute();

}

Uma observação importante é que as classes que implementarem essa interface devem possuir um construtor público sem argumentos, pois é dessa forma que o ServiceLoader consegue instanciá-las automaticamente, caso contrário será lançando uma Exception.

Agora é hora de varrer a pasta de plugins em busca dos arquivos JAR e, a partir deles, criar um ClassLoader responsável por carregar essas classes dinamicamente. Esse passo é fundamental, pois o ServiceLoader apenas descobre serviços já carregados por um ClassLoader.

File folder = new File("plugins");
        
List<URL> urls = new ArrayList<>();
for(File file : folder.listFiles()){
    if(file.isFile() && file.getName().endsWith(".jar")){
        System.out.println(String.format("Jar encontrado: %s", file.getName() ) );
        urls.add( file.toURI().toURL() );
    }
}

 URLClassLoader classLoader = new URLClassLoader(
    urls.toArray(new URL[0]),
    ClassLoader.getSystemClassLoader()
 );

No código acima, começamos criando um objeto File apontando para o diretório onde ficarão os plugins, ou seja, o local em que os arquivos JAR serão adicionados. Em seguida, criamos um ArrayList para armazenar as URLs dos JARs encontrados nesse diretório.

Depois disso, percorremos todos os arquivos presentes na pasta e verificamos se cada item é um arquivo regular e se o seu nome termina com a extensão .jar. Quando essa condição é atendida, exibimos no console o nome do JAR encontrado e convertemos o objeto File em uma URL, adicionando-a à lista que será utilizada na criação do URLClassLoader que será encarregado de carregar os plugins dinamicamente.

Com o ClassLoader criado, o próximo passo é localizar, dentro dos JARs carregados, as classes que implementam a interface Plugin, definida anteriormente.

ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class, classLoader);

for(Plugin plugin: serviceLoader){
    System.out.println(String.format("Plugin encontrando: %s", plugin.getName()));
    System.out.println("Executando plugin: ");
    plugin.execute();
    System.out.println("Execução do plugin finalizada: ");
}

No código acima, criamos um objeto ServiceLoader por meio do método estático load, informando a Service Interface (Plugin.class) e o ClassLoader previamente configurado com todos os JARs de plugins encontrados.

Em seguida, basta iterar sobre o ServiceLoader para obter uma instância de cada implementação de Plugin disponível. Dentro do laço, chamamos os métodos getName() e execute(), que são fornecidos por cada plugin.

As instâncias dos plugins são criadas apenas no momento da iteração, o que permite carregamento sob demanda.

Agora vamos gerar o JAR desse projeto, pois ele será necessário como dependência na criação dos plugins.

mvn clean package

Após a execução do comando, o arquivo service-loader-example-1.0-SNAPSHOT.jar é gerado dentro da pasta target do projeto.

O próximo passo é criar o projeto do plugin, que conterá a implementação da funcionalidade a ser carregada dinamicamente.

mvn archetype:generate -DgroupId=com.example -DartifactId=pluginA-example -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false

Logo após criar o projeto do plugin, precisamos adicionar o JAR service-loader-example-1.0-SNAPSHOT.jar como dependência. Para isso, copie o arquivo para uma pasta chamada libs, criada na raiz do projeto do plugin.

Em seguida, adicione essa dependência ao pom.xml do projeto do plugin.

<dependency>
    <groupId>com.example</groupId>
     <artifactId>service-loader-example</artifactId>
     <version>1.0-SNAPSHOT</version>
     <scope>system</scope>
     <systemPath>${basedir}/libs/service-loader-example-1.0-SNAPSHOT.jar</systemPath>
</dependency>

Agora vamos criar a implementação do plugin, que no nosso caso será apenas uma demonstração simples do funcionamento do mecanismo.

public class PluginA implements Plugin{

    @Override
    public String getName(){
        return "Plugin A!!!";
    }

    @Override
    public void execute(){
        System.out.println("Executando coisas do Plugin A");
    }

}

Em seguida, devemos criar as pastas META-INF/services e, dentro delas, um arquivo cujo nome seja o nome totalmente qualificado (fully qualified name) da Service Interface que estamos implementando, no nosso caso, com.example.Plugin.

Dentro desse arquivo, adicionamos uma lista com os nomes totalmente qualificados das classes do projeto do plugin que implementam essa interface, uma por linha.

Estrutura de diretórios do projeto do Plugin para ser carregado corretamente pelo ServiceLoader.

Com a implementação concluída e a estrutura corretamente configurada, agora podemos gerar o JAR do plugin.

mvn clean package

Como resultado do build, será gerado o arquivo target/pluginA-example-1.0-SNAPSHOT.jar.

Para executar a aplicação, crie uma pasta chamada plugins no mesmo diretório onde está o arquivo service-loader-example-1.0-SNAPSHOT.jar (e que iremos executá-lo) e coloque dentro dela os JARs dos plugins criados.

Por fim, basta executar a classe que contém o método main da aplicação e os plugins serão carregados automaticamente.

> java -cp service-loader-example-1.0-SNAPSHOT.jar com.example.App

Jar encontrado: pluginA-example-1.0-SNAPSHOT.jar
Plugin encontrando: Plugin A!!!
Executando plugin:
Executando coisas do Plugin A
Execução do plugin finalizada:

Como vimos, a implementação é bem simples. O ServiceLoader é um recurso poderoso e, embora a grande maioria dos projetos não precise desse tipo de extensibilidade, é sempre interessante conhecer a ferramenta e saber quando utilizá-la.

O código completo utilizado neste artigo está disponível no GitHub

T++ 🍻