前言
在 Java 和 Kotlin 中, 除了使用Spring Boot创建微服务外,还有很多其他的替代方案。
本文,基于这些微服务框架,创建了五个服务,并使用Consul的服务发现模式实现服务间的 相互通信。因此,它们形成了异构微服务架构(Heterogeneous Microservice Architecture, 以下简称 MSA):
本文简要考虑了微服务在各个框架上的实现,更多细节请查看源代码:
“
https://github.com/rkudryashov/heterogeneous-microservices
技术栈:
功能接口(HTTP API):
GET /application-info{?request-to=some-service-name}
GET /application-info/logo
实现方式:
MSA:
使用服务发现模式(在Consul中注册,通过客户端负载均衡的名称请求另一个微服务的HTTP API)
先决条件
从头开始创建应用程序
要基于其中一个框架上生成新项目,你可以使用web starter 或其他选项(例如,构建工具或 IDE):
Helidon服务
该框架是在 Oracle 中创建以供内部使用,随后成为开源。Helidon 非常简单和快捷,它提供了两个版本:标准版(SE)和MicroProfile(MP)。在这两种情况下,服务都是一个常规的 Java SE 程序。(在Helidon上了解更多信息)
Helidon MP 是Eclipse MicroProfile的实现之一,这使得使用许多 API 成为可能,包括 Java EE 开发人员已知的(例如 JAX-RS、CDI等)和新的 API(健康检查、指标、容错等)。在Helidon SE模型中,开发人员遵循“没有魔法”的原则,例如,创建应用程序所需的注解数量较少或完全没有。
Helidon SE被选中用于微服务的开发。因为Helidon SE 缺乏依赖注入的手段,因此为此使用了Koin。
以下代码示例,是包含 main 方法的类。为了实现依赖注入,该类继承自KoinComponent。
首先,Koin 启动,然后初始化所需的依赖并调用
startServer()
方法—-其中创建了一个WebServer类型的对象,应用程序配置和路由设置传递到该对象;
启动应用程序后在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 }
路由配置如下:
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()
该应用程序使用HOCON格式的配置文件:
webserver { port: 8081 } application-info { name: "helidon-service" framework { name: "Helidon SE" release-year: 2019 } }
还可以使用 JSON、YAML 和properties 格式的文件进行配置(在Helidon 配置文档中了解更多信息)。
Ktor服务
该框架是为 Kotlin 编写和设计的。和 Helidon SE 一样,Ktor 没有开箱即用的 DI,所以在启动服务器依赖项之前应该使用 Koin 注入:
val koinModule = module { single { ApplicationInfoService(get(), get()) } single { ApplicationInfoProperties() } single { ServiceClient(get()) } single { Consul.builder().withUrl("https://localhost:8500" ).build() } } fun main(args: Array) { startKoin { modules(koinModule) } val server = embeddedServer(Netty, commandLineEnvironment(args)) server.start(wait = true ) }
应用程序需要的模块在配置文件中指定(HOCON格式;更多配置信息参考Ktor配置文档 ),其内容如下:
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 } }
在 Ktor 和 Koin 中,术语“模块”具有不同的含义。在 Koin 中,模块类似于 Spring 框架中的应用程序上下文。
Ktor的模块是一个用户定义的函数,它接受一个Application类型的对象,可以配置流水线、注册路由、处理请求等:
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" ) } } } }
此代码是配置请求的路由,特别是静态资源logo.png。
下面是基于Round-robin算法结合客户端负载均衡实现服务发现模式的代码:
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 } } } }
主要逻辑在install方法中:在Render请求阶段(在Send阶段之前执行)首先确定被调用服务的名称,然后consulClient请求服务的实例列表,然后通过循环算法定义一个实例正在调用。因此,以下调用成为可能:
fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking { httpClient.get("http://$serviceName /application-info" ) }
Micronaut 服务
Micronaut 由Grails框架的创建者开发,灵感来自使用 Spring、Spring Boot 和 Grails 构建服务的经验。该框架目前支持 Java、Kotlin 和 Groovy 语言。依赖是在编译时注入的,与 Spring Boot 相比,这会导致更少的内存消耗和更快的应用程序启动。主类如下所示:
object MicronautServiceApplication { @JvmStatic fun main (args: Array) { Micronaut.build() .packages("io.heterogeneousmicroservices.micronautservice" ) .mainClass(MicronautServiceApplication.javaClass) .start() } }
基于 Micronaut 的应用程序的某些组件与它们在 Spring Boot 应用程序中的对应组件类似,例如,以下是控制器代码:
@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() }
Micronaut中对 Kotlin 的支持建立在kapt编译器插件的基础上(参考Micronaut Kotlin 指南了解更多详细信息)。
构建脚本配置如下:
plugins { ... kotlin("kapt" ) ... } dependencies { kapt("io.micronaut:micronaut-inject-java:$micronautVersion " ) ... kaptTest("io.micronaut:micronaut-inject-java:$micronautVersion " ) ... }
以下是配置文件的内容:
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和 Groovy 文件格式也可用于配置(参考Micronaut 配置指南查看更多详细信息)。
Quarkus服务
Quarkus是作为一种应对新部署环境和应用程序架构等挑战的工具而引入的,在框架上编写的应用程序将具有低内存消耗和更快的启动时间。此外,对开发人员也很友好,例如,开箱即用的实时重新加载。
Quarkus 应用程序目前没有 main 方法,但也许未来会出现(GitHub 上的问题)。
对于熟悉 Spring 或 Java EE 的人来说,Controller 看起来非常熟悉:
@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() }
如你所见,bean 是通过@Inject注解注入的,对于注入的 bean,你可以指定一个范围,例如:
@ApplicationScoped class ApplicationInfoService( ... ) { ... }
为其他服务创建 REST 接口,就像使用 JAX-RS 和 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
但是它现在缺乏对服务发现 ( Eureka和Consul ) 的内置支持,因为该框架主要针对云环境。因此,在 Helidon 和 Ktor 服务中, 我使用了Java类库方式的Consul 客户端。
首先,需要注册应用程序:
@ApplicationScoped class ConsulRegistrationBean ( @Inject private val consulClient : ConsulClient ) { fun onStart (@Observes event: StartupEvent) { consulClient.register() } }
然后需要将服务的名称解析到其特定位置;
解析是通过从 Consul 客户端获得的服务的位置替换
requestContext
的URI 来实现的:
@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也支持通过properties或 YAML 文件进行配置(参考Quarkus 配置指南了解更多详细信息)。
Spring Boot服务
创建该框架是为了使用 Spring Framework 生态系统,同时有利于简化应用程序的开发。这是通过
auto-configuration
实现的。
以下是控制器代码:
@RestController @RequestMapping (path["applicationinfo" ], produces = [MediaType.APPLICATION_JSON_VALUE]) class ApplicationInfoController ( private val applicationInfoService : ApplicationInfoService ) { @GetMapping fun get (@RequestParam("requestto" ) requestTo: String?): ApplicationInfo = applicationInfoService.get(requestTo) @GetMapping (path = ["/logo" ], produces = [MediaType.IMAGE_PNG_VALUE]) fun getLogo () : ByteArray = applicationInfoService.getLogo() }
微服务由 YAML 文件配置:
spring: application: name: spring-boot-service server: port: 8085 application-info: name: ${spring.application.name} framework: name: Spring Boot release-year: 2014
也可以使用properties文件进行配置(更多信息参考Spring Boot 配置文档)。
启动微服务
在启动微服务之前,你需要安装Consul和 启动代理-例如,像这样:consul agent -dev。
你可以从以下位置启动微服务:
IDE中启动微服务IntelliJ IDEA 的用户可能会看到如下内容:
要启动 Quarkus 服务,你需要启动quarkusDev的Gradle 任务。
console中启动微服务在项目的根文件夹中执行:
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
启动所有微服务后,访问http://localhost:8500/ui/dc1/services,你将看到:
API测试
以Helidon服务的API测试结果为例:
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信息
你可以使用Postman 、IntelliJ IDEA HTTP 客户端 、浏览器或其他工具测试微服务的 API接口 。
不同微服务框架对比
不同微服务框架的新版本发布后,下面的结果可能会有变化;你可以使用此GitHub项目自行检查最新的对比结果 。
程序大小
为了保证设置应用程序的简单性,构建脚本中没有排除传递依赖项,因此 Spring Boot 服务uber-JAR的大小大大超过了其他框架上的类似物的大小(因为使用 starters 不仅导入了必要的依赖项;如果需要,可以通过排除指定依赖来减小大小):
备注:什么是 maven的uber-jar
“