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.