Armeria 簡介

REST
Remote
2
03:08 AM · Dec 01 ,2025

1. 簡介

Armeria – a flexible framework for efficiently building microservices. We’ll see what it is, what we can do with it, and how to use it.">在本文中,我們將探討 Armeria——一個靈活的框架,用於高效地構建微服務。我們將瞭解它的是什麼,我們可以用它做什麼,以及如何使用它。

在最簡單的用法中,Armeria 允許我們輕鬆構建使用各種協議(包括 REST、gRPC、Thrift 和 GraphQL)進行通信的微服務客户端和服務器。Armeria 還提供與許多不同類型技術的集成。

例如,我們支持使用 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. Running a Server

Once we’ve got the appropriate dependencies, we can start using Armeria. The first thing we’ll look at is running an HTTP Server.

Armeria offers us the ServerBuilder mechanism to configure our server. We can configure this, and then build a Server to launch. The absolute minimum we need for this is:

ServerBuilder sb = Server.builder();
sb.service("/handler", (ctx, req) -> HttpResponse.of("Hello, world!"));

Server server = sb.build();
CompletableFuture<Void> future = server.start();
future.join();

This gives us a working server, running on a random port with a single, hard-coded handler. We’ll see more about how to configure all of this soon.

When we start running our program, the output tells us that the HTTP server is running:

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/

Amongst other things, we can now clearly see not only that the server is running but also what address and port it’s listening on.

3.1. Configuring the Server

We have a number of ways that we can configure our server before starting it.

The most useful of these is to specify the port that our server should listen on. Without this, the server will simply pick a randomly available port on startup.

Specifying the port for HTTP is done using the ServerBuilder.http() method:

ServerBuilder sb = Server.builder();
sb.http(8080);

Alternatively, we can specify that we want an HTTPS port using ServerBuilder.https(). However, before we can do this we’ll also need to configure our TLS certificates. Armeria offers all of the usual standard support for this, but also offers a helper for automatically generating and using a self-signed certificate:

ServerBuilder sb = Server.builder();
sb.tlsSelfSigned();
sb.https(8443);

3.2. Adding Access Logging

By default, our server won’t do any form of logging of incoming requests. This is often fine. For example, if we’re running our services behind a load balancer or other form of proxy that itself might do the access logging.

However, if we want to, then we can add logging support to our service directly. This is done using the ServerBuilder.accessLogWriter() method. This takes an AccessLogWriter instance, which is a SAM interface if we wish to implement it ourselves.

Armeria provides us with some standard implementations that we can use as well, with some standard log formats – specifically, the Apache Common Log and Apache Combined Log formats:

// Apache Common Log format
sb.accessLogWriter(AccessLogWriter.common(), true);
// Apache Combined Log format
sb.accessLogWriter(AccessLogWriter.combined(), true);

Armeria will write these out using SLF4J, utilizing whichever logging backend we have already configured for our application:

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

Out of the box, Armeria comes with support for adding standard HTTP request handlers in various forms. We can also add handlers for gRPC, Thrift, or GraphQL requests, though we need additional dependencies to support those.

ServerBuilder.service()method. This takes a URL pattern and anything that implements the HttpService interface and serves up this whenever a request comes in matching the provided URL pattern:

sb.service("/handler", handler);

The HttpService interface is a SAM interface, meaning we’re able to implement it either with a real class or directly in place with a lambda:

sb.service("/handler", (ctx, req) -> HttpResponse.of("Hello, world!"));

serve(ServiceRequestContext, HttpRequest)method – either explicitly in a subclass or implicitly as a lambda. Both the ServiceRequestContext and HttpRequest parameters exist to give access to different aspects of the incoming HTTP request, and the HttpResponse return type represents the response sent back to the client.

The most straightforward way is to use a simple string – /handler, for example – which represents this exact URL path.

However, we can also use path parameters using either curly-brace or colon-prefix notation:

sb.service("/curly/{name}", (ctx, req) -> HttpResponse.of("Hello, " + ctx.pathParam("name")));
sb.service("/colon/:name", (ctx, req) -> HttpResponse.of("Hello, " + ctx.pathParam("name")));

Here, we’re able to use ServiceRequestContext.pathParam() to get the value that was actually present in the incoming request for the named path parameter.

We can also use glob matches to match an arbitrary structured URL but without explicit path parameters. When we do this, we must use a prefix of “glob:” to indicate what we’re doing, and then we can use “*” to represent a single URL segment, and “**” to represent an arbitrary number of URL segments – including zero:

ssb.service("glob:/base/*/glob/**", 
  (ctx, req) -> HttpResponse.of("Hello, " + ctx.pathParam("0") + ", " + ctx.pathParam("1")));

This will match URLs of “/base/a/glob“, “/base/a/glob/b” or even “/base/a/glob/b/c/d/e” but not “/base/a/b/glob/c“. We can also access our glob patterns as path parameters, named after their position. ctx.pathParam(“0”) matches the “*” portion of this URL, and ctx.pathParam(“1”) matches the “**” portion of the URL.

Finally, we can use regular expressions to gain more precise control over what’s matched. This is done using the “regex:” prefix, after which the entire URL pattern is a regex to match against the incoming requests:

sb.service("regex:^/regex/[A-Za-z]+/[0-9]+$",
  (ctx, req) -> HttpResponse.of("Hello, " + ctx.path()));

When using regexes, we can also provide names to capturing groups to make them available as path params:

sb.service("regex:^/named-regex/(?<name>[A-Z][a-z]+)$",
  (ctx, req) -> HttpResponse.of("Hello, " + ctx.pathParam("name")));

This will make our URL match the provided regex, and expose a path parameter of “name” corresponding to our group – a single capital letter followed by 1-or-more lowercase letters.

We’ve so far seen how to do simple handler mappings. Our handlers will react to any calls to the given URL, regardless of HTTP method, headers, or anything else.

We do this using the ServerBuilder.route() method:

sb.route()
  .methods(HttpMethod.GET)
  .path("/get")
  .produces(MediaType.PLAIN_TEXT)
  .matchesParams("name")
  .build((ctx, req) -> HttpResponse.of("Hello, " + ctx.path()));

This will only match GET requests that are able to accept text/plain responses and, which have a query parameter of name. We’ll also automatically get the correct errors when an incoming request doesn’t match – HTTP 405 Method Not Allowed if the request wasn’t a GET request, and HTTP 406 Not Acceptable if the request couldn’t accept text/plain responses.

5. Annotated Handlers

As we’ve seen, in addition to adding handlers directly, Armeria allows us to provide an arbitrary class with appropriately annotated methods and automatically map these methods to handlers. This can make writing complex servers much easier to manage.

These handlers are mounted using the ServerBuilder.annotatedService() method, providing an instance of our handler:

sb.annotatedService(new AnnotatedHandler());

Exactly how we construct this is up to us, meaning we can provide it with any dependencies necessary for it to work.

Within this class, we must have methods annotated with @Get@Post, @Put, @Delete or any of the other appropriate annotations. These annotations take as a parameter the URL mapping to use – following the exact same rules as before – and indicate that the annotated method is our handler:

@Get("/handler")
public String handler() {
    return "Hello, World!";
}

Note that we don’t have to follow the same method signature here as we did before. Instead, we can require arbitrary method parameters to be mapped onto the incoming request, and the response type will be mapped into an HttpResponse type.

5.1. Handler Parameters

Any parameters to our method of types ServiceRequestContext, HttpRequest, RequestHeaders, QueryParams or Cookies will be automatically provided from the request. This allows us to get access to details from the request in the same way as normal handlers:

@Get("/handler")
public String handler(ServiceRequestContext ctx) {
    return "Hello, " + ctx.path();
}

However, we can make this even easier. Armeria allows us to have arbitrary parameters annotated with @Param and these will automatically be populated from the request as appropriate:

@Get("/handler/{name}")
public String handler(@Param String name) {
    return "Hello, " + name;
}

If we compile our code with the -parameters flag, the name used will be derived from the parameter name. If not, or if we want a different name, we can provide it as a value to the annotation.

This annotation will provide our method with both path and query parameters. If the name used matches a path parameter, then this is the value provided. If not, a query parameter is used instead.

By default, all parameters are mandatory. If they can’t be provided from the request, then the handler won’t match. We can change this by using an Optional<> for the parameter, or else by annotating it with @Nullable or @Default.

5.2. Request Bodies

In addition to providing path and query parameters to our handler, we can also receive the request body. Armeria has a few ways to manage this, depending on what we need.

Any parameters of type byte[] or HttpData will be provided with the full, unmodified request body that we can do with as we wish:

@Post("/byte-body")
public String byteBody(byte[] body) {
    return "Length: " + body.length;
}

Alternatively, any String or CharSequence parameter that isn’t annotated to be used in some other way will be provided with the full request body, but in this case, it will have been decoded based on the appropriate character encoding:

@Post("/string-body")
public String stringBody(String body) {
    return "Hello: " + body;
}

Finally, if the request has a JSON-compatible content type then any parameter that’s not a byte[], HttpData, String, AsciiString, CharSequence or directly of type Object, and isn’t annotated to be used in some other way will have the request body deserialized into it using Jackson.

@Post("/json-body")
public String jsonBody(JsonBody body) {
    return body.name + " = " + body.score;
}

record JsonBody(String name, int score) {}

However, we can go a step further than this. Armeria gives us the option to write custom request converters. These are classes that implement the RequestConverterFunction interface:

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();
    }
}

Our converter can then do anything it needs to with full access to the incoming request to produce the desired value. If we can’t do this—because the request doesn’t match the parameter, for example—then we return RequestConverterFunction.fallthrough() to cause Armeria to carry on with the default processing.

We then need to ensure the request converter is used. This is done using the @RequestConverter annotation, attached to either the handler class, handler method, or the parameter in question:

@Post("/uppercase-body")
@RequestConverter(UppercasingRequestConverter.class)
public String uppercaseBody(String body) {
    return "Hello: " + body;
}

5.3. Responses

In much the same way as requests, we can also return arbitrary values from our handler function to be used as the HTTP response.

If we directly return an HttpResponse object, then this will be the complete response. If not, Armeria will convert the actual return value into the correct type.

By standard, Armeria is capable of a number of standard conversions:

  • null as an empty response body with an HTTP 204 No Content status code.
  • byte[] or HttpData as raw bytes with an application/octet-stream content type.
  • Anything implementing CharSequence – which includes String – as UTF-8 text content with a text/plain content type.
  • Anything implementing JsonNode from Jackson as JSON with an application/json content type.

In addition, if the handler method is annotated with @ProducesJson or @Produces(“application/json”) then any return value will be converted to JSON using Jackson:

@Get("/json-response")
@ProducesJson
public JsonBody jsonResponse() {
    return new JsonBody("Baeldung", 42);
}

However, we can also write custom response converters similar to how we wrote our custom request converter. These implement the ResponseConverterFunction interface. This is called with the return value from our handler function and must return an HttpResponse object:

public class UppercasingResponseConverter implements ResponseConverterFunction {
    @Override
    public HttpResponse handleException(ServiceRequestContext ctx, HttpRequest req, Throwable cause) {
        if (cause instanceof IllegalStateException) {
            return HttpResponse.of(HttpStatus.CONFLICT);
        }

        return ResponseConverterFunction.fallthrough();
    }
}

As before, we can do anything we need to produce the desired response or return ExceptionHandlerFunction.fallthrough() to fall back to the standard processing.

And, as before, we need to annotate our function with @ResponseConverter to tell it to use our new response converter:

@Post("/uppercase-response")
@ResponseConverter(UppercasingResponseConverter.class)
public String uppercaseResponse(String body) {
    return "Hello: " + body;
}

We can apply this to either the handler method or the class as a whole

5.4. Exceptions

In addition to being able to convert arbitrary responses to an appropriate HTTP response, we can also handle exceptions however we wish.

By default, Armeria will handle a few well-known exceptions. IllegalArgumentException produces an HTTP 400 Bad Request, and HttpStatusException and HttpResponseException are converted into the HTTP response they represent. Anything else will produce an HTTP 500 Internal Server Error response.

However, as with return values from our handler function, we can also write converters for exceptions. These implement the ExceptionHandlerFunction, which takes the thrown exception as input and returns the HTTP response for the client:

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 ResponseConverterFunction.fallthrough();
    }
}

As before, we can do anything we need to produce the correct response or return ExceptionHandlerFunction.fallthrough() to fall back to the standard handling.

And, as before, we need to annotate our function with @ExceptionHandler to tell it to handle the exception:

@Get("/exception")
@ExceptionHandler(ConflictExceptionHandler.class)
public String exception() {
    throw new IllegalStateException();
}

6. GraphQL

到目前為止,我們已經研究瞭如何使用 Armeria 設置 RESTful 處理程序。但是,它能做的事情遠不止這些,包括 GraphQL、Thrift 和 gRPC。

為了使用這些額外的協議,我們需要添加一些額外的依賴項。例如,添加 GraphQL 處理程序需要將 com.linecorp.armeria:armeria-graphql 依賴項添加到我們的項目中:


    com.linecorp.armeria
    armeria-graphql

一旦我們完成了這些,我們就可以使用 Armeria 通過使用 GraphqlService 來暴露 GraphQL 模式:

sb.service("/graphql",
  GraphqlService.builder().graphql(buildSchema()).build());

這會從 GraphQL java 庫中獲取一個 GraphQL 實例,我們可以以我們想要的方式構建它,並在指定的端點上暴露它。

7. Running a Client

In addition to writing server components, Armeria allows us to write clients that can communicate with these (or any) servers.

在編寫服務器組件之外,Armeria 允許我們編寫客户端,這些客户端可以與這些(或任何)服務器進行通信。

為了連接到 HTTP 服務,我們使用隨核心 Armeria 依賴項提供的 WebClient 類。我們可以直接使用它,無需進行任何配置,輕鬆地發出 HTTP 調用:

我們可以直接使用隨核心 Armeria 依賴項提供的 WebClient 類。我們可以直接使用它,無需進行任何配置,輕鬆地發出 HTTP 調用:

WebClient webClient = WebClient.of();
AggregatedHttpResponse response = webClient.get("http://localhost:8080/handler")
  .aggregate()
  .join();

The call here to WebClient.get() will make an HTTP GET request to the provided URL, which then returns a streaming HTTP response. We then call HttpResponse.aggregate() to get a CompletableFuture for the fully resolved HTTP response once it’s complete.

調用這裏面的 WebClient.get() 將發出 HTTP GET 請求到提供的 URL,然後返回一個流式 HTTP 響應。我們然後調用 HttpResponse.aggregate() 以獲取 CompletableFuture ,用於在完全完成 HTTP 響應後。

在獲得 AggregatedHttpResponse 之後,我們可以使用它來訪問 HTTP 響應的各個部分:

System.out.println(response.status());
System.out.println(response.headers());
System.out.println(response.content().toStringUtf8());

如果我們要創建 WebClient 以進行特定基礎 URL 的操作,也可以:

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. Complex Requests

We’ve seen how to make simple requests, but what about more complex cases? The methods that we’ve seen so far are actually just wrappers around the execute() method, which allows us to provide a much more complicated representation of an HTTP request:

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. Client Configuration

There are also a number of configuration parameters that we can use to tune the client itself. We can configure these by using a ClientFactory when we construct our WebClient.

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進行了簡要介紹。該庫的功能遠不止如此,不妨嘗試一下並親自體驗吧!

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.