Java修炼终极指南:38. 从Proxy实例调用默认方法
从JDK 8开始,我们可以在接口中定义默认方法(在《Java编码问题,第一版》的问题198中有所涉及)。例如,让我们考虑以下接口(为了简洁起见,这些接口中的所有方法都声明为默认方法):
图 2.26 - 接口:Printable、Writable、Draft 和 Book
接下来,假设我们想要使用Java Reflection API来调用这些默认方法。在《Java编码问题,第一版》的第7章(Java反射类、接口、构造函数、方法和字段)中,我们涵盖了大量Java Reflection API主题,包括问题165中的java.lang.reflect.Proxy API。虽然我希望您能查看问题165以获取更多详细信息,但简要提醒一下,Proxy类的目标是提供运行时创建接口动态实现的支持。话虽如此,让我们看看如何使用Proxy API来调用我们的默认方法。
JDK 8
在JDK 8中调用接口的默认方法依赖于一个小技巧。基本上,我们从Lookup API从头开始创建一个包私有构造函数。接下来,我们使这个构造函数可访问——这意味着Java不会检查此构造函数的访问修饰符,因此当我们尝试使用它时不会抛出IllegalAccessException。最后,我们使用此构造函数来包装一个接口实例(例如,Printable),并使用反射访问该接口中声明的默认方法。因此,在代码行中,我们可以按如下方式调用默认方法Printable.print():
// 调用 Printable.print(String)
Printable pproxy = (Printable) Proxy.newProxyInstance(
Printable.class.getClassLoader(),
new Class>[]{Printable.class}, (o, m, p) -> {
if (m.isDefault()) {
Constructor 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;
});
// 调用 Printable.print()
pproxy.print("Chapter 2");
接下来,让我们关注Writable和Draft接口。Draft扩展了Writable并覆盖了默认的write()方法。现在,每次我们明确调用Writable.write()方法时,我们期望在幕后自动调用Draft.write()方法。一个可能的实现如下所示:
// 调用 Draft.write(String) 和 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")) {
// ...(此处省略了与JDK 8示例类似的代码)
}
return null;
});
// 调用 Writable.write(String)
dpproxy.write("Chapter 1");
最后,让我们关注Printable和Book接口。Book扩展了Printable并没有定义任何方法。因此,当我们调用继承的print()方法时,我们期望调用Printable.print()方法。虽然你可以在捆绑的代码中检查这个解决方案,但让我们使用JDK 9+来处理相同的任务。
JDK 9+,预JDK 16
正如您刚刚看到的,在JDK 9之前,Java Reflection API提供了对非公共类成员的访问。这意味着外部反射代码(例如,第三方库)可以深入访问JDK内部。但是,从JDK 9开始,这是不可能的,因为新的模块系统依赖于强封装。为了从JDK 8到JDK 9的平稳过渡,我们可以使用--illegal-access选项。此选项的值范围从deny(维持强封装,因此不允许非法的反射代码)到permit(最强的封装级别的最宽松级别,仅允许从未命名模块访问平台模块)。在permit(JDK 9中的默认值)和deny之间,我们还有另外两个值:warn和debug。在这种情况下,前面的代码在JDK 9+中可能无法工作,或者它仍然可以工作但您会看到一个警告,如“WARNING: An illegal reflective access operation has occurred”。但是,我们可以通过MethodHandles来“修复”我们的代码,以避免非法的反射访问。除了其他优点外,这个类还暴露了用于为字段和方法创建方法句柄的查找方法。一旦我们有了Lookup,我们就可以依赖其findSpecial()来获取对接口默认方法的访问。基于MethodHandles,我们可以按如下方式调用默认方法Printable.print():
// 调用 Printable.print(String doc)
Printable pproxy = (Printable) Proxy.newProxyInstance(
// ...(与前面的代码类似,但使用MethodHandles)
);
// 调用 Printable.print()
pproxy.print("Chapter 2");
虽然在捆绑的代码中您可以看到更多示例,但让我们从JDK 16开始处理相同的主题。
JDK 16+
从JDK 16开始,我们可以简化前面的代码,这要归功于新的静态方法
InvocationHandler.invokeDefault()。顾名思义,此方法对于调用默认方法很有用。在代码行中,我们之前的调用Printable.print()的示例可以通过invokeDefault()简化为如下形式:
// 调用 Printable.print(String doc)
Printable pproxy = (Printable) Proxy.newProxyInstance(
// ...(与前面的代码类似,但使用invokeDefault())
);
// 调用 Printable.print()
pproxy.print("Chapter 2");
在下一个示例中,每次我们明确调用Writable.write()方法时,我们期望在幕后自动调用Draft.write()方法:
// 调用 Draft.write(String) 和 Writable.write(String)
Writable dpproxy = (Writable) Proxy.newProxyInstance(
// ...(与前面的代码类似,但处理Draft和Writable的write方法)
);
// 调用 Writable.write(String)
dpproxy.write("Chapter 1");
在捆绑的代码中,您可以练习更多示例。