Proposal for JPMS issue #ReflectiveAccessByInstrumentationAgents
Andrew Dinn
adinn at redhat.com
Wed Jul 27 11:33:11 UTC 2016
Summary
Regarding the proposed solution of adding method redefineModule to class
Instrumentation:
I have a Proof Of Concept implementation which shows that that this
solution works for my agent and does not impose unworkable restrictions
on its operation or use.
The implementation can be generalised so that it will very likely work
just as well for other agents -- indeed, the current implementation has
already been implemented for the most part to address this general use case.
The current implementation involves some clumsiness interfacing the
agent to the additional code needed for the solution when (as will
likely be the case for most existing agents) the agent code needs to be
compiled to target pre-Jigsaw JDK runtimes.
I am still working on the current implementation to make it easier to
use, to fully document how it can be used and how it works and to
package it so it can employed by other agents.
More details of of what has been achieved (and how) plus the location of
the implementation and some of the changes I need to make are included
below in case they are of wider interest.
regards,
Andrew Dinn
-----------
What Does This POC Prove?
I have successfully modified my agent to achieve reflective access to
unexported packages using the redefineModule method of class
Instrumentation. This modification satisfies all the desired design goals:
1) Decoupling from non-Jigsaw operation:
the agent jar retains all the functionality required to operate on
non-Jigsaw JDKs (it is compiled target 6)
all functionality required to operate on a Jigsaw-enabled JDK is
located in a single supplementary jar (it is compiled target 9 and is
/not/ a module jar)
2) Simple deployment:
when deploying the agent on the command line full reflective access
under Jigsaw can be achieved either by adding the supplementary jar to
the system classpath or by requesting the agent to add it to the
classpath at install time (using method appendToSystemClassLoaderSearch
of class Instrumentation)
when deploying the agent dynamically the supplementary jar can be
installed as needed by requesting the agent to add it to the classpath
at install time (using method appendToSystemClassLoaderSearch of class
Instrumentation)
the convenience scripts used to manage both JVM-startup deployment
(bmjava) and dynamic deployment (bminstall) of the agent have been
supplemented with a simple command option to control installation of the
supplementary jar
the annotations used to configure auto-loading of the agent as part of
JUnit/TestNG testing can be (but have not yet been) extended with a
simple configuration field to control installation of the supplementary jar
3) Fallback operation:
if the supplementary jar is not deployed the agent defaults to being
able to access exported non-public classes + members -- it will throw an
exception when an attempt is made to access unexported non-public
classes + members
4) No prejudice to existing module visibility
the supplementary jar uses the Layer API to create a custom module
used as target for all exports installed by calls to redefineModules,
ensuring that module accessibility for all other (JDK or application)
modules is unaffected by the presence of the agent
n.b. the custom module is /not/ populated from a disk-based module jar
-- instead the agent must provide the module's classloader with byte[]
class definitions on demand (the expectation is that the agent will
synthesise the required byte[] definitions using ASM but it can arrange
to load definitions from disk if it wishes), the class load process
being driven by the agent by calling the classloader's loadClass method.
5) Controlled access to enabled Members
the sole class installed into the custom module by /my/ agent provides
the agent (and only the agent) with a method which will call setEnabled
for a specific Member on demand, if necessary first establishing
whatever export relation is required from the module of the Member's
owner class
Where Is The Implementation?
The current implementation is a full implementation of the functionality
required by my agent. It is almost in a state where other agents could
pick up and use the supplementary jar to provide themselves with a
similar capability. The code still needs a small amount of cleaning up
and quite a bit of javadoc needs to be added. There are also a few
details that I am unsure about the correctness of (although they don't
seem to inhibit it from working), most specifically the use of URLs in
the ModuleFinder and ModuleReader.
The code is located in my personal Byteman github repo in its own branch
https://github.com/adinn/byteman/tree/jigsaw-enablement
This branch builds the normal agent if you built with JDK{6-8}. It
builds the Jigsaw capable agent if you build with Jigsaw release
9-ea+126. I have no idea what happens if you use the current trunk jdk9
to build it (it may be missing critical Jigsaw features so, basically,
don't :-).
The branch contains two extra maven modules not present in the master
branch:
jigsawlayer provides a means to hoist a custom module into the runtime
without needing to add a module jar to the module path (in fact without
needing a jar at all)
jigsaw contains source for a class injected into the module by the
agent -- the source is just expository since the agent actually
synthesises an equivalent class, avoiding various problems which were
encountered in trying to make the module classes available from a module
jar on disk
maven module agent includes some changes to the agent's Transformer
class to configure Jigsaw operation plus a few minor changes to the
agent code to ensure that Member accesses obtains a correctly enabled
handle.
What Code Was Added Or Changed?
module jigsawlayer:
Maven submodule jigsawlayer contains all the code which goes into the
supplementary jar byteman-jigsawlayer.jar. Essentially, it provides the
code used to create the custom module and leaves it up to the agent to
synthesise classes which go into the module.
The API is a single method
JigsawLayer.installModule(String, String, Map<String, byte[]>)
The String arguments are the name of the custom module and the name of a
single package for the module to export. Ideally, the API should accept
a list of exported package names and a list of modules to import (it
currently hard wires import of java.instrument which my setEnabled
caller needs)
The Map provides a way for the agent to configure the module classloader
so it can retrieve a byte[] definition for any given (String) classname.
This avoids the need for the igsawlayer code to be involved in loading
or synthesising bytecode. The ModuleReader used by the classloader calls
the Map's get method when asked to load a class. In my case I provide a
pre-populated HashMap but you can provide a Map implementation whose get
method generates the required class definitons on demand.
Ideally, the Map argument would have type Function<String, byte[]> but
that doesn't work when the agent is compiled target JDK6. So, I settled
for Map. Note also that it is not possible for the agent to define an
equivalent to class Function for use in the module API to use because
the custom module cannot see classes defined by the agent.
[I am considering making the argument an Object and having the module
API check for a Function -- in which case it casts it and uses it -- or
a class which implements apply -- in which case it wraps it with a
standard wrapper which implements Function and calls the apply method
reflectively.]
module agent:
The agent checks during agent startup whether the runtime is
Jigsaw-enabled (it uses a call to ClassLoader.loadClass() to detect
whether platform class Module exists). If so it calls method
Transformer.initForJigsaw(). This is where the jigsawlayer installModule
API method gets called.
Note that the installModule call is done reflectively because you cannot
build the code as separate maven modules if you use direct linkage --
the agent code has to be compiled target JDK6 and the jigsawlayer API
method has to be compiled target JDK9.
The agent only needs to define one class, JigsawAccessEnabler, in the
custom module so it passes a HashMap into the installModule call
populated with a byte[] corresponding to the class definition. The code
synthesised for this class (by method getJigsawAccessEnablerBytes() of
agent class Transformer) is based on the source provided in (dummy)
module jigsaw. The generating method (getJigsawAccessEnablerBytes) is
annotated with the originating source lines.
Having created the Map and installed the module the agent calls
classloader.loadClass() to drive installation of class
JigsawAccessEnabler into the custom module. It uses reflection to create
an instance of this class. This instance is passed to each Byteman Rule
where it can be used to enable access to Members. The
JigsawAccessEnabler instance is given access to the agent's
Instrumentation instance at create time, allowing it to export packages
to it's own module as needed (n.b JigsawAccessEnabler throws an
exception if it finds it is not located in a named module).
jigsaw module:
If Jigsaw is not available or the supplementary jar has not been
configured the agent uses an instance of the default class
DefaultAccessEnabler to enable access. Ideally DefaultAccessEnabler and
JigsawAccessEnabler should both implement the common interface
AccessEnabler used by class Rule. However, this is not possible because
of the need for target JDK6/JDK9 deployment.
An interface deployed in the agent jar cannot be seen (and hence used)
from a class defined in the custom module. So, that explains why
JigsawAccessEnabler does not implement AccessEnabler. The alternative of
placing the interface in the supplementary jar or in the custom module
is not an option either. They are both compiled target JDK9 so are of no
use when the agent is deployed on JDK{6-8}.
So, the agent has to wrap the JigsawAccessEnabler instance with an
instance of a proxy class (JigsawAccessEnablerWrapper) which does
implement AccessEnabler. The proxy operates by forwarding method
invocations to the wrapped JigsawAccessEnabler using reflection.
More information about the jigsaw-dev
mailing list