Description
Key information
- RFC PR: (leave this empty)
- Related issue(s), if known:
- Area: HTTP binding
- Meet tenets: Yes
Summary
One paragraph explanation of the feature.
This RFC proposes a new module that will allow developers to easily implement lightweight, multi-endpoint ALB and API Gateway lambdas in Java with minimal (or no) additional dependencies outside of the Powertools and the JRE. This approach provides an alternative to using more heavy weight frameworks based around Spring or JaxRS, reducing GC and classloader burden to allow maximum cold start performance.
Motivation
Why are we doing this? What use cases does it support? What is the expected outcome?
Here's the value statement I think we should work towards:
As a developer
I would like to implement my HTTP microservice in Java on AWS Lambda
So that I can take advantage of the serverless platform without having to adopt an entirely new technology stack
We have been using a lightweight framework to achieve this goal rather successfully for the past couple of years. We have gone all-in on AWS lambda and have been able to solve many of the problems associated with implementing Java lambdas as we gained experience. We would like to contribute some of the technology that has enabled to do this so successfully.
To be more specific about what problem this solves, this RFC proposes a solution for lambdas that:
- Handle several HTTP endpoints or are generally RESTful in a structured and maintainable way
- Optimised to use the minimum additional dependencies
- Not use reflection that could increase cold start times
Many of the techniques and approaches used in this proposal come from this article:
https://medium.com/capital-one-tech/aws-lambda-java-tutorial-best-practices-to-lower-cold-starts-capital-one-dc1d8806118
This proposal does not make Java more performant than using other runtimes for your lambda, but it does prevent the framework being a performance burden.
Proposal
This is the bulk of the RFC.
Explain the design in enough detail for somebody familiar with Powertools for AWS Lambda (Java) to understand it, and for somebody familiar with the implementation to implement it.
This should get into specifics and corner-cases, and include examples of how the feature is used. Any new terminology should be defined here.
The design of this proposal is significantly different from the approach taken in Spring and JaxRS. It borrows concepts and idioms from other languages, Go in particular. Despite this it is still possible to use patterns familiar to Java developers such as 3-tier or Domain Driven Design. Examples of this will be shown
API examples
Basic route binding
A simple example of how URL path and HTTP method can be used to bind to a specific handler
public class ExampleRestfulController {
private final Router router;
{
/*
This router demonstrates some of the features you would expect to use when creating a RESTful
resource. They are all using the same root but differentiated by looking at the HTTP method
and the `id` path parameter. These individual routes are also named so that you can filter
log events based on that rather than having to use a regular expression on the path in
combination with method.
*/
final Router resourcesRouter =
new Router(
Route.bind("/", methodOf("POST")).to(this::createResource),
Route.bind("/(?<id>.+)", methodOf("GET")).to(this::getResource),
Route.bind("/(?<id>.+)", methodOf("DELETE")).to(this::deleteResource),
Route.fallback().to(this::invalidMethodFallthrough));
router =
new Router(
Route.HEALTH,
Route.bind("/resources(?<sub>/.*)?")
.to(subRouteHandler("sub", resourcesRouter)));
}
private AlbResponse createResource(Request request) {
return AlbResponse.builder().withStatus(Status.CREATED).withBody("created something").build();
}
private AlbResponse getResource(Request request) {
return AlbResponse.builder()
.withStatus(Status.OK)
.withBody("got resource with ID " + request.getPathParameters().get("id"))
.build();
}
private AlbResponse deleteResource(Request request) {
return AlbResponse.builder()
.withStatusCode(204)
.withBody("deleted resource with ID " + request.getPathParameters().get("id"))
.build();
}
private AlbResponse invalidMethodFallthrough(Request request) {
return AlbResponse.builder().withStatus(Status.METHOD_NOT_ALLOWED).build();
}
}
Filters
In this section, we will describe how we can add filters that allow us to perform operations on the request object before it is passed to the handler. I've called them filters
here but we can decide if there's a more appropriate name for this.
A simple example of how we can add filters to a binding.
Filter aSingleFilter = new YourFilter();
List<Filter> manyFilters = asList(new AnotherFilter(), new FilterWithParameters(42));
Router router =
new Router(Route.HEALTH, Route.bind("/path")
.to(aSingleFilter)
.to(manyFilters)
.to((handler, request) -> {
// Do something inline
return handler.apply(request);
})
.thenHandler(this::mainHandler));
Here is an example of a filter you might want perform some common logging.
public class RequestLogger implements Filter {
private final YourStructuredLogger log;
public RequestLogger(Logger log) {
this.log = log;
log.info("isColdStart", "true");
}
@Override
public AlbResponse apply(RouteHandler handler, Request request) {
log.info("path", request.getPath());
String method = request.getHttpMethod();
if (!Objects.equals(method, "")) {
log.info("http_method", method);
}
logHeaderIfPresent(request, "content-type");
logHeaderIfPresent(request, "accept");
try {
AlbResponse response = handler.apply(request);
log.info("status_code", String.valueOf(response.getStatusCode()));
return response;
} catch (Exception ex) {
log.error("Unhandled exception", ex);
throw ex;
}
}
Here is an example of how you could do request validation inside of a filter.
Filter validator = (next, request) -> {
if (request.getBody() != "") {
return next.apply(request);
} else {
return AlbResponse.builder().withStatusCode(400).withBody("you must have a request body").build();
}
};
Here is how you might implement a simple (or complex) exception mapper using filters.
public class ExceptionMapping {
private final Router router =
new Router(
Route.HEALTH,
Route.bind("/resources/123", methodOf("GET"))
.to(this::mapException)
.then(this::accessSubResource),
Route.bind("/resources/(?<id>.+)", methodOf("GET"))
.to(this::mapException)
.then(this::accessMissingResource),
Route.bind("/resources", methodOf("POST"))
.to(this::mapException)
.then(this::processRequestObject),
Route.bind("/some-gateway", methodOf("GET"))
.to(this::mapException)
.then(this::callDownstream));
/** This is a really simple example of how exception mapping can be done using forwarders. */
private AlbResponse mapException(RouteHandler handler, Request request) {
AlbResponse response;
try {
response = handler.apply(request);
} catch (RequestValidationException ex) {
response = AlbResponse.builder().withStatus(Status.BAD_REQUEST).build();
} catch (NotFoundException ex) {
response = AlbResponse.builder().withStatus(Status.NOT_FOUND).build();
} catch (BadDownstreamDependencyException ex) {
response = AlbResponse.builder().withStatus(Status.BAD_GATEWAY).build();
} catch (Exception e) {
response = AlbResponse.builder().withStatus(Status.INTERNAL_SERVER_ERROR).build();
}
return response;
}
private AlbResponse callDownstream(Request request) {
throw new BadDownstreamDependencyException();
}
private AlbResponse processRequestObject(Request request) {
throw new RequestValidationException();
}
private AlbResponse accessMissingResource(Request request) {
throw new NotFoundException();
}
private AlbResponse accessSubResource(Request request) {
return AlbResponse.builder().withStatus(Status.OK).withBody("Hello!").build();
}
private static class BadDownstreamDependencyException extends RuntimeException {}
private static class RequestValidationException extends RuntimeException {}
private static class NotFoundException extends RuntimeException {}
}
Dependency Injection
Dependency injection is outside of the scope of this RFC - but this approach is compatible with compile-time DI solutions like Dagger2. Our experience has shown that you retain a high degree of control over what code is executed and the number classes being managed by the class loader, allowing you to manage your cold start phase very well.
Drawbacks
Why should we not do this?
Do we need additional dependencies? Impact performance/package size?
The style of API used here will not be familiar to a lot of Java developers. This may add some cognitive burden to those that would like to adopt this module in their solution.
Rationale and alternatives
- What other designs have been considered? Why not them?
- What is the impact of not doing this?
Unresolved questions
Optional, stash area for topics that need further development e.g. TBD
Metadata
Metadata
Assignees
Type
Projects
Status