I found the question interesing and created a little tool for you:
https://github.com/kriegaex/ThreadSafeClassLoader
Currently it is not available as an official release on Maven Central yet, but you can get a snapshot like this:
<dependency>
<groupId>de.scrum-master</groupId>
<artifactId>threadsafe-classloader</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- (...) -->
<repositories>
<repository>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>ossrh</id>
<name>Sonatype OSS Snapshots</name>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
Class ThreadSafeClassLoader
:
It uses JCL (Jar Class Loader) under the hood because it already offers class-loading, object instantiation and proxy generation features discussed in other parts of this thread. (Why re-invent the wheel?) What I added on top is a nice interface for exactly what we need here:
package de.scrum_master.thread_safe;
import org.xeustechnologies.jcl.JarClassLoader;
import org.xeustechnologies.jcl.JclObjectFactory;
import org.xeustechnologies.jcl.JclUtils;
import org.xeustechnologies.jcl.proxy.CglibProxyProvider;
import org.xeustechnologies.jcl.proxy.ProxyProviderFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ThreadSafeClassLoader extends JarClassLoader {
private static final JclObjectFactory OBJECT_FACTORY = JclObjectFactory.getInstance();
static {
ProxyProviderFactory.setDefaultProxyProvider(new CglibProxyProvider());
}
private final List<Class> classes = new ArrayList<>();
public static ThreadLocal<ThreadSafeClassLoader> create(Class... classes) {
return ThreadLocal.withInitial(
() -> new ThreadSafeClassLoader(classes)
);
}
private ThreadSafeClassLoader(Class... classes) {
super();
this.classes.addAll(Arrays.asList(classes));
for (Class clazz : classes)
add(clazz.getProtectionDomain().getCodeSource().getLocation());
}
public <T> T newObject(ObjectConstructionRules rules) {
rules.validate(classes);
Class<T> castTo = rules.targetType;
return JclUtils.cast(createObject(rules), castTo, castTo.getClassLoader());
}
private Object createObject(ObjectConstructionRules rules) {
String className = rules.implementingType.getName();
String factoryMethod = rules.factoryMethod;
Object[] arguments = rules.arguments;
Class[] argumentTypes = rules.argumentTypes;
if (factoryMethod == null) {
if (argumentTypes == null)
return OBJECT_FACTORY.create(this, className, arguments);
else
return OBJECT_FACTORY.create(this, className, arguments, argumentTypes);
} else {
if (argumentTypes == null)
return OBJECT_FACTORY.create(this, className, factoryMethod, arguments);
else
return OBJECT_FACTORY.create(this, className, factoryMethod, arguments, argumentTypes);
}
}
public static class ObjectConstructionRules {
private Class targetType;
private Class implementingType;
private String factoryMethod;
private Object[] arguments;
private Class[] argumentTypes;
private ObjectConstructionRules(Class targetType) {
this.targetType = targetType;
}
public static ObjectConstructionRules forTargetType(Class targetType) {
return new ObjectConstructionRules(targetType);
}
public ObjectConstructionRules implementingType(Class implementingType) {
this.implementingType = implementingType;
return this;
}
public ObjectConstructionRules factoryMethod(String factoryMethod) {
this.factoryMethod = factoryMethod;
return this;
}
public ObjectConstructionRules arguments(Object... arguments) {
this.arguments = arguments;
return this;
}
public ObjectConstructionRules argumentTypes(Class... argumentTypes) {
this.argumentTypes = argumentTypes;
return this;
}
private void validate(List<Class> classes) {
if (implementingType == null)
implementingType = targetType;
if (!classes.contains(implementingType))
throw new IllegalArgumentException(
"Class " + implementingType.getName() + " is not protected by this thread-safe classloader"
);
}
}
}
I tested my concept with several unit and integration tests, among them one showing how to reproduce and solve the veraPDF problem.
Now this is what your code looks like when using my special classloader:
Class VeraPDFValidator
:
We are just adding a static ThreadLocal<ThreadSafeClassLoader>
member to our class, telling it which classes/libraries to put into the new classloader (mentioning one class per library is enough, subsequently my tool identifies the library automatically).
Then via threadSafeClassLoader.get().newObject(forTargetType(VeraPDFValidatorHelper.class))
we instantiate our helper class inside the thread-safe classloader and create a proxy object for it so we can call it from outside.
BTW, static boolean threadSafeMode
only exists to switch between the old (unsafe) and new (thread-safe) usage of veraPDF so as to make the original problem reproducible for the negative integration test case.
package de.scrum_master.app;
import de.scrum_master.thread_safe.ThreadSafeClassLoader;
import org.verapdf.core.*;
import org.verapdf.pdfa.*;
import javax.xml.bind.JAXBException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.function.Function;
import static de.scrum_master.thread_safe.ThreadSafeClassLoader.ObjectConstructionRules.forTargetType;
public class VeraPDFValidator implements Function<InputStream, byte[]> {
public static boolean threadSafeMode = true;
private static ThreadLocal<ThreadSafeClassLoader> threadSafeClassLoader =
ThreadSafeClassLoader.create( // Add one class per artifact for thread-safe classloader:
VeraPDFValidatorHelper.class, // - our own helper class
PDFAParser.class, // - veraPDF core
VeraGreenfieldFoundryProvider.class // - veraPDF validation-model
);
private String flavorId;
private Boolean prettyXml;
public VeraPDFValidator(String flavorId, Boolean prettyXml)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
this.flavorId = flavorId;
this.prettyXml = prettyXml;
}
@Override
public byte[] apply(InputStream inputStream) {
try {
VeraPDFValidatorHelper validatorHelper = threadSafeMode
? threadSafeClassLoader.get().newObject(forTargetType(VeraPDFValidatorHelper.class))
: new VeraPDFValidatorHelper();
return validatorHelper.validatePDF(inputStream, flavorId, prettyXml);
} catch (ModelParsingException | ValidationException | JAXBException | EncryptedPdfException e) {
throw new RuntimeException("invoking veraPDF validation", e);
}
}
}
Class VeraPDFValidatorHelper
:
In this class we isolate all access to the broken library. Nothing special here, just code copied from the OP's question. Everything done here happens inside the thread-safe classloader.
package de.scrum_master.app;
import org.verapdf.core.*;
import org.verapdf.pdfa.*;
import org.verapdf.pdfa.flavours.PDFAFlavour;
import org.verapdf.pdfa.results.ValidationResult;
import javax.xml.bind.JAXBException;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
public class VeraPDFValidatorHelper {
public byte[] validatePDF(InputStream inputStream, String flavorId, Boolean prettyXml)
throws ModelParsingException, ValidationException, JAXBException, EncryptedPdfException
{
VeraGreenfieldFoundryProvider.initialise();
PDFAFlavour flavour = PDFAFlavour.byFlavourId(flavorId);
PDFAValidator validator = Foundries.defaultInstance().createValidator(flavour, false);
PDFAParser loader = Foundries.defaultInstance().createParser(inputStream, flavour);
ValidationResult result = validator.validate(loader);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
XmlSerialiser.toXml(result, baos, prettyXml, false);
return baos.toByteArray();
}
}