Annotated services

Armeria provides a way to write an HTTP service using annotations. It helps a user make his or her code simple and easy to understand. A user is able to run an HTTP service by fewer lines of code using annotations as follows. hello() method in the example would be mapped to the path of /hello/{name} with an HTTP GET method.

ServerBuilder sb = Server.builder();
sb.annotatedService(new Object() {
    @Get("/hello/{name}")
    public HttpResponse hello(@Param("name") String name) {
        return HttpResponse.of(HttpStatus.OK,
                               MediaType.PLAIN_TEXT_UTF_8,
                               "Hello, %s!", name);
    }
});

Mapping HTTP service methods

To map a service method in an annotated HTTP service class to an HTTP path, it has to be annotated with one of HTTP method annotations. The following is the list of HTTP method annotations where each of them is mapped to an HTTP method.

To handle an HTTP request with a service method, you can annotate your service method simply as follows.

public class MyAnnotatedService {
    @Get("/hello")
    public HttpResponse hello() { ... }
}

There are 5 different path types that you can define:

  • Exact path, e.g. /hello or exact:/hello

    • a service method will handle the path exactly matched with the specified path.
  • Prefix path, e.g. prefix:/hello

    • a service method will handle every path which starts with the specified prefix.
  • Path containing path variables, e.g /hello/{name} or /hello/:name

    • a service method will handle the path matched with the specified path pattern. A path variable in the specified pattern may be mapped to a parameter of the service method.
  • Regular expression path, e.g. regex:^/hello/(?<name>.*)$

    • a service method will handle the path matched with the specified regular expression. If a named capturing group exists in the regular expression, it may be mapped to a parameter of the service method.
  • Glob pattern path, e.g. glob:/*/hello/**

    • a service method will handle the path matched with the specified glob pattern. Each wildcard is mapped to an index which starts with 0, so it may be mapped to a parameter of the service method.

You can get the value of a path variable, a named capturing group of the regular expression or wildcards of the glob pattern in your service method by annotating a parameter with @Param as follows. Please refer to Parameter injection for more information about @Param.

public class MyAnnotatedService {

    @Get("/hello/{name}")
    public HttpResponse pathVar(@Param("name") String name) { ... }

    @Get("regex:^/hello/(?<name>.*)$")
    public HttpResponse regex(@Param("name") String name) { ... }

    @Get("glob:/*/hello/**")
    public HttpResponse glob(@Param("0") String prefix,
                             @Param("1") String name) { ... }
}

Every service method in the examples so far had a single HTTP method annotation with it. What if you want to map more than one HTTP method or path to your service method? You can use @Path annotations to specify multiple paths, and use the HTTP method annotations without a path to map multiple HTTP methods, e.g.

public class MyAnnotatedService {
    @Get
    @Post
    @Put
    @Delete
    @Path("/hello")
    @Path("/hi")
    public HttpResponse greeting() { ... }
}

Every service method assumes that it returns an HTTP response with 200 OK or 204 No Content status according to its return type. If the return type is void or Void, 204 No Content would be applied. 200 OK would be applied for the other types. If you want to return an alternative status code for a method, you can use @StatusCode annotation as follows.

public class MyAnnotatedService {

    @StatusCode(201)
    @Post("/users/{name}")
    public User createUser(@Param("name") String name) { ... }

    // @StatusCode(200) would be applied by default.
    @Get("/users/{name}")
    public User getUser(@Param("name") String name) { ... }

    // @StatusCode(204) would be applied by default.
    @Delete("/users/{name}")
    public void deleteUser(@Param("name") String name) { ... }
}

You can define a service method which handles a request only if it contains a header or parameter the method requires. The following methods are bound to the same path /users but a request may be routed based on the client-type header.

public class MyAnnotatedService {

    // Handles a request which contains 'client-type: android' header.
    @Get("/users")
    @MatchesHeader("client-type=android")
    public User getUsers1() { ... }

    // Handles a request which contains 'client-type' header.
    // Any values of the 'client-type' header are accepted.
    @Get("/users")
    @MatchesHeader("client-type")
    public User getUsers2() { ... }

    // Handles a request which doesn't contain 'client-type' header.
    @Get("/users")
    public User getUsers3() { ... }
}

Parameter injection

Let's see the example in the above section again.

public class MyAnnotatedService {

    @Get("/hello/{name}")
    public HttpResponse pathvar(@Param("name") String name) { ... }

    @Get("regex:^/hello/(?<name>.*)$")
    public HttpResponse regex(@Param("name") String name) { ... }

    @Get("glob:/*/hello/**")
    public HttpResponse glob(@Param("0") String prefix,
                             @Param("1") String name) { ... }
}

A value of a parameter name is automatically injected as a String by Armeria. Armeria will try to convert the value appropriately if the parameter type is not String. IllegalArgumentException will be raised if the conversion fails or the parameter type is not one of the following supported types:

  • Primitive types and their wrapper types:
    • boolean or Boolean
    • byte or Byte
    • short or Short
    • integer or Integer
    • long or Long
    • float or Float
    • double or Double
  • Well-known JDK classes:
    • String, CharSequence or AsciiString
    • Enum
    • UUID
    • java.time package classes
      • Instant
      • Duration and Period
      • LocalDate, LocalDateTime and LocalTime,
      • OffsetDateTime and OffsetTime
      • ZonedDateTime, ZoneId and ZoneOffset
  • Custom types with either of the following public static methods/constructor taking a single String parameter. Note that these are scanned for in the order below. The first matching method being is used, and if that method throws an exception, no other methods will be tried and that exception will be propagated.
    • public static T of(String)
    • public static T valueOf(String)
    • public static T fromString(String)
    • public T(String) (constructor)

Note that you can omit the value of @Param if you compiled your code with -parameters javac option. In this case the variable name is used as the value.

public class MyAnnotatedService {
    @Get("/hello/{name}")
    public HttpResponse hello1(@Param String name) { ... }
}

Please refer to Configure -parameters javac option for more information.

Injecting a parameter as an Enum type

Enum type is also automatically converted if you annotate a parameter of your service method with @Param annotation. If your Enum type can be handled in a case-insensitive way, Armeria automatically converts the string value of a parameter to a value of Enum in a case-insensitive way. Otherwise, case-sensitive exact match will be performed.

public enum CaseInsensitive {
    ALPHA, BRAVO, CHARLIE
}
public enum CaseSensitive {
    ALPHA, alpha
}
public class MyAnnotatedService {

    @Get("/hello1/{there}")
    public HttpResponse hello1(@Param("there") CaseInsensitive there) {
        // 'there' is converted in a case-insensitive way.
    }

    @Get("/hello2/{there}")
    public HttpResponse hello2(@Param("there") CaseSensitive there) {
        // 'there' must be converted in a case-sensitive way.
        // So 'ALPHA' and 'alpha' are only acceptable.
    }
}

Getting a query parameter

When the value of @Param annotation is not shown in the path pattern, it will be handled as a parameter name of the query string of the request. If you have a service class like the example below and a user sends an HTTP GET request with URI of /hello1?name=armeria, the service method will get armeria as the value of parameter name.

If there is no parameter named name in the query string, the service method that requires it will not be invoked, but the client will get a 400 Bad Request response. If you want to allow null to be injected, you can use @Default annotation, @Nullable annotation or Optional<?> class, like demonstrated below in hello2(), hello3() and hello4() methods:

public class MyAnnotatedService {

    @Get("/hello1")
    // Will not be invoked but return a 400 status when 'name' parameter is missing.
    public HttpResponse hello1(@Param("name") String name) { ... }

    @Get("/hello2")
    public HttpResponse hello2(@Param("name") @Default("armeria") String name) { ... }

    @Get("/hello3")
    public HttpResponse hello3(@Param("name") @Nullable String name) {
        String clientName = name != null ? name : "armeria";
        // ...
    }

    @Get("/hello4")
    public HttpResponse hello4(@Param("name") Optional<String> name) {
        String clientName = name.orElse("armeria");
        // ...
    }
}

If multiple parameters exist with the same name in a query string, they can be injected as a List<?> or Set<?>, e.g. /hello1?number=1&number=2&number=3. You can use @Default annotation or Optional<?> class here, too.

public class MyAnnotatedService {
    @Get("/hello1")
    public HttpResponse hello1(@Param("number") List<Integer> numbers) { ... }

    // If there is no 'number' parameter, the default value "1" will be converted to
    // Integer 1, then it will be added to the 'numbers' list.
    @Get("/hello2")
    public HttpResponse hello2(@Param("number") @Default("1")
                               List<Integer> numbers) { ... }

    @Get("/hello3")
    public HttpResponse hello3(@Param("number")
                               Optional<List<Integer>> numbers) { ... }
}

If you specify query delimiter while building an annotated service, or a @Delimiter annotation with a query parameter, a single parameter will be converted into separated values if the parameter is mapped to a List<?> or Set<?>, e.g. /hello1?number=1,2,3.

sb.annotatedService()
  .queryDelimiter(",")
  .build(new MyAnnotatedService());
public class MyAnnotatedService {
    @Get("/hello1")
    public HttpResponse hello1(@Param("number") @Delimiter(",") List<Integer> numbers) { ... }
}

If an HTTP POST request with a Content-Type: x-www-form-urlencoded header is received and no @Param value appears in the path pattern, Armeria will aggregate the received request and decode its body as a URL-encoded form. After that, Armeria will inject the decoded value into the parameter.

public class MyAnnotatedService {
    @Post("/hello4")
    public HttpResponse hello4(@Param("name") String name) {
        // 'x-www-form-urlencoded' request will be aggregated.
        // The other requests may get a '400 Bad Request' because
        // there is no way to inject a mandatory parameter 'name'.
    }
}

Getting an HTTP header

Armeria also provides @Header annotation to inject an HTTP header value into a parameter. The parameter annotated with @Header can also be specified as one of the built-in types as follows. @Default and Optional<?> are also supported. @Header annotation also supports List<?> or Set<?> because HTTP headers can be added several times with the same name.

public class MyAnnotatedService {

    @Get("/hello1")
    public HttpResponse hello1(@Header("Authorization") String auth) { ... }

    @Post("/hello2")
    public HttpResponse hello2(@Header("Content-Length") long contentLength) { ... }

    @Post("/hello3")
    public HttpResponse hello3(@Header("Forwarded") List<String> forwarded) { ... }

    @Post("/hello4")
    public HttpResponse hello4(@Header("Forwarded")
                               Optional<Set<String>> forwarded) { ... }
}

Note that you can omit the value of @Header if you compiled your code with -parameters javac option. Read Parameter injection for more information. In this case, the variable name is used as the value, but it will be converted to hyphen-separated lowercase string to be suitable for general HTTP header names. e.g. a variable name contentLength or content_length will be converted to content-length as the value of @Header.

public class MyAnnotatedService {
    @Post("/hello2")
    public HttpResponse hello2(@Header long contentLength) { ... }
}

Other classes automatically injected

The following classes are automatically injected when you specify them on the parameter list of your method.

public class MyAnnotatedService {

    @Get("/hello1")
    public HttpResponse hello1(ServiceRequestContext ctx, HttpRequest req) {
        // Use the context and request inside a method.
    }

    @Post("/hello2")
    public HttpResponse hello2(AggregatedHttpRequest aggregatedRequest) {
        // Armeria aggregates the received HttpRequest and
        // calls this method with the aggregated request.
    }

    @Get("/hello3")
    public HttpResponse hello3(QueryParams params) {
        // 'params' holds the parameters parsed from a query string of a request.
    }

    @Post("/hello4")
    public HttpResponse hello4(QueryParams params) {
        // If a request has a url-encoded form as its body,
        // it can be accessed via 'params'.
    }

    @Post("/hello5")
    public HttpResponse hello5(Cookies cookies) {
        // If 'Cookie' header exists, it will be injected into
        // the specified 'cookies' parameter.
    }
}

Handling exceptions

It is often useful to extract exception handling logic from service methods into a separate common class. Armeria provides @ExceptionHandler annotation to transform an exception into a response. You can write your own exception handler by implementing ExceptionHandlerFunction interface and annotate your service object or method with @ExceptionHandler annotation. Here is an example of an exception handler. If your exception handler is not able to handle a given exception, you can call ExceptionHandlerFunction.fallthrough() to pass the exception to the next exception handler.

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

        // To the next exception handler.
        return ExceptionHandlerFunction.fallthrough();
    }
}

You can annotate at class level to catch an exception from every method in your service class.

@ExceptionHandler(MyExceptionHandler.class)
public class MyAnnotatedService {
    @Get("/hello")
    public HttpResponse hello() { ... }
}

You can also annotate at method level to catch an exception from a single method in your service class.

public class MyAnnotatedService {
    @Get("/hello")
    @ExceptionHandler(MyExceptionHandler.class)
    public HttpResponse hello() { ... }
}

If there is no exception handler which is able to handle an exception, the exception would be passed to the default exception handler. It handles IllegalArgumentException, HttpStatusException and HttpResponseException by default. IllegalArgumentException would be converted into 400 Bad Request response, and the other two exceptions would be converted into a response with the status code which they are holding. For another exceptions, 500 Internal Server Error would be sent to the client.

Conversion between an HTTP message and a Java object

Converting an HTTP request to a Java object

In some cases like receiving a JSON document from a client, it may be useful to convert the document to a Java object automatically. Armeria provides @RequestConverter and @RequestObject annotations so that such conversion can be done conveniently. You can write your own request converter by implementing RequestConverterFunction as follows. Similar to the exception handler, you can call RequestConverterFunction.fallthrough() when your request converter is not able to convert the request.

public class ToEnglishConverter implements RequestConverterFunction {
    @Override
    public Object convertRequest(ServiceRequestContext ctx,
                                 AggregatedHttpRequest request,
                                 Class<?> expectedResultType) {
        if (expectedResultType == Greeting.class) {
            // Convert the request to a Java object.
            return new Greeting(translateToEnglish(request.contentUtf8()));
        }

        // To the next request converter.
        return RequestConverterFunction.fallthrough();
    }

    private String translateToEnglish(String greetingInAnyLanguage) { ... }
}

Then, you can write your service method as follows. Custom request objects will be converted automatically by the converters you registered with @RequestConverter annotation. Note that @RequestConverter annotation can be specified on a class, a method or a parameter in an annotated service, and its scope is determined depending on where it is specified.

@RequestConverter(ToEnglishConverter.class)
public class MyAnnotatedService {

    @Post("/hello")
    public HttpResponse hello(Greeting greeting) {
        // ToEnglishConverter will be used to convert a request.
        // ...
    }

    @Post("/hola")
    @RequestConverter(ToSpanishConverter.class)
    public HttpResponse hola(Greeting greeting) {
        // ToSpanishConverter will be tried to convert a request first.
        // ToEnglishConverter will be used if ToSpanishConverter fell through.
        // ...
    }

    @Post("/greet")
    public HttpResponse greet(@RequestConverter(ToGermanConverter.class)
                              Greeting greetingInGerman,
                              Greeting greetingInEnglish) {
        // For the 1st parameter 'greetingInGerman':
        // ToGermanConverter will be tried to convert a request first.
        // ToEnglishConverter will be used if ToGermanConverter fell through.
        //
        // For the 2nd parameter 'greetingInEnglish':
        // ToEnglishConverter will be used to convert a request.
        // ...
    }
}

Armeria also provides built-in request converters such as, a request converter for a Java Bean, JacksonRequestConverterFunction for a JSON document, StringRequestConverterFunction for a string and ByteArrayRequestConverterFunction for binary data. They will be applied after your request converters, so you don't have to specify any @RequestConverter annotations:

public class MyAnnotatedService {

    // JacksonRequestConverterFunction will work for the content type of
    // 'application/json' or one of '+json' types.
    @Post("/hello1")
    public HttpResponse hello1(JsonNode body) { ... }

    @Post("/hello2")
    public HttpResponse hello2(MyJsonRequest body) { ... }

    // StringRequestConverterFunction will work regardless of the content type.
    @Post("/hello3")
    public HttpResponse hello3(String body) { ... }

    @Post("/hello4")
    public HttpResponse hello4(CharSequence body) { ... }

    // ByteArrayRequestConverterFunction will work regardless of the content type.
    @Post("/hello5")
    public HttpResponse hello5(byte[] body) { ... }

    @Post("/hello6")
    public HttpResponse hello6(HttpData body) { ... }
}

Injecting value of parameters and HTTP headers into a Java object

Armeria provides a generic built-in request converter that converts a request into a Java object. Just define a plain old Java class and specify it as a parameter of your service method.

public class MyAnnotatedService {
    @Post("/hello")
    public HttpResponse hello(MyRequestObject myRequestObject) { ... }
}

We also need to define the MyRequestObject class which was used in the method hello() above. To tell Armeria which constructor parameter, setter method or field has to be injected with what value, we should put @Param, @Header, @RequestObject annotations on any of the following elements:

  • Fields
  • Constructors with only one parameter
  • Methods with only one parameter
  • Constructor parameters
  • Method parameters
public class MyRequestObject {
    @Param("name") // This field will be injected by the value of parameter "name".
    private String name;

    @Header("age") // This field will be injected by the value of HTTP header "age".
    private int age;

    @RequestObject // This field will be injected by another request converter.
    private MyAnotherRequestObject obj;

    // You can omit the value of @Param or @Header if you compiled your code with
    // `-parameters` javac option.
    @Param // This field will be injected by the value of parameter "gender".
    private String gender;

    @Header // This field will be injected by the value of "accept-language" header.
    private String acceptLanguage;

    @Param("address") // You can annotate a single-parameter method
                      // with @Param or @Header.
    public void setAddress(String address) { ... }

    @Header("id") // You can annotate a single-parameter constructor
    @Default("0") // with @Param or @Header.
    public MyRequestObject(long id) { ... }

    // You can annotate all parameters of method or constructor with @Param or @Header.
    public void init(@Header("permissions") String permissions,
                     @Param("client-id") @Default("0") int clientId)
}

The usage of @Param or @Header annotations on Java object elements is much like using them on the parameters of a service method because even you can use @Default and @RequestObject annotations defined there. Please refer to Parameter injection, and Getting an HTTP header for more information.

Converting a Java object to an HTTP response

Every object returned by an annotated service method can be converted to an HTTP response message by response converters, except for HttpResponse and AggregatedHttpResponse which are already in a form of response message. You can also write your own response converter by implementing ResponseConverterFunction as follows. Also similar to RequestConverterFunction, you can call ResponseConverterFunction.fallthrough() when your response converter is not able to convert the result to an HttpResponse.

public class MyResponseConverter implements ResponseConverterFunction {
    @Override
    HttpResponse convertResponse(ServiceRequestContext ctx,
                                 ResponseHeaders headers,
                                 @Nullable Object result,
                                 HttpHeaders trailers) throws Exception {
        if (result instanceof MyObject) {
            return HttpResponse.of(HttpStatus.OK,
                                   MediaType.PLAIN_TEXT_UTF_8,
                                   "Hello, %s!", ((MyObject) result).processedName(),
                                   trailers);
        }

        // To the next response converter.
        return ResponseConverterFunction.fallthrough();
    }
}

You can annotate your service method and class as follows.

@ResponseConverter(MyResponseConverter.class)
public class MyAnnotatedService {

    @Post("/hello")
    public MyObject hello() {
        // MyResponseConverter will be used to make a response.
        // ...
    }

    @Post("/hola")
    @ResponseConverter(MySpanishResponseConverter.class)
    public MyObject hola() {
        // MySpanishResponseConverter will be tried to convert MyObject
        // to a response first, and then MyResponseConverter will be used
        // if MySpanishResponseConverter fell through.
        // ...
    }
}

Armeria supports Media type negotiation. So you may want to get a negotiated media type in order to set a Content-Type header on your response. In this case, you can access it in your response converter as follows.

public class MyResponseConverter implements ResponseConverterFunction {
    @Override
    HttpResponse convertResponse(ServiceRequestContext ctx,
                                 ResponseHeaders headers,
                                 @Nullable Object result,
                                 HttpHeaders trailers) throws Exception {
        MediaType mediaType = ctx.negotiatedResponseMediaType();
        if (mediaType != null) {
            // Do something based on the media type.
            // ...
        }
    }
}

Even if you do not specify any @ResponseConverter annotation, the response object can be converted into an HttpResponse by one of the following response converters which performs the conversion based on the negotiated media type and the type of the object.

  • JacksonResponseConverterFunction

    • converts an object to a JSON document if the negotiated media type is application/json. JsonNode object can be converted to a JSON document even if there is no media type negotiated.
  • StringResponseConverterFunction

    • converts an object to a string if the negotiated main media type is one of text types. If there is no media type negotiated, String and CharSequence object will be written as a text with Content-Type: text/plain; charset=utf-8 header.
  • ByteArrayResponseConverterFunction

    • converts an object to a byte array. Only HttpData and byte[] will be handled even if the negotiated media type is application/binary or application/octet-stream. If there is no media type negotiated, HttpData and byte[] object will be written as a binary with Content-Type: application/binary header.

Let's see the following example about the default response conversion.

public class MyAnnotatedService {

    // JacksonResponseConverterFunction will convert the return values
    // to JSON documents:
    @Get("/json1")
    @ProducesJson    // the same as @Produces("application/json; charset=utf-8")
    public MyObject json1() { ... }

    @Get("/json2")
    public JsonNode json2() { ... }

    // StringResponseConverterFunction will convert the return values to strings:
    @Get("/string1")
    @ProducesText    // the same as @Produces("text/plain; charset=utf-8")
    public int string1() { ... }

    @Get("/string2")
    public CharSequence string2() { ... }

    // ByteArrayResponseConverterFunction will convert the return values
    // to byte arrays:
    @Get("/byte1")
    @ProducesBinary  // the same as @Produces("application/binary")
    public HttpData byte1() { ... }

    @Get("/byte2")
    public byte[] byte2() { ... }
}

Specifying a blocking task executor

An annotated service is executed by an EventLoop, so you must not make a blocking call within the EventLoop. If you want to make a blocking call, you should delegate the call to another thread. You can use your own thread or a blocking task executor that Armeria provides.

If you specify the @Blocking annotation to the method, the method will be executed by the blocking task executor that is returned by ServiceRequestContext.blockingTaskExecutor():

public class MyAnnotatedService {

    @Blocking // 👈
    @Get("/hello")
    public HttpResponse hello(ServiceRequestContext ctx) {
        // ctx.blockingTaskExecutor() executes this method.
        assert !ctx.eventLoop().inEventLoop();

        try {
            Statement stmt = ...;
            // Make a blocking call.
            ResultSet resultSet = stmt.executeQuery(...);
            return responseFrom(resultSet);
        } catch (Throwable t) {
            return HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

You can also submit a task to the ServiceRequestContext.blockingTaskExecutor() or any Executor instead of using the @Blocking annotation:

public class MyAnnotatedService {

    @Get("/hello")
    public HttpResponse hello(ServiceRequestContext ctx) {
        // The current thread is an event loop.
        assert ctx.eventLoop().inEventLoop();

        CompletableFuture<HttpResponse> future = new CompletableFuture<>();
        // Delegate a blocking call to the blocking task executor.
        ctx.blockingTaskExecutor().execute(() -> { // 👈
            try {
                Statement stmt = ...;
                // Make a blocking call.
                ResultSet resultSet = stmt.executeQuery(...);
                HttpResponse res = responseFrom(resultSet);
                future.complete(res);
            } catch (Throwable t) {
                future.complete(HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR));
            }
        });
        return HttpResponse.from(future);
    }

Using ServerBuilder to configure converters and exception handlers

You can specify converters and exception handlers using ServerBuilder, without using the annotations explained in the previous sections:

sb.annotatedService(new MyAnnotatedService(),
                    // Exception handlers
                    new MyExceptionHandler(),
                    // Converters
                    new MyRequestConverter(), new MyResponseConverter());

Also, they have a different method signature for conversion and exception handling so you can even write them in a single class and add it to your ServerBuilder at once, e.g.

public class MyAllInOneHandler implements RequestConverterFunction,
                                          ResponseConverterFunction,
                                          ExceptionHandlerFunction {
    @Override
    public Object convertRequest(ServiceRequestContext ctx,
                                 AggregatedHttpRequest request,
                                 Class<?> expectedResultType) { ... }

    @Override
    HttpResponse convertResponse(ServiceRequestContext ctx,
                                 ResponseHeaders headers,
                                 @Nullable Object result,
                                 HttpHeaders trailers) throws Exception { ... }

    @Override
    public HttpResponse handleException(ServiceRequestContext ctx,
                                        HttpRequest req, Throwable cause) { ... }
}

// ...

sb.annotatedService(new MyAnnotatedService(), new MyAllInOneHandler());

When you specify exception handlers in a mixed manner like below, they will be evaluated in the following order commented. It is also the same as the evaluation order of the converters.

@ExceptionHandler(MyClassExceptionHandler3.class)           // order 3
@ExceptionHandler(MyClassExceptionHandler4.class)           // order 4
public class MyAnnotatedService {
    @Get("/hello")
    @ExceptionHandler(MyMethodExceptionHandler1.class)      // order 1
    @ExceptionHandler(MyMethodExceptionHandler2.class)      // order 2
    public HttpResponse hello() { ... }
}

// ...

sb.annotatedService(new MyAnnotatedService(),
                    new MyGlobalExceptionHandler5(),        // order 5
                    new MyGlobalExceptionHandler6());       // order 6

Returning a response

In the earlier examples, the annotated service methods only return HttpResponse, however there are more response types which can be used in the annotated service.

  • HttpResponse and AggregatedHttpResponse

    • It will be sent to the client without any modification. If an exception is raised while the response is being sent, exception handlers will handle it. If no message has been sent to the client yet, the exception handler can send an HttpResponse instead.
  • HttpResult

    • It contains the HttpHeaders and the object which can be converted into HTTP response body by response converters. A user can customize the HTTP status and headers including the trailers, with this type.
    public class MyAnnotatedService {
        @Get("/users")
        public HttpResult<List<User>> getUsers(@Param int start) {
            List<User> users = ...;
            ResponseHeaders headers = ResponseHeaders.builder()
                .status(HttpStatus.OK)
                .add(HttpHeaderNames.LINK,
                     String.format("<https://example.com/users?from=%s>; rel=\"next\"",
                                   start + 10))
                .build();
            return HttpResult.of(headers, users);
        }
    }
  • Reactive Streams Publisher

    • All objects which are produced by the publisher will be collected, then the collected ones will be converted to an HttpResponse by response converters. If a single object is produced, it will be passed into the response converters as it is. But if multiple objects are produced, they will be passed into the response converters as a list. If the producer produces an error, exception handlers will handle it. Note that RxJava ObservableSource will be treated in the same way as Publisher if you add armeria-rxjava to the dependencies.
  • CompletionStage and CompletableFuture

    • An object which is generated by the CompletionStage will be converted to an HttpResponse by response converters. If the CompletionStage completes exceptionally, exception handlers will handle the cause.
  • Other types

Decorating an annotated service

Every HttpService can be wrapped by another HttpService in Armeria (Refer to Decorating a service for more information). Simply, you can write your own decorator by implementing DecoratingHttpServiceFunction interface as follows.

public class MyDecorator implements DecoratingHttpServiceFunction {
    @Override
    public HttpResponse serve(HttpService delegate,
                              ServiceRequestContext ctx,
                              HttpRequest req) {
        // ... Do something ...
        return delegate.serve(ctx, req);
    }
}

Then, annotate your class or method with a @Decorator annotation. In the following example, MyDecorator will handle a request first, then AnotherDecorator will handle the request next, and finally hello() method will handle the request.

@Decorator(MyDecorator.class)
public class MyAnnotatedService {
    @Decorator(AnotherDecorator.class)
    @Get("/hello")
    public HttpResponse hello() { ... }
}

Decorating an annotated service with a custom decorator annotation

As you read earlier, you can write your own decorator with DecoratingHttpServiceFunction interface. If your decorator does not require any parameter, that is fine. However, what if your decorator requires a parameter? In this case, you can create your own decorator annotation. Let's see the following custom decorator annotation which applies LoggingService to an annotated service.

@DecoratorFactory(LoggingDecoratorFactoryFunction.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface LoggingDecorator {

    // Specify parameters for your decorator like below.
    LogLevel requestLogLevel() default LogLevel.TRACE;

    LogLevel successfulResponseLogLevel() default LogLevel.TRACE;

    LogLevel failureResponseLogLevel() default LogLevel.WARN;

    float samplingRate() default 1.0f;

    // A special parameter in order to specify the order of a decorator.
    int order() default 0;
}

public final class LoggingDecoratorFactoryFunction
        implements DecoratorFactoryFunction<LoggingDecorator> {

    @Override
    public Function<? super HttpService, ? extends HttpService>
    newDecorator(LoggingDecorator parameter) {
        return LoggingService
            .builder()
            .requestLogLevel(parameter.requestLogLevel())
            .successfulResponseLogLevel(parameter.successfulResponseLogLevel())
            .failureResponseLogLevel(parameter.failureResponseLogLevel())
            .samplingRate(parameter.samplingRate())
            .newDecorator();
    }
}

You can see @DecoratorFactory annotation at the first line of the example. It specifies a factory class which implements DecoratorFactoryFunction interface. The factory will create an instance of LoggingService with parameters which you specified on the class or method like below.

public class MyAnnotatedService {
    @LoggingDecorator(requestLogLevel = LogLevel.INFO)
    @Get("/hello1")
    public HttpResponse hello1() { ... }

    @LoggingDecorator(requestLogLevel = LogLevel.DEBUG, samplingRate = 0.05)
    @Get("/hello2")
    public HttpResponse hello2() { ... }
}

Evaluation order of decorators

Note that the evaluation order of the decorators is slightly different from that of the converters and exception handlers. As you read in Using ServerBuilder to configure converters and exception handlers, both the converters and exception handlers are applied in the order of method-level ones, class-level ones and global ones. Unlike them, decorators are applied in the opposite order as follows, because it is more understandable for a user to apply from the outer decorators to the inner decorators, which means the order of global decorators, class-level decorators and method-level decorators.

@Decorator(MyClassDecorator2.class)                 // order 2
@Decorator(MyClassDecorator3.class)                 // order 3
public class MyAnnotatedService {

    @Get("/hello")
    @Decorator(MyMethodDecorator4.class)            // order 4
    @Decorator(MyMethodDecorator5.class)            // order 5
    public HttpResponse hello() { ... }
}

// ...

sb.annotatedService(new MyAnnotatedService(),
                    new MyGlobalDecorator1());      // order 1

The first rule is as explained before. However, if your own decorator annotations and @Decorator annotations are specified in a mixed order like below, you need to clearly specify their order using @Decorator.order() attribute of the annotation. In the following example, you cannot make sure in what order they decorate the service because Java collects repeatable annotations like @Decorator into a single container annotation like @Decorators so it does not know the specified order between @Decorator and @LoggingDecorator.

public class MyAnnotatedService {

    @Get("/hello")
    @Decorator(MyMethodDecorator1.class)
    @LoggingDecorator
    @Decorator(MyMethodDecorator2.class)
    public HttpResponse hello() { ... }
}

To enforce the evaluation order of decorators, you can use order() attribute. Lower the order value is, earlier the decorator will be executed. The default value of order() attribute is 0. The order() attribute is applicable only to class-level and method-level decorators.

With the following example, the hello() will be executed with the following order:

  1. MyGlobalDecorator1
  2. MyMethodDecorator1
  3. LoggingDecorator
  4. MyMethodDecorator2
  5. MyAnnotatedService.hello()
public class MyAnnotatedService {

    @Get("/hello")
    @Decorator(value = MyMethodDecorator1.class, order = 1)
    @LoggingDecorator(order = 2)
    @Decorator(value = MyMethodDecorator2.class, order = 3)
    public HttpResponse hello() { ... }
}

// Global-level decorators will not be affected by 'order'.
sb.annotatedService(new MyAnnotatedService(),
                    new MyGlobalDecorator1());

Note that you can even make a method-level decorator executed before a class-level decorator by adjusting the order() attribute:

@LoggingDecorator
public class MyAnnotatedService {

    // LoggingDecorator -> MyMethodDecorator1 -> hello1()
    @Get("/hello1")
    @Decorator(MyMethodDecorator1.class)
    public HttpResponse hello1() { ... }

    // MyMethodDecorator1 -> LoggingDecorator -> hello2()
    @Get("/hello2")
    @Decorator(value = MyMethodDecorator1.class, order = -1)
    public HttpResponse hello2() { ... }
}

If you built a custom decorator annotation like @LoggingDecorator, it is recommended to add an order() attribute so that the user of the custom annotation is able to adjust the order value of the decorator:

public @interface MyDecoratorAnnotation {

    // Define your attributes.
    int myAttr1();

    // A special parameter in order to specify the order of a decorator.
    int order() default 0;
}

Media type negotiation

Armeria provides @Produces and @Consumes annotations to support media type negotiation. It is not necessary if you have only one service method for a path and an HTTP method. However, assume that you have multiple service methods for the same path and the same HTTP method as follows.

public class MyAnnotatedService {

    @Get("/hello")
    public HttpResponse hello1() {
        // Return a text document to the client.
        return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8,
                               "Armeria");
    }

    @Get("/hello")
    public HttpResponse hello2() {
        // Return a JSON object to the client.
        return HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8,
                               "{ \"name\": \"Armeria\" }");
    }
}

If the media type is not specified on any methods bound to the same path pattern, the first method declared will be used and the other methods will be ignored. In this example, hello1() will be chosen and the client will always receive a text document. What if you want to get a JSON object from the path /hello? You can just specify the type of the content which your method produces as follows and add an Accept header to your client request.

public class MyAnnotatedService {

    @Get("/hello")
    @Produces("text/plain")
    public HttpResponse helloText() {
        // Return a text document to the client.
        return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8,
                               "Armeria");
    }

    @Get("/hello")
    @Produces("application/json")
    public HttpResponse helloJson() {
        // Return a JSON object to the client.
        return HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8,
                               "{ \"name\": \"Armeria\" }");
    }
}

A request like the following would get a text document:

GET /hello HTTP/1.1
Accept: text/plain

A request like the following would get a JSON object:

GET /hello HTTP/1.1
Accept: application/json

If a client sends a request without an Accept header (or sending an Accept header with an unsupported content type), it would be usually mapped to helloJson() method because the methods are sorted by the name of the type in an alphabetical order.

In this case, you can adjust the order of the methods with @Order annotation. The default value of @Order annotation is 0. If you set the value less than 0, the method is used earlier than the other methods, which means that it would be used as a default when there is no matched produce type. In this example, it would also make the same effect to annotate helloJson() with @Order(1).

public class MyAnnotatedService {

    @Order(-1)
    @Get("/hello")
    @Produces("text/plain")
    public HttpResponse helloText() {
        // Return a text document to the client.
        return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8,
                               "Armeria");
    }

    @Get("/hello")
    @Produces("application/json")
    public HttpResponse helloJson() {
        // Return a JSON object to the client.
        return HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8,
                               "{ \"name\": \"Armeria\" }");
    }
}

Next, let's learn how to handle a Content-Type header of a request. Assume that there are two service methods that expect a text document and a JSON object as a content of a request, respectively. You can annotate them with @Consumes annotation.

public class MyAnnotatedService {

    @Post("/hello")
    @Consumes("text/plain")
    public HttpResponse helloText(AggregatedHttpRequest request) {
        // Get a text content by calling request.contentAscii().
    }

    @Post("/hello")
    @Consumes("application/json")
    public HttpResponse helloJson(AggregatedHttpRequest request) {
        // Get a JSON object by calling request.contentUtf8().
    }
}

A request like the following would be handled by helloText() method:

POST /hello HTTP/1.1
Content-Type: text/plain
Content-Length: 7

Armeria

A request like the following would be handled by helloJson() method:

POST /hello HTTP/1.1
Content-Type: application/json
Content-Length: 21

{ "name": "Armeria" }

However, if a client sends a request with a Content-Type: application/octet-stream header which is not specified with @Consumes, the client would get an HTTP status code of 415 which means Unsupported Media Type. If you want to make one of the methods catch-all, you can remove the annotation as follows. helloCatchAll() method would accept every request except for the request with a Content-Type: application/json header.

public class MyAnnotatedService {

    @Post("/hello")
    public HttpResponse helloCatchAll(AggregatedHttpRequest request) {
        // Get a content by calling request.content() and handle it as a text document or something else.
    }

    @Post("/hello")
    @Consumes("application/json")
    public HttpResponse helloJson(AggregatedHttpRequest request) {
        // Get a JSON object by calling request.contentUtf8().
    }
}

Creating user-defined media type annotations

Armeria provides pre-defined annotations such as @ConsumesJson, @ConsumesText, @ConsumesBinary and @ConsumesOctetStream which are aliases for @Consumes("application/json; charset=utf-8"), @Consumes("text/plain; charset=utf-8"), @Consumes("application/binary") and @Consumes("application/octet-stream") respectively. Also, @ProducesJson, @ProducesText, @ProducesBinary and @ProducesOctetStream are provided in the same manner.

If there is no annotation that meets your need, you can define your own annotations for @Consumes and @Produces as follows. Specifying your own annotations is recommended because writing a media type with a string is more error-prone.

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Consumes("application/xml")
public @interface MyConsumableType {}

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Produces("application/xml")
public @interface MyProducibleType {}

Then, you can annotate your service method with your annotation as follows.

public class MyAnnotatedService {
    @Post("/hello")
    @MyConsumableType  // the same as @Consumes("application/xml")
    @MyProducibleType  // the same as @Produces("application/xml")
    public MyResponse hello(MyRequest myRequest) { ... }
}

Specifying additional response headers/trailers

Armeria provides a way to configure additional headers/trailers via annotation, @AdditionalHeader for HTTP headers and @AdditionalTrailer for HTTP trailers.

You can annotate your service method with the annotations as follows.

import com.linecorp.armeria.server.annotation.AdditionalHeader;
import com.linecorp.armeria.server.annotation.AdditionalTrailer;

@AdditionalHeader(name = "custom-header", value = "custom-value")
@AdditionalTrailer(name = "custom-trailer", value = "custom-value")
public class MyAnnotatedService {
    @Get("/hello")
    @AdditionalHeader(name = "custom-header-2", value = "custom-value")
    @AdditionalTrailer(name = "custom-trailer-2", value = "custom-value")
    public HttpResponse hello() { ... }
}

The @AdditionalHeader or @AdditionalTrailer specified at the method level takes precedence over what's specified at the class level if it has the same name, e.g.

@AdditionalHeader(name = "custom-header", value = "custom-value")
@AdditionalTrailer(name = "custom-trailer", value = "custom-value")
public class MyAnnotatedService {
    @Get("/hello")
    @AdditionalHeader(name = "custom-header", value = "custom-overwritten")
    @AdditionalTrailer(name = "custom-trailer", value = "custom-overwritten")
    public HttpResponse hello() { ... }
}

In this case, the values of the HTTP header named custom-header and the HTTP trailer named custom-trailer will be custom-overwritten, not custom-value.

Note that the trailers will not be injected into the responses with the following HTTP status code, because they always have an empty content.

Status code

Description

204

No content

205

Reset content

304

Not modified

Using a composite annotation

To avoid specifying a common set of annotations repetitively, you may want to create a composite annotation which is annotated by other annotations. For example, let's assume that there is a service class like the below:

public class MyAnnotatedService {

    @Post("/create")
    @ConsumesJson
    @ProducesJson
    @LoggingDecorator
    @MyAuthenticationDecorator
    public HttpResponse create() { ... }

    @Post("/update")
    @ConsumesJson
    @ProducesJson
    @LoggingDecorator
    @MyAuthenticationDecorator
    public HttpResponse update() { ... }
}

In the above example, you had to add the same 4 annotations to the two different methods. It is obviously too verbose and duplicate, so we could simplify them by creating a composite annotation like the following:

@Retention(RetentionPolicy.RUNTIME)
@ConsumesJson
@ProducesJson
@LoggingDecorator
@MyAuthenticationDecorator
public @interface MyCreateOrUpdateApiSpec {}

Now, let's rewrite the service class with the composite annotation. It is definitely less verbose than before. Moreover, you don't need to update both create() and update() but only MyCreateOrUpdateApiSpec when you add more common annotations to them.

public class MyAnnotatedService {

    @Post("/create")
    @MyCreateOrUpdateApiSpec
    public HttpResponse create() { ... }

    @Post("/update")
    @MyCreateOrUpdateApiSpec
    public HttpResponse update() { ... }
}

Specifying the service name

A service name is human-readable string that is often used as a meter tag or distributed trace's span name. By default, an annotated service uses its class name as its service name. You can override it by annotating a class or method with @ServiceName like the following:

public class MyService {
    @Get("/")
    public String get(ServiceRequestContext ctx) {
        ctx.log().whenAvailable(RequestLogProperty.NAME).thenAccept(log -> {
            assert log.serviceName().equals(MyService.class.getName());
        });
    }
}

// Override the default service name by the class annotation
@ServiceName("my-service")
public class MyService {
    @Get("/")
    public String get(ServiceRequestContext ctx) {
        ctx.log().whenAvailable(RequestLogProperty.NAME).thenAccept(log -> {
            assert log.serviceName().equals("my-service");
        });
    }

    // Override the default service name by the method annotation
    @ServiceName("my-post-service")
    @Post("/")
    public String post(ServiceRequestContext ctx) {
        ctx.log().whenAvailable(RequestLogProperty.NAME).thenAccept(log -> {
            assert log.serviceName().equals("my-post-service");
        });
    }
}

// Or the default name could get overridden programmatically using a decorator.
ServerBuilder sb = Server.builder();
sb.annotatedService("/", new MyService(), service -> {
    return service.decorate((delegate, ctx, req) -> {
        ctx.logBuilder().name("my-decorated-service", ctx.method().name());
        return delegate.serve(ctx, req);
    });
});