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.