Java Modules: Serviços
Padrão service-loader no sistema de módulos Java usando as diretivas uses e provides.
Um objetivo central dos módulos é o desacoplamento: o código que usa uma capacidade não deve nomear a classe que a implementa. O Java Platform Module System (JPMS) transforma isso em um recurso de primeira classe com duas diretivas — uses e provides — conectadas em tempo de execução pelo ServiceLoader. Este é o substituto modular da antiga convenção de classpath de colocar um arquivo META-INF/services em um JAR.
Este capítulo pressupõe que você já sabe como escrever um descritor module-info.java; aqui adicionamos as diretivas de serviço a ele.
Os três papéis
Um serviço consiste em três partes, idealmente em três módulos diferentes:
- A interface de serviço (ou classe abstrata) — o contrato, por exemplo
PricingRule. Ela reside em um módulo que a exporta. - O consumidor — código que solicita implementações. Seu módulo declara
uses com.acme.PricingRule;e chamaServiceLoader.load(PricingRule.class). - Um ou mais provedores — módulos de implementação. Cada um declara
provides com.acme.PricingRule with com.acme.impl.StandardPricing;.
O consumidor nunca importa StandardPricing. Ele conhece apenas a interface. Adicione um novo módulo provedor ao caminho de módulo e o consumidor o detecta automaticamente — sem recompilação, sem alteração de código.
As diretivas em module-info.java
// module com.acme.api
module com.acme.api {
exports com.acme; // export the PricingRule interface
}
// module com.acme.app (the consumer)
module com.acme.app {
requires com.acme.api;
uses com.acme.PricingRule; // "I will ServiceLoader.load this"
}
// module com.acme.standard (a provider)
module com.acme.standard {
requires com.acme.api;
provides com.acme.PricingRule
with com.acme.standard.StandardPricing;
}uses informa ao resolvedor que o consumidor vai consultar aquele serviço, portanto o módulo tem permissão para chamar ServiceLoader.load. provides … with … registra uma implementação. A classe provedora deve ter um construtor público sem argumentos ou um método estático público provider() que retorna uma instância.
Consumindo com ServiceLoader
ServiceLoader<PricingRule> loader = ServiceLoader.load(PricingRule.class);
for (PricingRule rule : loader) {
System.out.println(rule.describe());
}
// or pick the first available
PricingRule rule = ServiceLoader.load(PricingRule.class)
.findFirst()
.orElseThrow();ServiceLoader é lazy — cada provedor é instanciado somente quando o iterador o alcança — e armazena instâncias em cache. Ele implementa Iterable, portanto um loop for-each percorre todos os provedores registrados.
Um exemplo prático: ServiceLoader sobre um serviço real do JDK
O JDK já inclui um serviço que você pode carregar sem precisar construir três módulos: java.util.spi.ToolProvider. O compilador (javac), a ferramenta JAR (jar) e outros estão registrados como provedores dessa interface dentro dos módulos JDK. Este programa os carrega por meio do ServiceLoader — exatamente o código do consumidor acima, aplicado a um serviço que já está configurado.
O que extrair da execução:
- O loop for-each percorreu todos os
ToolProviderque o runtime registrou (tipicamentejavac,jar,javadoc, …) sem que o programa importasse nenhuma dessas classes de implementação. Esse é exatamente o ponto dos serviços: o consumidor depende deToolProvider, a interface, e descobre provedores concretos em tempo de execução. - Cada provedor exibiu um nome de classe de implementação diferente, embora compartilhem uma única interface. Os módulos JDK declararam
provides java.util.spi.ToolProvider with …em seus descritores; oServiceLoaderos coletou todos. Adicionar outro módulo provedor o faria aparecer neste mesmo loop sem nenhuma alteração aqui. ToolProvider.findFirst("javac")retornou umOptionale o código tratou ambos os ramos. Consultas a serviços são inerentemente "pode estar ausente" — um runtime mínimo poderia não incluir nenhum provedor de ferramentas — portanto a API obriga você a planejar para o caso vazio em vez de presumir que existe uma implementação.- Executar
javac --versionpor meio do provedor carregado prova que o objeto é totalmente funcional, alcançado puramente pelo contrato de serviço. O consumidor invocou comportamento real sem uma dependência em tempo de compilação nas classes do compilador. ServiceLoaderinstancia de forma lazy e apenas o que você itera; em uma configuração real de três módulos, omodule-infodo consumidor precisaria deuses java.util.spi.ToolProvider;para que a chamada seja permitida. Os próprios módulos do JDK já o declaram, por isso isso funciona sem alterações.
Por que usar serviços
- Arquiteturas de plugins — coloque um JAR provedor no caminho de módulo para estender uma aplicação.
- Implementações opcionais — escolha um driver SSL, de log ou de banco de dados em tempo de execução conforme o módulo provedor presente.
- Inversão de dependência — o módulo consumidor de alto nível depende de um módulo de interface, nunca dos provedores de baixo nível, portanto as setas de dependência apontam para o contrato estável.
Esse é o mesmo mecanismo que o JDK usa para DriverManager, provedores de charset e as cronologias de java.time. O capítulo final desta parte, Migrando para Java Modules, une tudo: como mover uma aplicação de classpath existente para o caminho de módulo passo a passo.