
Atualmente, não há escassez de estruturas para a criação de microsserviços em Java e Kotlin. O artigo discute o seguinte:
Com base neles, foram criados quatro serviços que podem interagir entre si por meio da API HTTP usando o padrão Service Discovery implementado usando o
Consul . Assim, eles formam uma arquitetura heterogênea de microsserviço (no nível da estrutura) (a seguir denominada ISA):

Defina um conjunto de requisitos para cada serviço:
- pilha de tecnologia:
- JDK 12;
- Kotlin
- Gradle (Kotlin DSL);
- JUnit 5.
- funcionalidade (API HTTP):
GET /application-info{?request-to=some-service-name}
Retorna algumas informações básicas sobre o microsserviço (nome, estrutura, ano de lançamento da estrutura); ao especificar o nome de um dos quatro microsserviços no parâmetro request-to
à sua API HTTP, uma solicitação semelhante é executada que retorna informações básicas;GET /application-info/logo
Retorna a imagem.
- implementação:
- configuração usando o arquivo de configuração;
- Usando injeção de dependência
- testes que verificam a funcionalidade da API HTTP.
- ISA:
- usando o padrão Service Discovery (registrando-se no Consul, acessando a API HTTP de outro microsserviço por seu nome usando o balanceamento de carga do cliente);
- formação de artefato uber-jar.
A seguir, consideramos a implementação de um microsserviço em cada uma das estruturas e comparamos os parâmetros dos aplicativos recebidos.
Serviço Helidon
A estrutura de desenvolvimento foi criada na Oracle para uso interno, tornando-se posteriormente de código aberto. Existem dois modelos de desenvolvimento baseados nessa estrutura: Standard Edition (SE) e MicroProfile (MP). Nos dois casos, o serviço será um programa Java SE regular. Saiba mais sobre as diferenças
nesta página.
Em resumo, o Helidon MP é uma das implementações do Eclipse
MicroProfile , que possibilita o uso de várias APIs, conhecidas anteriormente pelos desenvolvedores Java EE (por exemplo, JAX-RS, CDI) e as mais recentes (Health Check, Metrics, Fault Tolerance) etc.) Na variante Helidon SE, os desenvolvedores foram guiados pelo princípio de “No magic”, expresso em particular em menos ou nenhuma anotação necessária para criar o aplicativo.
Helidon SE foi selecionado para o desenvolvimento de microsserviços. Entre outras coisas, falta ferramentas para implementar a Injeção de Dependências, portanto, o
Koin é usado para implementar dependências. A seguir está uma classe que contém o método principal. Para implementar a injeção de dependência, a classe herda de
KoinComponent . O Koin inicia primeiro, depois as dependências necessárias são inicializadas e o método
startServer()
é chamado, onde um objeto do tipo
WebServer é criado, para o qual as configurações e as configurações de roteamento do aplicativo foram transferidas anteriormente; após o início, o aplicativo é registrado no Consul:
object HelidonServiceApplication : KoinComponent { @JvmStatic fun main(args: Array<String>) { val startTime = System.currentTimeMillis() startKoin { modules(koinModule) } val applicationInfoService: ApplicationInfoService by inject() val consulClient: Consul by inject() val applicationInfoProperties: ApplicationInfoProperties by inject() val serviceName = applicationInfoProperties.name startServer(applicationInfoService, consulClient, serviceName, startTime) } } fun startServer( applicationInfoService: ApplicationInfoService, consulClient: Consul, serviceName: String, startTime: Long ): WebServer { val serverConfig = ServerConfiguration.create(Config.create().get("webserver")) val server: WebServer = WebServer .builder(createRouting(applicationInfoService)) .config(serverConfig) .build() server.start().thenAccept { ws -> val durationInMillis = System.currentTimeMillis() - startTime log.info("Startup completed in $durationInMillis ms. Service running at: http://localhost:" + ws.port())
O roteamento está configurado da seguinte maneira:
private fun createRouting(applicationInfoService: ApplicationInfoService) = Routing.builder() .register(JacksonSupport.create()) .get("/application-info", Handler { req, res -> val requestTo: String? = req.queryParams() .first("request-to") .orElse(null) res .status(Http.ResponseStatus.create(200)) .send(applicationInfoService.get(requestTo)) }) .get("/application-info/logo", Handler { req, res -> res.headers().contentType(MediaType.create("image", "png")) res .status(Http.ResponseStatus.create(200)) .send(applicationInfoService.getLogo()) }) .error(Exception::class.java) { req, res, ex -> log.error("Exception:", ex) res.status(Http.Status.INTERNAL_SERVER_ERROR_500).send() } .build()
O aplicativo usa a configuração no formato
HOCON :
webserver { port: 8081 } application-info { name: "helidon-service" framework { name: "Helidon SE" release-year: 2019 } }
Também é possível usar arquivos nos formatos JSON, YAML e propriedades para configuração (mais detalhes
aqui ).
Serviço Ktor
A estrutura está escrita em Kotlin. Um novo projeto pode ser criado de várias maneiras: usando o sistema de construção,
start.ktor.io ou o plug-in para o IntelliJ IDEA (mais
aqui ).
Como o Helidon SE, o Ktor não possui uma DI pronta para uso, portanto, as dependências são implementadas usando o Koin antes de iniciar o servidor:
val koinModule = module { single { ApplicationInfoService(get(), get()) } single { ApplicationInfoProperties() } single { ServiceClient(get()) } single { Consul.builder().withUrl("http://localhost:8500").build() } } fun main(args: Array<String>) { startKoin { modules(koinModule) } val server = embeddedServer(Netty, commandLineEnvironment(args)) server.start(wait = true) }
Os módulos exigidos pelo aplicativo são especificados no arquivo de configuração (somente o formato HOCON pode ser usado; mais sobre a configuração do servidor Ktor
aqui ), cujo conteúdo é apresentado abaixo:
ktor { deployment { host = localhost port = 8082 watch = [io.heterogeneousmicroservices.ktorservice] } application { modules = [io.heterogeneousmicroservices.ktorservice.module.KtorServiceApplicationModuleKt.module] } } application-info { name: "ktor-service" framework { name: "Ktor" release-year: 2018 }
Ktor e Koin usam o termo "módulo", que tem significados diferentes. Em Koin, um módulo é análogo ao contexto do aplicativo no Spring Framework. O módulo Ktor é uma função definida pelo usuário que aceita um objeto do tipo
Aplicativo e pode configurar um pipeline, definir recursos, registrar rotas, processar
pedidos, etc .:
fun Application.module() { val applicationInfoService: ApplicationInfoService by inject() if (!isTest()) { val consulClient: Consul by inject() registerInConsul(applicationInfoService.get(null).name, consulClient) } install(DefaultHeaders) install(Compression) install(CallLogging) install(ContentNegotiation) { jackson {} } routing { route("application-info") { get { val requestTo: String? = call.parameters["request-to"] call.respond(applicationInfoService.get(requestTo)) } static { resource("/logo", "logo.png") } } } }
Nesse trecho de código, o roteamento de solicitações é configurado, em particular, o recurso estático
logo.png
.
O serviço Ktor pode conter recursos. Um recurso é uma funcionalidade incorporada em um
pipeline de solicitação-resposta (
DefaultHeaders, Compression e outros no exemplo de código acima). É possível implementar seus próprios recursos, por exemplo, o código abaixo implementa o padrão Service Discovery em combinação com o balanceamento de carga do cliente com base no algoritmo Round-robin:
class ConsulFeature(private val consulClient: Consul) { class Config { lateinit var consulClient: Consul } companion object Feature : HttpClientFeature<Config, ConsulFeature> { var serviceInstanceIndex: Int = 0 override val key = AttributeKey<ConsulFeature>("ConsulFeature") override fun prepare(block: Config.() -> Unit) = ConsulFeature(Config().apply(block).consulClient) override fun install(feature: ConsulFeature, scope: HttpClient) { scope.requestPipeline.intercept(HttpRequestPipeline.Render) { val serviceName = context.url.host val serviceInstances = feature.consulClient.healthClient().getHealthyServiceInstances(serviceName).response val selectedInstance = serviceInstances[serviceInstanceIndex] context.url.apply { host = selectedInstance.service.address port = selectedInstance.service.port } serviceInstanceIndex = (serviceInstanceIndex + 1) % serviceInstances.size } } } }
A lógica principal está no método de
install
: durante a fase de solicitação de
renderização (que é executada antes da fase de
envio ), o nome do serviço chamado é determinado primeiro e, em seguida, uma lista de instâncias desse serviço é solicitada ao
consulClient
, após o qual a instância é determinada usando o algoritmo Round-robin. Assim, a seguinte chamada se torna possível:
fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking { httpClient.get<ApplicationInfo>("http://$serviceName/application-info") }
Serviço Micronaut
O Micronaut é desenvolvido pelos criadores da estrutura
Grails e é inspirado na experiência de criar serviços usando Spring, Spring Boot e Grails. A estrutura é uma poliglota que suporta Java, Kotlin e Groovy;
talvez haja suporte para o Scala. A injeção de dependência é realizada no estágio de compilação, o que leva a menos consumo de memória e inicialização mais rápida do aplicativo em comparação com o Spring Boot.
A classe principal tem o seguinte formato:
object MicronautServiceApplication { @JvmStatic fun main(args: Array<String>) { Micronaut.build() .packages("io.heterogeneousmicroservices.micronautservice") .mainClass(MicronautServiceApplication.javaClass) .start() } }
Alguns componentes de um aplicativo baseado em Micronaut são semelhantes aos de um aplicativo Spring Boot, por exemplo, o código do controlador é o seguinte:
@Controller( value = "/application-info", consumes = [MediaType.APPLICATION_JSON], produces = [MediaType.APPLICATION_JSON] ) class ApplicationInfoController( private val applicationInfoService: ApplicationInfoService ) { @Get fun get(requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo) @Get("/logo", produces = [MediaType.IMAGE_PNG]) fun getLogo(): ByteArray = applicationInfoService.getLogo() }
O suporte ao Kotlin no Micronaut é baseado no
plug-in do compilador
kapt (mais
informações aqui ). O script de montagem está configurado da seguinte maneira:
plugins { ... kotlin("kapt") ... } dependencies { kapt("io.micronaut:micronaut-inject-java") ... kaptTest("io.micronaut:micronaut-inject-java") ... }
A seguir está o conteúdo do arquivo de configuração:
micronaut: application: name: micronaut-service server: port: 8083 consul: client: registration: enabled: true application-info: name: ${micronaut.application.name} framework: name: Micronaut release-year: 2018
A configuração do microsserviço também é possível com JSON, propriedades e formatos de arquivo Groovy (mais detalhes
aqui ).
Serviço de inicialização do Spring
A estrutura foi criada para simplificar o desenvolvimento de aplicativos usando o ecossistema Spring Framework. Isso é alcançado através de mecanismos de configuração automática ao conectar bibliotecas. A seguir está o código do controlador:
@RestController @RequestMapping(path = ["application-info"], produces = [MediaType.APPLICATION_JSON_UTF8_VALUE]) class ApplicationInfoController( private val applicationInfoService: ApplicationInfoService ) { @GetMapping fun get(@RequestParam("request-to") requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo) @GetMapping(path = ["/logo"], produces = [MediaType.IMAGE_PNG_VALUE]) fun getLogo(): ByteArray = applicationInfoService.getLogo() }
O microsserviço está configurado com um arquivo YAML:
spring: application: name: spring-boot-service server: port: 8084 application-info: name: ${spring.application.name} framework: name: Spring Boot release-year: 2014
Também é possível usar arquivos de formato de propriedades para configuração (mais detalhes
aqui ).
Lançamento
O projeto funciona no JDK 12, embora provavelmente também esteja na versão 11, você só precisa alterar o parâmetro
jvmTarget
nos scripts de montagem de
jvmTarget
:
withType<KotlinCompile> { kotlinOptions { jvmTarget = "12" ... } }
Antes de iniciar os microsserviços, você precisa
instalar o Consul e
iniciar o agente - por exemplo, como este:
consul agent -dev
.
É possível iniciar microsserviços em:
Após iniciar todos os microsserviços em
http://localhost:8500/ui/dc1/services
você verá:

Teste de API
Os resultados do teste da API de serviço Helidon são dados como exemplo:
GET http://localhost:8081/application-info
{ "name": "helidon-service", "framework": { "name": "Helidon SE", "releaseYear": 2019 }, "requestedService": null }
GET http://localhost:8081/application-info?request-to=ktor-service
{ "name": "helidon-service", "framework": { "name": "Helidon SE", "releaseYear": 2019 }, "requestedService": { "name": "ktor-service", "framework": { "name": "Ktor", "releaseYear": 2018 }, "requestedService": null } }
GET http://localhost:8081/application-info/logo
Retorna a imagem.
É possível testar uma API arbitrária de microsserviço usando o
Postman (uma
coleção de solicitações), o
cliente HTTP do IntelliJ IDEA (uma
coleção de solicitações), um navegador ou outra ferramenta. Se você usar os dois primeiros clientes, precisará especificar a porta do microsserviço chamado na variável correspondente (no Postman, ele está no
menu de coleção -> Editar -> Variáveis , e no HTTP Client, ele está na variável de ambiente especificada
neste arquivo) e ao testar o método 2) A API também precisa especificar o nome do microsserviço "sob o capô" solicitado. As respostas serão semelhantes às fornecidas acima.
Comparação de configurações de aplicativos
Tamanho do Artefato
Para preservar a simplicidade de configurar e executar aplicativos nos scripts de montagem, nenhuma dependência transitiva foi excluída; portanto, o tamanho do serviço uber-JAR no Spring Boot excede significativamente o tamanho dos análogos em outras estruturas (porque, ao usar os iniciantes, não são importadas apenas as dependências necessárias; se desejado, o tamanho pode ser reduzido significativamente):
Hora de lançamento
O tempo de inicialização de cada aplicativo é inconsistente e cai em alguma "janela"; a tabela abaixo mostra o tempo de ativação do artefato sem especificar nenhum parâmetro adicional:
É importante notar que, se você "limpar" o aplicativo Spring Boot de dependências desnecessárias e prestar atenção na configuração do aplicativo para iniciar (por exemplo, verificar apenas os pacotes necessários e usar a inicialização lenta do compartimento), poderá reduzir significativamente o tempo de inicialização.
Teste de carga
Para o teste,
Gatling e
um script Scala foram usados. O gerador de carga e o serviço em teste foram executados na mesma máquina (Windows 10, um processador quad-core de 3,2 GHz, 24 GB de RAM, SSD). A porta deste serviço é indicada no script Scala.
Para cada microsserviço é determinado:
- a quantidade mínima de memória heap (
-Xmx
) necessária para executar um microsserviço em funcionamento (respondendo a solicitações) - memória heap mínima necessária para passar no teste de carga 50 usuários * 1000 solicitações
- memória heap mínima necessária para passar no teste de carga 500 usuários * 1000 solicitações
Passar em um teste de carga significa que o microsserviço respondeu a todas as solicitações a qualquer momento.
Vale ressaltar que todos os microsserviços usam o servidor HTTP Netty.
Conclusão
A tarefa - a criação de um serviço simples com API HTTP e a capacidade de funcionar no ISA - pôde ser concluída em todas as estruturas em questão. É hora de fazer um balanço e considerar seus prós e contras.
HelidonEdição Standard- vantagens
- configurações do aplicativo
Em todos os aspectos, apresentou bons resultados; - "Sem mágica"
A estrutura justificou o princípio declarado pelos desenvolvedores: foram necessárias apenas uma anotação para criar o aplicativo ( @JvmStatic
- para o interope Java-Kotlin).
- contras
- microframework
Alguns componentes necessários para o desenvolvimento industrial estão ausentes, por exemplo, injeção de dependência e implementação do Service Discovery.
MicroprofileO microsserviço não foi implementado nessa estrutura; portanto, observarei apenas alguns pontos:
- vantagens
- Implementação do Eclipse MicroProfile
Em essência, o MicroProfile é o Java EE otimizado para ISA. Assim, em primeiro lugar, você obtém acesso a toda a variedade de APIs Java EE, incluindo aquelas projetadas especificamente para o ISA, e em segundo lugar, pode alterar a implementação do MicroProfile para qualquer outro (Open Liberty, WildFly Swarm, etc.) .
- adicionalmente
- No MicroProfile Starter, você pode criar um projeto do zero com os parâmetros necessários, por analogia com ferramentas semelhantes para outras estruturas (por exemplo, Spring Initializr ). No momento da publicação do artigo, Helidon implementa o MicroProfile 1.2, enquanto a versão mais recente da especificação é 3.0.
Ktor- vantagens
- leveza
Permite conectar apenas as funções diretamente necessárias para concluir a tarefa; - configurações do aplicativo
Bons resultados em todos os aspectos.
- contras
- "Afiado" no Kotlin, ou seja, é possível, mas não necessário, desenvolver em Java;
- microframework (consulte o item semelhante para Helidon SE).
- adicionalmente
Por um lado, o conceito de desenvolvimento de estrutura não está incluído nos dois modelos de desenvolvimento Java mais populares (Spring-like (Spring Boot / Micronaut) e Java EE / MicroProfile), o que pode levar a:
- um problema em encontrar especialistas;
- mais tempo para concluir as tarefas em comparação com o Spring Boot devido à necessidade de configurar explicitamente a funcionalidade necessária.
Por outro lado, a diferença entre o Spring e Java EE “clássico” permite que você observe o processo de desenvolvimento de um ângulo diferente, talvez mais conscientemente.
Micronaut- vantagens
- Aot
Como observado anteriormente, o AOT permite reduzir o tempo de início e a memória consumidos pelo aplicativo em comparação com o seu equivalente no Spring Boot; - Modelo de desenvolvimento semelhante a uma mola
Programadores com experiência em desenvolvimento no Spring não levarão muito tempo para dominar essa estrutura; - configurações do aplicativo
Bons resultados em todos os aspectos; - poliglota
Suporte cidadão de primeira classe para Java, Kotlin, Groovy; talvez haja suporte para o Scala. Na minha opinião, isso pode afetar positivamente o crescimento da comunidade. A propósito, em junho de 2019 Groovy no ranking da popularidade das linguagens de programação, o TIOBE ocupa o 14º lugar, decolando do 60º ano anterior, ficando em um segundo lugar honroso entre as linguagens da JVM; - O projeto Micronaut for Spring também permite alterar o tempo de execução do aplicativo Spring Boot existente para Micronaut (com restrições).
Bota de mola- vantagens
- maturidade da plataforma e ecossistema
A estrutura "todos os dias". Para a maioria das tarefas cotidianas, já existe uma solução no paradigma de programação Spring, isto é, de uma maneira familiar para muitos programadores. O desenvolvimento é simplificado pelos conceitos de entradas e configurações automáticas; - a presença de um grande número de especialistas no mercado de trabalho, bem como uma base de conhecimento significativa (incluindo documentação e respostas ao Stack Overflow);
- perspectiva
Acho que muitos concordarão que, no futuro próximo, a Primavera continuará sendo a principal estrutura de desenvolvimento.
- contras
- configurações do aplicativo
A aplicação nessa estrutura não estava entre os líderes, no entanto, alguns parâmetros, conforme observado anteriormente, podem ser otimizados independentemente. Também vale lembrar a presença do projeto Spring Fu , que está em desenvolvimento ativo, cuja utilização permite reduzir esses parâmetros.
Você também pode destacar os problemas gerais associados às novas estruturas ausentes do Spring Boot:
- ecossistema menos desenvolvido;
- um pequeno número de especialistas com experiência com essas tecnologias;
- mais tempo para concluir tarefas;
- perspectivas obscuras.
As estruturas consideradas pertencem a diferentes categorias de peso: Helidon SE e Ktor são
microframes , Spring Boot é uma estrutura de pilha cheia, é mais provável que o Micronaut também seja pilha cheia; outra categoria é MicroProfile (por exemplo, Helidon MP). Nos microframes, a funcionalidade é limitada, o que pode retardar a execução das tarefas; Para esclarecer a possibilidade de implementar essa ou aquela funcionalidade com base em qualquer estrutura de desenvolvimento, recomendo que você se familiarize com a documentação.
Não me atrevo a julgar se essa ou aquela estrutura "disparará" em um futuro próximo; portanto, na minha opinião, é melhor continuar monitorando o desenvolvimento de eventos usando a estrutura de desenvolvimento existente para resolver tarefas de trabalho.
Ao mesmo tempo, como foi mostrado no artigo, as novas estruturas superam o Spring Boot pelos parâmetros considerados dos aplicativos recebidos. Se qualquer um desses parâmetros for crítico para qualquer um dos seus microsserviços, talvez seja necessário prestar atenção às estruturas que apresentaram os melhores resultados. No entanto, não esqueça que o Spring Boot, em primeiro lugar, continua a melhorar e, em segundo lugar, possui um ecossistema enorme e um número significativo de programadores Java está familiarizado com ele. Existem outras estruturas que não são abordadas neste artigo: Javalin, Quarkus etc.
Você pode visualizar o código do projeto no
GitHub . Obrigado pela atenção!
PS: Obrigado ao
artglorin por ajudar com este artigo.