Java 中的代理

1.1. 静态代理

优点:

  • 没有侵入原代码

缺点:

  • 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护
  • 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
    • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
    • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类

编写一个接口 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 进行功能增强,写一个代理类 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.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
14
15
16
17
18
public class Client1 {
public static void main(String[] args) {
UserService userServiceImpl = new UserServiceImpl();
UserService proxy = new UserServiceProxy(userServiceImpl);

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

/* 输出:
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 虚拟机类加载时,操作类的二进制字节流,然后再加载到 JVM 中使用。

常见的字节码操作类库:

  • 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动态代理

1.2.1. 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
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();
Object result = method.invoke(target, args); // 调用 target 的 method 方法
after();
return result; // 返回方法的执行结果
}
// 调用invoke方法之前执行
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
// 调用invoke方法之后执行
private void after() {
System.out.println(String.format("log end time [%s] ", 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
public class Client2 {
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();
}
}

1.2.2. CGLIB动态代理

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

  • 查找目标类上的所有非 final 的 public 类型的方法定义
  • 将这些方法的定义转换成字节码
  • 将组成的字节码转换成相应的代理的 class 对象
  • 实现 MethodInterceptor 接口,用来处理对代理类上所有方法的请求

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

public class CglibTest {
public static void main(String[] args) {
DaoProxy daoProxy = new DaoProxy();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Dao.class); // 设置超类,cglib 是通过继承来实现的
enhancer.setCallback(daoProxy);

Dao dao = (Dao)enhancer.create(); // 创建代理类
dao.update();
dao.select();
}
}

还可以进一步多个 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
public class LogInterceptor2 implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(object, objects);
after();
return result;
}
private void before() {
System.out.println(String.format("log2 start time [%s] ", new Date()));
}
private void after() {
System.out.println(String.format("log2 end time [%s] ", new Date()));
}
}

// 回调过滤器: 在CGLib回调时可以设置对不同方法执行不同的回调逻辑,或者根本不执行回调。
public class DaoFilter implements CallbackFilter {
@Override
public int accept(Method method) {
if ("select".equals(method.getName())) {
return 0; // Callback 列表第1个拦截器
}
return 1; // Callback 列表第2个拦截器,return 2 则为第3个,以此类推
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CglibTest2 {
public static void main(String[] args) {
LogInterceptor logInterceptor = new LogInterceptor();
LogInterceptor2 logInterceptor2 = new LogInterceptor2();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserDao.class); // 设置超类,cglib是通过继承来实现的
enhancer.setCallbacks(new Callback[]{logInterceptor, logInterceptor2, NoOp.INSTANCE}); // 设置多个拦截器,NoOp.INSTANCE是一个空拦截器,不做任何处理
enhancer.setCallbackFilter(new DaoFilter());

UserDao proxy = (UserDao) enhancer.create(); // 创建代理类
proxy.select();
proxy.update();
}
}

1.2.3. JDK 动态代理与 CGLIB 动态代理对比

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

JDK 动态代理的优势:

  • 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠
  • 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用
  • 代码实现简单

基于类似 cglib 框架的优势:

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