Java核心技术1-代理对象

  |  

摘要: 本文是《Java核心技术 10th》中代理的要点总结

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


本文是《Java核心技术1》第10版 Chap6 中关于【代理】的要点总结;在文章 Java核心技术1-接口 中,我们了解了接口;在文章 Java核心技术1-lambda表达式 中,我们了解了 lambda 表达式;在文章 Java核心技术1-内部类 中,我们了解了 内部类

本文介绍代理(proxy),这是一种实现任意接口的对象。代理是一种非常专业的构造工具,它可以用来构建系统级的工具。

利用代理可以在运行时创建一个实现了一组给定接口的新类。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。

对于系统编程来说,代理带来的灵活性很重要。


代理的适用场景

假设有一个表示接口的 Class 对象(有可能只包含一个接口),它的确切类型在编译时无法知道。要想构造一个实现这些接口的类,就需要使用 newInstance 方法或反射找出这个类的构造器。但是,不能实例化一个接口,而是需要在程序处于运行状态时定义一个新类。

代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。尤其是,它具有下列方法:

  • 指定接口所需要的全部方法。
  • Object 类中的全部方法,例如,toString、equals 等。

然而,不能在运行时定义这些方法的新代码。而是要提供一个调用处理器(invocation handler)。调用处理器是实现了 InvocationHandler 接口的类对象。在这个接口中只有一个方法

1
Object invoke(Object proxy, Method method, Object[] args)

无论何时调用代理对象的方法,调用处理器的 invoke 方法都会被调用,并向其传递 Method 对象和原始的调用参数。调用处理器必须给出处理调用的方式。


创建代理对象

要想创建一个代理对象,需要使用 Proxy 类的 newProxyInstance 方法。这个方法有三个参数:

  • 一个类加载器(class loader)。作为 Java 安全模型的一部分,对于系统类和从因特网上下载下来的类,可以使用不同的类加载器。有关类加载器的详细内容将在卷 2 第 9 章中讨论。目前,用 null 表示使用默认的类加载器。
  • 一个 Class 对象数组,每个元素都是需要实现的接口。
  • 一个调用处理器

还有两个需要解决的问题。

  1. 如何定义一个处理器?
  2. 能够用结果代理对象做些什么?

这两个问题的答案取决于打算使用代理机制解决什么问题。使用代理可能出于很多原因,例如:

  • 路由对远程服务器的方法调用。
  • 在程序运行期间,将用户接口事件与动作关联起来。
  • 为调试,跟踪方法调用。

例子:跟踪方法调用

下面的代码中,使用代理和调用处理器跟踪方法调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Random;

public class ProxyTest {
public static void main(String[] args) {
Object[] elements = new Object[1000];

for(int i = 0; i < elements.length; i++) {
Integer value = i + 1;
InvocationHandler handler = new TraceHandler(value);
Object proxy = Proxy.newProxyInstance(null, new Class[] { Comparable.class }, handler);
elements[i] = proxy;
}

Integer key = new Random().nextInt(elements.length) + 1;
int result = Arrays.binarySearch(elements, key);

if(result >= 0) {
System.out.println(elements[result]);
}
}
}

class TraceHandler implements InvocationHandler {
private Object target;

public TraceHandler(Object t) {
target = t;
}

public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
// 打印隐式参数
System.out.print(target);
// 打印方法名
System.out.print("." + m.getName() + "(");
// 打印显式参数
if(args != null) {
for (int i = 0; i < args.length; i++) {
System.out.print(args[i]);
if(i < args.length - 1) {
System.out.print(", ");
}
}
}
System.out.println(")");

return m.invoke(target, args);
}
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
500.compareTo(874)
750.compareTo(874)
875.compareTo(874)
812.compareTo(874)
843.compareTo(874)
859.compareTo(874)
867.compareTo(874)
871.compareTo(874)
873.compareTo(874)
874.compareTo(874)
874.toString()
874

下面我们分析上面的例子。

(1) 继承 TraceHandler 并覆盖 invoke 方法,实现调用处理器

其中的 invoke 方法打印出被调用方法的名字和参数,然后用包装好的对象作为隐式参数调用这个 invoke 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class TraceHandler implements InvocationHandler {
private Object target;

public TraceHandler(Object t) {
target = t;
}

public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
// 打印隐式参数
System.out.print(target);
// 打印方法名
System.out.print("." + m.getName() + "(");
// 打印显式参数
if(args != null) {
for (int i = 0; i < args.length; i++) {
System.out.print(args[i]);
if(i < args.length - 1) {
System.out.print(", ");
}
}
}
System.out.println(")");

return m.invoke(target, args);
}
}

(2) 构造【用于跟踪方法调用】的代理对象

1
2
3
4
5
6
Object value = ...;
// construct wrapper
InvocationHandler handler = new TraceHandler(value);
// construct proxy for one or more interfaces
Class[] interfaces = new Class[] {Comparable.class};
Object proxy = Proxy.newProxyInstance(null, interfaces, handler);

上面的代码创建好 proxy 对象后,根据调用处理器中的 invoke 方法,无论何时【用 proxy 对象调用某个方法】,该方法的名字和参数都会打印出来,之后再用 value 调用该方法。

Integer 类实现了 Comparable 接口、代理对象属于在运行时定义的类,也实现了 Comparable 接口,它的 compareTo 方法调用了代理对象处理器的 invoke 方法。

(3) 使用 proxy 对象对二分查找进行跟踪

首先用 【1 ~ 1000 整数的代理】填充数组,然后调用 Arrays.binarySearch 在数组中查找一个随机整数。

(4) 不属于 Comparable 接口的 toString 方法也被代理

从打印结果中看到 toString 方法,这是 System.out.println() 方法调用单利对象的 toString()。


代理类的特性

代理类最关键的一点是它是在程序运行过程中创建的。但是一旦被创建,它就成了常规类,与虚拟机的其它类没有任何区别。

  • 所有的代理类都扩展于 Proxy 类。

  • 代理类只有一个实例域:调用处理器,它定义在 Proxy 类的超类中。

  • 为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中。

  • 所有的代理类都覆盖了 Object 类中的方法 toString、equals 和 hashCode。

  • Object 类中的其它方法,例如 clone 和 getClass 没有被重新定义。

  • 对于特定的类加载器和预设的一组接口来说(newProxyInstance 的前两个参数),只能有一个代理类。可以通过以下代码获得这个类:

1
Class proxyClass = Proxy.getProxyClass(null, interfaces);
  • 代理类一定要是 public 和 final。如果代理类实现的所有接口都是 public,代理类就不属于某个特定的包。

  • 通过调用 Proxy 类中的 isProxyClass 方法检测是个特定的 Class 对象是否代表一个代理类。


总结与例子

本文主要内容是动态代理(静态代理需要自己实现 Proxy 类),利用 Java 的反射技术,在运行时创建一个实现某些给定接口的新类(动态代理类)及其实例,代理的是接口,不是类或抽象类。在运行时才知道具体的实现。

动态代理的核心是下面这个方法:

1
2
public static Object newProxyInstance(ClassLoader loader, Class<>[] interfaces, InvocationHandler h)
throws IllegalArgumentException

其中:

  • loader: 用哪个类加载器去加载代理对象
  • interfaces: 动态代理类需要实现的接口
  • h: 动态代理方法在执行时,会调用 h 里面的 invoke 方法去执行

前面我们看了跟踪方法调用的例子。这里我们通过一个例子看一下自定义类如何使用代理,主要步骤如下:

step1: 首先定义一个接口
step2: 然后定义一个实现该接口的类
step3: 然后定义调用处理器
step4: 在 Main 中测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.lang.reflect.Method;

// 接口
interface Agent {
void train();
}

// 实现接口的类
class DQNAgent implements Agent {
public void train() {
System.out.println("DQN Agent training");
}
}

// 调用处理器
class AgentInvacationHandler implements InvocationHandler {
private final Agent agent;

public AgentInvacationHandler(Agent agent) {
this.agent = agent;
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 三个参数
// proxy: 代理对象,即 newProxyInstance 返回的对象
// method: 调用的方法
// args: 方法中的参数
System.out.println("-----代理动作前----");
Object res = method.invoke(agent, args);
System.out.println("-----代理动作后----");
return res;
}
}

// 测试
public class MyProxy {
public static void main(String[] args) {
Agent dqn_agent = new DQNAgent();

Agent proxy_agent = (Agent) Proxy.newProxyInstance(dqn_agent.getClass().getClassLoader()
,DQNAgent.class.getInterfaces()
,new AgentInvacationHandler(dqn_agent)
);
proxy_agent.train();
}
}

运行结果:

1
2
3
-----代理动作前----
DQN Agent training
-----代理动作后----

Share