Armeria简介

2025/03/26

1. 简介

在本文中,我们将介绍Armeria–一个用于高效构建微服务的灵活框架。我们将了解它是什么、可以用它做什么以及如何使用它。

简单来说,Armeria为我们提供了一种构建微服务客户端和服务器的简单方法,这些客户端和服务器可以使用各种协议进行通信-包括REST、gRPCThriftGraphQL。但是,Armeria还提供与许多其他不同类型的技术的集成。

例如,我们支持使用ConsulEurekaZookeeper进行服务发现,支持使用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。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章