Java修炼终极指南:38. 从Proxy实例调用默认方法

createh54周前 (02-17)技术教程10


从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");


在捆绑的代码中,您可以练习更多示例。

相关文章

Java的访问修饰符_java的访问修饰符在继承中的作用

为了实现面向对象程序设计(OOP)的封装这个特性,需要程序设计语言提供一定的语法机制来支持。这个语法机制就是访问权限控制(访问修饰符:public、protected、private、default)...

java基础之——访问修饰符(private/default/protected/public)

1. 访问修饰符介绍  java中的访问修饰符包含了四种:private、default(没有对应的保留字)、protected和public。它们的含义如下:private:如果一个元素声明为pri...

Java基础分享,一篇文章说透Java访问修饰符详解

Java基础分享,一篇文章说透Java访问修饰符详解我是@老K玩代码,非著名IT创业者。专注分享实战项目和最新行业资讯,已累计分享超1000实战项目!0. 前言java中有四种访问修饰符:privat...

小白学编程:Java访问修饰符(访问控制符)

Java 通过修饰符来控制类、属性和方法的访问权限和其他功能,通常放在语句的最前端。例如:Java 的修饰符很多,分为访问修饰符和非访问修饰符。本节仅介绍访问修饰符,非访问修饰符会在后续介绍。访问修饰...

Java核心修饰符——abstract修饰符与抽象类、抽象方法

前言经过前面几篇文章的讲解,我们现在已经对面向对象有了基本的认知,掌握了面向对象的三大特征:封装、继承和多态。(这部分放在文末了,大家有需要的话可以点击相关标题查看具体文章)这三个特征可以说是面向对象...

Java反射(小白也能懂)_java反射总结

Java中的反射机制是指在运行时动态地获取一个类的信息,包括类的方法、属性、构造函数等,而不需要事先知道这个类的具体实现。通过反射机制,可以在程序运行时获取类的信息,并且可以在运行时调用类的方法、创建...