org.embulk.plugin.PluginClassLoader Maven / Gradle / Ivy
package org.embulk.plugin;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.embulk.deps.SelfContainedJarAwareURLClassLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PluginClassLoader extends SelfContainedJarAwareURLClassLoader {
private PluginClassLoader(
final ClassLoader parentClassLoader,
final Collection jarUrls,
final String selfContainedPluginName,
final Collection parentFirstPackages,
final Collection parentFirstResources) {
super(jarUrls.toArray(new URL[0]), parentClassLoader, selfContainedPluginName);
this.hasJep320LoggedWithStackTrace = false;
this.parentFirstPackagePrefixes = ImmutableList.copyOf(
parentFirstPackages.stream().map(pkg -> pkg + ".").collect(Collectors.toList()));
this.parentFirstResourcePrefixes = ImmutableList.copyOf(
parentFirstResources.stream().map(pkg -> pkg + "/").collect(Collectors.toList()));
}
/**
* Creates PluginClassLoader for plugins with dependency JARs.
*
* @param parentClassLoader the parent ClassLoader of this PluginClassLoader instance
* @param jarUrls collection of "file:" URLs of all JARs related to the plugin
* @param parentFirstPackages collection of package names that are to be loaded first before the plugin's
* @param parentFirstResources collection of resource names that are to be loaded first before the plugin's
* @return {@code PluginClassLoader} instance created
*/
public static PluginClassLoader create(
final ClassLoader parentClassLoader,
final Collection jarUrls,
final Collection parentFirstPackages,
final Collection parentFirstResources) {
return new PluginClassLoader(
parentClassLoader,
jarUrls,
null,
parentFirstPackages,
parentFirstResources);
}
public static PluginClassLoader forSelfContainedPlugin(
final ClassLoader parentClassLoader,
final String selfContainedPluginName,
final Collection parentFirstPackages,
final Collection parentFirstResources) {
return new PluginClassLoader(
parentClassLoader,
new ArrayList<>(),
selfContainedPluginName,
parentFirstPackages,
parentFirstResources);
}
/**
* Adds the specified path to the list of URLs (for {@code URLClassLoader}) to search for classes and resources.
*
* It internally calls {@code URLClassLoader#addURL}.
*
* Some plugins (embulk-input-jdbc, for example) are calling this method to load external JAR files.
*
* @see embulk-input-jdbc
*/
public void addPath(Path path) {
try {
addUrl(path.toUri().toURL());
} catch (MalformedURLException ex) {
throw new IllegalArgumentException(ex);
}
}
public void addUrl(URL url) {
super.addURL(url);
}
/**
* Loads the class with the specified binary name prioritized by the "parent-first" condition.
*
* It copy-cats {@code ClassLoader#loadClass} while the "parent-first" priorities are considered.
*
* If the specified class is "parent-first", it behaves the same as {@code ClassLoader#loadClass} ordered as below.
*
*
*
* Invoke the {@code #findLoadedClass} method to check if the class has already been loaded.
*
* Invoke the parent's {@code #loadClass} method.
*
*
Invoke the {@code #findClass} method of this class loader to find the class.
*
*
*
* If the specified class is "NOT parent-first", the 2nd and 3rd actions are swapped.
*
* @see Oracle Java7's ClassLoader#loadClass
* @see OpenJDK7's ClassLoader
*/
@Override
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// If the class has already been loaded by this {@code ClassLoader} or the parent's {@code ClassLoader},
// find the loaded class and return it.
final Class> loadedClass = findLoadedClass(name);
if (loadedClass != null) {
return resolveClass(loadedClass, resolve);
}
final boolean parentFirst = isParentFirstPackage(name);
// If the class is "not parent-first" (not to be loaded by the parent at first),
// try {@code #findClass} of the child's ({@code PluginClassLoader}'s).
if (!parentFirst) {
try {
// If a class that is removed by JEP 320 is found here, it should be fine.
// It means that the class is found on the plugin side -- the plugin contains its own one.
// Classes removed by JEP 320 are not in the "parent-first" list.
return resolveClass(findClass(name), resolve);
} catch (ClassNotFoundException ignored) {
// Passing through intentionally.
}
}
// If the class is "parent-first" (to be loaded by the parent at first), try this part at first.
// If the class is "not parent-first" (not to be loaded by the parent at first), the above part runs first.
try {
final Class> resolvedClass = resolveClass(getParent().loadClass(name), resolve);
logInfoIfJep320Class(name);
return resolvedClass;
} catch (final ClassNotFoundException ex) {
if (!parentFirst) {
rethrowIfJep320Class(name, ex);
}
// Passing through intentionally if the class is not not removed by JEP 320.
}
// If the class is "parent-first" (to be loaded by the parent at first), this part runs after the above.
if (parentFirst) {
return resolveClass(findClass(name), resolve);
}
throw new ClassNotFoundException(name);
}
}
private Class> resolveClass(Class> clazz, boolean resolve) {
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
@Override
public URL getResource(String name) {
boolean childFirst = isParentFirstPath(name);
if (childFirst) {
URL childUrl = findResource(name);
if (childUrl != null) {
return childUrl;
}
}
URL parentUrl = getParent().getResource(name);
if (parentUrl != null) {
return parentUrl;
}
if (!childFirst) {
URL childUrl = findResource(name);
if (childUrl != null) {
return childUrl;
}
}
return null;
}
@Override
public Enumeration getResources(String name) throws IOException {
List> resources = new ArrayList<>();
boolean parentFirst = isParentFirstPath(name);
if (!parentFirst) {
Iterator childResources = Iterators.forEnumeration(findResources(name));
resources.add(childResources);
}
Iterator parentResources = Iterators.forEnumeration(getParent().getResources(name));
resources.add(parentResources);
if (parentFirst) {
Iterator childResources = Iterators.forEnumeration(findResources(name));
resources.add(childResources);
}
return Iterators.asEnumeration(Iterators.concat(resources.iterator()));
}
private boolean isParentFirstPackage(String name) {
for (String pkg : parentFirstPackagePrefixes) {
if (name.startsWith(pkg)) {
return true;
}
}
return false;
}
private boolean isParentFirstPath(String name) {
for (String path : parentFirstResourcePrefixes) {
if (name.startsWith(path)) {
return true;
}
}
return false;
}
private synchronized void logInfoIfJep320Class(final String className) {
final int lastDotIndex = className.lastIndexOf('.');
if (lastDotIndex != -1) { // Found
final String packageName = className.substring(0, lastDotIndex);
if (JEP_320_PACKAGES.contains(packageName)) {
if (!this.hasJep320LoggedWithStackTrace) {
// Logging with details and stack trace only against the first one.
// When a class is loaded from a library, more classes are usually loaded.
// It would be too noisy if details and stack trace are dumped every time.
logger.info(
"Class " + className + " is loaded by the parent ClassLoader, which is removed by JEP 320. "
+ "The plugin needs to include it on the plugin side. "
+ "See https://github.com/embulk/embulk/issues/1270 for more details.", new DummyStackTraceDump());
this.hasJep320LoggedWithStackTrace = true;
} else {
logger.info("Class " + className + " is loaded by the parent ClassLoader, which is removed by JEP 320.");
}
}
}
}
private void rethrowIfJep320Class(final String className, final ClassNotFoundException ex) throws ClassNotFoundException {
final int lastDotIndex = className.lastIndexOf('.');
if (lastDotIndex != -1) { // Found
final String packageName = className.substring(0, lastDotIndex);
if (JEP_320_PACKAGES.contains(packageName)) {
throw new ClassNotFoundException(
"A plugin tried to load " + className + " with the parent ClassLoader. "
+ "It is removed from JDK by JEP 320. The plugin needs to include it on the plugin side. "
+ "See https://github.com/embulk/embulk/issues/1270 for more details.",
ex);
}
}
}
private static class DummyStackTraceDump extends Exception {
DummyStackTraceDump() {
super("[DUMMY] It is not an Exception. Just showing where classes which will be removed in JEP 320 are loaded.");
}
}
private static final Logger logger = LoggerFactory.getLogger(PluginClassLoader.class);
/**
* Packages that are deprecated and removed since Java 11 by JEP 320.
*
* @see JEP 320
*/
private static String[] JEP_320_PACKAGES_ARRAY = {
// Module java.xml.ws : JAX-WS, plus the related technologies SAAJ and Web Services Metadata
// https://docs.oracle.com/javase/9/docs/api/java.xml.ws-summary.html
"javax.jws",
"javax.jws.soap",
"javax.xml.soap",
"javax.xml.ws",
"javax.xml.ws.handler",
"javax.xml.ws.handler.soap",
"javax.xml.ws.http",
"javax.xml.ws.soap",
"javax.xml.ws.spi",
"javax.xml.ws.spi.http",
"javax.xml.ws.wsaddressing",
// Module java.xml.bind : JAXB
// https://docs.oracle.com/javase/9/docs/api/java.xml.bind-summary.html
"javax.xml.bind",
"javax.xml.bind.annotation",
"javax.xml.bind.annotation.adapters",
"javax.xml.bind.attachment",
"javax.xml.bind.helpers",
"javax.xml.bind.util",
// Module java.activation : JAF
// https://docs.oracle.com/javase/9/docs/api/java.activation-summary.html
"javax.activation",
// Module java.xml.ws.annotation : Common Annotations
// https://docs.oracle.com/javase/9/docs/api/java.xml.ws.annotation-summary.html
"javax.annotation",
// Module java.corba : CORBA
// https://docs.oracle.com/javase/9/docs/api/java.corba-summary.html
"javax.activity",
"javax.rmi",
"javax.rmi.CORBA",
"org.omg.CORBA",
"org.omg.CORBA_2_3",
"org.omg.CORBA_2_3.portable",
"org.omg.CORBA.DynAnyPackage",
"org.omg.CORBA.ORBPackage",
"org.omg.CORBA.portable",
"org.omg.CORBA.TypeCodePackage",
"org.omg.CosNaming",
"org.omg.CosNaming.NamingContextExtPackage",
"org.omg.CosNaming.NamingContextPackage",
"org.omg.Dynamic",
"org.omg.DynamicAny",
"org.omg.DynamicAny.DynAnyFactoryPackage",
"org.omg.DynamicAny.DynAnyPackage",
"org.omg.IOP",
"org.omg.IOP.CodecFactoryPackage",
"org.omg.IOP.CodecPackage",
"org.omg.Messaging",
"org.omg.PortableInterceptor",
"org.omg.PortableInterceptor.ORBInitInfoPackage",
"org.omg.PortableServer",
"org.omg.PortableServer.CurrentPackage",
"org.omg.PortableServer.POAManagerPackage",
"org.omg.PortableServer.POAPackage",
"org.omg.PortableServer.portable",
"org.omg.PortableServer.ServantLocatorPackage",
"org.omg.SendingContext",
"org.omg.stub.java.rmi",
// Module java.transaction : JTA
// https://docs.oracle.com/javase/9/docs/api/java.transaction-summary.html
"javax.transaction",
};
private static Set JEP_320_PACKAGES =
Collections.unmodifiableSet(new HashSet(Arrays.asList(JEP_320_PACKAGES_ARRAY)));
private final List parentFirstPackagePrefixes;
private final List parentFirstResourcePrefixes;
private boolean hasJep320LoggedWithStackTrace;
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy