Using different context handlers on different ports in Jetty 9

Today it’s popular to embed containers such as Jetty into the application itself so that it’s simple to start and distribute in, for example, a microservice architecture. But there are some things to consider when doing this. You probably want some health checks, metrics and other administrative resources that you don’t want to expose to the outside world. It’s a good idea to for deploy these on a different port than the external resources so that they can be protected by for example an AWS security group. Using something like Dropwizard gives you this capability out of the box which is very nice but sometimes you can’t use Dropwizard for various reasons (for example Dropwizard currently depends on an old version of Jersey which doesn’t let you use asynchronous request processing).

How to go about

Starting an embedded Jetty container quite simple, usually you do something like this and you’re more or less done:

ResourceConfig resourceConfig = new ResourceConfig();
resourceConfig.register(new MyJerseyResource());

ServletContextHandler contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
ServletHolder servletHolder = new ServletHolder(new ServletContainer(resourceConfig));
contextHandler.addServlet(servletHolder, "/*");

Server server = new Server(8080);
server.setHandler(contextHandler);
server.start();

This will start a Jetty server on port 8080 and expose the MyJerseyResource to the world. So now let’s add our health check resource, the naive way would be to do something like this:

ResourceConfig resourceConfig = new ResourceConfig();
resourceConfig.register(new MyJerseyResource());
resourceConfig.register(new HealthCheckResource());

However now the entire world will have access to the health check resource which is unnecessary. To allow the devops to easily restrict the access we want to deploy the HealthCheckResource on a different port. To do this in Jetty we need to modify our server to use two different ServerConnector‘s, one for the administrive part and one for the external part. For example:

Server server = new Server();
ServerConnector externalConnector = new ServerConnector(server);
externalConnector.setPort(8080);
externalConnector.setName("External");
server.addConnector(externalConnector);

ServerConnector adminConnector = new ServerConnector(server);
adminConnector.setPort(8081);
adminConnector.setName("Admin");
server.addConnector(adminConnector);

Setting a name is very important and the reason for this is that we also needs two different ServletContextHandler‘s:

// External
ResourceConfig externalResourceConfig = new ResourceConfig();
externalResourceConfig.register(new MyJerseyResource());

ServletContextHandler externalContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
ServletHolder servletHolder = new ServletHolder(new ServletContainer(externalResourceConfig));
externalContext.addServlet(servletHolder, "/*");

// Admin Context
ResourceConfig adminResourceConfig = new ResourceConfig();
adminResourceConfig.register(new new HealthCheckResource();

ServletContextHandler adminContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
ServletHolder servletHolder = new ServletHolder(new ServletContainer(adminResourceConfig));
adminContext.addServlet(servletHolder, "/*");
adminContext.setVirtualHosts(new String[]{"@Admin"});

Note the last line, adminContext.setVirtualHosts(new String[]{"@Admin"});, this is what will make the adminContext use port 8081 by refering to the adminConnector defined earlier. We prefix the name with an @ because this is the way Jetty finds a named connector in version 9 (in Jetty 8 you would instead use adminContext.setConnectorNames(new String[]{"Admin"}); but this option is no longer available in Jetty 9).

Heroku deployment

While this works fine in most cases there are scenarios where you might still need to deploy the administrative and external resources to the same port. One example is if you want to deploy on Heroku which only allows you to use a single port. To achieve this we’re going to use a “trick” from Dropwizard and assign the adminContext to the same port as the externalContext if the external and admin ports are configured to the same value. In order to differentiate between the admin and external resources we want to change the context path for the admin resources to something else, for example “/admin”. Since we’re using the same port we don’t need to create a new connector for the admin resources. However we need an if-statement to determine to which virtual host we should add the adminContext:

int adminPort = ..
int externalPort = ..

ServletContextHandler adminContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
ServletHolder servletHolder = new ServletHolder(new ServletContainer(adminResourceConfig));
adminContext.addServlet(servletHolder, "/*");
if (adminPort == externalPort ) {
    adminContext.setContextPath("/admin");
    adminContext.setVirtualHosts(new String[]{"@External"});
} else {
    adminContext.setVirtualHosts(new String[]{"@Admin"});
}

Conclusion

At the time of writing the Jetty 9 documentation was not quite up to date so I actually had to dig into the Jetty source code to find out some of the things mentioned this blog post. A full example is shown here for clarity:

int adminPort = ..
int externalPort = ..

// External
ResourceConfig externalResourceConfig = new ResourceConfig();
externalResourceConfig.register(new MyJerseyResource());

ServletContextHandler externalContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
ServletHolder servletHolder = new ServletHolder(new ServletContainer(externalResourceConfig));
externalContext.addServlet(servletHolder, "/*");

// Admin Context
ResourceConfig adminResourceConfig = new ResourceConfig();
adminResourceConfig.register(new new HealthCheckResource();

ServletContextHandler adminContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
ServletHolder servletHolder = new ServletHolder(new ServletContainer(adminResourceConfig));
adminContext.addServlet(servletHolder, "/*");
if (adminPort == externalPort) {
    adminContext.setContextPath("/admin");
    adminContext.setVirtualHosts(new String[]{"@External"});
} else {
    adminContext.setVirtualHosts(new String[]{"@Admin"});
}

// Configure the server
Server server = new Server();
ServerConnector externalConnector = new ServerConnector(server);
externalConnector.setPort(adminPort);
externalConnector.setName("External");
server.addConnector(externalConnector);

if (adminPort != externalPort) {
	ServerConnector adminConnector = new ServerConnector(server);
	adminConnector.setPort(externalPort);
	adminConnector.setName("Admin");
	server.addConnector(adminConnector);
}

HandlerCollection collection = new HandlerCollection();
collection.addHandler(adminContext);
collection.addHandler(externalContext);

server.setContext(collection);
server.start();

Note that in Jetty 9.1 it seems like you must add the adminContext handler before the externalContext handler to the HandlerCollection (collection) otherwise it won’t work.

That’s it!

This Post Has 4 Comments

  1. Hi,
    Very informative blog post. I am trying to achieve same thing but I have a standalone jetty server. I am able to create connector by going through jetty documentation. But I am not sure about how/where to specify settings about serving certain servlet using certain connector.
    Currently I am deploying my application as a war file by putting it into webapp directory of jetty. One way to achieve this would be to duplicating the war file and essentially running two applications, but this is not something I want. Can you shed some light on how to achieve this in standalone jetty?

    Thanks,
    Satish

  2. Hi,
    Very informative blog post. I am trying to achieve same thing but I have a standalone jetty server. I am able to create connector by going through jetty documentation. But I am not sure about how/where to specify settings about serving certain servlet using certain connector.
    Currently I am deploying my application as a war file by putting it into webapp directory of jetty. One way to achieve this would be to duplicating the war file and essentially running two applications, but this is not something I want. Can you shed some light on how to achieve this in standalone jetty?

    Thanks,
    Satish

  3. You might want to try compiling your example code before publishing it.
    In your final example, on line 44, you invoke “server.setContext(collection);”
    The Jetty Server doesn’t have a SetContext() method. Perhaps you mean setHandler() ?
    Also, in many of your examples (for example, in the last one, line 14) you double the “new” operator and are missing the closing parenthesis at the end of the statement.

  4. I notice that in the last example at lines 29&30 you use adminPort but the name “External” (and the reverse at lines 35&36)
    Is this intentional? It seems like the ports and names are mismatched

Leave a Reply

Close Menu