Not One Spring Boot: An Overview of Alternatives



    There is currently no shortage of frameworks for creating microservices in Java and Kotlin. The article discusses the following:
    TitleVersionYear of first releaseDeveloper
    Helidon se1.1.12019Oracle
    Ktor1.2.12018Jetbrains
    Micronaut1.1.32018Object computing
    Spring boot2.1.52014Pivotal

    Based on them, four services have been created that can interact with each other through the HTTP API using the Service Discovery pattern implemented using Consul . Thus, they form a heterogeneous (at the framework level) microservice architecture (hereinafter referred to as ISA):



    We define a set of requirements for each service:

    • technology stack:
      • JDK 12;
      • Kotlin
      • Gradle (Kotlin DSL);
      • JUnit 5.

    • functionality (HTTP API):
      • GET /application-info{?request-to=some-service-name}
        Returns some basic information about the microservice (name, framework, year of release of the framework); when specifying the request-toname of one of the four microservices in its HTTP API, a similar request is executed that returns basic information;
      • GET /application-info/logo
        Returns the image.

    • implementation:
      • setup using the configuration file;
      • Using dependency injection
      • tests that verify the functionality of the HTTP API.

    • ISA:
      • using the Service Discovery pattern (registering with Consul, accessing the HTTP API of another microservice by its name using client load balancing);
      • uber-jar artifact formation.


    Next, we consider the implementation of a microservice on each of the frameworks and compare the parameters of the received applications.

    Helidon service


    The development framework was created at Oracle for internal use, subsequently becoming open-source. There are two development models based on this framework: Standard Edition (SE) and MicroProfile (MP). In both cases, the service will be a regular Java SE program. Learn more about the differences on this page.

    In short, Helidon MP is one of the Eclipse MicroProfile implementations , which makes it possible to use many APIs, both previously known to Java EE developers (for example, JAX-RS, CDI), and newer ones (Health Check, Metrics, Fault Tolerance etc.). In the Helidon SE variant, the developers were guided by the principle of “No magic”, which is expressed, in particular, in less or no annotations necessary to create the application.

    Helidon SE was selected for microservice development. Among other things, it lacks tools for implementing Dependency Injection, so Koin is used to implement dependencies . The following is a class containing the main method. To implement Dependency Injection, the class inherits from KoinComponent . Koin starts first, then the required dependencies are initialized, and a method is called startServer()where an object of the WebServer type is created , to which the application configuration and routing settings are transferred; after start the application is registered in Consul:

    object HelidonServiceApplication : KoinComponent {
       @JvmStatic
       fun main(args: Array) {
           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())
           // register in Consul
           consulClient.agentClient().register(createConsulRegistration(serviceName, ws.port()))
       }
       return server
    }

    Routing is configured as follows:

    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()

    The application uses the config in the HOCON format :

    webserver {
     port: 8081
    }
    application-info {
     name: "helidon-service"
     framework {
       name: "Helidon SE"
       release-year: 2019
     }
    }

    It is also possible to use files in JSON, YAML and properties formats for configuration (more details here ).

    Ktor service


    The framework is written in Kotlin. A new project can be created in several ways: using the build system, start.ktor.io or the plug-in for IntelliJ IDEA (more here ).

    Like Helidon SE, Ktor does not have a DI out of the box, so dependencies are implemented using Koin before starting the server:

    val koinModule = module {
       single { ApplicationInfoService(get(), get()) }
       single { ApplicationInfoProperties() }
       single { ServiceClient(get()) }
       single { Consul.builder().withUrl("http://localhost:8500").build() }
    }
    fun main(args: Array) {
       startKoin {
           modules(koinModule)
       }
       val server = embeddedServer(Netty, commandLineEnvironment(args))
       server.start(wait = true)
    }

    Modules required by the application are specified in the configuration file (it is possible to use only the HOCON format; more about configuring the Ktor server here ), the contents of which are presented below:

    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 and Koin use the term “module”, which has different meanings. In Koin, a module is an analog of the application context in the Spring Framework. Ktor Module - a user defined function that accepts an object of type Application and may perform Pipeline configuration, installation of features ( features ), registration of routes, processing
    requests, 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")
               }
           }
       }
    }

    In this code fragment, routing of requests is configured, in particular, a static resource logo.png.

    Ktor service may contain features. A feature is a functionality that is embedded in a request-response pipeline ( DefaultHeaders, Compression, and others in the code example above). It is possible to implement your own features, for example, the code below implements the Service Discovery pattern in combination with client load balancing based on the Round-robin algorithm:

    class ConsulFeature(private val consulClient: Consul) {
       class Config {
           lateinit var consulClient: Consul
       }
       companion object Feature : HttpClientFeature {
           var serviceInstanceIndex: Int = 0
           override val key = AttributeKey("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
               }
           }
       }
    }

    The main logic is in the method install: during the Render request phase (which is performed before the Send phase ), the name of the service being called is first determined, then a consulClientlist of instances of this service is requested, and then the instance called using the Round-robin algorithm is called. Thus, the following call becomes possible:

    fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking {
       httpClient.get("http://$serviceName/application-info")
    }


    Micronaut service


    Micronaut is developed by the creators of the Grails framework and is inspired by the experience of building services using Spring, Spring Boot and Grails. The framework is a polyglot supporting Java, Kotlin and Groovy; perhaps there will be support for Scala. Dependency injection is carried out at the compilation stage, which leads to less memory consumption and faster application startup compared to Spring Boot.

    The main class has the following form:

    object MicronautServiceApplication {
       @JvmStatic
       fun main(args: Array) {
           Micronaut.build()
               .packages("io.heterogeneousmicroservices.micronautservice")
               .mainClass(MicronautServiceApplication.javaClass)
               .start()
       }
    }

    Some components of a Micronaut-based application are similar to their counterparts in a Spring Boot application, for example, the controller code is as follows:

    @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()
    }

    Kotlin support in Micronaut is based on the kapt compiler plugin (more info here ). The assembly script is configured as follows:

    plugins {
       ...
       kotlin("kapt")
       ...
    }
    dependencies {
       kapt("io.micronaut:micronaut-inject-java")
       ...
       kaptTest("io.micronaut:micronaut-inject-java")
       ...
    }

    The following is the contents of the configuration file:

    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 

    Microservice configuration is also possible with JSON, properties, and Groovy file formats (more details here ).

    Spring boot service


    The framework was created to simplify the development of applications using the Spring Framework ecosystem. This is achieved through autoconfiguration mechanisms when connecting libraries. The following is the controller code:

    @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()
    }

    The microservice is configured with a YAML file:

    spring:
     application:
       name: spring-boot-service
    server:
     port: 8084
    application-info:
     name: ${spring.application.name}
     framework:
       name: Spring Boot
       release-year: 2014

    It is also possible to use properties format files for configuration (more details here ).

    Launch


    The project works on JDK 12, although it is likely on version 11 too, you only need to change the parameter in the assembly scripts accordingly jvmTarget:

    withType {
       kotlinOptions {
           jvmTarget = "12"
           ...
       }
    }

    Before starting mikroservisov need to install the Consul and run the agent - for example, as follows: consul agent -dev.

    Starting microservices is possible from:

    • IDE
      IntelliJ IDEA users can see something like the following:

    • console
      To do this, go to the project folder and sequentially execute:

      java -jar helidon-service/build/libs/helidon-service-all.jar
      java -jar ktor-service/build/libs/ktor-service-all.jar
      java -jar micronaut-service/build/libs/micronaut-service-all.jar
      java -jar spring-boot-service/build/libs/spring-boot-service.jar


    After starting all microservices on, http://localhost:8500/ui/dc1/services you will see:



    API Testing


    The results of testing the Helidon service API are given as an example:

    1. GET http://localhost:8081/application-info

      {
        "name": "helidon-service",
        "framework": {
      	"name": "Helidon SE",
      	"releaseYear": 2019
        },
        "requestedService": null
      }
    2. 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
        }
      }
    3. GET http://localhost:8081/application-info/logo

      Returns the image.

    You can test an arbitrary microservice API using Postman (a collection of requests), IntelliJ IDEA HTTP client (a collection of requests), a browser, or another tool. If you use the first two clients, you need to specify the port of the called microservice in the corresponding variable (in Postman it is in the collection menu -> Edit -> Variables , and in HTTP Client it is in the environment variable specified in this file), and when testing method 2) The API also needs to specify the name of the requested “under the hood” microservice. The answers will be similar to those given above.

    Application Settings Comparison



    Artifact size


    In order to preserve the simplicity of configuring and running applications in assembly scripts, no transitive dependencies were excluded, so the size of the uber-JAR service on Spring Boot significantly exceeds the size of analogs on other frameworks (because when using starters, not only the necessary dependencies are imported; if desired, the size can be significantly reduced):
    MicroserviceArtifact Size, MB
    Helidon service16.6
    Ktor service20.9
    Micronaut service16.5
    Spring boot service42.7


    Launch time


    The launch time of each application is inconsistent and falls into some “window”; the table below shows the launch time of the artifact without specifying any additional parameters:
    MicroserviceStart time, seconds
    Helidon service2.2
    Ktor service1.4
    Micronaut service4.0
    Spring boot service10,2

    It is worth noting that if you “clean” the Spring Boot application from unnecessary dependencies and pay attention to configuring the application to start (for example, scan only the necessary packages and use lazy bin initialization), you can significantly reduce the startup time.

    Stress Testing


    For testing, Gatling and a Scala script were used . The load generator and the service under test were run on the same machine (Windows 10, a quad-core 3.2 GHz processor, 24 GB RAM, SSD). The port of this service is indicated in the Scala script.

    For each microservice is determined:

    • the minimum amount of heap-memory ( -Xmx) required to run a workable (responding to requests) microservice
    • minimum heap memory required to pass the load test 50 users * 1000 requests
    • minimum heap memory required to pass the load test 500 users * 1000 requests

    Passing a load test means that the microservice responded to all requests at any time.
    MicroserviceThe minimum amount of heap-memory, MB
    To start the serviceFor load 50 * 1000For load 500 * 1000
    Helidon service99eleven
    Ktor serviceeleveneleventhirteen
    Micronaut servicethirteenthirteen17
    Spring boot service222325

    It is worth noting that all microservices use the Netty HTTP server.

    Conclusion


    The task - the creation of a simple service with HTTP API and the ability to function in the ISA - was able to be completed on all the frameworks in question. It's time to take stock and consider their pros and cons.

    Helidon

    Standard Edition
    • pros
      • application parameters
        In all respects, it showed good results;
      • “No magic”
        The framework justified the principle stated by the developers: it took only one annotation to create the application ( @JvmStatic- for Java-Kotlin interope).
    • minuses
      • microframework
        Some of the components necessary for industrial development are missing out of the box, for example, dependency injection and implementation of Service Discovery.

    MicroProfile
    Microservice was not implemented on this framework, so I’ll only note a couple of points I know:

    • pros
      • implementation of Eclipse MicroProfile
        In fact, MicroProfile is Java EE optimized for ISA. Thus, firstly, you get access to the entire variety of Java EE APIs, including those designed specifically for the ISA, and secondly, you can change the MicroProfile implementation to any other (Open Liberty, WildFly Swarm, etc.) .
    • additionally
      • on MicroProfile Starter you can create a project from scratch with the necessary parameters by analogy with similar tools for other frameworks (for example, Spring Initializr ). At the time of publication of the article, Helidon implements MicroProfile 1.2, while the latest version of the specification is 3.0.


    Ktor

    • pros
      • легковесность
        Позволяет подключать только те функции, которые непосредственно нужны для выполнения поставленной задачи;
      • параметры приложения
        Хорошие результаты по всем параметрам.
    • минусы
      • “заточен” под Kotlin, то есть, разрабатывать на Java, вроде, можно, но не нужно;
      • микрофреймворк (см. аналогичный пункт для Helidon SE).
    • дополнительно
      С одной стороны, концепция разработки на фреймворке не входит в две наиболее популярных модели разработки на Java (Spring-подобную (Spring Boot/Micronaut) и Java EE/MicroProfile), что может привести к:

      • проблеме с поиском специалистов;
      • увеличению времени на выполнение задач по сравнению со Spring Boot из-за необходимости явного конфигурирования требуемой функциональности.

      С другой, непохожесть на “классические” Spring и Java EE позволяет взглянуть на процесс разработки под другим углом, возможно, более осознанно.


    Micronaut

    • плюсы
      • AOT
        Как ранее было отмечено, AOT позволяет уменьшить время старта и потребляемую приложением память по сравнению с аналогом на Spring Boot;
      • Spring-подобная модель разработки
        У программистов с опытом разработки на Spring не займёт много времени освоение этого фреймворка;
      • параметры приложения
        Хорошие результаты по всем параметрам;
      • полиглот
        Поддержка на уровне first-class citizen языков Java, Kotlin, Groovy; возможно, будет поддержка Scala. На мой взгляд, это может положительно повлиять на рост сообщества. К слову, на июнь 2019 Groovy в рейтинге популярности языков программирования TIOBE занимает 14-е место, взлетев с 60-го годом ранее, таким образом, находясь на почётном втором месте среди JVM-языков;
      • проект Micronaut for Spring позволяет в том числе изменить среду выполнения имеющегося Spring Boot приложения на Micronaut (с ограничениями).


    Spring Boot

    • плюсы
      • зрелость платформы и экосистема
        Фреймворк “на каждый день”. Для бОльшей части повседневных задач уже есть решение в парадигме программирования Spring, т. е. привычным для многих программистов способом. Разработку упрощают концепции стартеров и автоконфигураций;
      • наличие большого количества специалистов на рынке труда, а также значительная база знаний (включая документацию и ответы на Stack Overflow);
      • перспектива
        Думаю, многие согласятся, что в ближайшем будущем Spring останется лидирующим каркасом разработки.
    • минусы
      • application parameters The application
        on this framework was not among the leaders, however, some parameters, as noted earlier, can be optimized independently. It is also worth recalling the presence of the Spring Fu project , which is under active development , the use of which allows to reduce these parameters.

    You can also highlight the general problems associated with the new frameworks that are missing from Spring Boot:

    • less developed ecosystem;
    • a small number of specialists with experience with these technologies;
    • longer time to complete tasks;
    • obscure prospects.

    The considered frameworks belong to different weight categories: Helidon SE and Ktor are microframes , Spring Boot is a full-stack framework, Micronaut is more likely also full-stack; another category is MicroProfile (e.g. Helidon MP). In microframes, functionality is limited, which can slow down the execution of tasks; To clarify the possibility of implementing this or that functionality on the basis of any development framework, I recommend that you familiarize yourself with its documentation.

    I don’t dare to judge whether this or that framework will “shoot” in the near future, therefore, in my opinion, it’s better to continue to monitor the development of events using the existing development framework for solving work tasks.

    At the same time, as was shown in the article, the new frameworks outperform Spring Boot by the considered parameters of the received applications. If any of these parameters are critical for any of your microservices, then you may need to pay attention to the frameworks that showed the best results on them. However, do not forget that Spring Boot, firstly, continues to improve, and secondly, it has a huge ecosystem and a significant number of Java programmers are familiar with it. There are other frameworks that are not covered in this article: Javalin, Quarkus, etc.

    You can familiarize yourself with the project code on GitHub . Thank you for attention!

    PS: Thanks to artglorin for helping with this article.

    Also popular now: