Created
January 2, 2024 21:35
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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