阿爾梅里亞簡介
一、簡介
在本文中,我們將介紹Armeria——一個用於高效建立微服務的靈活框架。我們將了解它是什麼、我們可以用它做什麼以及如何使用它。
最簡單地說,Armeria 為我們提供了一種構建微服務客戶端和伺服器的簡單方法,這些客戶端和伺服器可以使用各種協定進行通信,包括 REST、gRPC、Thrift 和 GraphQL。然而,Armeria 也提供與許多不同類型的許多其他技術的整合。
例如,我們支援使用 Consul、Eureka 或 Zookeeper 進行服務發現,支援 Zipkin 進行分散式跟踪,或與 Spring Boot、Dropwizard 或 RESTEasy 等框架集成
2. 依賴關係
在使用 Armeria 之前,我們需要在建造中包含最新版本,在撰寫本文時為1.29.2 。
JetCache 附帶了我們需要的幾個依賴項,取決於我們的特定需求。此功能的核心依賴項位於com.linecorp.armeria:armeria
中。
如果我們使用 Maven,我們可以將其包含在pom.xml
中:
<dependency>
<groupId>com.linecorp.armeria</groupId>
<artifactId>armeria</artifactId>
<version>1.29.2</version>
</dependency>
我們還有許多其他依賴項,可以用於與其他技術集成,具體取決於我們正在做什麼。
2.1.物料清單用途
由於 Armeria 提供大量依賴項,我們也可以選擇使用 Maven BOM 來管理所有版本。我們透過在專案中添加適當的依賴管理部分來利用這一點:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.linecorp.armeria</groupId>
<artifactId>armeria-bom</artifactId>
<version>1.29.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
完成此操作後,我們可以包含我們需要的任何 Armeria 依賴項,而無需擔心為它們定義版本:
<dependency>
<groupId>com.linecorp.armeria</groupId>
<artifactId>armeria</artifactId>
</dependency>
當我們只使用一個依賴項時,這似乎不太有用,但隨著數量的增長,這很快就會變得有用。
3. 運行伺服器
一旦我們獲得了適當的依賴項,我們就可以開始使用 Armeria。我們首先要了解的是運行 HTTP 伺服器。
Armeria 為我們提供了ServerBuilder
機制來設定我們的伺服器。我們可以配置它,然後建立一個Server
來啟動。我們為此需要的絕對最小值是:
ServerBuilder sb = Server.builder();
sb.service("/handler", (ctx, req) -> HttpResponse.of("Hello, world!"));
Server server = sb.build();
CompletableFuture<Void> future = server.start();
future.join();
這為我們提供了一個工作伺服器,它在一個隨機連接埠上運行,並帶有一個硬編碼的處理程序。我們很快就會看到更多關於如何配置這一切的資訊。
當我們開始運行程式時,輸出告訴我們 HTTP 伺服器正在運行:
07:36:46.508 [main] INFO com.linecorp.armeria.common.Flags -- verboseExceptions: rate-limit=10 (default)
07:36:46.957 [main] INFO com.linecorp.armeria.common.Flags -- useEpoll: false (default)
07:36:46.971 [main] INFO com.linecorp.armeria.common.Flags -- annotatedServiceExceptionVerbosity: unhandled (default)
07:36:47.262 [main] INFO com.linecorp.armeria.common.Flags -- Using Tls engine: OpenSSL BoringSSL, 0x1010107f
07:36:47.321 [main] INFO com.linecorp.armeria.common.util.SystemInfo -- hostname: k5mdq05n (from 'hostname' command)
07:36:47.399 [armeria-boss-http-*:49167] INFO com.linecorp.armeria.server.Server -- Serving HTTP at /[0:0:0:0:0:0:0:0%0]:49167 - http://127.0.0.1:49167/
除此之外,我們現在不僅可以清楚地看到伺服器正在運行,還可以看到它正在偵聽的位址和連接埠。
3.1.設定伺服器
在啟動伺服器之前,我們可以透過多種方式對其進行配置。
其中最有用的是指定我們的伺服器應該偵聽的連接埠。如果沒有這個,伺服器將在啟動時簡單地選擇一個隨機可用的連接埠。
使用ServerBuilder.http()
方法指定 HTTP 連接埠:
ServerBuilder sb = Server.builder();
sb.http(8080);
或者,我們可以使用ServerBuilder.https()
指定需要 HTTPS 連接埠。但是,在執行此操作之前,我們還需要設定 TLS 憑證。 Armeria 對此提供了所有常見的標準支持,但還提供了自動生成和使用自簽名證書的幫助程序:
ServerBuilder sb = Server.builder();
sb.tlsSelfSigned();
sb.https(8443);
3.2.新增訪問日誌記錄
預設情況下,我們的伺服器不會對傳入請求進行任何形式的日誌記錄。這通常很好。例如,如果我們在負載平衡器或其他形式的代理程式後面運行我們的服務,那麼它本身可能會執行存取日誌記錄。
但是,如果我們願意,我們可以直接向我們的服務新增日誌記錄支援。這是使用ServerBuilder.accessLogWriter()
方法完成的。這需要一個AccessLogWriter
實例,如果我們希望自己實作的話,它是一個 SAM 介面。
Armeria 為我們提供了一些我們也可以使用的標準實現,以及一些標準日誌格式 - 具體來說, Apache 通用日誌和Apache 組合日誌格式:
// Apache Common Log format
sb.accessLogWriter(AccessLogWriter.common(), true);
// Apache Combined Log format
sb.accessLogWriter(AccessLogWriter.combined(), true);
Armeria 將使用 SLF4J 寫出這些內容,利用我們已經為應用程式配置的任何日誌記錄後端:
07:25:16.481 [armeria-common-worker-kqueue-3-2] INFO com.linecorp.armeria.logging.access -- 0:0:0:0:0:0:0:1%0 - - 17/Jul/2024:07:25:16 +0100 "GET /#EmptyServer$$Lambda/0x0000007001193b60 h1c" 200 13
07:28:37.332 [armeria-common-worker-kqueue-3-3] INFO com.linecorp.armeria.logging.access -- 0:0:0:0:0:0:0:1%0 - - 17/Jul/2024:07:28:37 +0100 "GET /unknown#FallbackService h1c" 404 35
4. 新增服務處理程序
一旦我們有了伺服器,我們需要向其添加處理程序才能使其發揮作用。 Armeria 開箱即用,支援以各種形式新增標準 HTTP 請求處理程序。我們也可以為 gRPC、Thrift 或 GraphQL 請求新增處理程序,儘管我們需要額外的依賴項來支援這些處理程序。
4.1.簡單處理程序
註冊處理程序最簡單的方法是使用ServerBuilder.service()
方法。這需要一個 URL 模式和任何實作HttpService
介面的內容,並在請求匹配所提供的 URL 模式時提供此服務:
sb.service("/handler", handler);
HttpService
介面是一個 SAM 接口,這意味著我們可以使用真實的類別或直接使用 lambda 來實現它:
sb.service("/handler", (ctx, req) -> HttpResponse.of("Hello, world!"));
我們的處理程序必須實作HttpResponse HttpService.serve(ServiceRequestContext, HttpRequest)
方法-要麼在子類別中明確實現,要麼以 lambda 形式隱含實作。 ServiceRequestContext
和HttpRequest
參數的存在都是為了存取傳入 HTTP 請求的不同方面,而HttpResponse
傳回類型表示發送回客戶端的回應。
4.2.網址模式
Armeria 讓我們可以使用各種不同的 URL 模式安裝我們的服務,使我們能夠靈活地根據需要存取我們的處理程序。
最直接的方法是使用一個簡單的字串 – 例如/handler
– 它代表這個確切的 URL 路徑。
但是,我們也可以使用大括號或冒號前綴表示法來使用路徑參數:
sb.service("/curly/{name}", (ctx, req) -> HttpResponse.of("Hello, " + ctx.pathParam("name")));
sb.service("/colon/:name", (ctx, req) -> HttpResponse.of("Hello, " + ctx.pathParam("name")));
在這裡,我們可以使用ServiceRequestContext.pathParam()
來取得傳入請求中實際存在的指定路徑參數的值。
我們也可以使用全域匹配來匹配任意結構化 URL,但無需明確路徑參數。當我們這樣做時,我們必須使用「 glob:
」前綴來表示我們正在做什麼,然後我們可以使用「*」來表示單一URL段,使用「**」來表示任意數量的URL段 – 包括零:
ssb.service("glob:/base/*/glob/**",
(ctx, req) -> HttpResponse.of("Hello, " + ctx.pathParam("0") + ", " + ctx.pathParam("1")));
這將匹配“ /base/a/glob
”、“ /base/a/glob/b
”甚至“ /base/a/glob/b/c/d/e
”的 URL,但不匹配“ /base/a/b/glob/c
“.我們也可以將 glob 模式作為路徑參數來訪問,並以它們的位置命名。 ctx.pathParam(“0”)
符合此 URL 的「*」部分, ctx.pathParam(“1”)
符合 URL 的「**」部分。
最後,我們可以使用正規表示式來更精確地控制匹配的內容。這是使用「 regex:
」前綴完成的,之後整個 URL 模式是一個與傳入請求相符的正規表示式:
sb.service("regex:^/regex/[A-Za-z]+/[0-9]+$",
(ctx, req) -> HttpResponse.of("Hello, " + ctx.path()));
使用正規表示式時,我們還可以為捕獲組提供名稱,以使它們可用作路徑參數:
sb.service("regex:^/named-regex/(?<name>[AZ][az]+)$",
(ctx, req) -> HttpResponse.of("Hello, " + ctx.pathParam("name")));
這將使我們的 URL 與提供的正規表示式相匹配,並公開與我們的群組相對應的“name”
路徑參數 - 單個大寫字母後面跟著 1 個或多個小寫字母。
4.3.配置處理程序映射
到目前為止,我們已經了解瞭如何進行簡單的處理程序映射。我們的處理程序將對給定 URL 的任何呼叫做出反應,無論 HTTP 方法、標頭或其他任何內容如何。
我們可以更具體地了解如何使用流暢的 API 來搭配傳入的請求。這可以讓我們只觸發非常特定的呼叫的處理程序。我們使用ServerBuilder.route()
方法來做到這一點:
sb.route()
.methods(HttpMethod.GET)
.path("/get")
.produces(MediaType.PLAIN_TEXT)
.matchesParams("name")
.build((ctx, req) -> HttpResponse.of("Hello, " + ctx.path()));
這將僅匹配能夠接受text/plain
回應並且具有name
查詢參數的 GET 請求。當傳入請求不符時,我們也會自動取得正確的錯誤 - 如果請求不是 GET 請求,則為「HTTP 405 方法不允許」;如果請求無法接受text/plain
回應,則為「HTTP 406 不可接受」。
5. 附註解的處理程序
正如我們所看到的,除了直接添加處理程序之外,Armeria 還允許我們提供帶有適當註釋方法的任意類,並自動將這些方法映射到處理程序。這可以使編寫複雜的伺服器變得更容易管理。
這些處理程序使用ServerBuilder.annotatedService()
方法安裝,提供處理程序的實例:
sb.annotatedService(new AnnotatedHandler());
具體如何建構它取決於我們,這意味著我們可以為其提供工作所需的任何依賴項。
在此類中,我們必須使用@Get
、 @Post
、 @Put
、 @Delete
或任何其他適當的註釋來註釋方法。這些註釋將要使用的 URL 映射作為參數 - 遵循與以前完全相同的規則 - 並指示帶註釋的方法是我們的處理程序:
@Get("/handler")
public String handler() {
return "Hello, World!";
}
請注意,我們不必像之前那樣遵循相同的方法簽名。相反,我們可以要求將任意方法參數對應到傳入請求,並且回應類型將會對應到HttpResponse
類型。
5.1.處理程序參數
ServiceRequestContext
、 HttpRequest
、 RequestHeaders
、 QueryParams
或Cookies
類型的方法的任何參數將從請求中自動提供。這使我們能夠以與普通處理程序相同的方式存取請求的詳細資訊:
@Get("/handler")
public String handler(ServiceRequestContext ctx) {
return "Hello, " + ctx.path();
}
然而,我們可以讓這變得更容易。 Armeria 允許我們使用@Param
註解任意參數,這些參數將根據請求自動填充:
@Get("/handler/{name}")
public String handler(@Param String name) {
return "Hello, " + name;
}
如果我們使用-parameters
標誌編譯程式碼,則使用的名稱將從參數名稱派生。如果沒有,或者如果我們想要一個不同的名稱,我們可以將其作為註釋的值提供。
該註釋將為我們的方法提供路徑和查詢參數。如果使用的名稱與路徑參數匹配,則這就是提供的值。如果沒有,則使用查詢參數。
預設情況下,所有參數都是必填的。如果無法從請求中提供它們,則處理程序將不符。我們可以透過使用參數的Optional<>
來更改它,或使用@Nullable
或@Default
對其進行註解。
5.2.請求主體
除了向我們的處理程序提供路徑和查詢參數之外,我們還可以接收請求正文。 Armeria 有幾種方法來管理這個問題,這取決於我們的需求。
任何byte[]
或HttpData
類型的參數都會提供完整的、未修改的請求正文,我們可以根據需要進行處理:
@Post("/byte-body")
public String byteBody(byte[] body) {
return "Length: " + body.length;
}
或者,任何未註釋為以其他方式使用的String
或CharSequence
參數都將隨完整的請求正文一起提供,但在這種情況下,它將根據適當的字元編碼進行解碼:
@Post("/string-body")
public String stringBody(String body) {
return "Hello: " + body;
}
最後,如果請求具有與 JSON 相容的內容類型,則任何不是byte[]
、 HttpData
、 String
、 AsciiString
、 CharSequence
或直接屬於Object
類型且未註釋為以其他方式使用的參數將具有使用Jackson 將請求主體反序列化到其中。
@Post("/json-body")
public String jsonBody(JsonBody body) {
return body.name + " = " + body.score;
}
record JsonBody(String name, int score) {}
然而,我們可以更進一步。 Armeria 為我們提供了編寫自訂請求轉換器的選項。這些是實作RequestConverterFunction
介面的類別:
public class UppercasingRequestConverter implements RequestConverterFunction {
@Override
public Object convertRequest(ServiceRequestContext ctx, AggregatedHttpRequest request,
Class<?> expectedResultType, ParameterizedType expectedParameterizedResultType)
throws Exception {
if (expectedResultType.isAssignableFrom(String.class)) {
return request.content(StandardCharsets.UTF_8).toUpperCase();
}
return RequestConverterFunction.fallthrough();
}
}
然後,我們的轉換器可以完全存取傳入請求來執行所需的任何操作,以產生所需的值。如果我們無法做到這一點(例如,因為請求與參數不符),那麼我們將返回RequestConverterFunction.fallthrough()
以使 Armeria 繼續進行預設處理。
然後我們需要確保使用請求轉換器。這是使用@RequestConverter
註解完成的,附加到處理程序類別、處理程序方法或相關參數:
@Post("/uppercase-body")
@RequestConverter(UppercasingRequestConverter.class)
public String uppercaseBody(String body) {
return "Hello: " + body;
}
5.3.回應
與請求的方式大致相同,我們也可以從處理函數傳回任意值以用作 HTTP 回應。
如果我們直接返回一個HttpResponse
對象,那麼這將是完整的響應。如果不是,Armeria 會將實際回傳值轉換為正確的類型。
按照標準,Armeria 能夠進行多種標準轉換:
-
null
作為 HTTP 204 No Content 狀態代碼的空響應正文。 -
byte[]
或HttpData
作為原始字節,具有application/octet-stream
內容類型。 - 任何實作
CharSequence
(包括String
)作為具有text/plain
內容類型的 UTF-8 文字內容的內容。 - 任何將 Jackson 的
JsonNode
實作為具有application/json
內容類型的 JSON 的內容。
此外,如果處理程序方法使用@ProducesJson
或@Produces(“application/json”)
進行註釋,則任何傳回值都會使用 Jackson 轉換為 JSON:
@Get("/json-response")
@ProducesJson
public JsonBody jsonResponse() {
return new JsonBody("Baeldung", 42);
}
除此之外,我們還可以編寫自己的自訂回應轉換器,類似於編寫自訂請求轉換器的方式。它們實作了ResponseConverterFunction
介面。這是用我們的處理函數的回傳值呼叫的,並且必須傳回一個HttpResponse
物件:
public class UppercasingResponseConverter implements ResponseConverterFunction {
@Override
public HttpResponse convertResponse(ServiceRequestContext ctx, ResponseHeaders headers,
@Nullable Object result, HttpHeaders trailers) {
if (result instanceof String) {
return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8,
((String) result).toUpperCase(), trailers);
}
return ResponseConverterFunction.fallthrough();
}
}
和以前一樣,我們可以做任何我們需要的事情來產生期望的回應。如果我們無法這樣做(例如,因為傳回值的類型錯誤),則呼叫ResponseConverterFucntion.fallthrough()
可確保改用標準處理。
與請求轉換器類似,我們需要使用@ResponseConverter
來註解我們的函數,以告訴它使用我們新的回應轉換器:
@Post("/uppercase-response")
@ResponseConverter(UppercasingResponseConverter.class)
public String uppercaseResponse(String body) {
return "Hello: " + body;
}
我們可以將其應用於處理程序方法或整個類
5.4.例外情況
除了能夠將任意回應轉換為適當的 HTTP 回應之外,我們還可以根據需要處理例外狀況。
預設情況下,Armeria 將處理一些眾所周知的異常。 IllegalArgumentException
會產生 HTTP 400 Bad Request,而HttpStatusException
和HttpResponseException
會轉換為它們表示的 HTTP 回應。其他任何情況都會產生 HTTP 500 內部伺服器錯誤回應。
然而,與處理函數的回傳值一樣,我們也可以為異常編寫轉換器。它們實作了ExceptionHandlerFunction
,它將拋出的異常作為輸入並傳回客戶端的 HTTP 回應:
public class ConflictExceptionHandler implements ExceptionHandlerFunction {
@Override
public HttpResponse handleException(ServiceRequestContext ctx, HttpRequest req, Throwable cause) {
if (cause instanceof IllegalStateException) {
return HttpResponse.of(HttpStatus.CONFLICT);
}
return ExceptionHandlerFunction.fallthrough();
}
}
和以前一樣,它可以執行任何需要的操作來產生正確的回應或返回ExceptionHandlerFunction.fallthrough()
以回退到標準處理。
並且,和以前一樣,我們在處理程序類別或方法上使用@ExceptionHandler
註解來連接它:
@Get("/exception")
@ExceptionHandler(ConflictExceptionHandler.class)
public String exception() {
throw new IllegalStateException();
}
6.GraphQL
到目前為止,我們已經研究瞭如何使用 Armeria 設定 RESTful 處理程序。然而,它能做的遠不止這些,包括 GraphQL、Thrift 和 gRPC。
為了使用這些附加協議,我們需要添加一些額外的依賴項。例如,新增 GraphQL 處理程序需要我們將com.linecorp.armeria:armeria-graphql
依賴項加入我們的專案:
<dependency>
<groupId>com.linecorp.armeria</groupId>
<artifactId>armeria-graphql</artifactId>
</dependency>
完成此操作後,我們可以透過使用GraphqlService
來公開使用 Armeria 的 GraphQL 模式:
sb.service("/graphql",
GraphqlService.builder().graphql(buildSchema()).build());
這從 GraphQL java 庫中取得一個GraphQL
實例,我們可以根據需要建立該實例,並將其公開在指定的端點上。
7. 運行客戶端
除了編寫伺服器元件之外,Armeria 還允許我們編寫可以與這些(或任何)伺服器通訊的客戶端。
為了連接到 HTTP 服務,我們使用核心 Armeria 依賴項附帶的WebClient
類別。我們可以直接使用它,無需配置即可輕鬆進行傳出 HTTP 呼叫:
WebClient webClient = WebClient.of();
AggregatedHttpResponse response = webClient.get("http://localhost:8080/handler")
.aggregate()
.join();
此處對WebClient.get()
的呼叫將向提供的 URL 發出 HTTP GET 請求,然後傳回串流 HTTP 回應。然後,一旦完成,我們就呼叫HttpResponse.aggregate()
來取得完全解析的 HTTP 回應的CompletableFuture
。
一旦我們獲得AggregatedHttpResponse,
我們就可以使用它來存取 HTTP 回應的各個部分:
System.out.println(response.status());
System.out.println(response.headers());
System.out.println(response.content().toStringUtf8());
如果我們願意,我們也可以為特定的基本 URL 建立一個WebClient
:
WebClient webClient = WebClient.of("http://localhost:8080");
AggregatedHttpResponse response = webClient.get("/handler")
.aggregate()
.join();
當我們需要從配置中提供基本 URL,但我們的應用程式可以理解我們在其下面調用的 API 的結構時,這尤其有用。
我們也可以使用這個客戶端來提出其他請求。例如,我們可以使用WebClient.post()
方法發出 HTTP POST 請求,並提供請求正文:
WebClient webClient = WebClient.of();
AggregatedHttpResponse response = webClient.post("http://localhost:8080/uppercase-body", "baeldung")
.aggregate()
.join();
有關此請求的其他所有內容都完全相同,包括我們處理回應的方式。
7.1.複雜的請求
我們已經了解如何提出簡單的請求,但是更複雜的情況又如何呢?到目前為止我們看到的方法其實只是對execute()
方法的包裝,它允許我們提供更複雜的HTTP請求表示:
WebClient webClient = WebClient.of("http://localhost:8080");
HttpRequest request = HttpRequest.of(
RequestHeaders.builder()
.method(HttpMethod.POST)
.path("/uppercase-body")
.add("content-type", "text/plain")
.build(),
HttpData.ofUtf8("Baeldung"));
AggregatedHttpResponse response = webClient.execute(request)
.aggregate()
.join();
在這裡,我們可以看到如何根據需要詳細指定傳出 HTTP 請求的所有不同部分。
我們還有一些輔助方法來讓這件事變得更容易。例如,我們可以使用 contentType() 等方法,而不是使用add()
來指定任意 HTTP 標頭contentType().
這些使用起來更明顯,而且類型更安全:
HttpRequest request = HttpRequest.of(
RequestHeaders.builder()
.method(HttpMethod.POST)
.path("/uppercase-body")
.contentType(MediaType.PLAIN_TEXT_UTF_8)
.build(),
HttpData.ofUtf8("Baeldung"));
我們可以在這裡看到contentType()
方法需要MediaType
物件而不是純字串,因此我們知道我們正在傳遞正確的值。
7.2.客戶端配置
我們還可以使用許多配置參數來調整客戶端本身。當我們建構WebClient
時,我們可以使用ClientFactory
來配置這些。
ClientFactory clientFactory = ClientFactory.builder()
.connectTimeout(Duration.ofSeconds(10))
.idleTimeout(Duration.ofSeconds(60))
.build();
WebClient webClient = WebClient.builder("http://localhost:8080")
.factory(clientFactory)
.build();
在這裡,我們將底層 HTTP 用戶端配置為在連接到 URL 時有 10 秒逾時,並在 60 秒不活動後關閉底層連線池中開啟的連線。
八、結論
在這篇文章中,我們簡單介紹了 Armeria。這個庫可以做更多的事情,所以為什麼不嘗試看看呢?
所有範例都可以在 GitHub 上找到。