Is there a way to modify the classloader so that I can use hooks that exist in the same jar where James is going to exist?
"modify the classloader"? ... not without re-compiling the apache/james-project
itself, I would presume.
Apache James likely uses a dedicated component for classloading, which could be based on Java's URLClassLoader or a similar class.
I can see org.apache.james.utils.ExtendedClassLoader
You would need to modify it to check the current application classpath (where your Quarkus app and embedded James reside) in addition to or instead of the conf/lib
directory.
Then, after making the changes, you will need to recompile James and integrate the custom version with your Quarkus application.
The XML configurations you have for server implementations will not need to change because of these modifications. The XML file merely instructs James on what class to instantiate and possibly initialize. By modifying the classloading behavior, you are just changing where James looks for these classes. The XML stays the same because it references classes by their fully qualified names, not their locations.
I initially propose an updated code for ExtendedClassLoader
, but, as noted by Holger in the comments:
A URLClassLoader
follows the standard parent delegation model. Since getClass().getClassLoader()
is passed to the constructor as the parent loader, it will be searched for classes, even before the URLs passed to the constructor are searched.
Merging currentClasspath
with extensionUrls
has no effect.
package org.apache.james.utils;
// ... [other imports]
public class ExtendedClassLoader {
private static final Logger LOGGER = LoggerFactory.getLogger(ExtendedClassLoader.class);
public static final String EXTENSIONS_JARS_FOLDER_NAME = "extensions-jars/";
private final URLClassLoader urlClassLoader;
@Inject
public ExtendedClassLoader(FileSystem fileSystem) {
URL[] extensionUrls = retrieveExtensionsUrls(fileSystem);
URL[] currentClasspath = ((URLClassLoader) getClass().getClassLoader()).getURLs();
URL[] combinedUrls = Stream.concat(
Stream.of(extensionUrls),
Stream.of(currentClasspath)
).distinct().toArray(URL[]::new);
this.urlClassLoader = new URLClassLoader(combinedUrls, getClass().getClassLoader());
}
private URL[] retrieveExtensionsUrls(FileSystem fileSystem) {
try {
File file = fileSystem.getFile("file://" + EXTENSIONS_JARS_FOLDER_NAME);
return recursiveExpand(file)
.toArray(URL[]::new);
} catch (IOException e) {
LOGGER.info("No " + EXTENSIONS_JARS_FOLDER_NAME + " folder.");
return new URL[]{};
}
}
private Stream<URL> recursiveExpand(File file) {
return StreamUtils.ofNullable(file.listFiles())
.flatMap(Throwing.function(this::expandFile).sneakyThrow());
}
private Stream<URL> expandFile(File file) throws MalformedURLException {
if (file.isDirectory()) {
return recursiveExpand(file);
}
LOGGER.info("Loading custom classpath resource {}", file.getAbsolutePath());
return Stream.of(file.toURI().toURL());
}
@SuppressWarnings("unchecked")
public <T> Class<T> locateClass(FullyQualifiedClassName className) throws ClassNotFoundException {
return (Class<T>) urlClassLoader.loadClass(className.getName());
}
}
In the constructor ExtendedClassLoader(FileSystem fileSystem)
, you can retrieve the URLs from the extensions-jars/
folder (just like the original code). And the URLs from the current application's classpath. The combined URLs are ensured to be unique to avoid duplication.
These combined URLs are then fed into the URLClassLoader
so that, when James tries to load a class, it will look in both the provided extension URLs and the current application's classpath.
By making these changes, James will first look for hooks in the extensions-jars/
folder (if provided) and then in the current application's classpath. That means if you have packaged James and your custom hooks into the same jar (or same classpath), it should be able to find and load them without issues.
See also "URLClassLoader can't find class thats in different directory".