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=""

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="" xmlns:xsi=""
    <name>Build executable jar</name>
        <!-- module dependencies which build the application -->
                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
                        <phase>package</phase> <!-- bind to the packaging phase -->

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());

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);
        try (InputStream input = jarFile.getInputStream(jarEntry)) {
            try (OutputStream output = new FileOutputStream(file)) {
                int readCount;
                byte[] buffer = new byte[4096];
                while ((readCount = != -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) {
    private void addJarResource(File file) throws IOException {
        JarFile jarFile = new JarFile(file);
        Enumeration<JarEntry> jarEntries = jarFile.entries();
        while (jarEntries.hasMoreElements()) {
            JarEntry jarEntry = jarEntries.nextElement();
            if (!jarEntry.isDirectory() && isJar(jarEntry.getName())) {
                addJarResource(jarEntryAsFile(jarFile, jarEntry));
    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);
            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());
        } finally {

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()
                    .addInitParameter("contextConfigLocation", "classpath:app-web-applicationContext.xml")
                    .addInitParameter("resteasy.logger.type", "SLF4J")
                    .addInitParameter("resteasy.wider.request.matching", "true")
                    .addListener(new ListenerInfo(RequestContextListener.class))
                    .addListener(new ListenerInfo(ResteasyBootstrap.class))
                    .addListener(new ListenerInfo(SpringContextLoaderListener.class))
                    .addServlets(Servlets.servlet("RESTEasy", HttpServletDispatcher.class)
                    .setResourceManager(new ClassPathResourceManager(classLoader, "web"));
            DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder);
            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");
                                String.format("Going to shutdown when all requests have ended or %s seconds, whichever occurs first.", shutdownWaitSeconds));
                        new Thread(() -> {
                            try {
                                shutdownHandler.awaitShutdown(shutdownWaitSeconds * 1000);
                            } catch (InterruptedException ie) {
                                LOG.warn("Wait for undertow requests to end was interrupted.", ie);
                  "Gracefully shut down.");
        }  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