Decorating a service

A ‘decorating service’ (or a ‘decorator’) is a Service that wraps another Service to intercept an incoming request or an outgoing response. As its name says, it is an implementation of the decorator pattern. Service decoration takes a crucial role in Armeria. A lot of core features such as logging, metrics and distributed tracing are implemented as decorators and you will also find it useful when separating concerns.

There are basically three ways to write a decorating service:

Implementing DecoratingServiceFunction

DecoratingServiceFunction is a functional interface that greatly simplifies the implementation of a decorating service. It enables you to write a decorating service with a single lambda expression:

import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.server.HttpService;

ServerBuilder sb = new ServerBuilder();
HttpService service = ...;
sb.serviceUnder("/web", service.decorate((delegate, ctx, req) -> {
    if (!authenticate(req)) {
        // Authentication failed; fail the request.
        return HttpResponse.of(HttpStatus.UNAUTHORIZED);
    }

    // Authenticated; pass the request to the actual service.
    return delegate.serve(ctx, req);
});

Extending SimpleDecoratingService

If your decorator is expected to be reusable, it is recommended to define a new top-level class that extends SimpleDecoratingService :

import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.server.Service;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.SimpleDecoratingService;

public class AuthService extends SimpleDecoratingService<HttpRequest, HttpResponse> {
    public AuthService(Service<HttpRequest, HttpResponse> delegate) {
        super(delegate);
    }

    @Override
    public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {
        if (!authenticate(req)) {
            // Authentication failed; fail the request.
            return HttpResponse.of(HttpStatus.UNAUTHORIZED);

        }

        Service<HttpRequest, HttpResponse> delegate = delegate();
        return delegate.serve(ctx, req);
    }
}

ServerBuilder sb = new ServerBuilder();
// Using a lambda expression:
sb.serviceUnder("/web", service.decorate(delegate -> new AuthService(delegate)));
// Using reflection:
sb.serviceUnder("/web", service.decorate(AuthService.class));

Extending DecoratingService

So far, we only demonstrated the case where a decorating service does not transform the type of the request and response. You can do that as well, of course, using DecoratingService:

import com.linecorp.armeria.common.RpcRequest;
import com.linecorp.armeria.common.RpcResponse;

// Transforms a Service<RpcRequest, RpcResponse> into Service<HttpRequest, HttpResponse>.
public class MyRpcService extends DecoratingService<RpcRequest, RpcResponse,
                                                    HttpRequest, HttpResponse> {

    public MyRpcService(Service<? super RpcRequest, ? extends RpcResponse> delegate) {
        super(delegate);
    }

    @Override
    public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {
        // This method has been greatly simplified for easier understanding.
        // In reality, we will have to do this asynchronously.
        RpcRequest rpcReq = convertToRpcRequest(req);
        RpcResponse rpcRes = delegate().serve(ctx, rpcReq);
        return convertToHttpResponse(rpcRes);
    }

    private RpcRequest convertToRpcRequest(HttpRequest req) { ... }
    private HttpResponse convertToHttpResponse(RpcResponse res) { ... }
}

Unwrapping decoration

Once a Service is decorated, the type of the service is not that of the original Service anymore. Therefore, you cannot simply down-cast it to access the method exposed by the original Service. Instead, you need to ‘unwrap’ the decorator using the Service.as() method:

MyService service = ...;
MyDecoratedService decoratedService = service.decorate(...);

assert !(decoratedService instanceof MyService);
assert decoratedService.as(MyService.class).get() == service;
assert decoratedService.as(MyDecoratedService.class).get() == decoratedService;
assert !decoratedService.as(SomeOtherService.class).isPresent();

as() is especially useful when you are looking for the Service instances that implements a certain type from a server:

import com.linecorp.armeria.server.ServerConfig;
import java.util.List;

Server server = ...;
ServerConfig serverConfig = server.config();
List<ServiceConfig> serviceConfigs = serverConfig.serviceConfigs();
for (ServiceConfig sc : serviceConfigs) {
    if (sc.service().as(SomeType.class).isPresent()) {
        // Handle the service who implements or extends SomeType.
    }
}

Decorating ServiceWithRoutes

ServiceWithRoutes is a special variant of Service which allows a user to register multiple routes for a single service. It has a method called routes() which returns a Set of Routes so that you do not have to specify path when registering your service:

import com.linecorp.armeria.server.Route;
import com.linecorp.armeria.server.ServiceWithRoutes;
import java.util.HashSet;
import java.util.Set;

public class MyServiceWithRoutes implements ServiceWithRoutes<HttpRequest, HttpResponse> {
    @Override
    public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) { ... }

    @Override
    public Set<Route> routes() {
        Set<Route> routes = new HashSet<>();
        routes.add(Route.builder().path("/services/greet").build());
        routes.add(Route.builder().path("/services/hello").build());
        return routes;
    }
}

ServerBuilder sb = new ServerBuilder();
// No path is specified.
sb.service(new MyServiceWithRoutes());
// Override the path provided by routes().
sb.service("/services/hola", new MyServiceWithRoutes());

However, decorating a ServiceWithRoutes can lead to a compilation error when you attempt to register it without specifying a path explicitly, because a decorated service is not a ServiceWithRoutes anymore but just a Service:

import com.linecorp.armeria.server.logging.LoggingService;

ServerBuilder sb = new ServerBuilder();

// Works.
ServiceWithRoutes<HttpRequest, HttpResponse> service =
        new MyServiceWithRoutes();
sb.service(service);

// Does not work - not a ServiceWithRoutes anymore due to decoration.
Service<HttpRequest, HttpResponse> decoratedService =
        service.decorate(LoggingService.newDecorator());
sb.service(decoratedService); // Compilation error

// Works if a path is specified explicitly.
sb.service("/services/bonjour", decoratedService);

Therefore, you need to specify the decorators as extra parameters:

ServerBuilder sb = new ServerBuilder();
// Register a service decorated with two decorators at multiple routes.
sb.service(new MyServiceWithRoutes(),
           MyDecoratedService::new,
           LoggingService.newDecorator())

A good real-world example of ServiceWithRoutes is GrpcService. See Decorating a GrpcService for more information.