Java动态代理

什么是代理

我们大家都知道微商代理,简单地说就是代替厂家卖商品,厂家“委托”代理为其销售商品。关于微商代理,首先我们从他们那里买东西时通常不知道背后的厂家究竟是谁,也就是说,“委托者”对我们来说是不可见的;其次,微商代理主要以朋友圈的人为目标客户,这就相当于为厂家做了一次对客户群体的“过滤”。我们把微商代理和厂家进一步抽象,前者可抽象为代理类,后者可抽象为委托类(被代理类)。通过使用代理,通常有两个优点,并且能够分别与我们提到的微商代理的两个特点对应起来:

  • 优点一:可以隐藏委托类的实现;

  • 优点二:可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。

代理模式

代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。

代理模式角色分为 3 种:

  • Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;

  • RealSubject(真实主题角色):真正实现业务逻辑的类;

  • Proxy(代理主题角色):用来代理和封装真实主题;

代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层

如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:

  • 静态代理
    就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。

  • 动态代理
    源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件。

静态代理

代码示例

我们先通过实例来学习静态代理,然后理解静态代理的缺点,再来学习本文的主角:动态代理

编写一个接口 UserService ,以及该接口的一个实现类 UserServiceImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface UserService {
public void select();
public void update();
}

public class UserServiceImpl implements UserService {
public void select() {
System.out.println("查询 selectById");
}
public void update() {
System.out.println("更新 update");
}
}

我们将通过静态代理对 UserServiceImpl 进行功能增强,在调用 select 和 update 之前记录一些日志。写一个代理类 UserServiceProxy,代理类需要实现 UserService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class UserServiceProxy implements UserService {
private UserService target; // 被代理的对象

public UserServiceProxy(UserService target) {
this.target = target;
}
public void select() {
before();
target.select(); // 这里才实际调用真实主题角色的方法
after();
}
public void update() {
before();
target.update(); // 这里才实际调用真实主题角色的方法
after();
}

private void before() { // 在执行方法之前执行
System.out.printf("log start time [%s] \n", new Date());
}
private void after() { // 在执行方法之后执行
System.out.printf("log end time [%s] \n", new Date());
}
}

测试;

1
2
3
4
5
6
7
8
9
public class Client1 {
public static void main(String[] args) {
UserService userServiceImpl = new UserServiceImpl();
UserService proxy = new UserServiceProxy(userServiceImpl);

proxy.select();
proxy.update();
}
}

输出:

1
2
3
4
5
6
log start time [Thu Dec 20 14:13:25 CST 2018] 
查询 selectById
log end time [Thu Dec 20 14:13:25 CST 2018]
log start time [Thu Dec 20 14:13:25 CST 2018]
更新 update
log end time [Thu Dec 20 14:13:25 CST 2018]

通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。

静态代理缺点

虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。

  • 1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:

    • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大;

    • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类;

  • 2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护

动态代理

生成原理

Java虚拟机类加载过程主要分为五个阶段:加载验证准备解析、`初始化。其中加载阶段需要完成以下3件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流

  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口

由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:

  • 从ZIP包获取,这是JAR、EAR、WAR等格式的基础

  • 从网络中获取,典型的应用是 Applet

  • 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流

  • 由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类

  • 从数据库中获取等等

所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。

常见的字节码操作类库

这里有一些介绍:java-source.net/open-source…

  • Apache BCEL (Byte Code Engineering Library):是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。

  • ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。

  • CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。

  • Javassist:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。

为了让生成的代理类与目标对象(真实主题角色)保持一致性,从现在开始将介绍以下两种最常见的方式:

通过实现接口的方式 -> JDK动态代理
通过继承类的方式 -> CGLIB动态代理

注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦

JDK动态代理

JDK动态代理主要涉及两个类:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler,我们仍然通过案例来学习.

编写一个调用逻辑处理器 LogHandler 类,提供日志增强功能,并实现 InvocationHandler 接口;在 LogHandler 中维护一个目标对象,这个对象是被代理的对象(真实主题角色);在 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
27
28
29
30
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;

public class LogHandler implements InvocationHandler {
Object target; // 被代理的对象,实际的方法执行者

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

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
// 调用 target 的 method 方法
Object result = method.invoke(target, args);
after();
return result; // 返回方法的执行结果
}

// 调用invoke方法之前执行
private void before() {
System.out.printf("log start time [%s] \n", new Date());
}

// 调用invoke方法之后执行
private void after() {
System.out.printf("log end time [%s] \n", new Date());
}
}

编写客户端,获取动态生成的代理类的对象须借助 Proxy 类的 newProxyInstance 方法:

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
import proxy.UserService;
import proxy.UserServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class UserClientJdkDynamic {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
// 设置变量可以保存动态代理类,默认名称以 $Proxy0 格式命名
// System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 1. 创建被代理的对象,UserService接口的实现类
UserServiceImpl userServiceImpl = new UserServiceImpl();

// 2. 获取对应的 ClassLoader
ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();

// 3. 获取所有接口的Class,这里的UserServiceImpl只实现了一个接口UserService,
Class[] interfaces = userServiceImpl.getClass().getInterfaces();

// 4. 创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用
// 这里创建的是一个自定义的日志处理器,须传入实际的执行对象 userServiceImpl
InvocationHandler logHandler = new LogHandler(userServiceImpl);
/*
5.根据上面提供的信息,创建代理对象 在这个过程中,
a.JDK会通过根据传入的参数信息动态地在内存中创建和.class 文件等同的字节码
b.然后根据相应的字节码转换成对应的class,
c.然后调用newInstance()创建代理实例
*/
UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
// 调用代理的方法
proxy.select();
proxy.update();

// 6. 保存JDK动态代理生成的代理类,类名保存为 UserServiceProxy
ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceProxy2");
}
}

运行结果

1
2
3
4
5
6
log start time [Thu Dec 20 16:55:19 CST 2018] 
查询 selectById
log end time [Thu Dec 20 16:55:19 CST 2018]
log start time [Thu Dec 20 16:55:19 CST 2018]
更新 update
log end time [Thu Dec 20 16:55:19 CST 2018]

InvocationHandler和Proxy核心方法介绍

java.lang.reflect.InvocationHandler

Object invoke(Object proxy, Method method, Object[] args) 定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用

java.lang.reflect.Proxy

static InvocationHandler getInvocationHandler(Object proxy) 用于获取指定代理对象所关联的调用处理器

static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) 返回指定接口的代理类

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的 invoke 方法

static boolean isProxyClass(Class<?> cl) 返回 cl 是否为一个代理类

代理类

JDK自动生成的代理类到底长什么样子呢?借助上面第6步操作,可以把代理类保存下来一探究竟, target 的类路径下找到 UserServiceProxy2.class,双击后IDEA的反编译插件会将该二进制class文件转变成java文件:

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
53
54
55
56
57
58
59
60
61
62
63
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.UserService;

public final class UserServiceProxy2 extends Proxy implements UserService {
private static Method m1;
private static Method m2;
private static Method m4;
private static Method m0;
private static Method m3;

public UserServiceProxy2(InvocationHandler var1) throws {
super(var1);
}

public final boolean equals(Object var1) throws {
// 省略...
}

public final String toString() throws {
// 省略...
}

public final void select() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final int hashCode() throws {
// 省略...
}

public final void update() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m4 = Class.forName("proxy.UserService").getMethod("select");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
m3 = Class.forName("proxy.UserService").getMethod("update");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

从 UserServiceProxy 的代码中我们可以发现:

  • UserServiceProxy2 继承了 Proxy 类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法

  • 由于UserServiceProxy2继承了 Proxy 类,所以每个代理类都会关联一个 InvocationHandler 方法调用处理器

  • 类和所有方法都被 public final 修饰,所以代理类只可被使用,不可以再被继承

  • 每个方法都有一个 Method 对象来描述,Method 对象在static静态代码块中创建,以 m+ 数字 的格式命名

  • 调用方法的时候通过 super.h.invoke(this, m1, (Object[])null) 调用,其中的 super.h.invoke 实际上是在创建代理的时候传递给 Proxy.newProxyInstanceLogHandler 对象,它继承 InvocationHandler 类,负责实际的调用处理逻辑

而 LogHandler 的 invoke 方法接收到 method、args 等参数后,进行一些处理,然后通过反射让被代理的对象 target 执行方法

1
2
3
4
5
6
7
8
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
// 调用 target 的 method 方法
Object result = method.invoke(target, args);
after();
return result; // 返回方法的执行结果
}

JDK动态代理执行方法调用的过程简图如下:

CGLIB动态代理

maven引入CGLIB包,然后编写一个UserDao类,它没有接口,只有两个方法,select() 和 update()

1
2
3
4
5
6
7
8
public class UserDao {
public void select() {
System.out.println("UserDao 查询 selectById");
}
public void update() {
System.out.println("UserDao 更新 update");
}
}

编写一个 LogInterceptor ,继承了 MethodInterceptor,用于方法的拦截回调

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
public class LogInterceptor implements MethodInterceptor {

private Object target; //目标类

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

/**
* 返回代理对象
* 具体实现,暂时先不追究。
*/
public Object createProxy() {
Enhancer enhancer = new Enhancer();
enhancer.setCallback(this); // 回调函数 拦截器
// 设置代理对象的父类,可以看到代理对象是目标对象的子类。所以这个接口类就可以省略了
enhancer.setSuperclass(this.target.getClass());
return enhancer.create();
}

/**
* @param object 表示要进行增强的对象
* @param method 表示拦截的方法
* @param objects 数组表示参数列表,基本数据类型需要传入其包装类型,如int-->Integer、long-Long、double-->Double
* @param methodProxy 表示对方法的代理,invokeSuper方法表示对被代理对象方法的调用
*
* @return 执行结果
*
* @throws Throwable
*/
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
// 注意这里是调用 invokeSuper 而不是 invoke,否则死循环,
// methodProxy.invokesuper执行的是原始类的方法,method.invoke执行的是子类的方法
Object result = methodProxy.invokeSuper(object, objects);
after();
return result;
}

private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}

private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CglibClient {

public static void main(String[] args){
//目标对象
UserDao userDao = new UserDao();
LogInterceptor interceptor = new LogInterceptor(userDao);
// 代理对象,调用cglib系统方法自动生成
// 注意:代理类是目标类的子类。
UserDao proxy = (UserDao) interceptor.createProxy();
proxy.select();
proxy.update();
}
}

结果:

1
2
3
4
5
6
log start time [Fri Nov 29 10:51:17 CST 2019] 
UserDao 查询 selectById
log end time [Fri Nov 29 10:51:17 CST 2019]
log start time [Fri Nov 29 10:51:17 CST 2019]
UserDao 更新 update
log end time [Fri Nov 29 10:51:17 CST 2019]

CGLIB 创建动态代理类的模式是:

  • 查找目标类上的所有非final 的public类型的方法定义;

  • 将这些方法的定义转换成字节码;

  • 将组成的字节码转换成相应的代理的class对象;

  • 实现 MethodInterceptor 接口,用来处理对代理类上所有方法的请求

JDK与CGLIB动态代理对比

  • JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。

  • Cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。

JDK动态代理优势

  • 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。

  • 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。

  • 代码实现简单。

基于类似 cglib 框架的优势

  • 无需实现接口,达到代理类无侵入
  • 只操作我们关心的类,而不必为其他相关类增加工作量。
  • 高性能

参考

[1] Java 动态代理详解

[2] Java动态代理

[3] Java反射机制详解

[4] 从代理模式再出发!Proxy.newProxyInstance的秘密

[5] JDK动态代理实现原理(jdk8)

附件

文中代码: https://github.com/austin-brant/dynamic-proxy-demo