jueves, 4 de agosto de 2011

Step by step: Executable WAR files

Have you ever executed Jenkins? One of the most valued features it packs is the embedded Winstone servlet container. It allows your favorite CI server to be launched without being deployed to a traditional server (say Tomcat) first. Unfortunately Winstone is not a viable choice for everybody, particulary if the app makes use of the latest servlet/JSP features. Fortunately there are other choices.

Jetty has always had a good reputation in this field. As of today Glassfish and Tomcat also offer solutions for this need. I'll focus on Jetty, basically because it's easy and it works. There are good articles around the net about the topic but no step by step tutorial so let's see all the tasks it takes one by one.

First of all, an executable WAR file is no different from an executable JAR file. That means that it needs an entry in the MANIFEST file that points to the class that will be launched. In this case, the main class has to start the server and deploy the very same WAR file.

The source of the class:

public final class Launcher {
   public static void main(String[] args) throws Exception {
      int port = Integer.parseInt(System.getProperty("port", "8080"));
      Server server = new Server(port);
      ProtectionDomain domain = Launcher.class.getProtectionDomain();
      URL location = domain.getCodeSource().getLocation();
      WebAppContext webapp = new WebAppContext();
      webapp.setContextPath("/");
      webapp.setWar(location.toExternalForm());
      server.setHandler(webapp);
      server.start();
      server.join();
   }
}

The above class was copypasted from here. All credit is due to the original author. It's a clever tweak over the official documentation to point to the WAR file location.

To compile, Maven needs to download the dependencies. Easy:

<dependency>
   <groupId>org.eclipse.jetty</groupId>
   <artifactId>jetty-server</artifactId>
   <version>7.3.0.v20110203</version>
   <scope>provided</scope>
</dependency>
<dependency>
   <groupId>org.eclipse.jetty</groupId>
   <artifactId>jetty-webapp</artifactId>
   <version>7.3.0.v20110203</version>
   <scope>provided</scope>
</dependency>

Notice the scope is set as provided. This will later be explained. For now the class compiles so the next step is to add the MANIFEST entry. This is usually done using the maven-jar-plugin so a WAR file will use the equivalent maven-war-plugin:

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-war-plugin</artifactId>
   <version>2.1-beta-1</version>
   <configuration>
      <archive>
         <manifest>
            <mainClass>Launcher</mainClass>
         </manifest>
      </archive>
   </configuration>
</plugin>

Right now the WAR file can be executed....but it will fail miserably. There are two problems, first the compiled class is located inside WEB-INF (as all classes in a WAR file) and second Jetty dependencies are not present in the classpath yet.

The next task is to move the Launcher class to the root of the WAR (classloading behaves the same as a JAR file). Maven can't do this easily so Ant to the rescue.

<plugin>
   <artifactId>maven-antrun-plugin</artifactId>
   <executions>
      <execution>
         <id>main-class-placement</id>
         <phase>prepare-package</phase>
         <configuration>
            <tasks>
               <move todir="${project.build.directory}/${project.artifactId}-${project.version}/">
                  <fileset dir="${project.build.directory}/classes/">
                     <include name="Launcher.class" />
                  </fileset>
               </move>
            </tasks>
         </configuration>
         <goals>
            <goal>run</goal>
         </goals>
      </execution>
   </executions>
</plugin>

The second task is to provide Jetty dependencies. Java does not support JARs-inside-JARs (or JARs-inside-WARs for the matter). The only option is to unzip the dependencies and again at the root of the WAR file (WEB-INF cannot be reached). Remember the scope of the Jetty dependencies from above! Another plugin will help with the task:

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-dependency-plugin</artifactId>
   <version>2.3
   <executions>
      <execution>
         <id>jetty-classpath</id>
         <phase>prepare-package</phase>
         <goals>
            <goal>unpack-dependencies</goal>
         </goals>
         <configuration>
            <includeGroupIds>org.eclipse.jetty,javax.servlet</includeGroupIds>
            <excludeArtifactIds>jsp-api,jstl</excludeArtifactIds>
            <outputDirectory>
               ${project.build.directory}/${project.artifactId}-${project.version}
            </outputDirectory>
         </configuration>
      </execution>
   </executions>
</plugin>

Right now the WAR can be executed using java -jar name_of_the_project.war. Unfortunately, the user would notice this on first access, it lacks JSP support throwing a 500 error. To add it another dependency has to be included. This dependency does not need to be unpacked, it's readed from WEB-INF/lib as any other library.

<dependency>
   <groupId>org.mortbay.jetty</groupId>
   <artifactId>jsp-2.1-glassfish</artifactId>
   <version>2.1.v20100127</version>
</dependency>

And that would be it! That would be it if the WAR doesn't need to be deployed to Tomcat any more. Yes, adding that last dependency breaks JSP support in the most used servlet container. Sad. That forces even more work to prepare everything. There's no good solution to this problem, if the JAR is downloaded and copied the WAR will not be deployable otherwise it won't have JSP support. The best workaround is to conditionally add the dependency if the desired outcome is a standalone, distributable, file (so the development process is not affected). In Maven this is achieved using a specific profile. What does it mean in the end? Two things, some refactoring in the POM file (so the specific tasks required by Jetty are relocated inside the profile) and starting the Maven process with the -P flag.

And finally we are there! If you need a complete working example I've comitted the changes to OSSMoney which was my original intent anyway (distributing something that end users could execute in their machines without any knowledge of the Java ecosystem).