Description
Problem
When attempting to use Jakarta Mail API 2.1.1 within a Jenkins plugin, the following error occurs:
java.lang.IllegalStateException: Not provider of jakarta.mail.util.StreamProvider was found
at jakarta.mail.util.FactoryFinder.find(FactoryFinder.java:64)
at jakarta.mail.util.StreamProvider.provider(StreamProvider.java:186)
at jakarta.mail.Session.<init>(Session.java:254)
at jakarta.mail.Session.getInstance(Session.java:308)
at hudson.tasks.Mailer$DescriptorImpl.createSession(Mailer.java:415)
at hudson.tasks.Mailer$DescriptorImpl.doSendTestMail(Mailer.java:704)
Evaluation
The cause is explained in this page. The caller hudson.tasks.Mailer
is a Jenkins plugin whose class loader is not Thread#getContextClassLoader
but which can see the classes in the Jenkins Mailer plugin and its dependencies (including the Jenkins Jakarta Mail plugin). Thread#getContextClassLoader
is set to the Jenkins core class loader, which cannot see any Jenkins plugins. Since we bundle Jakarta Activation and Jakarta Mail as Jenkins plugins, invoking
ServiceLoader#load(Class service, ClassLoader loader)
, uses Thread#getContextClassLoader
) from a Jenkins plugin attempts to find the class in the Jenkins core class loader (which cannot see classes in Jenkins plugins) and fails.
At its core, the problem is that Jakarta Mail generally (and StreamProvider
in particular) assumes that Thread#getContextClassLoader
has access to the classes that the calling class has access to. This assumption is usually true, but it is not true for Jenkins and other modular applications with a plugin system implemented with a class loader hierarchy.
Workaround
We can successfully work around the problem with code like
Thread t = Thread.currentThread();
ClassLoader orig = t.getContextClassLoader();
t.setContextClassLoader(getClass().getClassLoader());
try {
Session.getInstance([…]);
} finally {
t.setContextClassLoader(orig);
}
at each call site in each Jenkins plugin, but there are hundreds of such call sites, making it prohibitively difficult to work around the problem in this way throughout the Jenkins ecosystem.
Suggested solution
Please modify the default behavior to load classes with getClass().getClassLoader()
instead of Thread#getContextClassLoader
by changing
ServiceLoader.load(factory, getClass().getClassLoader())
. Such a change might seem risky, but it was successfully done (at the request of the Jenkins core developers) after a Twitter discussion in Jackson in FasterXML/jackson-dataformat-xml@97fe9eb without causing regressions for end users.
Note
Note that a similar problem exists in Jakarta Activation in https://github.com/jakartaee/jaf-api/blob/b7fb44ae9d1872bf86b4098f8f1462e7f7b05a3c/api/src/main/java/jakarta/activation/ServiceLoaderUtil.java#L31, but prior to #579 I was able to work around the problem with gross hacks like https://github.com/jenkinsci/jakarta-mail-api-plugin/tree/32b196156bd5774402bd1d5ec6de8ff3665e4b9c/src/main/java/io/jenkins/plugins/jakarta/activation. As of #579 the same problem is also present in Jakarta Mail, and I see no workaround. The ask is for both Jakarta Mail and Jakarta Activation to load classes with getClass().getClassLoader()
instead of Thread#getContextClassLoader
.