43. Invoking default methods from Proxy instances
Starting with JDK 8, we can define default
methods in interfaces. For instance, let’s consider the following interfaces (for brevity, all methods from these interfaces are declared as default
):
Figure 2.26: Interfaces: Printable, Writable, Draft, and Book
Next, let’s assume that we want to use the Java Reflection API to invoke these default methods. As a quick reminder, the Proxy
class goal is used to provide support for creating dynamic implementations of interfaces at runtime.
That being said, let’s see how we can use the Proxy API for calling our default
methods.
JDK 8
Calling a default
method of an interface in JDK 8 relies on a little trick. Basically, we create from scratch a package-private constructor from the Lookup API. Next, we make this constructor accessible – this means that Java will not check the access modifiers to this constructor and, therefore, will not throw an IllegalAccessException
when we try to use it. Finally, we use this constructor to wrap an instance of an interface (for instance, Printable
) and use reflective access to the default
methods declared in this interface.
So, in code lines, we can invoke the default method Printable.print()
as follows:
// invoke Printable.print(String)
Printable pproxy = (Printable) Proxy.newProxyInstance(
Printable.class.getClassLoader(),
new Class<?>[]{Printable.class}, (o, m, p) -> {
if (m.isDefault()) {
Constructor<Lookup> cntr = Lookup.class
.getDeclaredConstructor(Class.class);
cntr.setAccessible(true);
return cntr.newInstance(Printable.class)
.in(Printable.class)
.unreflectSpecial(m, Printable.class)
.bindTo(o)
.invokeWithArguments(p);
}
return null;
});
// invoke Printable.print()
pproxy.print("Chapter 2");
Next, let’s focus on the Writable
and Draft
interfaces. Draft
extends Writable
and overrides the default write()
method. Now, every time we explicitly invoke the Writable.write()
method, we expect that the Draft.write()
method is invoked automatically behind the scenes. A possible implementation looks as follows:
// invoke Draft.write(String) and Writable.write(String)
Writable dpproxy = (Writable) Proxy.newProxyInstance(
Writable.class.getClassLoader(),
new Class<?>[]{Writable.class, Draft.class}, (o, m, p) -> {
if (m.isDefault() && m.getName().equals("write")) {
Constructor<Lookup> cntr = Lookup.class
.getDeclaredConstructor(Class.class);
cntr.setAccessible(true);
cntr.newInstance(Draft.class)
.in(Draft.class)
.findSpecial(Draft.class, "write",
MethodType.methodType(void.class, String.class),
Draft.class)
.bindTo(o)
.invokeWithArguments(p);
return cntr.newInstance(Writable.class)
.in(Writable.class)
.findSpecial(Writable.class, "write",
MethodType.methodType(void.class, String.class),
Writable.class)
.bindTo(o)
.invokeWithArguments(p);
}
return null;
});
// invoke Writable.write(String)
dpproxy.write("Chapter 1");
Finally, let’s focus on the Printable
and Book
interfaces. Book
extends Printable
and doesn’t define any methods. So, when we call the inherited print()
method, we expect that the Printable.print()
method is invoked. While you can check this solution in the bundled code, let’s focus on the same tasks using JDK 9+.
JDK 9+, pre-JDK 16
As you just saw, before JDK 9, the Java Reflection API provides access to non-public class members. This means that external reflective code (for instance, third-party libraries) can have deep access to JDK internals. But, starting with JDK 9, this is not possible because the new module system relies on strong encapsulation.
For a smooth transition from JDK 8 to JDK 9, we can use the --illegal-access
option. The values of this option range from deny
(sustains strong encapsulation, so no illegal reflective code is permitted) to permit
(the most relaxed level of strong encapsulation, allowing access to platform modules only from unnamed modules). Between permit
(which is the default in JDK 9) and deny
, we have two more values: warn
and debug
. However, --illegal-access=permit;
support was removed in JDK 17.
In this context, the previous code may not work in JDK 9+, or it might still work but you’ll see a warning such as WARNING: An illegal reflective access operation has occurred.
But, we can “fix” our code to avoid illegal reflective access via MethodHandles
. Among its goodies, this class exposes lookup methods for creating method handles for fields and methods. Once we have a Lookup
, we can rely on its findSpecial()
method to gain access to the default
methods of an interface.
Based on MethodHandles
, we can invoke the default method Printable.print()
as follows:
// invoke Printable.print(String doc)
Printable pproxy = (Printable) Proxy.newProxyInstance(
Printable.class.getClassLoader(),
new Class<?>[]{Printable.class}, (o, m, p) -> {
if (m.isDefault()) {
return MethodHandles.lookup()
.findSpecial(Printable.class, "print",
MethodType.methodType(void.class, String.class),
Printable.class)
.bindTo(o)
.invokeWithArguments(p);
}
return null;
});
// invoke Printable.print()
pproxy.print("Chapter 2");
While in the bundled code, you can see more examples; let’s tackle the same topic starting with JDK 16.
JDK 16+
Starting with JDK 16, we can simplify the previous code thanks to the new static method, InvocationHandler.invokeDefault()
. As its name suggests, this method is useful for invoking default
methods. In code lines, our previous examples for calling Printable.print()
can be simplified via invokeDefault()
as follows:
// invoke Printable.print(String doc)
Printable pproxy = (Printable) Proxy.newProxyInstance(
Printable.class.getClassLoader(),
new Class<?>[]{Printable.class}, (o, m, p) -> {
if (m.isDefault()) {
return InvocationHandler.invokeDefault(o, m, p);
}
return null;
});
// invoke Printable.print()
pproxy.print("Chapter 2");
In the next example, every time we explicitly invoke the Writable.write()
method, we expect that the Draft.write()
method is invoked automatically behind the scenes:
// invoke Draft.write(String) and Writable.write(String)
Writable dpproxy = (Writable) Proxy.newProxyInstance(
Writable.class.getClassLoader(),
new Class<?>[]{Writable.class, Draft.class}, (o, m, p) -> {
if (m.isDefault() && m.getName().equals("write")) {
Method writeInDraft = Draft.class.getMethod(
m.getName(), m.getParameterTypes());
InvocationHandler.invokeDefault(o, writeInDraft, p);
return InvocationHandler.invokeDefault(o, m, p);
}
return null;
});
// invoke Writable.write(String)
dpproxy.write("Chapter 1");
In the bundled code, you can practice more examples.