前言 在 Java 和 Kotlin 中, 除了使用Spring Boot創建微服務外,還有很多其他的替代方案。 | 名稱 | 開發商 | | : : | : : | | Helidon SE | 甲骨文 | | Ktor | JetBrains | | Micronaut | Object Com ...
前言
在 Java 和 Kotlin 中, 除了使用Spring Boot創建微服務外,還有很多其他的替代方案。
名稱 | 開發商 |
---|---|
Helidon SE | 甲骨文 |
Ktor | JetBrains |
Micronaut | Object Computing |
Quarkus | Red Hat |
Spring Boot | Pivotal |
本文,基於這些微服務框架,創建了五個服務,並使用Consul的服務發現模式實現服務間的 相互通信。因此,它們形成了異構微服務架構(Heterogeneous Microservice Architecture, 以下簡稱 MSA):
本文簡要考慮了微服務在各個框架上的實現(更多細節請查看源代碼:https : //github.com/rkudryashov/heterogeneous-microservices)
- 技術棧:
- JDK 13
- Kotlin
- Gradle (Kotlin DSL)
- JUnit 5
- 功能介面(HTTP API):
- GET /application-info{?request-to=some-service-name}
- GET /application-info/logo
- 實現方式:
- 使用文本文件的配置方式
- 使用依賴註入
- HTTP API
- MSA:
- 使用服務發現模式(在Consul中註冊,通過客戶端負載均衡的名稱請求另一個微服務的HTTP API)
- 構建一個 uber-JAR
先決條件
- JDK 13
- Consul
從頭開始創建應用程式
要基於其中一個框架上生成新項目,你可以使用web starter 或其他選項(例如,構建工具或 IDE):
名稱 | 支持的開發語言 |
---|---|
Helidon | Java,Kotlin |
Ktor | Kotlin |
Micronaut | Groovy、Java、Kotlin |
Quarkus | Java、Kotlin、Scala |
Spring Boot | Groovy、Java、Kotlin |
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<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())
// 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<String>) {
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<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
}
}
}
}
主要邏輯在install方法中:在Render請求階段(在Send階段之前執行)首先確定被調用服務的名稱,然後consulClient請求服務的實例列表,然後通過迴圈演算法定義一個實例正在調用。因此,以下調用成為可能:
fun getApplicationInfo(serviceName: String): ApplicationInfo = runBlocking {
httpClient.get<ApplicationInfo>("http://$serviceName/application-info")
}
Micronaut 服務
Micronaut 由Grails框架的創建者開發,靈感來自使用 Spring、Spring Boot 和 Grails 構建服務的經驗。該框架目前支持 Java、Kotlin 和 Groovy 語言。依賴是在編譯時註入的,與 Spring Boot 相比,這會導致更少的記憶體消耗和更快的應用程式啟動。
主類如下所示:
object MicronautServiceApplication {
@JvmStatic
fun main(args: Array<String>) {
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實現的。
Spring Boot 基礎就不介紹了,推薦下這個實戰教程:
https://github.com/javastacks/javastack
以下是控制器代碼:
@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()
}
微服務由 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
在maven的一些文檔中我們會發現 “uber-jar”這個術語,許多人看到後感到困惑。其實在很多編程語言中會把super叫做uber (因為super可能是關鍵字), 這是上世紀80年代開始流行的,比如管superman叫uberman。所以uber-jar從字面上理解就是super-jar,這樣的jar不但包含自己代碼中的class ,也會包含一些第三方依賴的jar,也就是把自身的代碼和其依賴的jar全打包在一個jar裡面了,所以就很形象的稱其為super-jar ,uber-jar來歷就是這樣的。
微服務 | 程式大小(MB) |
---|---|
Helidon服務 | 17,3 |
Ktor服務 | 22,4 |
Micronaut 服務 | 17,1 |
Quarkus服務 | 24,4 |
Spring Boot服務 | 45,2 |
啟動時長
每個應用程式的啟動時長都是不固定的:
微服務 | 開始時間(秒) |
---|---|
Helidon服務 | 2,0 |
Ktor服務 | 1,5 |
Micronaut 服務 | 2,8 |
Quarkus服務 | 1,9 |
Spring Boot服務 | 10,7 |
值得註意的是,如果你將 Spring Boot 中不必要的依賴排除,並註意設置應用的啟動參數(例如,只掃描必要的包並使用 bean 的延遲初始化),那麼你可以顯著地減少啟動時間。
記憶體使用情況
對於每個微服務,確定了以下內容:
- 通過-Xmx參數,指定微服務所需的堆記憶體大小
- 通過負載測試服務健康的請求(能夠響應不同的請求)
- 通過負載測試50 個用戶 * 1000 個的請求
- 通過負載測試500 個用戶 * 1000 個的請求
堆記憶體只是為應用程式分配的總記憶體的一部分。例如,如果要測量總體記憶體使用情況,可以參考本指南。
對於負載測試,使用了Gatling和Scala腳本 。
1、負載生成器和被測試的服務在同一臺機器上運行(Windows 10、3.2 GHz 四核處理器、24 GB RAM、SSD)。
2、服務的埠在 Scala 腳本中指定。
3、通過負載測試意味著微服務已經響應了所有時間的所有請求。
微服務 | 堆記憶體大小(MB) | 堆記憶體大小(MB) | 堆記憶體大小(MB) |
---|---|---|---|
對於健康服務 | 對於 50 * 1000 的負載 | 對於 500 * 1000 的負載 | |
Helidon服務 | 11 | 9 | 11 |
Ktor服務 | 13 | 11 | 15 |
Micronaut 服務 | 17 | 15 | 19 |
Quarkus服務 | 13 | 17 | 21 |
Spring Boot服務 | 18 | 19 | 23 |
需要註意的是,所有微服務都使用 Netty HTTP 伺服器。
結論
通過上文,我們所需的功能——一個帶有 HTTP API 的簡單服務和在 MSA 中運行的能力——在所有考慮的框架中都取得了成功。
是時候開始盤點並考慮他們的利弊了。
Helidon標準版
優點
創建的應用程式,只需要一個註釋(@JvmStatic)
缺點
開發所需的一些組件缺少開箱即用(例如,依賴註入和與服務發現伺服器的交互)
Helidon MicroProfile
微服務還沒有在這個框架上實現,所以這裡簡單說明一下。
優點
1、Eclipse MicroProfile 實現
2、本質上,MicroProfile 是針對 MSA 優化的 Java EE。因此,首先你可以訪問各種 Java EE API,包括專門為 MSA 開發的 API,其次,你可以將 MicroProfile 的實現更改為任何其他實現(例如:Open Liberty、WildFly Swarm 等)
Ktor
優點
1、輕量級的允許你僅添加執行任務直接需要的那些功能
2、應用參數所有參數的良好結果
缺點
1、依賴於Kotlin,即用其他語言開發可能是不可能的或不值得的
2、微框架:參考Helidon SE
3、目前最流行的兩種 Java 開發模型(Spring Boot/Micronaut)和 Java EE/MicroProfile)
4、中沒有包含該框架,這會導致:
難以尋找專家
由於需要顯式配置所需的功能,因此與 Spring Boot 相比,執行任務的時間有所增加
Micronaut
優點
1、AOT如前所述,與 Spring Boot 上的模擬相比,AOT 可以減少應用程式的啟動時間和記憶體消耗
2、類Spring開發模式有 Spring 框架經驗的程式員不會花太多時間來掌握這個框架
3、Micronaut for Spring可以改變現有的Spring Boot應用程式的執行環境到Micronaut中(有限制)
Quarkus
優點
1、Eclipse MicroProfile 的實現
2、該框架為多種 Spring 技術提供了相容層:DI、 Web、Security、Data JPA
Spring Boot
優點
1、平臺成熟度和生態系統對於大多數日常任務,Spring的編程範式已經有瞭解決方案,也是很多程式員習慣的方式。此外,starter和auto-configuration的概念簡化了開發
2、專家多,文檔詳細
我想很多人都會同意 Spring 在不久的將來仍將是 Java/Kotlin開發領域領先的框架。
缺點
- 應用參數多且複雜但是,有些參數,如前所述,你可以自己優化。還有一個Spring Fu項目的存在,該項目正在積極開發中,使用它可以減少參數。
Helidon SE 和 Ktor 是 微框架,Spring Boot 和 Micronaut 是全棧框架,Quarkus 和 Helidon MP 是 MicroProfile 框架。微框架的功能有限,這會減慢開發速度。
我不敢判斷這個或那個框架會不會在近期“大更新”,所以在我看來,目前最好繼續觀察,使用熟悉的框架解決工作問題。
同時,如本文所示,新框架在應用程式參數設置方面贏得了 Spring Boot。如果這些參數中的任何一個對你的某個微服務至關重要,那麼也許值得關註。但是,我們不要忘記,Spring Boot 一是在不斷改進,二是它擁有龐大的生態系統,並且有相當多的 Java 程式員熟悉它。此外,還有未涉及的其他框架:Vert.x、Javalin 等,也值得關註。
譯文:https://www.kubernetes.org.cn/9526.html
原文:https://dzone.com/articles/not-only-spring-boot-a-review-of-alternatives
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
覺得不錯,別忘了隨手點贊+轉發哦!