
Actualmente no hay escasez de marcos para crear microservicios en Java y Kotlin. El artículo discute lo siguiente:
En base a ellos, se han creado cuatro servicios que pueden interactuar entre sí a través de la API HTTP utilizando el patrón de descubrimiento de servicios implementado mediante
Consul . Por lo tanto, forman una arquitectura de microservicio heterogénea (a nivel de marco) (en lo sucesivo, ISA):

Defina un conjunto de requisitos para cada servicio:
- pila de tecnología:
- JDK 12;
- Kotlin
- Gradle (Kotlin DSL);
- JUEGO 5.
- funcionalidad (API HTTP):
GET /application-info{?request-to=some-service-name}
Devuelve información básica sobre el microservicio (nombre, marco, año de lanzamiento del marco); al especificar el nombre de uno de los cuatro microservicios en el parámetro de request-to
a su API HTTP, se ejecuta una solicitud similar que devuelve información básica;GET /application-info/logo
Devuelve la imagen.
- implementación:
- configuración utilizando el archivo de configuración;
- Usando inyección de dependencia
- pruebas que verifican la funcionalidad de la API HTTP.
- ISA
- usando el patrón de Descubrimiento de Servicio (registrándose con Consul, accediendo a la API HTTP de otro microservicio por su nombre usando el equilibrio de carga del cliente);
- Formación de artefactos uber-jar.
A continuación, consideramos la implementación de un microservicio en cada uno de los marcos y comparamos los parámetros de las aplicaciones recibidas.
Servicio de Helidon
El marco de desarrollo se creó en Oracle para uso interno, y luego se convirtió en código abierto. Hay dos modelos de desarrollo basados en este marco: Standard Edition (SE) y MicroProfile (MP). En ambos casos, el servicio será un programa Java SE normal. Obtenga más información sobre las diferencias en
esta página.
En resumen, Helidon MP es una de las implementaciones de Eclipse
MicroProfile , que permite utilizar muchas API, ambas conocidas anteriormente por los desarrolladores de Java EE (por ejemplo, JAX-RS, CDI) y las más nuevas (Health Check, Metrics, Fault Tolerance). etc.) En la variante Helidon SE, los desarrolladores se guiaron por el principio de "Sin magia", que se expresa, en particular, en menos o ninguna anotación necesaria para crear la aplicación.
Helidon SE fue seleccionado para el desarrollo de microservicios. Entre otras cosas, carece de herramientas para implementar la inyección de dependencias, por lo que
Koin se usa para implementar dependencias. La siguiente es una clase que contiene el método principal. Para implementar la inyección de dependencia, la clase hereda de
KoinComponent . Koin comienza primero, luego se inicializan las dependencias requeridas y se llama al método
startServer()
, donde se crea un objeto del tipo
WebServer , al que se transfieren previamente la configuración de la aplicación y la configuración de enrutamiento; después de comenzar la aplicación se registra en 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())
El enrutamiento se configura de la siguiente manera:
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()
La aplicación usa la configuración en el formato
HOCON :
webserver { port: 8081 } application-info { name: "helidon-service" framework { name: "Helidon SE" release-year: 2019 } }
También es posible usar archivos en JSON, YAML y formatos de propiedades para la configuración (más detalles
aquí ).
Servicio de Ktor
El marco está escrito en Kotlin. Se puede crear un nuevo proyecto de varias maneras: usando el sistema de compilación,
start.ktor.io o el complemento para IntelliJ IDEA (más
aquí ).
Al igual que Helidon SE, Ktor no tiene una DI lista para usar, por lo que las dependencias se implementan utilizando Koin antes de iniciar el 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) }
Los módulos requeridos por la aplicación se especifican en el archivo de configuración (es posible usar solo el formato HOCON; más sobre la configuración del servidor Ktor
aquí ), cuyo contenido se presenta a continuación:
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 y Koin usan el término "módulo", que tiene diferentes significados. En Koin, un módulo es un análogo del contexto de la aplicación en Spring Framework. El módulo Ktor es una función definida por el usuario que acepta un objeto de tipo
Aplicación y puede configurar una tubería, establecer características, registrar rutas, procesar
solicitudes, 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") } } } }
En este fragmento de código, el enrutamiento de solicitudes se configura, en particular, el recurso estático
logo.png
.
El servicio de Ktor puede contener funciones. Una característica es una funcionalidad que está incrustada en una
tubería de solicitud-respuesta (
DefaultHeaders, Compression y otros en el ejemplo de código anterior). Es posible implementar sus propias características, por ejemplo, el siguiente código implementa el patrón de Descubrimiento de Servicio en combinación con el equilibrio de carga del cliente basado en el 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 } } } }
La lógica principal está en el método de
install
: durante la fase de solicitud de
Render (que se ejecuta antes de la fase de
envío ), primero se determina el nombre del servicio que se llama, luego se solicita una lista de instancias de este servicio a
consulClient
, después de lo cual se determina la instancia utilizando el algoritmo Round-robin. Por lo tanto, se hace posible la siguiente llamada:
fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking { httpClient.get<ApplicationInfo>("http://$serviceName/application-info") }
Servicio de Micronaut
Micronaut está desarrollado por los creadores del marco
Grails y está inspirado en la experiencia de crear servicios utilizando Spring, Spring Boot y Grails. El marco es un políglota que admite Java, Kotlin y Groovy;
tal vez habrá soporte para Scala. La inyección de dependencia se lleva a cabo en la etapa de compilación, lo que conduce a un menor consumo de memoria y un inicio de aplicación más rápido en comparación con Spring Boot.
La clase principal tiene la siguiente forma:
object MicronautServiceApplication { @JvmStatic fun main(args: Array<String>) { Micronaut.build() .packages("io.heterogeneousmicroservices.micronautservice") .mainClass(MicronautServiceApplication.javaClass) .start() } }
Algunos componentes de una aplicación basada en Micronaut son similares a sus contrapartes en una aplicación Spring Boot, por ejemplo, el código del controlador es el siguiente:
@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() }
El soporte de Kotlin en Micronaut se basa en el
complemento del compilador
kapt (más
información aquí ). El script de ensamblaje se configura de la siguiente manera:
plugins { ... kotlin("kapt") ... } dependencies { kapt("io.micronaut:micronaut-inject-java") ... kaptTest("io.micronaut:micronaut-inject-java") ... }
El siguiente es el contenido del archivo de configuración:
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
La configuración de microservicio también es posible con JSON, propiedades y formatos de archivo Groovy (más detalles
aquí ).
Servicio de arranque de primavera
El marco fue creado para simplificar el desarrollo de aplicaciones utilizando el ecosistema Spring Framework. Esto se logra a través de mecanismos de autoconfiguración al conectar bibliotecas. El siguiente es el código del 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() }
El microservicio está configurado con un archivo YAML:
spring: application: name: spring-boot-service server: port: 8084 application-info: name: ${spring.application.name} framework: name: Spring Boot release-year: 2014
También es posible usar archivos de formato de propiedades para la configuración (más detalles
aquí ).
Lanzamiento
El proyecto funciona en JDK 12, aunque es probable que también esté en la versión 11, solo necesita cambiar el parámetro
jvmTarget
en los scripts de ensamblaje en
jvmTarget
:
withType<KotlinCompile> { kotlinOptions { jvmTarget = "12" ... } }
Antes de iniciar microservicios, debe
instalar Consul e
iniciar el agente, por ejemplo, así:
consul agent -dev
.
Es posible iniciar microservicios desde:
Después de iniciar todos los microservicios en
http://localhost:8500/ui/dc1/services
verá:

Prueba de API
Los resultados de probar la API del servicio Helidon se dan como ejemplo:
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
Devuelve la imagen.
Puede probar una API de microservicio arbitraria utilizando
Postman (una
colección de solicitudes), el
cliente IntelliJ IDEA
HTTP (una
colección de solicitudes), un navegador u otra herramienta. Si usa los dos primeros clientes, debe especificar el puerto del microservicio llamado en la variable correspondiente (en Postman está en el
menú de colección -> Editar -> Variables , y en el Cliente HTTP está en la variable de entorno especificada en
este archivo), y al probar el método 2) La API también necesita especificar el nombre del microservicio "bajo el capó" solicitado. Las respuestas serán similares a las dadas anteriormente.
Comparación de configuraciones de aplicaciones
Tamaño del artefacto
Para preservar la simplicidad de configurar y ejecutar aplicaciones en scripts de ensamblaje, no se excluyeron las dependencias transitivas, por lo que el tamaño del servicio uber-JAR en Spring Boot excede significativamente el tamaño de los análogos en otros marcos (porque cuando se usan iniciadores, no solo se importan las dependencias necesarias; si lo desea, el tamaño puede reducirse significativamente):
Tiempo de lanzamiento
El tiempo de inicio de cada aplicación es inconsistente y cae en alguna "ventana"; La siguiente tabla muestra el tiempo de lanzamiento del artefacto sin especificar ningún parámetro adicional:
Vale la pena señalar que si "limpia" la aplicación Spring Boot de dependencias innecesarias y presta atención a la configuración de la aplicación para que se inicie (por ejemplo, escanee solo los paquetes necesarios y use la inicialización de bin diferido), puede reducir significativamente el tiempo de inicio.
Prueba de carga
Para las pruebas, se utilizaron
Gatling y
un script Scala. El generador de carga y el servicio bajo prueba se ejecutaron en la misma máquina (Windows 10, un procesador quad-core de 3.2 GHz, 24 GB de RAM, SSD). El puerto de este servicio se indica en el script Scala.
Para cada microservicio se determina:
- la cantidad mínima de memoria de almacenamiento
-Xmx
( -Xmx
) requerida para ejecutar un microservicio de trabajo (que responde a las solicitudes) - memoria mínima necesaria para pasar la prueba de carga 50 usuarios * 1000 solicitudes
- memoria de almacenamiento dinámico mínima requerida para pasar la prueba de carga 500 usuarios * 1000 solicitudes
Pasar una prueba de carga significa que el microservicio respondió a todas las solicitudes en cualquier momento.
Vale la pena señalar que todos los microservicios usan el servidor Netty HTTP.
Conclusión
La tarea, la creación de un servicio simple con HTTP API y la capacidad de funcionar en el ISA, se pudo completar en todos los marcos en cuestión. Es hora de hacer un balance y considerar sus ventajas y desventajas.
HelidonEdición estándar- ventajas
- configuración de la aplicación
En todos los aspectos mostró buenos resultados; - "Sin magia"
El marco justificó el principio establecido por los desarrolladores: solo se necesitó una anotación para crear la aplicación ( @JvmStatic
- para la interoperación Java-Kotlin).
- contras
- microframework
Faltan algunos componentes necesarios para el desarrollo industrial, por ejemplo, la inyección de dependencias y la implementación de Service Discovery.
MicroperfilEl microservicio no se implementó en este marco, por lo que solo notaré un par de puntos que conozco:
- ventajas
- Implementación de Eclipse MicroProfile
En esencia, MicroProfile es Java EE optimizado para ISA. Por lo tanto, en primer lugar, obtiene acceso a toda la variedad de API Java EE, incluidas las diseñadas específicamente para el ISA, y en segundo lugar, puede cambiar la implementación de MicroProfile a cualquier otra (Open Liberty, WildFly Swarm, etc.) .
- adicionalmente
- en MicroProfile Starter puede crear un proyecto desde cero con los parámetros necesarios por analogía con herramientas similares para otros marcos (por ejemplo, Spring Initializr ). En el momento de la publicación del artículo, Helidon implementa MicroProfile 1.2, mientras que la última versión de la especificación es 3.0.
Ktor- ventajas
- ligereza
Le permite conectar solo aquellas funciones que se necesitan directamente para completar la tarea; - configuración de la aplicación
Buenos resultados en todos los aspectos.
- contras
- "Afilado" bajo Kotlin, es decir, es posible, pero no necesario, desarrollarse en Java;
- microframework (ver artículo similar para Helidon SE).
- adicionalmente
Por un lado, el concepto de desarrollo de marco no está incluido en los dos modelos de desarrollo de Java más populares (Spring-like (Spring Boot / Micronaut) y Java EE / MicroProfile), lo que puede conducir a:
- un problema con encontrar especialistas;
- más tiempo para completar tareas en comparación con Spring Boot debido a la necesidad de configurar explícitamente la funcionalidad requerida.
Por otro lado, la diferencia con respecto a los "clásicos" Spring y Java EE le permite ver el proceso de desarrollo desde un ángulo diferente, tal vez de manera más consciente.
Micronaut- ventajas
- Mucho
Como se señaló anteriormente, AOT le permite reducir el tiempo de inicio y la memoria consumida por la aplicación en comparación con su contraparte en Spring Boot; - Modelo de desarrollo primaveral
Los programadores con experiencia en desarrollo en primavera no tardarán mucho en dominar este marco; - configuración de la aplicación
Buenos resultados en todos los aspectos; - políglota
Apoyo ciudadano de primera clase para Java, Kotlin, Groovy; tal vez habrá soporte para Scala. En mi opinión, esto puede afectar positivamente el crecimiento de la comunidad. Por cierto, en junio de 2019 Groovy en el ranking de la popularidad de los lenguajes de programación, TIOBE ocupa el 14 ° lugar, despegando del 60 ° año anterior, por lo que se encuentra en un honorable segundo lugar entre los idiomas JVM; - El proyecto Micronaut for Spring también le permite cambiar el tiempo de ejecución de su aplicación Spring Boot existente a Micronaut (con restricciones).
Bota de primavera- ventajas
- madurez de la plataforma y ecosistema
El marco "todos los días". Para la mayoría de las tareas cotidianas, ya existe una solución en el paradigma de programación Spring, es decir, de una manera familiar para muchos programadores. El desarrollo se simplifica por los conceptos de iniciadores y configuraciones automáticas; - la presencia de una gran cantidad de especialistas en el mercado laboral, así como una importante base de conocimiento (que incluye documentación y respuestas a Stack Overflow);
- perspectiva
Creo que muchos estarán de acuerdo en que en el futuro cercano, Spring seguirá siendo el marco de desarrollo líder.
- contras
- configuración de la aplicación
La aplicación en este marco no estaba entre los líderes, sin embargo, algunos parámetros, como se señaló anteriormente, se pueden optimizar de forma independiente. También vale la pena recordar la presencia del proyecto Spring Fu , que se encuentra en desarrollo activo, cuyo uso permite reducir estos parámetros.
También puede resaltar los problemas generales asociados con los nuevos marcos que faltan en Spring Boot:
- ecosistema menos desarrollado;
- un pequeño número de especialistas con experiencia en estas tecnologías;
- mayor tiempo para completar tareas;
- perspectivas oscuras
Los marcos considerados pertenecen a diferentes categorías de peso: Helidon SE y Ktor son
microframes , Spring Boot es un marco de pila completa, Micronaut es más probable que también sea de pila completa; otra categoría es MicroProfile (por ejemplo, Helidon MP). En microframes, la funcionalidad es limitada, lo que puede ralentizar la ejecución de tareas; Para aclarar la posibilidad de implementar esta o aquella funcionalidad sobre la base de cualquier marco de desarrollo, le recomiendo que se familiarice con su documentación.
No me atrevo a juzgar si este o aquel marco se "disparará" en el futuro cercano, por lo tanto, en mi opinión, es mejor continuar monitoreando el desarrollo de eventos usando el marco de desarrollo existente para resolver tareas de trabajo.
Al mismo tiempo, como se mostró en el artículo, los nuevos marcos superan a Spring Boot en los parámetros considerados de las aplicaciones recibidas. Si alguno de estos parámetros es crítico para cualquiera de sus microservicios, es posible que deba prestar atención a los marcos que mostraron los mejores resultados en ellos. Sin embargo, no olvide que Spring Boot, en primer lugar, continúa mejorando y, en segundo lugar, tiene un ecosistema enorme y un número significativo de programadores de Java están familiarizados con él. Hay otros marcos que no están cubiertos en este artículo: Javalin, Quarkus, etc.
Puede ver el código del proyecto en
GitHub . Gracias por su atencion!
PD: Gracias a
artglorin por
ayudarme con este artículo.