Annotated services
Table of contents
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
orexact:/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.
- a service method will handle the path matched with the specified glob pattern. Each wildcard is mapped to
an index which starts with
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
orBoolean
byte
orByte
short
orShort
integer
orInteger
long
orLong
float
orFloat
double
orDouble
- Well-known JDK classes:
String
,CharSequence
orAsciiString
Enum
UUID
java.time
package classesInstant
Duration
andPeriod
LocalDate
,LocalDateTime
andLocalTime
,OffsetDateTime
andOffsetTime
ZonedDateTime
,ZoneId
andZoneOffset
- 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) { ... }
}
Getting a custom attribute value
Armeria also provides the @Attribute
annotation to inject a
custom attribute value of a RequestContext
into the annotated
parameter. The @Attribute
annotation has two attributes:
value
and prefix
, as shown in the example below. If both prefix
and value
are specified, AttributeKey.valueOf(prefix, value)
will
be used to look up the AttributeKey
of the custom attribute.
If prefix
is unspecified, the class of the annotated service will be
tried as the prefix
(e.g. Attribute.valueOf(MyAnnotatedService.class, value)
),
and then if there's no such attribute, Attribute.valueOf(value)
will be tried as a fallback.
public class MyAttributes {
public static final AttributeKey<String> USERNAME =
AttributeKey.valueOf(MyAttributes.class, "USERNAME");
}
public class MyAnnotatedService {
// Uses `AttributeKey.valueOf(MyAttributes.class, "USERNAME")`
@Get("/hello1")
public HttpResponse hello1(@Attribute(prefix = MyAttributes.class,
value = "USERNAME")
String username) { ... }
// Uses `AttributeKey.valueOf(MyAnnotatedService.class, "USERNAME")`
// if possible. If non-existent, use `AttributeKey.valueOf("USERNAME")`.
@Post("/hello2")
public HttpResponse hello2(@Attribute("USERNAME") String username) { ... }
}
Note that if no attribute for the specified AttributeKey
is found in RequestContext
, IllegalArgumentException
will occur at runtime.
Other classes automatically injected
The following classes are automatically injected when you specify them on the parameter list of your method.
ServiceRequestContext
(orRequestContext
)RequestHeaders
(orHttpHeaders
)HttpRequest
(orRequest
)AggregatedHttpRequest
QueryParams
Cookies
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.
- converts an object to a JSON document if the negotiated media type is
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
andCharSequence
object will be written as a text withContent-Type: text/plain; charset=utf-8
header.
- converts an object to a string if the negotiated main media type is one of
ByteArrayResponseConverterFunction
- converts an object to a byte array. Only
HttpData
andbyte[]
will be handled even if the negotiated media type isapplication/binary
orapplication/octet-stream
. If there is no media type negotiated,HttpData
andbyte[]
object will be written as a binary withContent-Type: application/binary
header.
- converts an object to a byte array. Only
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
andAggregatedHttpResponse
- 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.
- 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
- 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 ResponseEntity<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 ResponseEntity.of(headers, users); } }
- It contains the
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 addarmeria-rxjava
to the dependencies.
- All objects which are produced by the publisher will be collected, then the collected ones will be
converted to an
CompletionStage
andCompletableFuture
- An object which is generated by the
CompletionStage
will be converted to anHttpResponse
by response converters. If theCompletionStage
completes exceptionally, exception handlers will handle the cause.
- An object which is generated by the
Other types
- As described in Converting a Java object to an HTTP response, you can use any response types with response converters that convert them. If a service method raises an exception, exception handlers will handle it.
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:
MyGlobalDecorator1
MyMethodDecorator1
LoggingDecorator
MyMethodDecorator2
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);
});
});