On 11/12/2024 19:31, David Lloyd wrote:
I'm once again experimenting with modularizing some of our runtime
projects (Quarkus, WildFly) using JDK modules. Among the various
problems I've encountered, this one is one I haven't cracked yet and
seems like an oversight.
I am currently experimenting with a one-module-per-layer design which
allows us certain capabilities (like lazy loading of modules, late
binding of dependencies, and circularity in dependencies), and also
allows us to create module graphs that can mix in "unnamed" modules
for libraries which don't yet work properly as named modules, as well
as automatic modules for libraries which can work as modules but don't
yet define a descriptor.
However I have discovered an important difference between service
loaders which load services by class loader (for example, those found
in Microprofile and Jakarta frameworks) and those which load by module
layer (none?) which is preventing things from working properly.
When I load a service by layer, after searching the given layer, the
parent layers of that layer are then searched in order, recursing up
the graph. (Sometimes the same service could be returned multiple
times when there are diamonds in the layer graph, but that's a
different problem.)
When I load a service by class loader, if the service is not found in
the class loader's defined layers, then the search terminates and the
service is considered not found, even if the service provider exists
in a parent layer of one of those layers. This essentially means that
all frameworks which are loading services by class loader (and using
the class loader of the framework itself is typical here) will also
need all of their implementations to coexist in one of the layers
defined by that class loader - or else they must be defined in the
unnamed module (in which case it will always be found).
Ironically, it *will* search the parent class loading delegation chain
for layers which contain service implementations. The point of using
module layers though is that they can have multiple parents, departing
from this single-delegation model. In our class-loader-per-module
setup, we do not use the parent class loader delegation as it would be
meaningless in that context.
A good solution would be to modify service loading by class loader to
also search parent layers of each layer defined to that class loader.
This seems like it would be fairly easy to implement, but might
possibly have subtle compatibility implications for very specific
environments which have a module-per-class-loader situation along with
multiple layers.
If ServiceLoader.load is invoked with a ClassLoader then it lazily
iterates through the class loader delegation chain. If
ServiceLoader.load is invoked with a ModuleLayer then it iterates
through the module layers. The former has deliberately limited support
for cases where a class loader is used by a module layer but doing what
you propose is adding more complexity for what seems like a really niche
usage. I would worry that changing it along the lines you propose would
result in something that only a few people could understand.
Have you looked at changing these frameworks to work with modules and
specify a module layer to ServiceLoader.load?
-Alan