Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save rseitz/59bff56936756ff29b2e151e2857aa37 to your computer and use it in GitHub Desktop.
Save rseitz/59bff56936756ff29b2e151e2857aa37 to your computer and use it in GitHub Desktop.
Demononstrate IllegalAccessError that occurs when a package-private method is invoked across two different classloaders
package org.example;
import java.lang.reflect.Method;
import org.apache.commons.io.IOUtils;
/**
* Large Java systems often include a plugin framework that allows a developer to
* contribute new functionality by packaging custom code as a JAR that is installed and run
* inside the primary system. The code for a plugin often exists in its own dedicated codebase,
* referencing the primary codebase as a dependency.
*
* When writing a plugin, it is common to want to subclass
* various base classes that exist in the primary codebase. An effort might be made to
* place the subclass in a package with the same name as the base class's package, so that
* package-private methods and instance variables from the base class can be referenced
* in the subclass. This can be done without generating compile-time errors. However, if
* the plugin JAR (and hence the subclass in question) is loaded
* by a different classloader from the base class,
* IllegalAccessErrors will be encountered at runtime. Ultimately, this is due
* to the fact that package access in Java is scoped per classloader.
* See, for example: https://stackoverflow.com/a/10538366
*
* The code here demonstrates how this problem can arise. The base class Hello and
* its subclass MyHello have the same package name (org.example), but when they are loaded
* by different classloaders they do not truly exist in the "same" package. A method
* in MyHello that overrides and delegates to the superclass method generates
* an IllegalAccessError when invoked.
*
* Takeaway: To allow for code in a primary system to be usable and extendable in the context of a
* plugin that will be loaded by a different classloader, methods and variables that aren't private
* should be declared as public or protected, not left as package-private by default.
*/
public class PackagePrivateMethodCallAcrossClassLoaders {
public static void main(String[] args) throws Exception {
// represent Hello and MyHello Classes as byte arrays
final byte[] helloBytes = IOUtils.toByteArray(
Hello.class.getClassLoader().getResourceAsStream(Hello.class.getName().replace('.', '/').concat(".class")));
final byte[] myHelloBytes = IOUtils.toByteArray(
MyHello.class.getClassLoader().getResourceAsStream(MyHello.class.getName().replace('.', '/').concat(".class")));
try {
// load Hello and MyHello Classes using the same ClassLoader
SimpleClassLoader loader = new SimpleClassLoader(null);
Class hello = loader.loadFromBuffer(helloBytes);
Class myHello = loader.loadFromBuffer(myHelloBytes);
// invoke a method on MyHello that calls a package-private method of its base class
Method printMessage = myHello.getMethod("printMessage");
printMessage.invoke(myHello.getDeclaredConstructor().newInstance(), null);
} catch (Exception e) {
// the above code should work without throwing an exception
e.printStackTrace();
}
try {
// instantiate a new ClassLoader and load Hello
SimpleClassLoader helloLoader = new SimpleClassLoader(null);
Class hello = helloLoader.loadFromBuffer(helloBytes);
// instantiate a second ClassLoader with the first ClassLoader as its parent;
// load MyHello using this second ClassLoader;
// note that when MyHello is loaded, its base class Hello will be found in the parent ClassLoader;
// if we didn't specify helloLoader as the parent of myHelloLoader, we'd get a ClassNotFound exception here
SimpleClassLoader myHelloLoader = new SimpleClassLoader(helloLoader);
Class myHello = myHelloLoader.loadFromBuffer(myHelloBytes);
// invoke a method on MyHello that calls a package-private method of its base class
Method method = myHello.getMethod("printMessage");
method.invoke(myHello.getDeclaredConstructor().newInstance(), null);
} catch (Exception e) {
// the above code should throw an InvocationTargetException with a cause similar to:
// java.lang.IllegalAccessError: class org.example.Demo$MyHello tried to access method 'void org.example.Demo$Hello.printMessage()'
// (org.example.Demo$MyHello is in unnamed module of loader org.example.Demo$SimpleClassLoader @16b98e56;
// org.example.Demo$Hello is in unnamed module of loader org.example.Demo$SimpleClassLoader @5fd0d5ae)
e.printStackTrace();
}
}
/**
* Example of a base class with a package-private method.
*/
public static class Hello {
void printMessage() {
System.out.println("\nHello World!\n");
}
}
/**
* Example of a subclass that overrides a package-private method of the parent, and delegates to it.
*/
public static class MyHello extends Hello {
@Override
public void printMessage() {
super.printMessage();
}
}
/**
* ClassLoader that allows for loading a class from a byte array.
*/
public static class SimpleClassLoader extends ClassLoader {
public SimpleClassLoader(ClassLoader parent) {
super(parent);
}
public Class<?> loadFromBuffer(final byte[] buffer) {
return defineClass(null, buffer, 0, buffer.length);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment