Módulos - alandrade21/docsCompartilhados GitHub Wiki

Nesta página:

Módulos

A estrutura de um projeto modular é diferente:

src
 |__ hello
      |__ br.com.pacote
      |    |__ HelloModules.java
      |__ module-info.java

Há um novo nível entre a pasta src e os pacotes. Esse novo nível é a pasta onde fica o arquivo module-info.java de declaração do módulo.

O módulo deve ter o mesmo nome da pasta do módulo. Assim, no exemplo acima, a declaração do módulo ficaria:

module hello {
  
}

Na prática, numa IDE, a pasta de código fonte deve ser configurada para src/hello (seguindo o exemplo acima), e a pasta de output também deve replicar essa estrutura, ou seja, ela ficaria bin/hello.

No caso de outros módulos nesse mesmo projeto, cada um teria sua pasta de fonte e de output separadas.

Dependências

Agora imagine uma situação onde um módulo cliente precise consumir um módulo api, que precisa consumir um módulo domínio.

cliente ---> api ---> domínio

A definição dos módulos fica:

module cliente {
  requires br.com.api; 
}

module api {
  exports br.com.api; 
  requires br.com.dominio; 
}

module dominio {
  exports br.com.dominio;
}

Dependências necessárias apenas para compilação (como anotações, por exemplo) podem ser expressas com requires static.

module framework {
  requires static lombok;
}

Dependências Transitivas

Usando o mesmo exemplo da seção anterior, imagine agora que no módulo api há uma classe chamada Serviço que publique o método public Retorno fazAlgumaCoisa(), e que a classe Retorno está definida no módulo dominio, de forma que o código no módulo cliente chame algo do tipo Retorno retorno = servico.fazAlgumaCoisa();. Essa chamada retornará o seguinte erro: Retorno is defined in an inaccessible class or interface.

Há duas soluções para esse problema. A primeira, menos elegante, é colocar a dependência ao módulo dominio no módulo cliente:

module cliente {
  requires br.com.api; 
  requires br.com.dominio; 
}

Essa solução é pouco elegante porque transforma o grafo de dependências de:

cliente ---> api ---> domínio

Em:

cliente ---> api
        |     |
        |     |
        |     v
        --> dominio     

A melhor solução seria transformar a dependência do módulo api ao módulo dominio numa dependência transitiva:

module api {
  exports br.com.api; 
  requires transitive br.com.dominio; 
}

Esse mesmo raciocínio deve ser utilizado para tipos utilizados em atributos e tipos de exceções lançadas pelos serviços de um módulo que residam em outros módulos.

Se a compilação for feita com a opção -Xlint:exports, o compilador gerará um warning para todos os requires que deveriam ser transitivos e não são.

Módulos Agregadores

Imagine um projeto de uma biblioteca que possui vários módulos pouco relacionados entre si, de forma que um consumidor possa utilizar apenas alguns módulos e ignorar outros.

Para evitar que um consumidor tenha que ficar declarando módulos específicos da lib, o que pode ser confuso, é possível declarar um módulo agregador que possua apenas as dependências transitivas para todos os demais módulos da lib, de forma que o cliente use apenas este módulo agregador. Na prática, esse módulo agregador funciona como uma fachada para sua lib.

Exemplo desse módulo agregador:

module library {
  requires transitive library.one;
  requires transitive library.two;
  requires transitive library.three;
}

Ainda seria possível para um cliente usar apenas um módulo específico.

O uso dessa solução é particularmente útil quando é necessário quebrar um módulo que ficou muito grande em módulos menores, mantendo a retrocompatibilidade.

Serviços

Imagine que seu módulo cliente precisa consumir serviços de provedores desconhecidos. Então uma interface para esses serviços é criada num módulo api.

Usemos a interface hipotética abaixo:

package br.eti.alandrade21.api;
...
public interface Servico {

   String getName();

   double execute(...);
}

O módulo api ficaria:

module api {
   exports br.eti.alandrade21.api;
}

Enquanto o módulo client ficaria:

module client {
   requires br.eti.alandrade21.api;
}

Um primeiro provedor desse serviço, implementando-o em um módulo servicoX, utilizando uma classe concreta br.com.provedor1.ServicoImpl (que implementa a interface br.eti.alandrade21.api.Servico), teria seu módulo definido da seguinte forma:

module servicoX {

   requires br.eti.alandrade21.api;

   provides br.eti.alandrade21.api.Servico
       with br.com.provedor1.ServicoImpl;
}

A cláusula provides informa que esse módulo fornece uma implementação para a interface Serviço através da classe ServicoImpl. Repare que essa classe concreta não é exportada pelo módulo, ficando encapsulada.

Para o cliente consumir este serviço é necessário alterar seu módulo da seguinte forma:

module client {
   requires br.eti.alandrade21.api;

   uses br.eti.alandrade21.api.Servico;
}

A cláusula uses informa que este módulo deseja consumir classes que implementem a interface br.eti.alandrade21.api.Servico como serviços.

Para atender essa necessidade, o sistema de módulos do java implementa um ServiceLoader.

O client precisa inserir em seu código o seguinte bloco:

Iterable<Servico> servicos = ServiceLoader.load(Servico.class); 

for (Servico servico: servicos) { 
   System.out.println(servico.getName() + ": " + servico.execute(...));
}

ServiceLoader.load(Servico.class) busca, no module path por todos os provedores do serviço br.eti.alandrade21.api.Servico, os instancia e retorna um Iterable com todas as instâncias criadas.

Dessa forma, as classes concretas que implementam os serviços não precisam estar presentes em tempo de compilação do cliente. É importante perceber que em tempo de execução pode não haver nenhum implementação disponível.

Para que a classe que implementa a interface seja instanciada pelo service loader, essa classe deve ter um construtor padrão público, ou deve prover um método estático de instanciamento, chamado provider(), e cujo tipo de retorno é o tipo da classe concreta que implementa a interface. No caso do exemplo acima, esse método provider para a classe ServicoImpl ficaria:

public class ServicoImpl implements Servico {
  ...

  public static ServicoImpl provider() {
    return new ServicoImpl();
  }

  private ServicoImpl(){};

  ...
}

Por fim, é recomendável colocar o ServiceLoader.load(Servico.class) dentro da interface, assim:

package br.eti.alandrade21.api;
...
public interface Servico {

   String getName();

   double execute(...);

  static Iterable<Analyzer> getServices() {
    return ServiceLoader.load(Analyzer.class); 
  }
}

Isso faz com que o cliente não tenha que saber nada sobre service loaders. Agora, para obter a lista dos impl é só chamar Servico.getServices().

O módulo api precisa ser alterado da seguinte forma:

O módulo api ficaria:

module api {
   exports br.eti.alandrade21.api;

   uses br.eti.alandrade21.api.Servico;
}

Obviamente a cláusula uses sai do módulo Cliente.

Dependências opcionais

module framework {
  requires static fastjsonlib;
  uses javamodularity.fastjsonlib.FastJson;
}

FastJson fastJson =
  ServiceLoader.load(FastJson.class)
               .findFirst() // Retorna um empty Optional se não achar
               .orElse(getFallBack());
⚠️ **GitHub.com Fallback** ⚠️