Serving static content outside the webapp directory with Jetty and Guice

I suppose, a very common use-case in any CMS is uploading a file, say, an image or a video. This task actually consists of two subtasks: enabling users to upload content, and ensuring that the same content can be viewed.

There can be various ways how to accomplish this depending on the setup and the whole system performance requirements, like serving the content from a separate web server for static content or even from a dedicated machine for just the static content only.

My case was quite simple: all I wanted was to serve the static content from the same Jetty 8 Java webapp that this content is uploaded to. Single webapp, single process, single server, that’s all, except just a few, in my opinion, sane requirements:

  • The directory root where the static files are kept must be configurable, as well as the context path where these files are retrieved from. Moreover, I would like to avoid any duplicate configuration, most probably in servlet container configuration and the webapp’s, i.e, “one config to rule them all”;
  • Avoid writing my own servlet for static files. If any widely used servlet container can serve static files by default, then it should be able to be reused.

The first requirement means that we have to map the servlet context paths dynamically at our application context startup, that is, in the ServletContextListener’s method contextInitialized, or, like in my case, in Guice’s ServletModule.configureServlets. We read our configuration settings and map the static content path to some servlet that serves the content.

The second requirement is more tricky. First, we have to ensure that the servlet container we are using actually provides such a servlet serving static content. I believe, most of them does, at least Jetty and Apache Tomcat for sure, and each of them provides a servlet with the same name: DefaultServlet.

The next question that pops out is whether these servlets are configurable so that they serve content from an arbitrary directory instead of just the webapp’s root. Looks like Jetty’s DefaultServlet allows such a thing since version 6 and this feature is preserved through all subsequent releases as well: 7, 8, and 9, see the resourceBase init parameter. I haven’t checked Tomcat’s DefaultServet, however at the first glance I could not find a similar parameter.

OK, what’s next? How do we re-use Jetty’s DefaultServlet? Do we add another dependency “jar” to our project? What about the version conflicts between our dependency jar and the webapp’s? I think the correct answer is: we don’t add a new dependency. We just define strong servlet container requirements for our application, i.e., the set of servlet containers we are going to support, and assume that the “default” servlet will always be there provided by Jetty (or other container we support). We can always get the java.lang.Class using the Class.forName method and use it in our dynamic servlet mapping at the servlet context initialization.

So, I’ve come up with this solution, remember, I use Google Guice and Jetty 8:

public class MyServletModule extends ServletModule {
    @Override
    public void configureServlets() {
        /* blah blah blah another initialization */

        /* You acquire these somehow from your configuration*/
        String uploadRoot = "/var/data/myapp/upload/images";
        String imagePath = "/images/";

        Class<HttpServlet> defaultServletClass = getJettyDefaultServletClass();
        bind(defaultServletClass).in(Singleton.class);
        Map<String, String> initParams = new HashMap<String, String>();
        initParams.put("dirAllowed", "false"); //that's up to you
        initParams.put("pathInfoOnly", "true"); //exclude the servlet's context path,
        //so that the local dir paths are shorter
        initParams.put("resourceBase", uploadRoot);
        serve(imagePath + "*").with(defaultServletClass, initParams);
        /* foo bar foo rest of the initialization */
    }

    private Class<HttpServlet> getJettyDefaultServletClass() {
        try {
            Class<?> clazz = Class.forName("org.eclipse.jetty.servlet.DefaultServlet");
            if (HttpServlet.class.isAssignableFrom(clazz)) {
                return (Class<HttpServlet>) clazz;
            }
            throw new IllegalStateException("Class " + clazz + " cannot be used as a HttpServlet");
        } catch (ClassNotFoundException e) {
            throw new IllegalStateException("Could not acquire Class of Jetty's DefaultServlet");
        }
    }
}

All the Class acquiring magic happens in the private method getJettyDefaultServletClass. If you are going to support more servlet containers, then you will have to try more class names until you succeed. In that case probably acquiring of servlet’s init parameters should be refactored as well, it’s up to you.

That’s it. I’ve described a quite common problem in Java world, and the solution I use, which is quite good as long as you do not want to support ALL the possible servlet containers instead of just a few. Jetty rocks, by the way 🙂

One thought on “Serving static content outside the webapp directory with Jetty and Guice

Leave a Reply

Your email address will not be published. Required fields are marked *