<div dir="ltr"><div dir="ltr"><div class="gmail_default" style="font-family:arial,helvetica,sans-serif"><br></div></div><br><div class="gmail_quote gmail_quote_container"><div dir="ltr" class="gmail_attr">On Fri, Dec 13, 2024 at 12:06 PM Ron Pressler <<a href="mailto:ron.pressler@oracle.com">ron.pressler@oracle.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex"><br>
<br>
> On 12 Dec 2024, at 14:35, David Lloyd <<a href="mailto:david.lloyd@redhat.com" target="_blank">david.lloyd@redhat.com</a>> wrote:<br>
> <br>
> The problem is that 100% of these frameworks are required to work on the class path as well (and some are not yet being tested on the module path, or even defining an automatic module name, though I've been working to improve that for a couple years now).<br>
<br>
If this is not because these frameworks still support JDK 8, could you explain why a single JAR should work both on the classpath and the module path, given that the two can be freely mixed? I've heard of such issues, but never in much detail, and my sense was that they stem from build tool limitations that would more easily and naturally be fixed by the build tool.<br></blockquote><div><br></div><div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">I want to start by making it clear (in case it isn't) that "classpath mode" is being used as a widely-understood colloquial expression meaning "not running in module mode" aka "loaded in unnamed module(s)". It doesn't specifically mean that one is using, say, `URLClassLoader` to construct and use a literal flat classpath, and it's referring to run time, not compile time. Otherwise a lot of what follows isn't going to make a lot of sense.</div></div><div><br></div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">That said: it really has nothing to do with JDK 8, or with build tools. Most frameworks and libraries have moved past JDK 8, many of them long ago. It's more because there are few application containers or even libraries which are known to work in module mode (service loading is really the prime example of how this can break; see below). The idea that once people move past JDK 8 they will embrace modules and not look back is essentially a complete fiction. As a result, if I design a library or framework to work *only* in module mode, the user might still try to use it in a non-module application container, possibly leading to (potentially subtle) breakage. If I design it only for classpath mode, then it may subtly break in module mode. If I design it for both, my users will be happy, but I need to test in both environments (which is rarely done today) and be sure to cope with behavioral differences that arise as a result. The build tool end is not a problem. I can easily have Maven test in both modes for example. The problem is the run time behavioral differences.</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif"><br></div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">Ultimately it's the library (think Jakarta EE, Microprofile, Hibernate, reactive frameworks, Netty, etc.) and application container authors (think Quarkus, Micronaut, Helidon, Spring, etc.) who are doing nearly 100% of the heavy lifting in terms of paving the way for users to be able to embrace modules. However, we also have had approximately 0% of the corresponding influence on the design, and the result is really not ergonomic for our purposes, which is probably at least part of why we're seeing limited uptake of modules even after 7 years and 15 major versions - a geological age in Java terms.</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif"><br></div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">However, we are crafty and can find a way to make things work. For example, the recommended solution for fully dynamic loading of modules has been to use a layer per module, hence my current experimental direction. But, one (thus-far) insurmountable compatibility difficulty revolves around service loading. A good puzzler is: can you guess which is the correct way for a library to find a service provider for one of its interfaces in Java, so that it works for modules *and* regular class loading?</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif"><br></div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">Choice 1. ServiceLoader.load(TheService.class)</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">Choice 2. ServiceLoader.load(TheService.class, Thread.currentThread().getContextClassLoader())</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">Choice 3. ServiceLoader.load(TheService.class, getClass().getClassLoader())</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">Choice 4. ServiceLoader.load(TheService.class, TheService.class.getClassLoader())</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">Choice 5. ServiceLoader.load(TheService.class, stackWalker.getCallerClass().getClassLoader())</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">Choice 6. ServiceLoader.load(getClass().getModule().getLayer(), TheService.class)</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">Choice 7. ServiceLoader.load(TheService.class.getModule().getLayer(), TheService.class)</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">Choice 8. ServiceLoader.load(stackWalker.getCallerClass().getModule().getLayer(), TheService.class)</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif"><br></div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">It should not be a surprise, based on this thread at least, to discover that the correct answer is "none of these". Other than recognizing that 1 & 2 are the same, which is only obvious to people who bother to read documentation (a surprisingly small crowd, even amongst framework and specification designers), none of these will work in an expected way for the given environment, because no two environments work the same way in terms of class loading or module layers. We normally recommend #4 (or perhaps #3, when there is an effective difference) for our non-JPMS products (which is basically all of our products) because historically (i.e. outside of modules) we can reroute service loading requests fairly easily, allowing our containers to "do the right thing" based on the user's intent, and this way we can know "who is asking" for the service and route accordingly.</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif"><br></div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">Another behavioral quirk is that service loaders don't actually work the same if the service was found in a named module. If a service provider class was found in a named module, then the loader will also look for a `provider` static method which can return the service instance, whereas services found via the classic mechanism will only invoke the constructor of the service class to acquire its instance. Additionally, the classic `META-INF/services` mechanism is still used in module mode, however the service provider classes are filtered out if they are found to be located in a named module after loading the class. Since behavior differs between module and non-module mode, a library author which seeks to run in both environments while using ServiceLoader has to carefully consider how services might be loaded in a given environment. The smartest solution is probably "don't use ServiceLoader". However, this API is unfortunately widely used among frameworks today, so short of boiling the ocean there's not much to be done about it, except perhaps find a way to make it work consistently.</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif"><br></div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">If we could continue to have some control over how services are resolved, we would not have an issue. The idea of hard-wiring the service graph in custom layers seems attractively proximate to the concepts of static compilation and the closed world (at least on paper), however in practice, none of these kinds of benefits have ever materialized from enforcing these strict limitations on custom layers. Having a custom layer in the first place already invalidates these concepts from the JDK or JVM's perspective (but importantly, not from the application container perspective, since it's generally the application container that understands the static properties of the application, not the JDK). In fact I would be shocked to learn that this restriction has *ever*, in the past 7 years, been found to be useful to anyone.</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif"><br></div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">If it were my decision, I would let the class loader have control over service resolution in *all* cases, modular or classpath, and abandon the service catalog idea. Only one variation (perhaps `ServiceLoader.load(TheService.class)`) would be recommended with the other variants being deprecated for removal, and it would be caller-sensitive (yes, I know it already is) so that the class loader's handler could consider any or all of the caller class, the class loader of the caller class, the TCCL, the class loader of the service, etc. This is because the library author is the *last* one to have an accurate idea how its services can or should be located. It is by definition dependent on the environment, and in fact that is usually why they are using service loading in the first place: so they don't have to care about *where* the service implementation comes from, only that it is provided. Whenever the library author has a great idea about how to locate their service (say, via TCCL or perhaps the caller's layer), they're necessarily going to be wrong most (if not all) of the time, and it's the container developers who have to deal with the problems that result.</div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif"><br></div><div class="gmail_default" style="font-family:arial,helvetica,sans-serif">There are other behavioral differences too that have caused problems - for example, once I added a class loader name to the class loader of a project with a very large test suite. This ended up breaking thousands of tests which were, as it happens, parsing the *text* of exception stack traces, and breaking because the class loader name began appearing in the stack frame text. Obviously, any fault here lies with the author of such a brittle test. So while even innocuous behavior changes like this one could have unexpected consequences, it is important that it is at least possible to bridge the gap between old and new behaviors. For these bad tests, the change would be to fix the test. But for something like ServiceLoader, the answer is not so clear.</div></div><span class="gmail_signature_prefix">-- </span><br><div dir="ltr" class="gmail_signature"><div dir="ltr">- DML • he/him<br></div></div></div>