1. 簡介
在本文中,我們將探討 Armeria——一個靈活的框架,用於高效地構建微服務。我們將瞭解它的用途、我們可以用它做什麼,以及如何使用它。
最簡單地説,Armeria 提供了便捷的方式來構建使用多種協議進行通信的微服務客户端和服務器,包括 REST、gRPC、Thrift 和 GraphQL。
例如,我們支持使用 Consul、Eureka 或 Zookeeper 進行服務發現,使用 Zipkin 進行分佈式追蹤,或與 Spring Boot、Dropwizard 或 RESTEasy 等框架集成。
2. 依賴項
在開始使用 Armeria 之前,我們需要將其最新版本包含在我們的構建中,截至寫作時,該版本為 1.29.2。
Armeria 包含多個依賴項,具體取決於我們的需求。核心依賴項用於功能性,位於 com.linecorp.armeria:armeria。
如果使用 Maven,可以在 pom.xml 中添加如下內容:
<dependency>
<groupId>com.linecorp.armeria</groupId>
<artifactId>armeria</artifactId>
<version>1.29.2</version>
</dependency>我們還擁有許多其他依賴項,可以根據我們所做的事情的不同,用於與其他技術的集成。
2.1. 使用 BOM
由於 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. 服務器配置
我們有多種方法可以配置服務器,在啓動之前。
最實用的一種方法是指定服務器應該監聽的端口。如果沒有指定,服務器啓動時會隨機選擇一個可用的端口。
指定 HTTP 端口是通過使用 <em ServerBuilder.http()</em> 方法完成的:
ServerBuilder sb = Server.builder();
sb.http(8080);
當然,以下是翻譯後的內容:
或者,我們也可以指定使用 HTTPS 端口,通過使用 ServerBuilder.https()。但是,在我們能夠執行此操作之前,我們還需要配置我們的 TLS 證書。Armeria 提供了標準的全部支持,還提供了一個輔助功能,用於自動生成和使用自簽名證書:
ServerBuilder sb = Server.builder();
sb.tlsSelfSigned();
sb.https(8443);
3.2. 添加訪問日誌
默認情況下,我們的服務器不會對傳入的請求進行任何形式的日誌記錄。這通常是可以接受的。例如,如果我們的服務運行在負載均衡器或其他形式的代理後面,那麼這些代理本身可能會進行訪問日誌記錄。
但是,如果我們希望這樣做,則可以使用 ServerBuilder.accessLogWriter() 方法直接為我們的服務添加日誌支持。該方法接受一個 AccessLogWriter 實例,如果我們要自己實現它,則它是一個 SAM 接口。
Armeria 提供了我們可用的標準實現,這些實現使用標準日誌格式,特別是 Apache Common Log 格式和 Apache Combined Log 格式。
// 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 URL 模式
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() 來獲取傳入請求中命名前向參數實際的值。
我們還可以使用 glob 匹配來匹配任意結構化的 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>[A-Z][a-z]+)$",
(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 響應的 GET 請求,並且具有名為 name 的查詢參數。我們還會自動獲取當傳入的請求不匹配時正確的錯誤信息——如果請求不是 GET 請求,則返回 HTTP 405 Method Not Allowed;如果請求無法接受 text/plain 響應,則返回 HTTP 406 Not Acceptable。
5. 註解處理程序
正如我們所見,除了直接添加處理程序外,Armeria 允許我們為具有適當註解的方法的任意類提供處理程序,並自動將這些方法映射為處理程序。 這可以極大地簡化複雜服務器的管理。
這些處理程序使用 ServerBuilder.annotatedService() 方法進行掛載,從而提供處理程序的實例:
sb.annotatedService(new AnnotatedHandler());我們構建方式完全由我們決定,這意味着我們可以為其提供任何必要的依賴項,使其能夠正常工作。
在此類中,我們必須擁有帶有 @Get、@Post、@Put、@Delete 或其他適當註解的方法。這些註解作為參數接受 URL 映射,遵循之前確定的規則,並指示所註解的方法是我們的處理程序:
@Get("/handler")
public String handler() {
return "Hello, World!";
}
請注意,此處我們不必遵循之前的方法簽名。相反,我們可以要求任意方法參數映射到傳入的請求,並且響應類型會被映射到 HttpResponse 類型。
5.1. Handler 參數
我們方法的任何參數,包括對類型 ServiceRequestContext、HttpRequest、RequestHeaders、QueryParams 或 Cookies 的參數,都將自動從請求中提供。這使得我們能夠以與普通 Handler 相同的方式訪問請求的詳細信息。
@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 提供了幾種管理請求體的方法,具體取決於我們需要什麼。
任何類型為 <em >byte[]</em> 或 <em >HttpData</em> 的參數,都將提供完整的、未修改的請求體,我們可以對其進行任何操作:
@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 響應使用。
如果直接返回 <em >HttpResponse</em > 對象,則該對象將成為完整的響應。否則,Armeria 會將實際返回值轉換為正確的類型。
默認情況下,Armeria 能夠進行多種標準轉換:
- 將
<em >null</em >視為空響應體,並使用 HTTP 204 No Content 狀態碼。 - 將
byte[]或HttpData作為原始字節,並使用application/octet-stream內容類型。 - 任何實現
<em >CharSequence</em >的對象(包括<em >String</em >>)都將作為 UTF-8 文本內容,並使用text/plain` 內容類型。 - 任何從 Jackson 庫中實現
<em >JsonNode</em >的對象都將作為 JSON,並使用application/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 會處理一些已知的異常。<em >IllegalArgumentException</em > 會產生 HTTP 400 Bad Request 響應,而HttpStatusException 和 `HttpResponseException 會被轉換為它們所代表的 HTTP 響應。 其他任何異常都會產生 HTTP 500 Internal Server Error 響應。
但是,與處理返回值的處理函數返回值類似,我們也可以編寫異常轉換器。 這些實現 `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 處理程序。然而,Armeria 能夠做的事情遠不止這些,還包括 GraphQL、Thrift 和 gRPC。
為了使用這些額外的協議,我們需要添加一些額外的依賴項。例如,添加 GraphQL 處理程序需要我們將 com.linecorp.armeria:armeria-graphql 依賴項添加到我們的項目中:
<dependency>
<groupId>com.linecorp.armeria</groupId>
<artifactId>armeria-graphql</artifactId>
</dependency>一旦我們完成了此項工作,我們就可以使用 Armeria 通過使用 GraphqlService 來暴露一個 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 請求的各個部分。
我們還提供了一些輔助方法,以簡化操作。例如,與其使用 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秒後關閉未打開的連接。
8. 結論
在本文中,我們對 Armeria 進行了簡要介紹。該庫的功能遠不止如此,不妨親自嘗試一下,看看它的強大之處。