I have an embedded Tomcat application packaged as an executable (thin) jar
with multiple external jar
dependencies.
The build process generates a META-INF/MANIFEST.MF
with header fields Main-Class
and Class-Path
(with an entry per runtime dependency).
I want to execute the application using a simple java -jar my_app.jar
, but I am unable to make Tomcat scan these dependent jars (in order to discover TLDs or @HandlesTypes
classes like Spring WebApplicationInitializer
).
I am configuring the jar scanning this way:
StandardJarScanner jarScanner = (StandardJarScanner) ctx.getJarScanner();
jarScanner.setScanBootstrapClassPath(true);
jarScanner.setScanClassPath(true);
And all the jars has a META-INF
folder, but the scanner completely ignores them.
Any ideas?
Note: I can make this work using different approaches (fat jar, running from maven, ...) but I am interested in making it work this way, as any other java application.
Tomcat gets the jar URLs to scan by repeatedly calling URLClassLoader.getURLS()
in the class loader hierarchy (bottom up)
The problem arises for the system class loader since URLClassLoader.getURLS()
does not return the classpath jars when the java application is executed as java -jar <executable-jar>
See: How does a classloader load classes reference in the manifest classpath?
In the previous post it is suggested to use reflection to access a private field in the system classloader instance, but this presents several problems:
So I figured out another way:
cl.getResources("META-INF/MANIFEST.MF")
. These manifests can be of jars managed by the current class loader or its ascendants classloaders.The only requirement for this method to work is that jars in the classpath must have a manifest in order to be returned (not much to ask).
/**
* Returns the search path of URLs for loading classes and resources for the
* specified class loader, including those referenced in the
* {@code Class-path} header of the manifest of a executable jar, in the
* case of class loader being the system class loader.
* <p>
* Note: These last jars are not returned by
* {@link java.net.URLClassLoader#getURLs()}.
* </p>
* @param cl
* @return
*/
public static URL[] getURLs(URLClassLoader cl) {
if (cl.getParent() == null || !(cl.getParent()
instanceof URLClassLoader)) {
return cl.getURLs();
}
Set<URL> urlSet = new LinkedHashSet();
URL[] urLs = cl.getURLs();
URL[] urlsFromManifest = getJarUrlsFromManifests(cl);
URLClassLoader parentCl = (URLClassLoader) cl.getParent();
URL[] ancestorUrls = getJarUrlsFromManifests(parentCl);
for (int i = 0; i < urlsFromManifest.length; i++) {
urlSet.add(urlsFromManifest[i]);
}
for (int i = 0; i < ancestorUrls.length; i++) {
urlSet.remove(ancestorUrls[i]);
}
for (int i = 0; i < urLs.length; i++) {
urlSet.add(urLs[i]);
}
return urlSet.toArray(new URL[urlSet.size()]);
}
/**
* Returns the URLs of those jar managed by this classloader (or its
* ascendant classloaders) that have a manifest
* @param cl
* @return
*/
private static URL[] getJarUrlsFromManifests(ClassLoader cl) {
try {
Set<URL> urlSet = new LinkedHashSet();
Enumeration<URL> manifestUrls =
cl.getResources("META-INF/MANIFEST.MF");
while (manifestUrls.hasMoreElements()) {
try {
URL manifestUrl = manifestUrls.nextElement();
if(manifestUrl.getProtocol().equals("jar")) {
urlSet.add(new URL(manifestUrl.getFile().substring(0,
manifestUrl.getFile().lastIndexOf("!"))));
}
} catch (MalformedURLException ex) {
throw new AssertionError();
}
}
return urlSet.toArray(new URL[urlSet.size()]);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
Tomcat registered issue: https://bz.apache.org/bugzilla/show_bug.cgi?id=59226