1. 简介
在本文中,我们将介绍Armeria–一个用于高效构建微服务的灵活框架。我们将了解它是什么、可以用它做什么以及如何使用它。
简单来说,Armeria为我们提供了一种构建微服务客户端和服务器的简单方法,这些客户端和服务器可以使用各种协议进行通信-包括REST、gRPC、Thrift和GraphQL。但是,Armeria还提供与许多其他不同类型的技术的集成。
例如,我们支持使用Consul、Eureka或Zookeeper进行服务发现,支持使用ZipkinDropwizard进行分布式跟踪,或支持与Spring Boot、或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机制来配置我们的服务器,我们可以对其进行配置,然后构建一个要启动的服务器。为此,我们所需的最低要求是:
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 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段(包括0个):
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”路径参数-一个大写字母后跟一个或多个小写字母。
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实现为JSON且内容类型为application/json的东西。
此外,如果处理程序方法用@ProducesJson或@Produces(“application/json”)标注,那么任何返回值都将使用Jackson转换为JSON:
@Get("/json-response")
@ProducesJson
public JsonBody jsonResponse() {
return new JsonBody("Tuyucheng", 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();
}
}
和以前一样,我们可以做任何需要的事情来产生所需的响应。如果我们无法做到这一点-例如因为返回值的类型错误,那么对ResponseConverterFunction.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 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处理程序,但它能做的远不止这些,还包括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响应。然后,一旦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", "tuyucheng")
.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("Tuyucheng"));
AggregatedHttpResponse response = webClient.execute(request)
.aggregate()
.join();
在这里我们可以看到如何根据需要详细地指定传出HTTP请求的所有不同部分。
我们还有一些辅助方法可以使此操作更加简单。例如,我们可以使用contentType()等方法,而不是使用add()来指定任意HTTP标头。这些方法更易于使用,而且类型更安全:
HttpRequest request = HttpRequest.of(
RequestHeaders.builder()
.method(HttpMethod.POST)
.path("/uppercase-body")
.contentType(MediaType.PLAIN_TEXT_UTF_8)
.build(),
HttpData.ofUtf8("Tuyucheng"));
我们可以在这里看到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。
Post Directory
