Dynamic Content with HandlerMapping
- 24 min read - Text OnlyRecently I was challenged to enable a Spring Boot API micro service with serving a frontend, but without the frontend being packaged with the service, and in such a way that the frontend could change at any time without redeploying the backend. Not only that, but multiple frontends could be served from the same hostname depending on the route the browser requests. Here's my solution.
First, I'll describe what's running in production and then share a minimal reproduction of the concept. This reproduction was done on my own hardware and does not share any code with the production solution. While creating this reproduction, I figured out how to use HandlerMapping
and HandlerExecutionChain
which unfortunately have very little documentation online. I hope someone some day finds this demonstration of HandlerMapping
useful.
Production
Let me introduce the micro service on the side: this is what manages our deployments at this time. It instruments AWS to change elastic beanstalk, push updated cloudformation stacks, and so on.
This service was initially made so that our new quality assurance team could deploy our applications without depending on someone to run command line scripts with a specific git commit hash.
This microservice was extended to support a new concept: Dynamic Frontend. Previously it only supported whole applications, where the app update would either be updating beanstalk, running a CDK script, or syncing an S3 bucket with some frontend content at the root and invalidating index.html. Now it tracks which frontends have been built, deployed, which hostnames have which frontends deployed. With this information and the metadata described next it generates a frontend manifest that describes which content is to be shown on which routes.
Additional metadata such as the routes for this frontend is collected on every commit and saved alongside a zip of the frontend content and the html to inject into the head, body, and footer. This all goes into the deployables bucket during Continuous Integration (CI) and are called artifacts. Each of these is tracked and submitted to the microservice during CI with information from the commit.
The old static sites each have a dedicated bucket, CloudFront distribution, and hosted zone record per app and stage (e.g. dev, prod), these will still be supported but dynamic frontend is quite different. The assets are unpacked into the bucket in an immutable fashion prefixed by app-name/commit-hash/. This bucket will serve multiple application contents for all stages and no objects are replaced. Some time later we will figure out garbage collection.
This means the assets need to be deployed before the html points to it. That’s the second to last step of the deployment process: unpacking the zip from the deployables bucket onto the public distribution bucket. Once done, then the manifest can be generated. This is stored atomically into the database, rather than being computed on demand. Within some Time To Live (TTL), the backends will query the current manifest and cache it in memory.
This manifest contains all the html to inject for every active frontend app and version, every host name that is configured, a default host name to fall back to, and within each host name a configuration of route patterns and app-version to use. It is under 10kb, can be fetched in 30ms, and is only hit once per minute per production server. Sandbox servers have a TTL of 5 seconds for convenience.
A micro library of sorts reviews the request and provides dynamic content to use, or if no content will fall back on the clunky ancient frontend bundled with the monolith. This is done within the index.jsp that serves all unmapped paths (convenient for SPAs), so no API routes are affected.
Just in case, the crunchy old monolith keeps a cached copy in redis if the deployment microservice is down and a new instance arrives without a copy in memory. Production has to keep on running even if other things are down.. including the thing that deploys production.
So in short the following happens in this order:
- When an API path with a certain prefix is used, spring will handle the request.
- When an old old tomcat servlet controller is mapped to a uri path it will handle the request.
- If a file exists that should be served (and isn’t a jsp) and the path matches, send that
- When the fallback jsp is used, it will see if the uri path is managed by dynamic frontend, if so it will inject the new html into the page and omit the html for the old single page app
- Otherwise the fallback jsp is used with the old single page app bundled with the monolith.
The solution is by no means clean, but is low risk backwards compatibility ever clean?
A spring boot version
After this launched successfully and with praise from all teams affected, I worked on a solution for our spring boot projects. This isn’t needed yet, but I expect it to be useful for internal Microservices and new public backends going forward. Our current split frontend / backend projects struggle with CORS, often requiring my intervention or… someone just outright turning CORS bypass everything on so another can do their job.
While developing the spring solution, I searched for a different design. While trying out Vaadin, a Java client and server solution, I saw it was able to inject its own routes into spring and I dug for hours to figure out what was going on.
I pieced together some hints from stackoverflow, documentation going back to 2003, debug breakpoints in the heart of Spring to figure out a solution which does not rely on servlet filters.
Servlet filters
While the initial reimplementation did in fact use a filter, doing so comes with several consequences. First I lose control of prioritizing API requests, instead I would have to watch for signals like NotMappedException
to implement this. And somehow inject the filter between the application and the top level filter. No thank you.
Servlet filters are great for stuff like logging, attaching info to requests, syncing and flushing sessions or the like. You know, like express middleware.
I’ve seen how filters are used in that old monolith, conditionally handling requests at different levels makes first and second class citizens in a way almost no one can debug. It’s not a good long term decision even though it seems to be the easiest one.
HandlerMapping
What is a HandlerMapping? It is an abstraction that provides a found handler or null. What is a Handler? I did not know exactly what going into this. But it seems to be a lot like a Servlet Filter that can choose the right Controller to feed a request to and send the response back to the client (or up the filter chain). I suspect that Spring returns a handler that renders views, encodes json, etc. the HandlerMapping just tracks all the routes found during startup and returns that handler on a successful match.
At application start, the application will instantiate several HandlerMapping classes, one is set up with all of the controller RequestMapping
/ GetMapping
/ etc. annotated functions, another is set up for resources. By default there are five of them. I won’t go into detail on each one. To make this work I need to insert a custom HandlerMapping in the middle. API and local files should be served without being interrupted while dynamic frontend routes can take the rest.
So with reflection, spring collects the HandlerMapping instances and orders them.
Next is the HandlerMapping interface, the parameters aren’t even httpServlets, which makes getting the URI out an annoying exercise of unchecked casting. I’d recommend using this class called AbstractHandlerMapping
. It’s getHandlerInternal
function provides more useful inputs but the output is just… an Object.
I get that Java 1.4 from 2002 wasn’t capable and all sorts of type losing hacks had to be used. But really? An Object? And no useful documentation as to what it should be?
With more browsing around, I found the interface HttpRequestHandler
in another spring dependency. It seems obvious enough and so I try it. Thankfully it works! It looks a lot like a Servlet Filter though.
Simple HandlerMapping
What's the shortest useful HandlerMapping possible? I think it's this:
package dev.cendyne.handlerdemo;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import javax.servlet.http.HttpServletRequest;
@Configuration
public class ProofOfConceptHandler extends AbstractHandlerMapping implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
setOrder(11);
}
@Override
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
String uri = request.getRequestURI();
if (uri.equals("/proof-of-concept")) {
return new SimpleStaticHandler("text/plain", "Proof!");
}
return null;
}
}
Here's what to make note of:
- Extend
AbstractHandlerMapping
which implementsHandlerMapping
- Match the request in the
getHandlerInternal
and return a handler when it is appropriate - Explicitly set the order with
setOrder
withinafterPropertiesSet
which comes fromInitializingBean
The Handler
Earlier I referenced SimpleStaticHandler
, it really isn't too complicated:
package dev.cendyne.handlerdemo;
import lombok.Value;
import org.springframework.web.HttpRequestHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Value
public class SimpleStaticHandler implements HttpRequestHandler {
String contentType;
String responseContent;
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setStatus(200);
response.setContentType(contentType);
try (var writer = response.getWriter()) {
writer.write(responseContent);
}
}
}
Anyway, here you can see what makes this look a lot like a Servlet Filter. The handler receives a request
and response
and is expected to respond to the client or throw an exception. A filter however would pass on the request if it does not handle it. The HandlerMapping handles that concern.
Dynamic HandlerMapping
So what can we do to make this dynamic? First, I'll make a simple interface to represent the domain here. Either we get a result to send to the client, or we do not.
Also, it's dynamic so it can change, either by API or something in the environment.
package dev.cendyne.handlerdemo;
import java.util.Optional;
public interface DynamicFrontendService {
Optional<String> getHtmlForRoute(String uri);
void addHtml(String uri, String html);
}
The actual implementation isn't all that interesting, it wraps around a ConcurrentHashMap
To add some data on startup, the following runs each time.
@Bean
public CommandLineRunner loadData(DynamicFrontendService dynamicFrontend) {
return (args) -> {
dynamicFrontend.addHtml("/hello", "<h1>Hello World</h1>");
dynamicFrontend.addHtml("/aloha", "<h1>Aloha kakahiaka</h1>");
};
}
To actually do something dynamically, here's the HandlerMapping
before we add exclusions for resources.
@Configuration
@RequiredArgsConstructor
public class DynamicFrontendHandler extends AbstractHandlerMapping implements InitializingBean {
private final DynamicFrontendService dynamicFrontend;
@Override
public void afterPropertiesSet() throws Exception {
setOrder(10);
}
@Override
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
return dynamicFrontend.getHtmlForRoute(request.getRequestURI())
.map(content -> new SimpleStaticHandler("text/html", content))
.orElse(null);
}
}
See? It wasn't all that complicated.
But if I add content to /robots.txt
and there is a resource for robots.txt
, then this handler will obscure the original resource which is not desirable.
Accounting for static resources
To allow the dynamic handler mapping to take on nearly any url for a single page application, but pass through resources such as.. favicon.ico
, robots.txt
, images/cendyne-does-java.jpg
they need to be excluded.
This can be done with the AntPathMatcher
mentioned before and a configurable property value.
@Configuration
@RequiredArgsConstructor
public class DynamicFrontendHandler extends AbstractHandlerMapping implements InitializingBean {
private final DynamicFrontendService dynamicFrontend;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
@Value("${frontend.static-paths:}")
String staticPathsStr;
List<String> staticPaths = Collections.emptyList();
@Override
public void afterPropertiesSet() throws Exception {
setOrder(10);
if (staticPathsStr != null && !staticPathsStr.isBlank()) {
staticPaths = Arrays.asList(staticPathsStr.split(","));
}
}
@Override
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
String uri = request.getRequestURI();
// Do not consider static paths
for (String staticPath : staticPaths) {
if (antPathMatcher.match(staticPath, uri)) {
return null;
}
}
return dynamicFrontend.getHtmlForRoute(uri)
.map(content -> new SimpleStaticHandler("text/html", content))
.orElse(null);
}
}
Notably, this means now application.properties
can be set with
frontend.static-paths=/robots.txt,/favicon.ico,/images/**
See the **
? That's part of the Ant path matching expression. It means all files within that directory and any sub directories. So, /images/cendyne-does-java.jpg
and /images/anything/really.png
will be avoided with a succinct expression.
Next, that @Value
annotation..
@Autowired
@Value("${frontend.static-paths:}")
String staticPathsStr;
@Autowired
tells Spring to inject in this object, it happens to be a String. That type alone won't be useful. While @Qualifier
can be used to specify which named bean should be injected, we want something easily configurable. That can come through either a configuration object or directly with a Spring Expression Language (SpEL) expression. Here, the value will be pulled from frontend.static-paths
, the colon at the end is the fallback expression which is blank. So the empty string may be the supplied value if not provided by the application developer.
aplication.properties
is an accessible mechanism for libraries and projects to coordinate together without costly initialization rituals and boilerplate. Best to allow library tuning through that by default.
Conclusion
HandlerMapping
from 2003 is an incredibly flexible invention. It justs lacks constructive by-example documentation. If you're interested in seeing the full sources for this minimal reproduction, see the github repository cendyne/spring-boot-handler-demo.