Skip to content

Custom event types

sapessi edited this page Feb 26, 2019 · 2 revisions

Serverless Java Container supports API Gateway and the Application Load Balancer (ALB)'s proxy event types by default. However, the library is written to support any custom event type.

Background and architecture

Before we describe how to support custom event types, it is important to understand how the Serverless Java Container library works behind the scenes. Below you can see a UML diagram of the class relationships. The primary object is the LambdaContainerHandler class. The container handler exposes a typed proxy() method as well as a stream-compatible proxyStream() method. To enable the handler class to receive different event types and communicate with multiple implementations, it uses four different generics:

  • RequestType: The incoming event type. In most cases, this will be an instance of AwsProxyRequest.
  • ResponseType: The expected output of the handler. Normally, this is AwsProxyResponse.
  • ContainerRequestType: The request type object that the framework-specific implementation receives.
  • ContainerResponseType: The response type the framework-specific implementation returns.

Make a note of these generic type names becuase they will recurr throughout this document. The constructor for the LambdaContainerHandler class also receives implementations of the RequestReader and ResponseWriter clases. As the name suggests, these classes are in charge of receiving a RequestType object and returning a ContainerRequestType object (guess what the ResponseWriter does?). The core library of the Serverless Java Container framework includes a simple implementation of a servlet-compatible handler, request reader, and response writer. This allows us to support most frameworks such as Jersey, Spring, Spark, and Struts with minimal changes to the code - as long as the RequestType and ResponseType objects are compatible with AWS proxy specifications.

Serverless Java Container UML diagram

What actually happens

To make the relationship between the various classes clearer, let's go through a practical example. Consider the code below:

1   public class StreamLambdaHandler implements RequestStreamHandler {
2       private static SpringLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
3       static {
4           try {
5               handler = SpringLambdaContainerHandler.getAwsProxyHandler(PetStoreSpringAppConfig.class);
6           } catch (ContainerInitializationException e) {
7               // if we fail here. We re-throw the exception to force another cold start
8               e.printStackTrace();
9               throw new RuntimeException("Could not initialize Spring framework", e);
10          }
11      }
12
13      @Override
14      public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
15              throws IOException {
16          handler.proxyStream(inputStream, outputStream, context);
17      }
18  }

Line 1

Here, we obviously declare our handler class and implement Lambda's RequestStreamHandler. The RequestStreamHandler interface is declared in Lambda's Java core library and defines the handleRequest method as implemented on line 14 of our sample.

Line 2

Here we declare a static class member for the Spring implementation of the Serverless Java Container framework. The SpringLambdaContainerHandler class extends the AwsLambdaServletContainerHandler class, which in turn extends LambdaContainerHandler. The AwsLambdaServletContainerHandler specifies the ContainerRequestType and ContainerResponseType as AwsProxyHttpServletRequest and AwsHttpServletResponse. The LambdaContainerHandler class gives us access to the proxy() and proxyStream() methods. With the container request and response types already specified by the subclass, we only need to specify two additional generics, the event types that Lambda receives and returns: RequestType and ResponseType. In this case, they are set to the built-in models AwsProxyRequest and AwsProxyResponse.

Line 3 through 11

In this static block, we use the getAwsProxyHandler() static method of the SpringLambdaContainerHandler class to instatiate a default handler. In the UML diagram, you can see that framework-specific implementations define a constructor that takes multiple parameters:

  • Class<RequestType> requestTypeClass: The type of the request. This is necessary because of type erasure.
  • Class<ResponseType> responseTypeClass: Same as above.
  • RequestReader<RequestType, AwsProxyHttpServletRequest>: An implementation of the RequestReader interface that can receive our request type - in this case AwsProxyRequest - and transform it into an AwsProxyHttpServletRequest, our ContainerRequestType as specified by the AwsLambdaServletContainerHandler.
  • ResponseWriter<AwsHttpServletResponse, ResponseType>: Same as above for the ResponseWriter inteface.
  • SecurityContextWriter<RequestType>: The security context is used by the JAX-RS specifications to provide resource implementations with principal information. Behind the scenes, the framework uses the JAX-RS object in all its implementations, including in the servlet methods. The Serverless Java Container framework providers a default SecurityContextWriter implementation that given an AwsProxyRequest can return a JAX-RS SecurityContext object.
  • ExceptionHandler<ResponseType>: A top level exception handler object. The framework uses this object to intercept exceptions that are allowed to reach the LambdaContainerHandler proxy() method and transform them into a valid response object. The framework includes a default AwsProxyExceptionHandler for the AwsProxyResponse type.

When we call the getAwsProxyHandler() static method, the class calls its constructor with the default parameters to handle AWS' proxy event types:

new SpringLambdaContainerHandler<>(
    AwsProxyRequest.class,
    AwsProxyResponse.class,
    new AwsProxyHttpServletRequestReader(),
    new AwsProxyHttpServletResponseWriter(),
    new AwsProxySecurityContextWriter(),
    new AwsProxyExceptionHandler()
);

The constructor in each framework-specific is also in charge of initializing the underlying framework and preparing it to receive requests. Since the initialization process is different for each framework, we won't cover it in this section of the documentation. In Spring's case, initializing an application may throw an exception. The Serverless Java Container framework wraps the underlying exception in a ContainerInitializationException that we catch on line 6.

Line 14 through 17

Here we implement the handleRequest() mehod as defined in the RequestStreamHandler interface. This is the main entry point that Lambda itself calls whenever it receives a new event. Because our handler is already initialized - the static block is executed first - we can simply call the proxyStream method with all of the incoming parameters to handle the request.

The proxyStream() method uses Jackson's ObjectMapper object to transform the incoming InputStream into the expected RequestType, in this case an AwsProxyRequest object. With the request object read, it calls the proxy() method of the LambdaContainerHandler object to pass the request to the underlying framework.

The proxy() method first uses the readers passed in the constructor - RequestReader and SecurityContextWriter - to generate the ContainerRequestType and SecurityContext objects. In Spring's case, the ContainerRequestType is an AwsProxyHttpServletRequest because Spring can natively receive Servlet-compatible objects. With the request objects ready for the framework, generic LambdaContainerHandler calls the a method in the framework-specific implementation to pass handle the request object.

When the framework returns a response - or writes the respone OutputStream - the Serverless Java Container library uses the ResponseWriter passed in the constructor to transform the ContainerResponseType into a ResponseType. The default Servlet implementation provided with the framework uses a latch to turn asynchronous responses into a sync invocation. In Spring's case, the ContainerResponseType is an AwsServletResponse and, in our code, the ResponseType is an AwsProxyResponse because the default object only supports the AWS proxy specifications.

Supporting custom event types

Now that we have all the background information, you can hopefully start to see how support for custom event types would work. Out of the four key generic types, we need to customize the outward-facing ones: RequestType and ResponseType. Since the underlying frameworks do not change - I assume you are still planning to use one of the supported ones - the ContainerRequestType and ContainerResponseType do not need to change; you can keep leveraging the framwork's AwsProxyHttpServletRequest and AwsHttpServletResponse.

To accept the new event types. You will need to first create a new implementation of the RequestReader and ResponseWriter abstract classes. Specifically, you will want a RequestReader that can receive your CustomEvent and return an AwsProxyHttpServletRequest (or any other implementation of HttpServletRequest):

public class CustomRequestReader extends RequestReader<CustomEvent, AwsProxyHttpServletRequest> {
    @Override
    public AwsProxyHttpServletRequest readRequest(CustomEvent request, SecurityContext securityContext, Context lambdaContext, ContainerConfig config) {
        ...
    }

    @Override
    protected Class<? extends CustomEventType> getRequestClass() {
        return CustomEvent.class;
    }
}

Similarly, you will want to create a ResponseWriter implementation that can receive an AwsHttpServletResponse object and transform it into your CustomEventResponse value:

public class CustomResponseWriter extends ResponseWriter<AwsHttpServletResponse, CustomEventResponse> {
    @Override
    public CustomEventResponse writeResponse(AwsHttpServletResponse containerResponse, Context lambdaContext) {
        ...
    }
}

With these two new objects, you can now initialize a framework-specific implementation of the library using the constructor rather than the simple getAwsProxyHandler() method. For example, in Spring's case we would write the following handler class:

public class StreamLambdaHandler implements RequestStreamHandler {
    private static SpringLambdaContainerHandler<CustomEvent, CustomEventResponse> handler;
    static {
        try {
            AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
            applicationContext.register(MySpringApplication.class);
            handler = new SpringLambdaContainerHandler<>(
                CustomEvent.class,
                CustomEventResponse.class,
                new CustomRequestReader(),
                new CustomResponseWriter(),
                // this assumes you are still happy to use the default security context writers
                // and exception handler. Obviously, you can create custom implementations of 
                // these objects too.
                new AwsProxySecurityContextWriter(),
                new AwsProxyExceptionHandler(),
                applicationContext
            );
        } catch (ContainerInitializationException e) {
            // if we fail here. We re-throw the exception to force another cold start
            e.printStackTrace();
            throw new RuntimeException("Could not initialize Spring framework", e);
        }
    }

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
        handler.proxyStream(inputStream, outputStream, context);
    }
}