Hello,
Michael Warres identified a number of issues with class loading and
codebase annotation loss, none of which were fully addressed.
https://dl.acm.org/doi/pdf/10.5555/1698139
I wanted to share with you an new JERI InvocationLayerFactory I've been
working on, on and off for some time.
Existing implementations that marshal objects between two endpoints,
associate a codebase annotation with every class in the stream.
I have done something much different.
First I don't send any codebase annotations in the stream. I recognised
that we shouldn't have a second class resolution mechanism, that only
ClassLoaders should control class resolution and that each node may not
only have different ClassLoader trees or maps, but have different ways
of assigning and wiriing them up.
Instead I decided to make the codebase annotation a Configuration
concern of the service.
Both endpoints of the service are wired up with ClassLoaders, the
ServerEndpoint ClassLoader is defined in the Configuration by specifying
a class (typically the services proxy implementation class) whose
ClassLoader will be used for class resolution at the ServerEndpoint.
The client Endpoint is assigned a ClassLoader using a Service Provider
interface ProxyCodebaseSpi, which can be used to support modular
environments. The default service provider implementation assigns a
PreferredClassLoader. The codebase annotation is configured by the
service implementation, the codebase annotation is obtained at the
client using a bootstrap proxy that implements an interface called
CodebaseAnnotation.
These marshalling streams are only responsible for resolving classes
related to one service, if another service proxy is passed as a
parameter to a service, it will be marshalled and resolved with it's own
independent endpoints and client ClassLoader assigned specifically for
that service.
Services now have independent class resolution determined by the
ClassLoader's at their Endpoint's.
All services must implement the ProxyAccessor interface, whenever the
stream detects an instance of ProxyAccessor, it obtains the service
proxy from the service, then marshalls it into a MarshalledInstance.
The stream also asks the service for a bootstrap proxy which implements
CodebaseAccessor ( classes for which are always resolvable by the
stream), then the CodebaseAccessor proxy is sent with the
MarshalledInstance , over the marshalling stream, so as to avoid
polluting the stream with classes from other services it cannot resolve.
When the MarshalledInstance is deserialized/unmarshalled by the client
endpoint, the CodebaseAccessor is used to obtain the codebase
annotation, a ClassLoader is created by the provider and the service
proxy is unmarshalled using the ClassLoader provided. The
CodebaseAccessor proxy's InvocationHandler (which contains the
ObjectEndpoint) is used along with the codebase annotation to represent
the service's Identity. This allows service proxy's to be assigned a
different ClassLoader to other services who share identical annotations,
this is important as the ClassLoader represents the ServerEndpoint's
Subject and whatever permissions it's granted at the cleint.
Note that constraints are applied and authentication occurs prior to
unmarshalling the proxy.
Modular environments can choose how to create this ClassLoader and how
to resolve it, using the CodebaseAccessor (which can also provide code
signer certificates, the codebase annotation and the identity of the
service).
The default provider implementation PreferredProxyCodebaseProvider
utilises PreferredClassLoader and typically uses the client's
ClassLoader as the parent ClassLoader for the service proxy ClassLoader,
so the service proxy has the same class visibility as the client (with
the exception of preferred classes). The default provider is not
perfect for modular environments like Maven and OSGi, since it is a
compromise that must function in all environments, but it is an
acceptable compromise.
1. Firstly if a service proxy returns to a node from which it was
exported, it will always be loaded by the ClassLoader from which it
was exported, in this case it is the responsibility of the local
environment to ensure visibility.
2. Secondly if a service proxy is unmarshalled by itself, it will not
create a new ClassLoader, it must confirm the annotation doesn't
match the stream loader.
3. Thirdly, if a proxy has been unmarshalled previously, has the same
InvocationHandler, annotations and parent loader as the stream, it
will be loaded by the cached ClassLoader.
4. A new ClassLoader will be created with the stream ClassLoader as
it's parent loader (the client that's unmarshalled it).
This provides a reasonable implementation that works in all cases, in
OSGi for example, different bundles may download the same service proxy,
each will have a separate copy that resolves to the classes visible to
each client's bundle as the client bundle ClassLoader will be the parent
of the service proxy ClassLoader. In this case service codebase
annotations would include the proxy and the service API jar URL's, in
case some interfaces were not imported by the client's OSGi bundle (not
resolvable by local code).
Code that is resolved locally and reserialized across the marshalling
stream will not lose it's identity as it is resolved by the ClassLoader
at the remote Endpoint, therefore codebase annotation loss issues don't
occur.
The challenge for modular environments is creating a ClassLoader that is
unique to the service while also wiring dependencies for maximum
compatibility.
Once the ClassLoader has been assigned to a client Endpoint it will
continue to be used without consulting codebase annotations again.
With this model, there are no codebase annotations to lose and the
server is always consulted for the configured codebase annotation
whenever a service proxy is serialized and the use of ClassLoader's is
compatible with Modular environments like OSGi and Maven / Plexus
Classworlds.
This also allows for evolution of serialized proxy state, to a new later
versioned codebase for example, rather than transferring the old
codebase annotation which was subject to change from codebase annotation
loss from node to node.
I think this has considerably simplified class resolution for services
by handing back responsibility to ClassLoaders.
Note that it's complimentary to existing infrastructure, without
requiring anyone to change if they don't want to.
This will also eliminate the many problems that codebase annotation loss
causes for new developers.
Regards,
Peter Firmstone.