-3

I have the following legacy code that I migrated to Java 16 but, due to the strong encapsulation introduced by this new version, it doesn't work:

try {
    Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
    method.setAccessible(true);
    method.invoke(new URLClassLoader(
        new URL[] {}),
        new File("C:/external-folder/my.jar").toURI().toURL()
    );
} catch (Exception exc) {
    exc.printStackTrace();
}

Is there a way to make it work?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
JJB
  • 202
  • 4
  • 11
  • 5
    Instead of hacking the system class loader, refactor your external .jar file to be a [service provider](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/util/ServiceLoader.html). – VGR Sep 14 '21 at 18:22
  • 5
    You're using this internal API to add a JAR to the class path. Depending on your reasons for doing that, there are better (i.e. supported) ways to achieve your goal. Would you mind explaining why you need to add that JAR? – Nicolai Parlog Sep 16 '21 at 10:48
  • It's an old code that compiles a class at runtime and has to load it later – JJB Sep 16 '21 at 12:20
  • you should just load that class using other means, like separate class loader. But if you want to hack and fix it again in next java version, you can use a lot of ways: instrumentation, unsafe, internal lookup accessed via unsafe – GotoFinal Sep 20 '21 at 16:00
  • 2
    [What is the XY problem?](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378) – Holger Sep 21 '21 at 13:20
  • If you just have to load the bytecode, then [`Lookup.defineClass`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/invoke/MethodHandles.Lookup.html#defineClass%28byte%5B%5D%29) might just be what you need. – Johannes Kuhn Sep 21 '21 at 19:49

2 Answers2

23

The problem is not that reflection "doesn't work"; it is that reflection is finally enforcing more of the accessibility model that the compiler and runtime have always enforced. URLClassLoader::addUrl is intended only for subclasses; it is not intended to be accessed from outside the implementation, which is what you're doing. Over time, starting with Java 9 and continuing in later versions (including 17), access restrictions are increasingly recognized by reflection, with warnings, to give broken code a chance to migrate to something supportable.

The code in question only really ever worked accidentally; it depended on being able to break into an unsupported interface. Using setAccessible should be a clue. Sure, you can get into locked houses by breaking windows, but if you have to break a window (and it’s not your house), you should be aware of where the problem lies.

Look at it as glass-half-full; this accidentally-working code worked for a long time. But the bill has come due; it is time to fix your code.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Brian Goetz
  • 90,105
  • 23
  • 150
  • 161
  • And what about [this answer](https://github.com/projectlombok/lombok/issues/2681#issuecomment-917409345)? – JJB Sep 14 '21 at 14:13
  • 12
    @ChrisF If you just want to kick the can a few months down the road, do what you've gotta do, but you'll be back in the same place pretty soon. Fixing your code to not rely on internal implementation details is the sustainable solution. – Brian Goetz Sep 14 '21 at 20:06
  • 8
    [This is bad](https://github.com/burningwave/jvm-driver/blob/main/java/src/main/java/org/burningwave/jvm/DriverFunctionSupplierUnsafe.java#L311). May work today, may not tomorrow. And if/when it fails, it could crash the entire JVM. (Worst case: `MethodHandles.Lookup` gets an additional reference field - which this code may then overwrite. The next GC cycle may crash the VM.) – Johannes Kuhn Sep 14 '21 at 23:28
  • I tried it and it works without crashing the JVM and I noticed which is [tested on a broad spectrum of configurations](https://github.com/burningwave/jvm-driver/actions/runs/1234817015) – JJB Sep 15 '21 at 05:44
  • 7
    **May work today**. Yes testing helps, but writing against the spec is more future-proof. – Johannes Kuhn Sep 15 '21 at 09:35
  • 10
    @ChrisF The lesson here is: "I tried it and it worked" is how you got into this problem in the first place. This is a great opportunity to learn from this experience, rather than repeating the same mistake. – Brian Goetz Sep 15 '21 at 14:21
  • 3
    All this morality doesn't solve my problem unlike the working code I found in [this post](https://dev.to/jjbrt/how-to-make-reflection-fully-work-on-jdk-16-and-later-ihp) – JJB Sep 15 '21 at 15:40
  • 9
    @ChrisF You are trying to **break Java SE.** If something isn’t public, that means you should not be using it. If you try to get around that protection, and later down the road it fails, it’s your fault, not Java SE’s fault. This isn’t about morality; it’s about writing something that happens to work today, versus writing something reliable that will always work, forever. Other types of engineers don’t accept creating machines or structures that are destined to break; neither should software engineers. – VGR Sep 15 '21 at 16:46
  • Computer science is a field that evolves continuously and quickly and often with drastic changes and therefore nothing last foreve: in this situation I am happy to make the software work today, for tomorrow we will see – JJB Sep 15 '21 at 18:19
  • 3
    If this is your application, you can use this command line parameter `--add-opens=java.base/java.net=ALL-UNNAMED` (replace `ALL-UNNAMED` with your module if you use named modules). If this is a library, then instruct your users to add this to their java command line arguments. – Johannes Kuhn Sep 15 '21 at 19:44
  • 2
    @JohannesKuhn that’s a great piece of software, demonstrating where Java’s policy of “*we strongly encapsulate everything, except for the worst of all, `sun.misc.Unsafe`*” will lead to. Interestingly, it seems the maintainer of that code didn’t notice yet that `Unsafe.defineAnonymousClass` doesn’t exist anymore in JDK 17… – Holger Sep 17 '21 at 12:17
  • I tested on JDK 17 and it works: you can try to contact the maintainer to hear what he says: maybe there is something you don't know – JJB Sep 17 '21 at 14:35
  • reimplementing ObjectInput/OutputStream requires to call private methods by design. e.g. serialization SPEC requires calling private methods such as readObject / writeObject. – R.Moeller May 02 '22 at 10:30
10

The code is very weird. At first glance, I thought it used reflection to access URLClassLoader internals to add myJar to an existing class loader, but instead it uses it to add it to a new class loader. There's no reason to do that - you can just use the URLClassLoader constructor for that.

It should look something like this (untested because I am away from an IDE):

URL jar = new File("C:/external-folder/my.jar").toURI().toURL();
URL[] urls = { jar };
new URLClassLoader(urls);
Nicolai Parlog
  • 47,972
  • 24
  • 125
  • 255
  • The question here is "how can I make reflection work on JDK 16 and later?": does your answer solves the problem? – JJB Sep 19 '21 at 15:49
  • 7
    @Chris F do you have code sample that demonstrates why the above answer doesn't work, and when the question code is required instead? The source code for addUrl updates the same members as when urls are supplied to the constructor so this answer looks helpful. – DuncG Sep 21 '21 at 13:29
  • @ChrisF sometimes, discussing the whys instead of the hows is the right thing to do. – ymajoros Aug 24 '22 at 07:59