There could be some advantages in creating standalone web application versus packaging it as WAR and deploying it to container. Avoiding delving into debate, what I can think of is that such application could be easier to deploy/upgrade/restart, especially in situation when there are more web applications living in the container. For example, in case of Jetty, hot-deployment does not always work as expected, causing the need to restart whole thing, which in turn takes time during which applications are offline. Such app is also less prone to possible problems associated with setting up and later upgrading of the container as they come "bundled" with exact flavour they were probably integration-tested with.

Full source code for this article is here on GitHub

Adding Jetty as dependency

Add this to pom.xml to <dependencies> block:

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-webapp</artifactId>
    <version>${jetty.version}</version>
</dependency>

This provides embedded server with support for servlets, which is exactly what is needed.

Writing initializer

The purpose of initializer will be to run embedded Jetty server on specified port, after seting up Spring context with selected profile, and adding it to DispatcherServlet.

public class EmbeddedJetty {

    private static final int DEFAULT_PORT = 8080;
    private static final String CONTEXT_PATH = "/";
    private static final String CONFIG_LOCATION = "eu.kielczewski.example.config";
    private static final String MAPPING_URL = "/*";
    private static final String DEFAULT_PROFILE = "dev";

    public static void main(String[] args) throws Exception {
        new EmbeddedJetty().startJetty(getPortFromArgs(args));
    }

    private static int getPortFromArgs(String[] args) {
        if (args.length > 0) {
            try {
                return Integer.valueOf(args[0]);
            } catch (NumberFormatException ignore) {
            }
        }
        return DEFAULT_PORT;
    }

    private void startJetty(int port) throws Exception {
        Server server = new Server(port);
        server.setHandler(getServletContextHandler(getContext()));
        server.start();
        server.join();
    }

    private static ServletContextHandler getServletContextHandler(WebApplicationContext context) throws IOException {
        ServletContextHandler contextHandler = new ServletContextHandler();
        contextHandler.setErrorHandler(null);
        contextHandler.setContextPath(CONTEXT_PATH);
        contextHandler.addServlet(new ServletHolder(new DispatcherServlet(context)), MAPPING_URL);
        contextHandler.addEventListener(new ContextLoaderListener(context));
        contextHandler.setResourceBase(new ClassPathResource("webapp").getURI().toString());
        return contextHandler;
    }

    private static WebApplicationContext getContext() {
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.setConfigLocation(CONFIG_LOCATION);
        context.getEnvironment().setDefaultProfiles(DEFAULT_PROFILE);
        return context;
    }

}

Explaining on what happens here:

  1. There is ordinary static main() method that will become entry point of the application.
  2. The commandline arguments are parsed for port number, if none found the default is used.
  3. Spring's AnnotationConfigWebApplicationContext is created, with the following settings:
    • setConfigLocation() points to package where @Configuration classes are located.
    • Default profile is set to "dev", this can be overriden by setting spring.profiles.active environment variable.
  4. ServletContextHandler gets created in such way that:
    • Default Jetty ErrorHandler is removed, we want errors to be handled by DispatcherServlet.
    • Context path for application is set to "/" - there will be one application, so it makes sense for it to be served from root.
    • DispatcherServlet is added and is set up with previously created WebApplicationContext.
    • ContextLoaderListener is added for Spring application to bind WebApplicationContext to the lifecycle of server.
    • setResourceBase() sets the base path for web application resources to the /webapp directory on the classpath. This will be used to serve resources such as JavaScript files, stylesheets or images, just like in ordinary WAR file.

Packaging

The application can be packaged as ordinary JAR. To make it truly stand-alone however, like in WAR archive, all of the dependencies should be contained within it. That's where maven-shade-plugin comes in handy. This needs to be added to the <plugins> section of pom.xml:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <createDependencyReducedPom>false</createDependencyReducedPom>
                <transformers>
                    <transformer
                            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>eu.kielczewski.example.initializer.EmbeddedJetty</mainClass>
                    </transformer>
                </transformers>
                <filters>
                    <filter>
                        <artifact>*:*</artifact>
                        <excludes>
                            <exclude>META-INF/*.SF</exclude>
                            <exclude>META-INF/*.DSA</exclude>
                            <exclude>META-INF/*.RSA</exclude>
                        </excludes>
                    </filter>
                </filters>
            </configuration>
        </execution>
    </executions>
</plugin>

Little remark - the <mainClass> section points to the initializer class we just wrote, and the purpose of <filter> section is to stop Java complaining about invalid signature file by removing them. The reason why it could be complaining is because some of the dependency JARs might be signed, and this signature will be no longer valid when they get repackaged into single JAR.

Another thing to take care of is to include contents of src/main/webapp directory in the JAR on "/webapp" location. This needs to be added to the <build> section of pom.xml:

<resources>
    <resource>
        <targetPath>/webapp</targetPath>
        <directory>src/main/webapp</directory>
    </resource>
    <resource>
        <directory>src/main/resources</directory>
    </resource>
</resources>

That instructs maven-resources-plugin to copy resources into output directory before it gets packaged. In case of src/main/resources everything goes into root while src/main/webapp goes into /webapp. They will all later be packaged into JAR and available on classpath.

Making DispatcherServlet to serve resources

Let's say we want every request coming to URL starting with "/images" to be served with contents of "/webapp/images" directory. This is done in @Configuration class for Spring MVC.

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("images/**").addResourceLocations("images/");
    }
}

Having resource base directory set to "/webapp" by the initializer, and the contents of src/main/webapp copied to "/webapp" directory on the classpath by maven-resources-plugin, any image contained therein will be available under "/images" URLs.

Running

After packaging by running mvn package, the JAR will be created in /target directory of the project. It now can be run:

java -jar name-version.jar <port>

And it should start the embedded server together with Spring MVC application on selected (or default) port. To change the active profile of the Spring MVC application the environment variable could be set:

java -Dspring.profiles.active=prod -jar name-version.jar <port>

This site uses cookies. By continuing to browse the site, you are agreeing to our use of cookies. Find out more in Privacy and Cookies Policy.