Rafael Winterhalter
2018-04-01 21:02:25 UTC
Hello,
I am the/a maintainer of the libraries Byte Buddy, cglib and Mockito with
countless dependencies upstream and I wanted to give a summary of adopting
the JPMS and migrating away from sun.misc.Unsafe.
1. Java agents cannot define auxiliary classes.
Byte Buddy does support the JPMS fully, however, it still relies on
sun.misc.Unsafe::defineClass for its Java agent API and currently breaks on
Java 11 as this method was removed in a recent EA build. The reason for
using Unsafe is that many instrumentations need to define auxiliary classes
to aid an instrumentation similar to javac which sometimes needs to define
anonymous classes or even synthetic classes. For example, if a Java agent
wants to register an event listener to some framework, such listeners often
declare multiple methods what makes it impossible to fullfil the listener
contract using a lambda expression. Instead, one typically injects an
additional class into the same package as the instrumented class. In this
case, it is not possible to use MethodHandles.Lookup::defineClass as the
class file transformer does not necessarily have private access to the
lookup of the instrumented class.
The current workarounds are:
a) Open the package jdk.internal.misc to gain access to this package's
Unsafe class. This can be done via Instrumentation::redefineModule.
b) Open the java.lang package to access ClassLoader via reflection.
c) Open the java.lang package to access the internal lookup with global
access rights.
Of these solutions only (b) relies on standard API and is guaranteed to
function in the future but the solution still feels hacky and does not work
for instrumentations of classes on the bootstrap loader. Opening packages
also implies a risk of being applied carelessly since opening the package
to the agent's module most likely opens the package to the unnamed module
of the system class loader what invites to breaches of the JPMS
encapsulation by code that does not ship with the agent.
To offer a better solution, I would like to suggest one of the following:
a) Add a method defineClass(ClassLoader, byte[], ProtectionDomain) to the
Instrumentation interface that works similar to Unsafe::defineClass. This
would provide a very simple migration path. Since agents have access to
jdk.internal.misc, adding this method does not add any capabilities to the
agent, it merley avoids using internal API that might change.
b) Supply a MethodHandles.Lookup instance to the
ClassFileTransformer::transform API where the instance represents the
instrumented class's access rights. This does however carry the risk of
invoking the lookupClass method which would either load the instrumented
class prematurely causing a circularity error or return an unexpected value
such as null. Since the lookup API generally relies on loaded types, there
are probably other problems such as invoking Lookup::privateLookupIn before
all involved types are loaded.
For the sake of simlicity and since easy migration paths make a quick
adoption easier, I would suggestion solution (a), also in the light that
quick and dirty migrations might still choose option (b) to save time and
also since (b) might cause problems when types are not yet loaded.
2. Java proxies cannot invoke default methods of proxied interfaces
The Java proxy API does not currently allow the invocation of an overridden
default method since
the InvocationHandler API only supplies an instance of
java.lang.reflection.Method. In Java 8, it was always possible to get hold
of method handle of any method of the proxied interface and to specialize
the handle on the interface type to invoke the default implementation. With
the JPMS, even if an interface type is exported, this same type might not
be opened to another module. This implies that if an InvocationHandler is
defined by this module to which the interface is exported, this module's
InvocationHandler cannot resolve a specialized method handle to a default
method of the proxied interface. As a matter of fact, such a call is
impossible in this scenario whereas the same call is possible if the proxy
is implemented manually at compile time.
As a solution, I suggest to provide an argument to the InvocationHandler
that represents a lookup instance with the access rights of the proxy
class. Using this lookup, the specialized method handles could be resolved.
3. Mocking and serialization libraries still require
Unsafe::allocateInstance.
For Mockito, it is required to instantiate any class without invoking a
constructor with potential side-effects. This is of course a grose
violation of Java's general contract for class instantiation but this
feature is very useful.
Using a Java agent, it is already possible to emulate this feature without
internal API by instrumenting all constructors of all classes in the
hierarchy of a mocked class by transforming all constructors into the
following pseudo-code:
SomeConstructor(Void arg) {
if (MockitoBootHelper.THREAD_LOCAL.isMockInstantiatingMode()) {
super(null); // any constructor of the super class with default values
for all arguments
} else {
// original constructor code...
}
}
Before instantiating a mock, the thread local value that is held by a
bootstrap-loader injected class is set to true such that a side-effect free
construction is achieved.
This is of course too expensive and has negative effects on performance due
to additional branching and JIT-byte code limits such that one would rather
open jdk.internal.misc to access the Unsafe instantiation mechanism if a
Java agent is already available.
However, mocking and serialization libraries are not typically loaded as a
Java agent. Also, I think that the actual requirements are different. My
suggestion here is:
a) For serialization libraries, I think that adding
MethodHandles.Lookup::newInstance(Class<? extends Serializable>) with
standard deserialization mechanics would be sufficient.
b) For mocking libraries, this does not suffice as mocks can be of any
class. I understand that this breaks encapsulation but for unit tests, I
argue that the benefit of using these libraries outweights the benefit of
full encapsulation within a unit test.
As Mockito is typically run within a build which uses a JDK, we could
attach to the current VM using the attachment API. Since Java 9, this is no
longer possible to attach to the same VM but instead we start a helper JVM
process that applies the attachment indirectly. Unfortunately, this is a
rather costly operation what is especially problematic when running a
single unit test. (The difference to this approach over Unsafe is about
half a second on average.)
To overcome this, I would like to suggest to:
a) Add a method Instrumentation::allocateInstance(Class). Java agents can
already emulate this privilege as described above, this is therefore merely
a convenience.
b) Add a module jdk.test to JDK 11 with a class
JavaTest::getInstrumentation that returns an instrumentation instance for
the current JVM. This module should not be resolved by default but only
when requiring it explicitly similar to the EE modules after Java 9.
I think this solution carries two benefits:
a) Test libraries like Mockito can only be used in a testing scope. We
experience regularly that Mockito is used in production environments. The
library is not meant for that and we warn and document that this is not
intended use but people regularly ignore this directive. By requiring this
module, this form of abuse would no longer be possible and the JVM could
even give a meaningful error message if this use was intended.
b) The Instrumentation instance can be used for other meaningful testing
approaches. For example, Mockito offers an inline mock maker where the
mocking logic is inlined into a method body rather then creating a
subclass. This approach mainly targets final classes which have become more
common especially since the Kotlin language became popular. To supply this
alternative mock maker, Mockito attempts attaching an agent to the current
VM (directly or indirectly, depending on the VM's version) which suffers
the additional costs for attaching an agent that I mentioned before.
Thanks for reading this and I hope that you can consider these, as of
today, very practiced use cases. The JPMS migration has gone quite well, I
find. With these outstanding problems, JDK 11 could be the first release
where a majority of Java programs would no longer need to rely on internal
API.
Best regards, Rafael
I am the/a maintainer of the libraries Byte Buddy, cglib and Mockito with
countless dependencies upstream and I wanted to give a summary of adopting
the JPMS and migrating away from sun.misc.Unsafe.
1. Java agents cannot define auxiliary classes.
Byte Buddy does support the JPMS fully, however, it still relies on
sun.misc.Unsafe::defineClass for its Java agent API and currently breaks on
Java 11 as this method was removed in a recent EA build. The reason for
using Unsafe is that many instrumentations need to define auxiliary classes
to aid an instrumentation similar to javac which sometimes needs to define
anonymous classes or even synthetic classes. For example, if a Java agent
wants to register an event listener to some framework, such listeners often
declare multiple methods what makes it impossible to fullfil the listener
contract using a lambda expression. Instead, one typically injects an
additional class into the same package as the instrumented class. In this
case, it is not possible to use MethodHandles.Lookup::defineClass as the
class file transformer does not necessarily have private access to the
lookup of the instrumented class.
The current workarounds are:
a) Open the package jdk.internal.misc to gain access to this package's
Unsafe class. This can be done via Instrumentation::redefineModule.
b) Open the java.lang package to access ClassLoader via reflection.
c) Open the java.lang package to access the internal lookup with global
access rights.
Of these solutions only (b) relies on standard API and is guaranteed to
function in the future but the solution still feels hacky and does not work
for instrumentations of classes on the bootstrap loader. Opening packages
also implies a risk of being applied carelessly since opening the package
to the agent's module most likely opens the package to the unnamed module
of the system class loader what invites to breaches of the JPMS
encapsulation by code that does not ship with the agent.
To offer a better solution, I would like to suggest one of the following:
a) Add a method defineClass(ClassLoader, byte[], ProtectionDomain) to the
Instrumentation interface that works similar to Unsafe::defineClass. This
would provide a very simple migration path. Since agents have access to
jdk.internal.misc, adding this method does not add any capabilities to the
agent, it merley avoids using internal API that might change.
b) Supply a MethodHandles.Lookup instance to the
ClassFileTransformer::transform API where the instance represents the
instrumented class's access rights. This does however carry the risk of
invoking the lookupClass method which would either load the instrumented
class prematurely causing a circularity error or return an unexpected value
such as null. Since the lookup API generally relies on loaded types, there
are probably other problems such as invoking Lookup::privateLookupIn before
all involved types are loaded.
For the sake of simlicity and since easy migration paths make a quick
adoption easier, I would suggestion solution (a), also in the light that
quick and dirty migrations might still choose option (b) to save time and
also since (b) might cause problems when types are not yet loaded.
2. Java proxies cannot invoke default methods of proxied interfaces
The Java proxy API does not currently allow the invocation of an overridden
default method since
the InvocationHandler API only supplies an instance of
java.lang.reflection.Method. In Java 8, it was always possible to get hold
of method handle of any method of the proxied interface and to specialize
the handle on the interface type to invoke the default implementation. With
the JPMS, even if an interface type is exported, this same type might not
be opened to another module. This implies that if an InvocationHandler is
defined by this module to which the interface is exported, this module's
InvocationHandler cannot resolve a specialized method handle to a default
method of the proxied interface. As a matter of fact, such a call is
impossible in this scenario whereas the same call is possible if the proxy
is implemented manually at compile time.
As a solution, I suggest to provide an argument to the InvocationHandler
that represents a lookup instance with the access rights of the proxy
class. Using this lookup, the specialized method handles could be resolved.
3. Mocking and serialization libraries still require
Unsafe::allocateInstance.
For Mockito, it is required to instantiate any class without invoking a
constructor with potential side-effects. This is of course a grose
violation of Java's general contract for class instantiation but this
feature is very useful.
Using a Java agent, it is already possible to emulate this feature without
internal API by instrumenting all constructors of all classes in the
hierarchy of a mocked class by transforming all constructors into the
following pseudo-code:
SomeConstructor(Void arg) {
if (MockitoBootHelper.THREAD_LOCAL.isMockInstantiatingMode()) {
super(null); // any constructor of the super class with default values
for all arguments
} else {
// original constructor code...
}
}
Before instantiating a mock, the thread local value that is held by a
bootstrap-loader injected class is set to true such that a side-effect free
construction is achieved.
This is of course too expensive and has negative effects on performance due
to additional branching and JIT-byte code limits such that one would rather
open jdk.internal.misc to access the Unsafe instantiation mechanism if a
Java agent is already available.
However, mocking and serialization libraries are not typically loaded as a
Java agent. Also, I think that the actual requirements are different. My
suggestion here is:
a) For serialization libraries, I think that adding
MethodHandles.Lookup::newInstance(Class<? extends Serializable>) with
standard deserialization mechanics would be sufficient.
b) For mocking libraries, this does not suffice as mocks can be of any
class. I understand that this breaks encapsulation but for unit tests, I
argue that the benefit of using these libraries outweights the benefit of
full encapsulation within a unit test.
As Mockito is typically run within a build which uses a JDK, we could
attach to the current VM using the attachment API. Since Java 9, this is no
longer possible to attach to the same VM but instead we start a helper JVM
process that applies the attachment indirectly. Unfortunately, this is a
rather costly operation what is especially problematic when running a
single unit test. (The difference to this approach over Unsafe is about
half a second on average.)
To overcome this, I would like to suggest to:
a) Add a method Instrumentation::allocateInstance(Class). Java agents can
already emulate this privilege as described above, this is therefore merely
a convenience.
b) Add a module jdk.test to JDK 11 with a class
JavaTest::getInstrumentation that returns an instrumentation instance for
the current JVM. This module should not be resolved by default but only
when requiring it explicitly similar to the EE modules after Java 9.
I think this solution carries two benefits:
a) Test libraries like Mockito can only be used in a testing scope. We
experience regularly that Mockito is used in production environments. The
library is not meant for that and we warn and document that this is not
intended use but people regularly ignore this directive. By requiring this
module, this form of abuse would no longer be possible and the JVM could
even give a meaningful error message if this use was intended.
b) The Instrumentation instance can be used for other meaningful testing
approaches. For example, Mockito offers an inline mock maker where the
mocking logic is inlined into a method body rather then creating a
subclass. This approach mainly targets final classes which have become more
common especially since the Kotlin language became popular. To supply this
alternative mock maker, Mockito attempts attaching an agent to the current
VM (directly or indirectly, depending on the VM's version) which suffers
the additional costs for attaching an agent that I mentioned before.
Thanks for reading this and I hope that you can consider these, as of
today, very practiced use cases. The JPMS migration has gone quite well, I
find. With these outstanding problems, JDK 11 could be the first release
where a majority of Java programs would no longer need to rely on internal
API.
Best regards, Rafael