其实从“动态”和“代理”两个词也可以看出其具体作用,动态可以理解为不修改源码,运行时进行;代理这里可以理解为干预,经过一个“代理层”,对目标对象的方法进行拦截和增强(例如添加日志、权限校验、事务管理等),这些是设置代理模式的典型应用。在一篇文章中也看到一个很精练的词汇,即对程序的无侵入式扩展

核心组件

JDK 动态代理完全依赖 Java 的反射机制,主要围绕 java.lang.reflect 包下的两个核心组件:

  • InvocationHandler 接口:需要编写一个类来实现这个接口,并重写 invoke() 方法。当客户端调用代理对象的方法时,这个调用会被自动转发到 invoke() 方法中,这里重写的就是方法执行前、执行后的增强逻辑
  • Proxy 类:生成动态代理类,使用静态方法 Proxy.newProxyInstance() 动态构建代理类的实例

Proxy 类动态创建 class 对象

在 Java 9 之前,proxy 类中存在一个 native 方法 defineClass0,我们可以通过该方法创建 class 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package org.example;  

import org.example.student.Student;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Main {
public static void main(String[] args) throws Exception{
ClassLoader classLoader = new ClassLoader() {};
Class<?> testProxy = Proxy.class;

Method method = testProxy.getDeclaredMethod("defineClass0", ClassLoader.class, String.class, byte[].class, int.class, int.class);
method.setAccessible(true);

Class studentClass = (Class) method.invoke(null, classLoader, "org.example.student.Student", Student.studentClassBytes, 0, Student.studentClassBytes.length);

System.out.println(studentClass);
}
}
1
class org.example.student.Student

由于引用了 Student.studentClassBytes 时,JVM 已经自动加载了 Student.class,之后再用 defineClass0 尝试重复定义同名类,就会错误。因此我们创建一个自定义类加载器,与 AppClassLoader 隔离

构建代理类实例

首先我们需要构建一个场景,这样会更好理解

简单说说接口的一些概念

接口通过关键字 interface 来定义,其主要作用是制定规范和解耦。
这个规范可以理解为一个标准,只定义了一个类有哪些方法(可以做什么),至于具体的方法逻辑(具体怎么做)则需要具体实现(implements)接口
而实现接口我们可以通过重写(override)将接口中的抽象方法体现各自的微小或巨大差异,这就是多态的部分
最后“要做什么”和“具体做什么”分开了,这两步可以单独进行开发,维护等,很好地降低了耦合度

因此我们先构建一个接口类 Student

1
2
3
4
5
package org.example;  

public interface Student {
public String getName();
}

现在有一个 StudentService 类来实现这个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.example;  

public class StudentService implements Student {

private String name;

public StudentService(String name) {
this.name = name;
}

@Override
public String getName() {
System.out.println("正在执行" + this.getClass().getName() + "类的方法");
return name;
}
}

现在我们构建代理类的实例,我们代理的对象就是 StudentService 类的实例 student,因此具体拦截和增强需要通过 InvocationHandler 接口的 invoke 方法来实现,而这里的 JDKInvocationHandler() 方法就是我们对 InvocationHandler 接口的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;  

import java.lang.reflect.Proxy;

public class StudentProxy {
public static void main(String[] args) throws Exception{
Student student = new StudentService("张三");
Student proxyInstance = (Student) Proxy.newProxyInstance(
student.getClass().getClassLoader(),
student.getClass().getInterfaces(),
new JDKInvocationHandler(student));

System.out.println(proxyInstance.getName());
}
}

JDKInvocationHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.example;  

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class JDKInvocationHandler implements InvocationHandler{

private final Object target;

public JDKInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("即将调用" + target.getClass().getName() + "类的" + method.getName() + "方法");
// 委托真实对象执行方法
Object result = method.invoke(target, args);
System.out.println("已完成" + target.getClass().getName() + "类的" + method.getName() + "方法调用");
return result;
}
}


我们下断点看看整个调用链

这里我们选择步入 proxyInstance.getName() 方法

当客户端调用代理对象的方法时,这个调用会被自动转发到 invoke() 方法中

进入真正客户端执行的方法,整个调用链

1
2
3
4
5
6
7
8
at org.example.StudentService.getName(StudentService.java:13)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498) // 反射调用
at org.example.JDKInvocationHandler.invoke(JDKInvocationHandler.java:18) // 我们的自定义拦截器
at com.sun.proxy.$Proxy0.getName(Unknown Source:-1) // JDK动态生成的代理类
at org.example.StudentProxy.main(StudentProxy.java:13)

在调试模式下多出的 toString 方法调用,是 IntelliJ IDEA 调试器自动触发的,变量面板展示对象时,IDE 需要显示对象的字符串表示;断点暂停时,调试器会对当前作用域内的对象调用 toString() 来展示预览值
由于 proxyInstance 是代理对象,调试器对它调用 toString() 时,同样会经过 JDKInvocationHandler.invoke()