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