Microservice Framework:Who Else Can You Choose if You Don't Use Spring Boot

Microservice framework introduction

Microservice framework: Who else can you choose if you don't use Spring Boot

Foreword
Microservice framework.In Java and Kotlin , there are many other alternatives besides using Spring Boot to create microservices .

Microservice framework.In this paper, based on these microservice frameworks, five services are created, and the service discovery mode of Consul is used to realize the mutual communication between services. Therefore, they form a heterogeneous microservice architecture (Heterogeneous Microservice Architecture, hereinafter referred to as MSA):

This article briefly considers the implementation of microservices on various frameworks (see the source code for more details : https://github.com/rkudryashov/heterogeneous-microservices )

•Technology stack :
•JDK 13
•Kotlin
•Gradle (Kotlin DSL)
•JUnit 5
•Functional interface (HTTP API):
•GET /application-info{?request-to=some-service-name}
o-- Return some basic information about the microservice (name, framework, release year)
•GET /application-info/logo
o-- return logo information
•Method to realize:
•Configuration using text files
•Use dependency injection
•HTTP API
•MSA:
•the HTTP API of another microservice by the name of the client load balancer )
•Build an uber-JAR

prerequisites
•JDK 13
•Consul

Create an application from scratch
To generate a new project based on one of the frameworks, you can use the web starter or other options (eg, build tools or IDE):

Microservice.Helidon Services


The framework was created in Oracle for internal use and later became open source. Helidon is very simple and fast, and it comes in two versions: Standard Edition (SE) and MicroProfile (MP). In both cases, the service is a regular Java SE program. ( Learn more on Helidon )
Helidon MP is one of the implementations of Eclipse MicroProfile , which makes it possible to use many APIs, both known to Java EE developers (eg JAX-RS, CDI, etc.) and new (health checks, metrics, fault tolerance, etc.). In the Helidon SE model, developers follow the principle of "no magic", for example, creating applications with fewer or no annotations at all.

Microservice framework.Helidon SE was selected for the development of microservices . Because Helidon SE lacks the means of dependency injection, Koin is used for this purpose .
The following code example is a class that contains a main method. To implement dependency injection, this class inherits from KoinComponent .

Microservice framework.First, Koin starts, then initializes the required dependencies and calls the startServer () method - which creates an object of type WebServer to which application configuration and routing settings are passed;
Register with Consul after starting the application:
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
}
路由配置如下:
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 configuration files in HOCON format:
webserver {
port: 8081
}

application-info {
name: " helidon -service "
framework {
name: " Helidon SE"
release-year: 2019
}
}
Configuration can also be done using JSON, YAML, and properties formatted files ( learn more in the Helidon configuration documentation ).

Ktor service
The framework is written and designed for Kotlin. Like Helidon SE, Ktor doesn't have DI out of the box, so Koin injection should be used before starting server dependencies :
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)
}
The modules required by the application are specified in the configuration file (HOCON format; refer to the Ktor configuration documentation for more configuration information ), and the content is as follows:
ktor {
deployment {
host = localhost
port = 8082
environment = prod
// for dev purpose
autoreload = true
watch = [io.heterogeneousmicroservices.ktorservice]
}
application {
modules = [io.heterogeneousmicroservices.ktorservice.module.KtorServiceApplicationModuleKt.module]
}
}

application-info {
name: "ktor-service"
framework {
name: "Ktor"
release-year: 2018
}
}
In Ktor and Koin , the term "module" has different meanings.
In Koin , modules are analogous to application contexts in the Spring framework. A Ktor module is a user-defined function that accepts an object of type Application, which can configure pipelines, register routes, process 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")
}
}
}
}
This code is to configure the routing of requests, specifically the static resource logo.png.
The following is the code to implement the service discovery mode based on the Round-robin algorithm combined with client load balancing:
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 install method: in the Render request phase (executed before the Send phase), first determine the name of the called service, then consulClient requests a list of instances of the service, and then defines an instance that is being called through a round-robin algorithm. Therefore, the following calls are possible:
fun getApplicationInfo ( serviceName : String): ApplicationInfo = runBlocking {
httpClient.get("http://$serviceName/application-info")
}

Microservice.Micronaut Services


Grails framework, Micronaut was HYPERLINK "https://grails.org/" "_blank" inspired by experience building services with Spring, Spring Boot, and Grails. The framework currently supports Java, Kotlin and Groovy languages. Dependencies are injected at compile time, which results in less memory consumption and faster application startup compared to Spring Boot.
The main class looks like this:
object MicronautServiceApplication {

@JvmStatic _
fun main( args : Array) {
Micronaut.build ()
.packages (" io.heterogeneousmicroservices.micronautservice ")
.mainClass ( MicronautServiceApplication.javaClass ) _
.start ()
}
}
Some components of Micronaut-based applications are similar to their counterparts in Spring Boot applications, for example, here is the controller code:
@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 (refer to the Micronaut Kotlin guide for more details).
The build script is configured as follows:
plugins {
...
kotlin (" kapt ")
...
}

dependencies {
kapt (" io.micronaut :micronaut-inject-java :$ micronautVersion ")
...
kaptTest (" io.micronaut :micronaut-inject-java :$ micronautVersion ")
...
}
Here is the content 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
JSON, properties and Groovy file formats can also be used for configuration (refer to the Micronaut Configuration Guide for more details).

Quarkus Services
Quarkus was introduced as a tool to address challenges such as new deployment environments and application architectures, applications written on the framework will have low memory consumption and faster startup times. Also, developer friendly, e.g. live reloading out of the box.
Quarkus apps don't currently have a main method, but maybe in the future ( issue on GitHub ).
For those familiar with Spring or Java EE, the Controller will look very familiar:
@Path("/application-info")
@Produces ( MediaType.APPLICATION_JSON )
@Consumes ( MediaType.APPLICATION_JSON )
class ApplicationInfoResource (
@Inject private val applicationInfoService : ApplicationInfoService
) {

@GET
fun get(@QueryParam("request-to") requestTo: String?): Response =
Response.ok(applicationInfoService.get(requestTo)).build()

@GET
@Path("/logo")
@Produces("image/png")
fun logo(): Response = Response.ok(applicationInfoService.getLogo()).build()
}
As you can see, beans are injected via @Inject annotation, for injected beans you can specify a scope, for example:
@ApplicationScoped _
class ApplicationInfoService (
...
) {
...
}
Creating REST interfaces for other services is as easy as creating interfaces using JAX-RS and MicroProfile :
@ApplicationScoped _
@ Path( "/")
interface ExternalServiceClient {
@GET
@Path("/application-info")
@Produces("application/json")
fun getApplicationInfo(): ApplicationInfo
}

@RegisterRestClient(baseUri = "http://helidon-service")
interface HelidonServiceClient : ExternalServiceClient

@RegisterRestClient(baseUri = "http://ktor-service")
interface KtorServiceClient : ExternalServiceClient

@RegisterRestClient(baseUri = "http://micronaut-service")
interface MicronautServiceClient : ExternalServiceClient

@RegisterRestClient(baseUri = "http://quarkus-service")
interface QuarkusServiceClient : ExternalServiceClient

@RegisterRestClient(baseUri = "http://spring-boot-service")
interface SpringBootServiceClient : ExternalServiceClient
But it currently lacks built-in support for service discovery ( Eureka and Consul ) as the framework is mainly aimed at cloud environments. Therefore, in the Helidon and Ktor services, I use the Java class library style Consul client .
First, the application needs to be registered:
@ApplicationScoped _
class ConsulRegistrationBean (
@Inject private val consulClient : ConsulClient
) {

fun onStart ( @Observes event: StartupEvent ) {
consulClient.register ()
}
}
The name of the service then needs to be resolved to its specific location;
requestContext 's URI with the location of the service obtained from the Consul client :
@Provider
@ApplicationScoped _
class ConsulFilter (
@Inject private val consulClient: ConsulClient
) : ClientRequestFilter {

override fun filter(requestContext: ClientRequestContext) {
val serviceName = requestContext.uri.host
val serviceInstance = consulClient.getServiceInstance(serviceName)
val newUri: URI = URIBuilder(URI.create(requestContext.uri.toString()))
.setHost(serviceInstance.address)
.setPort(serviceInstance.port)
.build()

requestContext.uri = newUri
}
}
Quarkus also supports configuration via properties or YAML files (see the Quarkus Configuration Guide for more details).

Microservice.Spring Boot service


The framework was created to use the Spring Framework ecosystem while helping to simplify application development. This is achieved through auto-configuration.
Here is the controller code:
@RestController _
@RequestMapping ( path = ["application-info"], produces = [ MediaType.APPLICATION_JSON_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 ()
}

Microservices are configured by YAML files:
spring:
application:
name: spring-boot-service

server:
port: 8085

application-info:
name: ${spring.application.name}
framework:
name: Spring Boot
release-year: 2014
It can also be configured using a properties file (see the Spring Boot configuration documentation for more information ).

Start microservices
Before starting the microservice , you need to install Consul and start the agent - for example, like this: consul agent -dev.
You can start microservices from:

To start the Quarkus service, you need to start the quarkusDev Gradle task.
•the microservice in the console and execute it in the root folder of the project:
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 quarkus -service/build/quarkus-service-1.0.0-runner.jar
java -jar spring-boot-service/build/libs/spring-boot-service.jar
starting all microservices , visit http://localhost:8500/ui/dc1/services and you will see:

API testing
Take the API test results of the Helidon service as an example:
•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返回logo信息
You can use Postman , IntelliJ IDEA HTTP client , browser or other tools to test the API interface of the microservice .

different microservice frameworks
The results below may change as new versions of different microservice frameworks are released; you can check the latest comparison for yourself using this GitHub project .

program size
To guarantee simplicity of setting up the application, transitive dependencies are not excluded in the build script, so the size of the Spring Boot service uber-JAR greatly exceeds the size of its analogs on other frameworks (because using starters not only imports the necessary dependencies; If needed, the size can be reduced by excluding specified dependencies):
Remark: what is maven's uber-jar
In some maven documentation we will find the term "uber-jar", many people are confused after seeing it. In fact, in many programming languages, super is called uber (because super may be a keyword), which became popular in the 1980s, for example, superman is called uberman . Therefore, uber-jar is literally a super-jar. Such a jar not only contains the classes in its own code, but also contains some third-party dependent jars, that is, its own code and its dependent jars are all packaged in one jar It is inside, so it is called super-jar vividly, and the origin of uber-jar is like this.

Related Articles

Explore More Special Offers

  1. Short Message Service(SMS) & Mail Service

    50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00