This is intended to be the first post in a series on building straight-forward web-apps with Spring-MVC and embedded Jetty. You can check out the complete source of this simple project from github.

This post describes configuring your Jetty and Spring MVC with XML-based configuration. If you want to use annotations/java-config take a look at this post.

Its been a while since I last posted. I've been busy starting up two big projects at work, one using Google App Engine (and Spring MVC ;)), the other using Jetty 8 (embedded), Spring MVC, MongoDB and ElasticSearch.

The Jetty/Spring combination is one which I've used before - several times since Jetty-5/Spring-1.2 - and I really like. You just can't beat it for quick debug cycles, complete control of the environment, minimal configuration, single-jar deployment, etc., etc.

On this most recent project I'm using jsp as the view technology - this is probably the easiest way to go since most things "just work" out of the box. Other view technologies can be used too - for example last year we used this same combination with FreeMarker - its just a little more effort to get things wired up right.

Here's how I set my projects up:

Maven POM

<?xml version="1.0" encoding="UTF-8"?>
<project 
 xsi:schemaLocation="
 http://maven.apache.org/POM/4.0.0 
 http://maven.apache.org/xsd/maven-4.0.0.xsd" 
 xmlns="http://maven.apache.org/POM/4.0.0" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>

<groupId>com.sjl</groupId>
<artifactId>webapp</artifactId>
<version>1.0-SNAPSHOT</version>
<name>WebApp</name>
<description></description>

<repositories>
  <repository>
    <id>springsource-repo</id>
    <name>SpringSource Repository</name>
    <url>http://repo.springsource.org/release</url>
  </repository>
</repositories>

<properties>
  <jetty.version>8.1.5.v20120716</jetty.version>
  <jetty.jsp.version>8.1.4.v20120524</jetty.jsp.version>
  <spring.version>3.1.2.RELEASE</spring.version>
</properties>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-source-plugin</artifactId>
      <version>2.1.2</version>
      <executions>
        <execution>
          <id>attach-sources</id>
          <phase>verify</phase>
          <goals>
            <goal>jar-no-fork</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
    <plugin>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>2.3.2</version>
      <configuration>
        <source>1.6</source>
        <target>1.6</target>
      </configuration>
    </plugin>
    <plugin> 
      <artifactId>maven-eclipse-plugin</artifactId> 
      <configuration> 
        <downloadSources>true</downloadSources>
      </configuration> 
    </plugin> 
  </plugins>    
</build>

<dependencies>

  <!-- SPRING DEPENDENCIES -->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>${spring.version}</version>
  </dependency> 
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.version}</version>
  </dependency>

  <!-- JETTY DEPENDENCIES -->
  <dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-server</artifactId>
    <version>${jetty.version}</version>
  </dependency>
  <dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-servlet</artifactId>
    <version>${jetty.version}</version>
  </dependency>
  <dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-webapp</artifactId>
    <version>${jetty.version}</version>
  </dependency>
  <dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-servlets</artifactId>
    <version>${jetty.version}</version>
  </dependency>

  <!-- JSP and JSTL SUPPORT -->
  <dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-jsp</artifactId>
    <version>${jetty.jsp.version}</version>
  </dependency>    
  <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

</project>

A couple of things worth pointing out:

  • I've added the spring-source maven repo in order to get the spring dependencies
  • I'm using Jetty 8 from eclipse, who have taken over from codehaus (who took over from mortbay)
  • I'm attaching sources for all dependencies that make them available, so that you can drill down into the source while debugging
  • I'm setting the source and target jdk compliance to 6
  • I'm using Jetty-jsp 8.1.4 - many other jstl implementations I tried have bugs, including a nasty one where recursive calls in tag-files would not compile.
  • I've never really bothered with things like maven archetypes, which is probably lazy-stupid of me. I tend to start from a pom that i've created previously then modify it to suit my needs.

After creating the pom in my project directory I create the src/main/java and src/test/java directories and then run mvn eclipse:eclipse to fetch the dependencies and create the eclipse .project and .classpath files. Having done that I import the project to Eclipse.

Once I have the project in Eclipse I create a few more directories - specifically the META-INF/webapp/WEB-INF directory to host my web.xml and spring context files (amongst other things).

I typically start with a spring application context and a spring web context, so I can specify beans at a larger scope than the web-application. Here's some simple example web and spring configs, all of which I place in WEB-INF:

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" 
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="
  http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">

<listener>
  <listener-class>
    org.springframework.web.context.ContextLoaderListener
  </listener-class>
</listener>

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
        /WEB-INF/application-context.xml
    </param-value>
</context-param>

<!-- Handles all requests into the application -->
<servlet>
    <servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
    <servlet-class>
      org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/web-context.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>  

<servlet-mapping>
    <servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>      

</web-app>

application-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="
http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context 
http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<!-- Define your application beans here. They will be available to the
   beans defined in your web-context because it is a sub-context.

   Beans defined in the web-context will not be available in the 
   application context.
-->

</beans>

web-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xsi:schemaLocation="
http://www.springframework.org/schema/mvc 
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context 
http://www.springframework.org/schema/context/spring-context-3.0.xsd">

<!-- Configures the @Controller programming model -->
<mvc:annotation-driven/>

<!-- Forwards requests to the "/" resource to the "home" view -->
<mvc:view-controller path="/" view-name="index"/>    

<mvc:resources mapping="/i/**" location="WEB-INF/images/" />
<mvc:resources mapping="/c/**" location="WEB-INF/css/" />
<mvc:resources mapping="/s/**" location="WEB-INF/scripts/" />
<mvc:resources mapping="/favicon.ico" 
  location="WEB-INF/images/favicon.ico" />

<!-- Resolve jsp's -->
<bean id="viewResolver" 
  class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <property name="viewClass" 
      value="org.springframework.web.servlet.view.JstlView"/>
    <property name="prefix" value="/WEB-INF/views/"/>
    <property name="suffix" value=".jsp"/>
</bean>

<!-- i18n message source -->
<bean id="messageSource" 
  class="
    org.springframework.context.support.
      ReloadableResourceBundleMessageSource">
    <property name="basename" value="/WEB-INF/i18n/messages" />
    <property name="defaultEncoding" value="UTF-8"/>
    <property name="cacheSeconds" value="30" />
</bean>

</beans>

Some important things to note in this file are:

  • We're using the annotation model for spring Controllers, hence there aren't any Controller beans specified in this xml
  • We're mapping static resources to be served efficiently by the Spring dispatcher servlet
  • We've set up a view resolver to look for jsp's in /WEB-INF/views. Note that when specifying a view in Controller code you drop the .jsp suffix.
  • We're configuring an internationalization message source, just so we can demonstrate use of a spring taglib a bit later...

Now we need to create a class to set up the embedded Jetty.

Embedded Jetty

package com.sjl;

import java.io.*;
import java.net.*;
import java.util.*;

import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.*;
import org.eclipse.jetty.server.nio.*;
import org.eclipse.jetty.util.thread.*;
import org.eclipse.jetty.webapp.*;

/**
 * Example WebServer class which sets up an embedded Jetty 
 * appropriately whether running in an IDE or in "production" 
 * mode in a shaded jar.
 */
public class WebServer
{
    // TODO: You should configure this appropriately for 
    // your environment
    private static final String LOG_PATH = 
      "./var/logs/access/yyyy_mm_dd.request.log";

    private static final String WEB_XML = 
      "META-INF/webapp/WEB-INF/web.xml";
    private static final String CLASS_ONLY_AVAILABLE_IN_IDE = 
      "com.sjl.IDE";
    private static final String PROJECT_RELATIVE_PATH_TO_WEBAPP = 
      "src/main/java/META-INF/webapp";

    public static interface WebContext
    {
        public File getWarPath();
        public String getContextPath();
    }

    private Server server;
    private int port;
    private String bindInterface;

    public WebServer(int aPort)
    {
        this(aPort, null);
    }

    public WebServer(int aPort, String aBindInterface)
    {        
        port = aPort;
        bindInterface = aBindInterface;
    }

    public void start() throws Exception
    {
        server = new Server();

        server.setThreadPool(createThreadPool());
        server.addConnector(createConnector());
        server.setHandler(createHandlers());        
        server.setStopAtShutdown(true);

        server.start();       
    }

    public void join() throws InterruptedException
    {
        server.join();
    }

    public void stop() throws Exception
    {        
        server.stop();
    }

    private ThreadPool createThreadPool()
    {
        // TODO: You should configure these appropriately
        // for your environment - this is an example only
        QueuedThreadPool _threadPool = new QueuedThreadPool();
        _threadPool.setMinThreads(10);
        _threadPool.setMaxThreads(100);
        return _threadPool;
    }

    private SelectChannelConnector createConnector()
    {
        SelectChannelConnector _connector = 
            new SelectChannelConnector();
        _connector.setPort(port);
        _connector.setHost(bindInterface);
        return _connector;
    }

    private HandlerCollection createHandlers()
    {                
        WebAppContext _ctx = new WebAppContext();
        _ctx.setContextPath("/");

        if(isRunningInShadedJar())
        {          
            _ctx.setWar(getShadedWarUrl());
        }
        else
        {            
            _ctx.setWar(PROJECT_RELATIVE_PATH_TO_WEBAPP);
        }

        List&lt;Handler> _handlers = new ArrayList&lt;Handler>();

        _handlers.add(_ctx);

        HandlerList _contexts = new HandlerList();
        _contexts.setHandlers(_handlers.toArray(new Handler[0]));

        RequestLogHandler _log = new RequestLogHandler();
        _log.setRequestLog(createRequestLog());

        HandlerCollection _result = new HandlerCollection();
        _result.setHandlers(new Handler[] {_contexts, _log});

        return _result;
    }

    private RequestLog createRequestLog()
    {
        NCSARequestLog _log = new NCSARequestLog();

        File _logPath = new File(LOG_PATH);
        _logPath.getParentFile().mkdirs();

        _log.setFilename(_logPath.getPath());
        _log.setRetainDays(90);
        _log.setExtended(false);
        _log.setAppend(true);
        _log.setLogTimeZone("GMT");
        _log.setLogLatency(true);
        return _log;
    }  

    private boolean isRunningInShadedJar()
    {
        try
        {
            Class.forName(CLASS_ONLY_AVAILABLE_IN_IDE);
            return false;
        }
        catch(ClassNotFoundException anExc)
        {
            return true;
        }
    }

    private URL getResource(String aResource)
    {
        return Thread.currentThread().
            getContextClassLoader().getResource(aResource); 
    }

    private String getShadedWarUrl()
    {
        String _urlStr = getResource(WEB_XML).toString();
        // Strip off "WEB-INF/web.xml"
        return _urlStr.substring(0, _urlStr.length() - 15);
    }
}

Notice that here we try to load a class that will only ever be available if we're running in test mode (e.g. directly from Eclipse).

If the class is found we assume we're running in exploded form, otherwise we assume we're running in a shaded jar - this is so that we can use the correct path to locate the web resources.

If you're trying this out you must make sure that the com.sjl.IDE class exists in your test source tree!.

Spring MVC Controller

package com.sjl.web;

import org.springframework.stereotype.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.*;

@Controller
public class Home {
    @RequestMapping("/")
    public ModelAndView home()
    {
        return new ModelAndView("index");
    }
}

A very simple Controller that simply tells Spring to render the index.jsp view when a request is made for the root of the web-app.

index.jsp

<%@taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
  <body>
    <p><spring:message code="hello"/></p>
  </body>
</html>

messages.properties

hello=Hi

That's almost everything. You will need a couple of other little pieces, for example a main class to instantiate the WebServer.

Project directory structure

Your project directory structure should end up looking something like this:

|_src
|___main
|_____java
|_______META-INF
|_________webapp
|___________WEB-INF
|_____________[web.xml and spring context configs here]
|_____________css
|_____________i18n
|_____________images
|_____________scripts
|_____________views
|_______________[jsp files here]
|_______com
|_________sjl
|___________web
|_____________[Spring MVC Controllers here]
|___test
|_____java
|_______com
|_________sjl

Check out the github project for the complete working example.

blog comments powered by Disqus