
Il n'y a actuellement pas de pénurie de frameworks pour créer des microservices en Java et Kotlin. L'article traite des points suivants:
Sur cette base, quatre services ont été créés qui peuvent interagir les uns avec les autres via l'API HTTP en utilisant le modèle de découverte de service implémenté à l'aide de
Consul . Ainsi, ils forment une architecture de microservices hétérogène (au niveau du framework) (ci-après dénommée ISA):

Définissez un ensemble d'exigences pour chaque service:
- pile technologique:
- JDK 12;
- Kotlin
- Gradle (Kotlin DSL);
- JUnité 5.
- fonctionnalité (API HTTP):
GET /application-info{?request-to=some-service-name}
Renvoie des informations de base sur le microservice (nom, framework, année de sortie du framework); lors de la spécification dans le paramètre request-to
du nom d'un des quatre microservices à son API HTTP, une requête similaire est exécutée qui renvoie des informations de base;GET /application-info/logo
Renvoie l'image.
- mise en œuvre:
- configuration à l'aide du fichier de configuration;
- Utilisation de l'injection de dépendance
- des tests qui vérifient la fonctionnalité de l'API HTTP.
- ISA:
- en utilisant le modèle de découverte de service (enregistrement auprès de Consul, accès à l'API HTTP d'un autre microservice par son nom à l'aide de l'équilibrage de charge du client);
- formation d'artefact uber-jar.
Ensuite, nous considérons l'implémentation d'un microservice sur chacun des frameworks et comparons les paramètres des applications reçues.
Service Helidon
Le cadre de développement a été créé chez Oracle pour un usage interne, puis est devenu open-source. Il existe deux modèles de développement basés sur ce cadre: Standard Edition (SE) et MicroProfile (MP). Dans les deux cas, le service sera un programme Java SE standard. En savoir plus sur les différences sur
cette page.
En bref, Helidon MP est l'une des implémentations Eclipse
MicroProfile , qui permet d'utiliser de nombreuses API, à la fois connues des développeurs Java EE (par exemple, JAX-RS, CDI), et des plus récentes (Health Check, Metrics, Fault Tolerance etc.). Dans la variante Helidon SE, les développeurs ont été guidés par le principe «No magic», qui s'exprime notamment en moins ou pas d'annotations nécessaires à la création de l'application.
Helidon SE a été sélectionné pour le développement de microservices. Entre autres choses, il manque d'outils pour implémenter l'injection de dépendances, donc
Koin est utilisé pour implémenter les dépendances. Ce qui suit est une classe contenant la méthode principale. Pour implémenter l'injection de dépendances, la classe hérite de
KoinComponent . Koin démarre en premier, puis les dépendances requises sont initialisées et la méthode
startServer()
est appelée, où un objet de type
WebServer est créé, vers lequel les paramètres de configuration et de routage d'application sont précédemment transférés; après le démarrage, la demande est enregistrée au 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())
Le routage est configuré comme suit:
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()
L'application utilise la configuration au format
HOCON :
webserver { port: 8081 } application-info { name: "helidon-service" framework { name: "Helidon SE" release-year: 2019 } }
Il est également possible d'utiliser des fichiers aux formats JSON, YAML et propriétés pour la configuration (plus de détails
ici ).
Service Ktor
Le cadre est écrit en Kotlin. Un nouveau projet peut être créé de plusieurs manières: en utilisant le système de construction,
start.ktor.io ou le plug-in pour IntelliJ IDEA (plus
ici ).
Comme Helidon SE, Ktor n'a pas de DI prêt à l'emploi, donc les dépendances sont implémentées en utilisant Koin avant de démarrer le serveur:
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) }
Les modules requis par l'application sont spécifiés dans le fichier de configuration (seul le format HOCON peut être utilisé; plus d'informations sur la configuration du serveur Ktor
ici ), dont le contenu est présenté ci-dessous:
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 et Koin utilisent le terme «module», qui a des significations différentes. Dans Koin, un module est un analogue du contexte d'application dans Spring Framework. Le module Ktor est une fonction définie par l'utilisateur qui accepte un objet de type
Application et peut configurer un pipeline, définir des fonctionnalités, enregistrer des itinéraires, traiter
demandes, 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") } } } }
Dans cet extrait de code, le routage des demandes est configuré, en particulier, la ressource statique
logo.png
.
Le service Ktor peut contenir des fonctionnalités. Une fonctionnalité est une fonctionnalité qui est intégrée dans un
pipeline de demande-réponse (
DefaultHeaders, Compression et autres dans l'exemple de code ci-dessus). Il est possible d'implémenter vos propres fonctionnalités, par exemple, le code ci-dessous implémente le modèle de découverte de service en combinaison avec l'équilibrage de charge client basé sur l'algorithme 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 logique principale réside dans la méthode d'
install
: pendant la phase de demande de
rendu (qui s'exécute avant la phase d'
envoi ), le nom du service appelé est d'abord déterminé, puis une liste d'instances de ce service est demandée à
consulClient
, après quoi l'instance est déterminée à l'aide de l'algorithme Round-robin. Ainsi, l'appel suivant devient possible:
fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking { httpClient.get<ApplicationInfo>("http://$serviceName/application-info") }
Service Micronaut
Micronaut est développé par les créateurs du framework
Grails et s'inspire de l'expérience des services de construction utilisant Spring, Spring Boot et Grails. Le cadre est un polyglotte prenant en charge Java, Kotlin et Groovy;
il y aura peut-être un soutien pour Scala. L'injection de dépendances est effectuée au stade de la compilation, ce qui réduit la consommation de mémoire et le démarrage de l'application plus rapidement que Spring Boot.
La classe principale a la forme suivante:
object MicronautServiceApplication { @JvmStatic fun main(args: Array<String>) { Micronaut.build() .packages("io.heterogeneousmicroservices.micronautservice") .mainClass(MicronautServiceApplication.javaClass) .start() } }
Certains composants d'une application basée sur Micronaut sont similaires à leurs homologues dans une application Spring Boot, par exemple, le code du contrôleur est le suivant:
@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() }
Le support de Kotlin dans Micronaut est basé sur le
plugin du compilateur
kapt (plus d'
informations ici ). Le script d'assemblage est configuré comme suit:
plugins { ... kotlin("kapt") ... } dependencies { kapt("io.micronaut:micronaut-inject-java") ... kaptTest("io.micronaut:micronaut-inject-java") ... }
Voici le contenu du fichier de configuration:
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 configuration du microservice est également possible avec JSON, les propriétés et les formats de fichier Groovy (plus de détails
ici ).
Service de démarrage de printemps
Le cadre a été créé pour simplifier le développement d'applications à l'aide de l'écosystème Spring Framework. Ceci est réalisé grâce à des mécanismes d'autoconfiguration lors de la connexion des bibliothèques. Voici le code du contrôleur:
@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() }
Le microservice est configuré avec un fichier YAML:
spring: application: name: spring-boot-service server: port: 8084 application-info: name: ${spring.application.name} framework: name: Spring Boot release-year: 2014
Il est également possible d'utiliser des fichiers de format de propriétés pour la configuration (plus de détails
ici ).
Lancement
Le projet fonctionne sur JDK 12, bien qu'il soit probable sur la version 11 également, il vous suffit de modifier le paramètre
jvmTarget
dans les scripts d'assembly en
jvmTarget
:
withType<KotlinCompile> { kotlinOptions { jvmTarget = "12" ... } }
Avant de démarrer les microservices, vous devez
installer Consul et
démarrer l' agent - par exemple, comme ceci:
consul agent -dev
.
Le démarrage des microservices est possible à partir de:
Après avoir démarré tous les microservices sur
http://localhost:8500/ui/dc1/services
vous verrez:

Test d'API
Les résultats des tests de l'API du service Helidon sont donnés à titre d'exemple:
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
Renvoie l'image.
Vous pouvez tester une API de microservice arbitraire à l'aide de
Postman (une
collection de demandes), du
client HTTP IntelliJ IDEA (une
collection de demandes), d'un navigateur ou d'un autre outil. Si vous utilisez les deux premiers clients, vous devez spécifier le port du microservice appelé dans la variable correspondante (dans Postman, il se trouve dans le
menu de collecte -> Modifier -> Variables , et dans HTTP Client, il se trouve dans la variable d'environnement spécifiée dans
ce fichier), et lors du test de la méthode 2) L'API doit également spécifier le nom du microservice «sous le capot» demandé. Les réponses seront similaires à celles données ci-dessus.
Comparaison des paramètres d'application
Taille d'artefact
Afin de préserver la simplicité de configuration et d'exécution des applications dans les scripts d'assemblage, aucune dépendance transitive n'a été exclue, de sorte que la taille du service uber-JAR sur Spring Boot dépasse considérablement la taille des analogues sur d'autres cadres (car lors de l'utilisation de démarreurs, non seulement les dépendances nécessaires sont importées; si vous le souhaitez, la taille peut être considérablement réduite):
Heure de lancement
L'heure de lancement de chaque application est incohérente et tombe dans une «fenêtre»; le tableau ci-dessous montre l'heure de lancement de l'artefact sans spécifier de paramètres supplémentaires:
Il convient de noter que si vous «nettoyez» l'application Spring Boot des dépendances inutiles et faites attention à la configuration de l'application pour qu'elle démarre (par exemple, analysez uniquement les packages nécessaires et utilisez l'initialisation paresseuse), vous pouvez réduire considérablement le temps de démarrage.
Test de charge
Pour les tests,
Gatling et
un script Scala ont été utilisés. Le générateur de charge et le service sous test ont été exécutés sur la même machine (Windows 10, processeur quatre cœurs 3,2 GHz, 24 Go de RAM, SSD). Le port de ce service est indiqué dans le script Scala.
Pour chaque microservice est déterminé:
- mémoire de
-Xmx
minimale ( -Xmx
) requise pour exécuter un microservice fonctionnel (répondant aux demandes) - mémoire de segment minimale requise pour réussir le test de charge 50 utilisateurs * 1 000 requêtes
- mémoire de segment minimale requise pour réussir le test de charge 500 utilisateurs * 1 000 requêtes
La réussite d'un test de charge signifie que le microservice a répondu à toutes les demandes à tout moment.
Il convient de noter que tous les microservices utilisent le serveur HTTP Netty.
Conclusion
La tâche - la création d'un service simple avec l'API HTTP et la possibilité de fonctionner dans l'ISA - a pu être réalisée sur tous les frameworks en question. Il est temps de faire le point et de considérer leurs avantages et leurs inconvénients.
HélidonÉdition standard- avantages
- paramètres d'application
A tous égards, a montré de bons résultats; - "Pas de magie"
Le framework a justifié le principe énoncé par les développeurs: il n'a fallu qu'une seule annotation pour créer l'application ( @JvmStatic
- pour l' @JvmStatic
Java-Kotlin).
- contre
- microframework
Certains composants nécessaires au développement industriel font défaut, par exemple, l'injection de dépendances et l'implémentation de Service Discovery.
MicroprofileLe microservice n'a pas été implémenté sur ce framework, donc je ne noterai que quelques points que je connais:
- avantages
- Implémentation d'Eclipse MicroProfile
Essentiellement, MicroProfile est optimisé pour Java EE pour ISA. Ainsi, d'une part, vous avez accès à toute la variété des API Java EE, y compris celles conçues spécifiquement pour l'ISA, et d'autre part, vous pouvez changer l'implémentation de MicroProfile pour n'importe quelle autre (Open Liberty, WildFly Swarm, etc.) .
- en plus
- sur MicroProfile Starter, vous pouvez créer un projet à partir de zéro avec les paramètres nécessaires par analogie avec des outils similaires pour d'autres frameworks (par exemple, Spring Initializr ). Au moment de la publication de l'article, Helidon implémente MicroProfile 1.2, tandis que la dernière version de la spécification est 3.0.
Ktor- avantages
- légèreté
Vous permet de connecter uniquement les fonctions directement nécessaires pour terminer la tâche; - paramètres d'application
De bons résultats à tous égards.
- contre
- «Sharpened» sous Kotlin, c'est-à-dire qu'il est possible, mais pas nécessaire, de développer en Java;
- microframework (voir article similaire pour Helidon SE).
- en plus
D'une part, le concept de développement de framework n'est pas inclus dans les deux modèles de développement Java les plus populaires (Spring-like (Spring Boot / Micronaut) et Java EE / MicroProfile), ce qui peut conduire à:
- un problème pour trouver des spécialistes;
- temps plus long pour terminer les tâches par rapport à Spring Boot en raison de la nécessité de configurer explicitement les fonctionnalités requises.
D'un autre côté, la dissemblance avec le «classique» Spring et Java EE vous permet de regarder le processus de développement sous un angle différent, peut-être plus consciemment.
Micronaut- avantages
- Aot
Comme indiqué précédemment, AOT vous permet de réduire l'heure de démarrage et la mémoire consommée par l'application par rapport à son homologue sur Spring Boot; - Modèle de développement de type printanier
Les programmeurs ayant une expérience de développement sur Spring ne prendront pas beaucoup de temps pour maîtriser ce cadre; - paramètres d'application
De bons résultats à tous égards; - polyglotte
Support citoyen de première classe pour Java, Kotlin, Groovy; il y aura peut-être un soutien pour Scala. À mon avis, cela peut avoir une incidence positive sur la croissance communautaire. Soit dit en passant, en juin 2019, Groovy dans le classement de la popularité des langages de programmation TIOBE prend la 14e place, décollant de la 60e année plus tôt, se trouvant ainsi à une honorable deuxième place parmi les langages JVM; - Le projet Micronaut for Spring vous permet également de modifier le temps d'exécution de votre application Spring Boot existante en Micronaut (avec restrictions).
Botte de printemps- avantages
- maturité et écosystème de la plateforme
Le cadre «tous les jours». Pour la plupart des tâches quotidiennes, il existe déjà une solution dans le paradigme de programmation Spring, c'est-à-dire d'une manière familière à de nombreux programmeurs. Le développement est simplifié par les concepts de démarreurs et de configurations automatiques; - un grand nombre de spécialistes du marché du travail, ainsi qu'une base de connaissances importante (y compris la documentation et les réponses à Stack Overflow);
- perspective
Je pense que beaucoup conviendront que dans un proche avenir, le printemps restera le principal cadre de développement.
- contre
- paramètres d'application
L'application sur ce framework ne faisait pas partie des leaders, cependant, certains paramètres, comme indiqué précédemment, peuvent être optimisés indépendamment. Il convient également de rappeler la présence du projet Spring Fu , en cours de développement actif, dont l'utilisation permet de réduire ces paramètres.
Vous pouvez également mettre en évidence les problèmes généraux associés aux nouveaux cadres qui manquent à Spring Boot:
- écosystème moins développé;
- un petit nombre de spécialistes ayant une expérience de ces technologies;
- plus de temps pour terminer les tâches;
- perspectives obscures.
Les frameworks considérés appartiennent à différentes catégories de poids: Helidon SE et Ktor sont des
microframes , Spring Boot est un framework full-stack, Micronaut est plus probable aussi full-stack; une autre catégorie est MicroProfile (par exemple Helidon MP). Dans les microframes, la fonctionnalité est limitée, ce qui peut ralentir l'exécution des tâches; Pour clarifier la possibilité d'implémenter telle ou telle fonctionnalité sur la base de tout framework de développement, je vous recommande de vous familiariser avec sa documentation.
Je n'ose pas juger si tel ou tel cadre va "tirer" dans un avenir proche, donc, à mon avis, il est préférable de continuer à surveiller le développement des événements en utilisant le cadre de développement existant pour résoudre les tâches de travail.
Dans le même temps, comme cela a été montré dans l'article, les nouveaux frameworks surpassent Spring Boot par les paramètres considérés des applications reçues. Si l'un de ces paramètres est critique pour l'un de vos microservices, vous devrez peut-être faire attention aux cadres qui ont donné les meilleurs résultats. Cependant, n'oubliez pas que Spring Boot, d'une part, continue de s'améliorer, et d'autre part, il a un écosystème énorme et un nombre important de programmeurs Java le connaissent. Il existe d'autres cadres qui ne sont pas traités dans cet article: Javalin, Quarkus, etc.
Vous pouvez afficher le code du projet sur
GitHub . Merci de votre attention!
PS: Merci à
artglorin pour sa contribution à cet article.