在本文中,我们简要介绍了 Armeria:一个用于高效构建微服务的灵活框架。我们将了解它是什么、我们可以用它做什么以及如何使用它。
简单来说,Armeria 为我们提供了一种构建微服务客户端和服务器的简单方法,这些客户端和服务器可以使用多种协议进行通信 - 包括 REST、gRPC、Thrift和GraphQL。然而,Armeria 还提供与许多其他不同类型的技术的集成。
例如,我们支持使用Consul、Eureka或Zookeeper进行服务发现,支持使用Zipkin进行分布式跟踪,或支持与 Spring Boot、Dropwizard或RESTEasy等框架集成
依赖项
在我们可以使用 Armeria 之前,我们需要在我们的构建中包含最新版本,在撰写本文时是1.29.2 。
JetCache 带有我们需要的几个依赖项,具体取决于我们的确切需求。该功能的核心依赖项位于com.linecorp.armeria:armeria中。
如果我们使用 Maven,我们可以将其包含在pom.xml中:
<dependency> |
我们还有许多其他依赖项可用于与其他技术集成,具体取决于我们正在做的事情。
BOM 使用
由于 Armeria 提供的依赖项数量众多,我们还可以选择使用Maven BOM来管理所有版本。我们通过在项目中添加适当的依赖项管理部分来利用此功能:
<dependencyManagement> |
完成此操作后,我们可以包含所需的任何 Armeria 依赖项,而不必担心为它们定义版本:
<dependency> |
当我们仅使用一个依赖项时,这似乎不是很有用,但随着数量的增长,它很快变得有用。
运行服务器
一旦我们获得了适当的依赖项,我们就可以开始使用 Armeria。我们首先要看的是运行 HTTP 服务器。
Armeria 为我们提供了ServerBuilder机制来配置我们的服务器。我们可以对其进行配置,然后构建一个要启动的服务器。为此,我们所需的最低要求是:
ServerBuilder sb = Server.builder(); |
这为我们提供了一个工作服务器,它在一个随机端口上运行,并带有一个硬编码的处理程序。我们很快会看到有关如何配置所有这些的更多信息。
当我们开始运行程序时,输出告诉我们 HTTP 服务器正在运行:
07:36:46.508 [main] INFO com.linecorp.armeria.common.Flags -- verboseExceptions: rate-limit=10 (default) |
除此之外,我们现在不仅可以清楚地看到服务器正在运行,还可以看到它正在监听的地址和端口。
配置服务器
在启动服务器之前,我们可以通过多种方式来配置服务器。
其中最有用的是指定我们的服务器应侦听的端口。如果没有这个,服务器将在启动时随机选择一个可用的端口。
使用ServerBuilder.http()方法指定 HTTP 端口:
ServerBuilder sb = Server.builder(); |
或者,我们可以使用ServerBuilder.https()指定我们想要的 HTTPS 端口。但是,在执行此操作之前,我们还需要配置我们的 TLS 证书。Armeria 提供了所有常见的标准支持,但也提供了自动生成和使用自签名证书的帮助程序:
ServerBuilder sb = Server.builder(); |
添加访问日志
默认情况下,我们的服务器不会对传入请求进行任何形式的记录。这通常没有问题。例如,如果我们在负载平衡器或其他形式的代理后面运行我们的服务,这些代理本身可能会进行访问记录。
但是,如果我们愿意,我们可以直接为我们的服务添加日志支持。这是使用 ServerBuilder.accessLogWriter ()方法完成的。这需要一个AccessLogWriter实例,如果我们想自己实现它,它是一个 SAM 接口。
Armeria 为我们提供了一些我们也可以使用的标准实现,以及一些标准日志格式 – 具体来说,是 Apache 通用日志和Apache 组合日志格式:
// Apache Common Log format |
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 |
添加服务处理程序
一旦我们有了服务器,我们就需要向其中添加处理程序,以便它能够发挥作用。Armeria开箱即用,支持以各种形式添加标准 HTTP 请求处理程序。我们还可以添加 gRPC、Thrift 或 GraphQL 请求的处理程序,但我们需要额外的依赖项来支持这些请求。
简单处理程序
注册处理程序的最简单方法是使用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返回类型表示发送回客户端的响应。
URL 模式
Armeria 允许我们使用各种不同的 URL 模式来挂载我们的服务,让我们可以灵活地根据需要访问我们的处理程序。
最直接的方法是使用一个简单的字符串 -例如/handler - 它代表这个精确的 URL 路径。
但是,我们也可以使用花括号或冒号前缀表示法来使用路径参数:
sb.service("/curly/{name}", (ctx, req) -> HttpResponse.of("Hello, " + ctx.pathParam("name"))); |
在这里,我们可以使用ServiceRequestContext.pathParam()来获取命名路径参数的传入请求中实际存在的值。
我们还可以使用 glob 匹配来匹配任意结构化的 URL,但不包含显式路径参数。当我们这样做时,我们必须使用“ glob: ”前缀来表明我们在做什么,然后我们可以使用“*”来表示单个 URL 段,使用“”来表示任意数量的 URL 段 - 包括零个:
ssb.service("glob:/base/*/glob/", |
这将匹配“ /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]+$", |
使用正则表达式时,我们还可以为捕获组提供名称,以使它们可用作路径参数:
sb.service("regex:^/named-regex/(?<name>[A-Z][a-z]+)$", |
这将使我们的 URL 与提供的正则表达式匹配,并公开与我们的组相对应的“名称”路径参数- 一个大写字母后跟一个或多个小写字母。
配置处理程序映射
到目前为止,我们已经了解了如何进行简单的处理程序映射。我们的处理程序将对给定 URL 的任何调用做出反应,无论 HTTP 方法、标头或其他任何内容如何。
我们可以更加具体地说明如何使用流畅的 API 来匹配传入的请求。这样我们就可以只为非常特定的调用触发处理程序。我们使用ServerBuilder.route()方法来实现这一点:
sb.route() |
这将仅匹配能够接受text/plain响应且具有name查询参数的 GET 请求。当传入请求不匹配时,我们还会自动获取正确的错误 - 如果请求不是 GET 请求,则为 HTTP 405 方法不允许;如果请求无法接受text/plain响应,则为 HTTP 406 不可接受。
带注释的处理程序
正如我们所见,除了直接添加处理程序外,Armeria 还允许我们提供具有适当注释方法的任意类,并自动将这些方法映射到处理程序。这可以使编写复杂的服务器变得更容易管理。
这些处理程序使用ServerBuilder.annotatedService()方法安装,提供我们的处理程序的一个实例:
sb.annotatedService(new AnnotatedHandler()); |
具体如何构建它取决于我们自己,这意味着我们可以为其提供其工作所需的任何依赖项。
在这个类中,我们必须使用@Get、 @Post、@Put、@Delete或任何其他适当的注释来注释方法。这些注释将要使用的 URL 映射作为参数 - 遵循与以前完全相同的规则 - 并指示注释的方法是我们的处理程序:
@Get("/handler") |
请注意,我们不必像以前一样遵循相同的方法签名。相反,我们可以要求将任意方法参数映射到传入的请求上,并且响应类型将映射到HttpResponse类型。
处理程序参数
我们方法的任何ServiceRequestContext、HttpRequest、RequestHeaders、QueryParams或Cookies类型的参数都将自动从请求中提供。这使我们能够以与普通处理程序相同的方式从请求中获取详细信息:
@Get("/handler") |
但是,我们可以让这变得更容易。Armeria 允许我们使用@Param注释任意参数,这些参数将根据请求自动填充:
@Get("/handler/{name}") |
如果我们使用-parameters标志编译代码,则使用的名称将从参数名称中派生出来。如果没有,或者我们想要一个不同的名称,我们可以将其作为注释的值提供。
此注释将为我们的方法提供路径和查询参数。如果使用的名称与路径参数匹配,则这就是提供的值。如果不匹配,则使用查询参数。
默认情况下,所有参数都是必需的。如果请求中无法提供这些参数,则处理程序将不匹配。我们可以通过使用Optional<>作为参数来更改此设置,或者使用@Nullable或@Default对其进行注释。
请求主体
除了向我们的处理程序提供路径和查询参数外,我们还可以接收请求主体。Armeria 有几种方法来管理这一点,具体取决于我们的需求。
任何byte[]或HttpData类型的参数都将提供完整的、未修改的请求体,我们可以根据需要进行处理:
@Post("/byte-body") |
或者,任何未注释以其他方式使用的String或 CharSequence参数都将与完整的请求正文一起提供,但在这种情况下,它将根据适当的字符编码进行解码:
@Post("/string-body") |
最后,如果请求具有与 JSON 兼容的内容类型,则任何不是byte[]、HttpData、String、AsciiString、CharSequence或直接属于Object类型的参数,并且未注释为以其他方式使用的参数都将使用Jackson将请求主体反序列化为它。
@Post("/json-body") |
但是,我们可以更进一步。Armeria 为我们提供了编写自定义请求转换器的选项。这些是实现RequestConverterFunction接口的类:
public class UppercasingRequestConverter implements RequestConverterFunction { |
然后,我们的转换器可以完全访问传入的请求,以生成所需的值。如果我们无法做到这一点(例如,因为请求与参数不匹配),那么我们返回RequestConverterFunction.fallthrough()以使 Armeria 继续进行默认处理。
然后我们需要确保使用了请求转换器。这是使用@RequestConverter注释完成的,该注释附加到处理程序类、处理程序方法或相关参数:
@Post("/uppercase-body") |
回应
与请求类似,我们也可以从处理函数返回任意值作为 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") |
此外,我们还可以编写自己的自定义响应转换器,类似于编写自定义请求转换器的方式。它们实现了ResponseConverterFunction接口。它使用处理程序函数的返回值进行调用,并且必须返回一个HttpResponse对象:
public class UppercasingResponseConverter implements ResponseConverterFunction { |
和以前一样,我们可以做任何需要的事情来产生所需的响应。如果我们无法做到这一点——例如因为返回值的类型错误——那么对ResponseConverterFucntion.fallthrough()的调用可以确保改用标准处理。
与请求转换器类似,我们需要用@ResponseConverter注释我们的函数来告诉它使用我们的新响应转换器:
@Post("/uppercase-response") |
我们可以将其应用于处理程序方法或整个类
异常
除了能够将任意响应转换为适当的 HTTP 响应之外,我们还可以随意处理异常。
默认情况下,Armeria 将处理一些众所周知的异常。IllegalArgumentException会产生 HTTP 400 Bad Request,HttpStatusException和HttpResponseException会转换为它们所代表的 HTTP 响应。其他任何情况都会产生 HTTP 500 Internal Server Error 响应。
但是,与处理函数的返回值一样,我们也可以编写异常转换器。它们实现了ExceptionHandlerFunction,它将抛出的异常作为输入并返回客户端的 HTTP 响应:
public class ConflictExceptionHandler implements ExceptionHandlerFunction { |
与以前一样,它能够做任何需要的事情来产生正确的响应或者返回ExceptionHandlerFunction.fallthrough()来回退到标准处理。
和以前一样,我们在处理程序类或方法上使用@ExceptionHandler注释来连接它:
@Get("/exception") |
GraphQL
到目前为止,我们已经研究了如何使用 Armeria 设置 RESTful 处理程序。但它能做的远不止这些,还包括 GraphQL、Thrift 和 gRPC。
为了使用这些附加协议,我们需要添加一些额外的依赖项。例如,添加 GraphQL 处理程序需要我们将 com.linecorp.armeria :armeria-graphql依赖项添加到我们的项目中:
<dependency> |
完成此操作后,我们可以使用 GraphqlService 使用 Armeria 公开 GraphQL模式:
sb.service("/graphql", |
这将从GraphQL Java 库中获取一个GraphQL实例,我们可以按照自己的意愿构建它,并将其公开在指定的端点上。
运行客户端
除了编写服务器组件之外,Armeria 还允许我们编写可以与这些(或任何)服务器通信的客户端。
为了连接到 HTTP 服务,我们使用核心 Armeria 依赖项附带的WebClient类。我们可以直接使用它而无需任何配置,轻松进行传出 HTTP 调用:
WebClient webClient = WebClient.of(); |
此处对WebClient.get()的调用将向提供的 URL 发出 HTTP GET 请求,然后返回流式 HTTP 响应。然后,一旦 HTTP 响应完成,我们调用HttpResponse.aggregate()以获取完全解析的 HTTP 响应的CompletableFuture 。
一旦我们获得了AggregatedHttpResponse,我们就可以使用它来访问 HTTP 响应的各个部分:
System.out.println(response.status()); |
如果愿意,我们还可以为特定的基本 URL创建一个WebClient :
WebClient webClient = WebClient.of("http://localhost:8080"); |
当我们需要从配置中提供基本 URL 时,这尤其有用,但我们的应用程序可以理解我们在下面调用的 API 的结构。
我们还可以使用此客户端发出其他请求。例如,我们可以使用WebClient.post()方法发出 HTTP POST 请求,并提供请求主体:
WebClient webClient = WebClient.of(); |
关于此请求的所有其他内容完全相同,包括我们如何处理响应。
复杂请求
我们已经了解了如何发出简单的请求,但更复杂的情况呢?到目前为止,我们看到的方法实际上只是对execute()方法的包装,这使我们能够提供更复杂的 HTTP 请求表示:
WebClient webClient = WebClient.of("http://localhost:8080"); |
在这里我们可以看到如何根据需要详细地指定传出 HTTP 请求的所有不同部分。
我们还有一些辅助方法可以使此操作更加简单。例如,我们可以使用contentType()等方法,而不是使用add()来指定任意 HTTP 标头。这些方法更易于使用,而且类型更安全:
HttpRequest request = HttpRequest.of( |
我们可以在这里看到contentType()方法需要一个MediaType对象而不是纯字符串,所以我们知道我们传递了正确的值。
客户端配置
我们还可以使用许多配置参数来调整客户端本身。我们可以在构建WebClient时使用ClientFactory来配置这些参数。
ClientFactory clientFactory = ClientFactory.builder() |
在这里,我们将底层 HTTP 客户端配置为在连接到 URL 时有 10 秒的超时时间,并在 60 秒不活动后关闭底层连接池中打开的连接。