WAR-less Java Web Apps
Have you ever thought about why in Java we package up web apps into WAR files (or WAR directory structures)? It certainly is a convenient way to move an application and its dependencies from one place to another. But wouldn’t it be nice if everything could just stay in its original location and there wouldn’t be any moving of files around? Wouldn’t it also be nice if you specified your required version of Jetty or Tomcat just like you do with every other dependency? The WAR-less approach is one that is catching on as emerging Java web frameworks like Play! ditch the WAR files. With standard Java web apps we can also ditch the WAR files by simply launching an embedded Jetty or Tomcat server. Let’s give this a try and see how it goes.
For this experiment I’m going to use Maven and Jetty. This will still use the same standard source structure for a WAR file (src/main/java, src/main/webapp, etc). The major difference is that I will actually startup Jetty using a good-old static void main. This is similar to using the jetty:run goal but will allow us to have the same exact setup in development and in production. The static stuff will be in src/main/webapp, the compiled classes will be in target/classes, and the dependencies will be right were Maven downloaded them to. First, here is a little Java class (src/main/java/foo/Main.java) that sets up a Jetty server and starts it:
package foo;
import java.io.File;
import java.net.URL;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.*;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.webapp.WebAppContext;
public class Main
{
public static void main(String[] args) throws Exception
{
String webappDirLocation = "src/main/webapp/";
Server server = new Server(8080);
WebAppContext root = new WebAppContext();
root.setContextPath("/");
root.setDescriptor(webappDirLocation + "/WEB-INF/web.xml");
root.setResourceBase(webappDirLocation);
root.setParentLoaderPriority(true);
server.setHandler(root);
server.start();
server.join();
}
}
As you can see, Main just references the webapp directory so I don’t have to copy the stuff from there to another place. Next I have a little test servlet (src/main/java/foo/HelloServlet.java):
package foo;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.*;
public class HelloServlet extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
{
PrintWriter out = resp.getWriter();
out.println("hello, world");
out.close();
}
}
And now the web.xml file (src/main/webapp/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_2_5.xsd"
version="2.5">
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>foo.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
And finally a pom.xml file that specifies Jetty as a dependency and provides an easy way to run the Main class:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jamesward</groupId>
<version>1.0-SNAPSHOT</version>
<name>warless_java_web_app</name>
<artifactId>warless_java_web_app</artifactId>
<packaging>jar</packaging>
<properties>
<jettyVersion>7.3.1.v20110307</jettyVersion>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${jettyVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jettyVersion}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2</version>
<configuration>
<mainClass>foo.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
And now simply run:
mvn compile exec:java
Maven compiles my Java classes into target/classes and then the exec:java goal runs the Main which finds the other WAR assets in the src/main/webapp directory. If you have been following along, make a request to http://localhost:8080/ to verify that it works (which it should).
There are two alternatives to running Jetty from Maven. You can use the Maven appassembler plugin to create start scripts containing the correct CLASSPATH references and then launch Main class using the generated scripts. Or you can use the Maven assembly or shade plugin to create a JAR containing the application and all of its dependencies.
Here is an example section of a pom.xml file for using the appassembler plugin:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>appassembler-maven-plugin</artifactId>
<version>1.1.1</version>
<configuration>
<assembleDirectory>target</assembleDirectory>
<generateRepository>false</generateRepository>
<programs>
<program>
<mainClass>foo.Main</mainClass>
<name>main</name>
</program>
</programs>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>assemble</goal>
</goals>
</execution>
</executions>
</plugin>
To generate the start scripts simply run:
mvn install
Then to run the script set the REPO environment variable to your Maven repository:
export REPO=~/.m2/repository
And then simply run the script:
sh target/bin/main
All of the code for this example is on github.com:
https://github.com/jamesward/warless_java_web_apps
To make all of this even easier, Jetty has a Maven archetype for generating everything for you. To create a new project containing this setup run:
mvn archetype:generate -DarchetypeGroupId=org.mortbay.jetty.archetype -DarchetypeArtifactId=jetty-archetype-assembler -DarchetypeVersion=8.1.9.v20130131
And now you are ready to build a WAR-less Java web app!
This setup is really the bare minimum required to handle web resource and servlet requests you will need to do a little more work if you want to add JSP support. Find out more about this in the Jetty documentation.
So… What do you think about Java Web apps without WAR files & WAR packaging? I’d love to hear your thoughts!