Skip to content

Jakarta Mail erroneously assumes that classes can be loaded from Thread#getContextClassLoader #665

Closed
@basil

Description

@basil

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<T> sl = ServiceLoader.load(factory);
(which, unlike 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<T> sl = ServiceLoader.load(factory);
to 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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions