Building an executable jar with embedded web server

To make using and deploying our application easier, I want it to be easy to run the application. Instead of requiring the installation of a servlet server like Tomcat, Jetty or a full blown application server, I aim to use an embedded server.

The end result is the ability to run our application using the following command:

java -jar application.jar

Possibly using some additional parameters like

java -DserverPort=8080 -DshutdownPort=8089 -jar application.jar

As web server I chose Undertow, a relatively new server which is also used in WildFly.

I want the result jar to still show the included libraries, so I do not want to make an uber-jar using something like the maven shade plugin. I rather want to build a jar which includes jars with all the dependencies.

Building this jar can be done using the maven assembly plugin, using a configuration like the following:

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
    <id>with-deps</id>
    <formats>
        <format>jar</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <fileSets>
        <fileSet>
            <outputDirectory>/</outputDirectory>
            <directory>${basedir}/target/classes</directory>
            <excludes>
                <exclude>.PLACEHOLDER.txt</exclude>
            </excludes>
        </fileSet>
    </fileSets>
    <dependencySets>
        <dependencySet>
            <excludes>
                <exclude>ggg:aaa-exe</exclude>
            </excludes>
            <outputDirectory>/lib</outputDirectory>
            <useProjectArtifact>true</useProjectArtifact>
            <unpack>false</unpack>
            <scope>runtime</scope>
        </dependencySet>
    </dependencySets>
</assembly>

I use a separate module to build the jar. In this module the main pom mainly contains all the application dependencies and the following configuration:

<?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>
 
    <parent>
        <groupId>ggg</groupId>
        <artifactId>aaa</artifactId>
        <version>vvv-SNAPSHOT</version>
    </parent>
 
    <artifactId>aaa-exe</artifactId>
    <packaging>jar</packaging>
 
    <name>Build executable jar</name>
 
    <dependencies>
        <!-- module dependencies which build the application -->
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>2.5.4</version>
                <configuration>
                    <descriptors>
                        <descriptor>${basedir}/src/assembly/include-deps.xml</descriptor>
                    </descriptors>
                    <archive>
                        <manifest>
                            <mainClass>myapp.WarRunner</mainClass>
                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                        </manifest>
                        <manifestEntries>
                            <Implementation-Version>${project.version}</Implementation-Version>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
                        <phase>package</phase> <!-- bind to the packaging phase -->
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

This module will now build two jars, a normal jar (very small) and one with the dependencies which has the “with-deps” suffix. The latter can be used to start the application.

In the module itself we need only two classes.

We need a main class to start the application (the “mainClass” which is referenced in the manifest). This should start the application. To assure that the embedded jars are used, we need a special classloader. Apart from that, the main method refers to a different class which runs the application.

public final class WarRunner {
 
    private WarRunner() {
        // hide constructor
    }
 
    /**
     * Run de "real" runner from the application.
     *
     * @param args command line parameters
     */
    public static void main(String[] args) {
        try {
            JarClassLoader.invokeMain("myapp.AppRunner", args);
        } catch (Throwable e) {
            System.err.println("Cannot run myapp.AppRunner: " + e.getMessage());
            e.printStackTrace();
        }
    }
 
}

The second class is the JarClassLoader. This is based on the code from embedded jar classloader, but with some fixes.

public class JarClassLoader extends URLClassLoader {
 
    private static boolean isJar(String fileName) {
        return fileName != null && fileName.toLowerCase().endsWith(".jar");
    }
 
    private static File jarEntryAsFile(JarFile jarFile, JarEntry jarEntry) throws IOException {
        String name = jarEntry.getName().replace('/', '_');
        int i = name.lastIndexOf(".");
        String extension = i > -1 ? name.substring(i) : "";
        File file = File.createTempFile(name.substring(0, name.length() - extension.length()) + ".", extension);
        file.deleteOnExit();
        try (InputStream input = jarFile.getInputStream(jarEntry)) {
            try (OutputStream output = new FileOutputStream(file)) {
                int readCount;
                byte[] buffer = new byte[4096];
                while ((readCount = input.read(buffer)) != -1) {
                    output.write(buffer, 0, readCount);
                }
                return file;
            }
        } 
    }
 
    /**
     * Build classpath with extra jars in it.
     * @param urls urls
     * @param parent parent class loader
     */
    public JarClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
        try {
            ProtectionDomain protectionDomain = getClass().getProtectionDomain();
            CodeSource codeSource = protectionDomain.getCodeSource();
            URL rootJarUrl = codeSource.getLocation();
            String rootJarName = rootJarUrl.getFile();
            if (isJar(rootJarName)) {
                addJarResource(new File(rootJarUrl.getPath()));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    private void addJarResource(File file) throws IOException {
        JarFile jarFile = new JarFile(file);
        addURL(file.toURL());
        Enumeration<JarEntry> jarEntries = jarFile.entries();
        while (jarEntries.hasMoreElements()) {
            JarEntry jarEntry = jarEntries.nextElement();
            if (!jarEntry.isDirectory() && isJar(jarEntry.getName())) {
                addJarResource(jarEntryAsFile(jarFile, jarEntry));
            }
        }
    }
 
    @Override
    public synchronized Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            Class<?> clazz = findLoadedClass(name);
            if (clazz == null) {
                clazz = findClass(name);
            }
            return clazz;
        } catch (ClassNotFoundException e) {
            return super.loadClass(name);
        }
    }
 
    /**
     * Invoke main method in given class using this classloader.
     * @param name class to invoke
     * @param args command line arguments
     * @throws ClassNotFoundException oops
     * @throws NoSuchMethodException oops
     * @throws InvocationTargetException oops
     */
    public static void invokeMain(String name, String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
        try {
            final String mainMethodName = "main";
            JarClassLoader loader = new JarClassLoader(((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs(), contextClassLoader);
            Thread.currentThread().setContextClassLoader(loader); // replace contextClassloader
            Class<?> clazz = loader.loadClass(name);
            Method method = clazz.getMethod(mainMethodName, String[].class);
            method.setAccessible(true);
            int mods = method.getModifiers();
            if (method.getReturnType() != void.class || !Modifier.isStatic(mods) || !Modifier.isPublic(mods)) {
                throw new NoSuchMethodException(mainMethodName);
            }
            try {
                method.invoke(null, (Object) args); // Crazy cast "(Object)args" because param is: "Object... args"
            } catch (IllegalAccessException e) {
                // This should not happen, as we have disabled access checks
                System.err.println("Probleem during JarClassLoader.invokeMain: " + e.getMessage());
                e.printStackTrace();
            }
        } finally {
            Thread.currentThread().setContextClassLoader(contextClassLoader);
        }
    }
 
}

What remains to be done is write the AppRunner class (which should be in the module with the application code):

public final class AppRunner {
 
    private static final Logger LOG = LoggerFactory.getLogger(AppRunner.class);
 
    private static final String CONTEXT_PATH = "/app";
    private static final int SERVER_PORT = 8080;
    private static final int SHUTDOWN_PORT = 8089;
    private static final int SHUTDOWN_WAIT = 5;
 
    private static GracefulShutdownHandler shutdownHandler;
    private static Undertow server;
 
    private AppRunner() {
        // hide constructor
    }
 
    /**
     * Main method to start the application
     *
     * @param args command line parameters - not used at the moment - configuration through properties
     */
    public static void main(final String[] args) {
        int serverPort = getProperty("km.server.port").orElse(SERVER_PORT);
        int shutdownPort = getProperty("km.shutdown.port").orElse(SHUTDOWN_PORT);
        int shutdownWaitSeconds = getProperty("km.shutdown.wait").orElse(SHUTDOWN_WAIT);
 
        try {
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            DeploymentInfo servletBuilder = Servlets.deployment()
                    .setClassLoader(AppRunner.class.getClassLoader())
                    .setContextPath(CONTEXT_PATH)
                    .setDeploymentName("app.war")
                    .addInitParameter("contextConfigLocation", "classpath:app-web-applicationContext.xml")
                    .addInitParameter("resteasy.logger.type", "SLF4J")
                    .addInitParameter("resteasy.wider.request.matching", "true")
                    .addWelcomePage("index.html")
                    .setDefaultSessionTimeout(60)
                    .addListener(new ListenerInfo(RequestContextListener.class))
                    .addListener(new ListenerInfo(ResteasyBootstrap.class))
                    .addListener(new ListenerInfo(SpringContextLoaderListener.class))
                    .addServlets(Servlets.servlet("RESTEasy", HttpServletDispatcher.class)
                            .addMapping("/rest/*"))
                    .setResourceManager(new ClassPathResourceManager(classLoader, "web"));
 
            DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder);
            manager.deploy();
 
            HttpHandler servletHandler = manager.start();
            PathHandler path = Handlers.path(Handlers.redirect(CONTEXT_PATH))
                    .addPrefixPath(CONTEXT_PATH, servletHandler);
            shutdownHandler = Handlers.gracefulShutdown(path);
            server = Undertow.builder()
                    .addHttpListener(serverPort, "localhost")
                    .addHttpListener(shutdownPort, "localhost", exchange -> {
                        exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
                        exchange.getResponseSender().send(
                                String.format("Going to shutdown when all requests have ended or %s seconds, whichever occurs first.", shutdownWaitSeconds));
                        new Thread(() -> {
                            try {
                                shutdownHandler.shutdown();
                                shutdownHandler.awaitShutdown(shutdownWaitSeconds * 1000);
                            } catch (InterruptedException ie) {
                                LOG.warn("Wait for undertow requests to end was interrupted.", ie);
                            }
                            server.stop();
                            LOG.info("Gracefully shut down.");
                            System.exit(0);
                        }).run();
                    })
                    .setHandler(shutdownHandler)
                    .build();
            server.start();
 
        }  catch (ServletException e) {
            LOG.error("Kan servlet niet starten.", e);
        }
    }
 
    private static Optional<Integer> getProperty(String propertyName) {
        String propertyValue = System.getProperty("km.server.port");
        if (StringUtils.isNotBlank(propertyValue)) {
            try {
                return Optional.of(Integer.parseInt(propertyValue));
            } catch (NumberFormatException nfe) {
                LOG.error(String.format("Cannot parse property %s with value %s to a number, ignoring value.", propertyName, propertyValue));
            }
        }
        return Optional.empty();
    }
 
}

This code starts creates a servlet. It says that the resources to be served can be found in the “web” package (as there is no webapp (folder) you have to give the explicit location.
It also registers a shutdown handler. Any request on the shutdown port will block incoming requests and wait for max 5s until pending requests are all handled. Then the server is shut down.

Leave a Reply

Your email address will not be published. Required fields are marked *

question razz sad evil exclaim smile redface biggrin surprised eek confused cool lol mad twisted rolleyes wink idea arrow neutral cry mrgreen

*