Recently Adam, Steve (the Chief) and myself have been working on a new service that involves a brand new web-application. I won’t bore you with details of what it does because it won’t be publicly facing. Regardless, we had to consider deployment options and after a few false starts we decided to build an “executable WAR”. Basically a war file that you can start up by calling “java -jar mywebapp.war”.

Why?
First we liked the idea of having a fully contained application in a neat and tidy package. Second, this isn’t a case where we can build the war and let our operations team pick the container. Our application requires a servlet 3.0 container so our options were limited. And finally, we just wanted to learn how to do it :)

How?
Our first naive thought was that if a war file has a manifest with a Main-Class declaration that everything would just work. We were very, very wrong. Since war files keep their classes in WEB-INF/classes and not in the root of the jar this failed. Next we put our main class in the root of the jar, it started but our app server classes couldn’t be found because we put them in WEB-INF/lib, *sigh*. Then it all started coming together, any dependency that we typically consider “provided” will get un-jarred along with our main class into the root of the jar, then the rest will be the typical war format. It looks like this:

executable_war_layout

We did this all in buildr by adding our own packager and calling it :executable_war.

module Rally
  module Packaging
    module ExecutableWar
      class ExecutableWarTask < Buildr::Packaging::Java::JarTask
        attr_accessor :main_class, :libs, :provided, :classes
 
        def initialize(*args) #:nodoc:
          super
          enhance { |jar|
            libs.each { |lib| lib.invoke if lib.respond_to?(:invoke) }
            classes.each { |dir| jar.include dir, :as => 'WEB-INF/classes' }
 
            classes.each do |dir|
              class_dir = dir.to_s
              class_dir_classes = "#{class_dir}/**/*.class"
              main_class_path = main_class.gsub("\.", "/")
 
              Dir.glob(class_dir_classes) do |file|
                jar.path(File.dirname(file.sub(class_dir, ""))).include(file) if file.include?(main_class_path)
              end
            end
 
            provided_target_dir = "target/provided"
            mkdir_p(provided_target_dir)
            cd(provided_target_dir) do
              provided.each do |dependency|
                `jar -xf #{Buildr.artifact(dependency)}`
              end
            end
            jar.path("/").include("#{provided_target_dir}/*")
            jar.path('WEB-INF/lib').include Buildr.artifacts(libs)
          }
        end
 
        def main_class=(main_class)
          @main_class = main_class
          add_to_manifest({'Main-Class' => main_class})
        end
 
        def version=(version)
          @version = version
          add_to_manifest({'Implementation-Build' => version})
        end
 
        private
        def add_to_manifest(options)
          if self.manifest
            self.manifest = self.manifest.merge(options)
          else
            self.manifest = options
          end
        end
      end
 
      def package_as_executable_war_spec(spec) #:nodoc:
        spec.merge(:type=>:jar)
      end
 
      def package_as_executable_war(file_name) #:nodoc:
        ExecutableWarTask.define_task(file_name).tap do |war|
          war.with path_to(:source, :main, :webapp)
          war.with :classes => [compile.target, resources.target]
        end
      end
    end
  end
end
 
class Buildr::Project
  include Rally::Packaging::ExecutableWar
end

And here is how we use it from our buildfile, where PROVIDED represents the buildr dependencies on things like Jetty and the Servlet API

package(:executable_war).with(
    :libs => compile.dependencies - PROVIDED,
    :main_class => "com.rallydev.server.jetty.EmbeddedJetty",
    :provided => PROVIDED,
    :version => "1.0")

And to start the server, as alluded to above chose Jetty 8, here is our code. Note the DEV_MODE flag so we can still do in place development when not deploying the jar file.

package com.rallydev.server.jetty;
 
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
 
import java.net.InetSocketAddress;
 
public class EmbeddedJetty {
 
    public static void main(String[] args) throws Exception {
        InetSocketAddress address = new InetSocketAddress("0.0.0.0", 8080);
 
        String warPath = Boolean.getBoolean("DEV_MODE") ?
                "src/main/webapp" :
                EmbeddedJetty.class.getProtectionDomain().getCodeSource().getLocation().getPath();
 
        Server server = new Server(address);
 
        WebAppContext context = new WebAppContext();
        context.setContextPath("/");
        context.setWar(warPath);
        server.setHandler(context);
 
        server.start();
        server.join();
    }
 
}

What does everyone think? Leave a comment and tell us what we screwed up :)

Request a Call

Looking for support?

Send Us Your Feedback

Provide us with some information about yourself and we'll be in touch soon. * Required Field