1 概述
代理模式是一种结构型设计模式,它通过提供一个代理对象来控制对另一个对象的访问。在代理模式中,代理对象充当原始对象的接口,客户端可以通过代理对象来访问原始对象,代理对象则可以控制对原始对象的访问,并在必要时进行一些额外的处理。
- 分类
代理模式根据代理对象(应用场景)有以下几种类型:
- 远程代理:代理对象控制对远程对象的访问。
- 虚拟代理:代理对象控制对大对象的访问,只有在需要时才会实例化大对象。
- 保护代理:代理对象控制对敏感对象的访问,可以对访问进行授权和认证。
- 缓存代理:代理对象缓存对原始对象的访问结果,以提高访问效率。
代理模式根据实现方式有以下2种实现:
-
静态代理:见#3
-
动态代理:见#4
- 功能
代理模式主要提供以下功能:
- 功能增强:下面单独说明。
- 控制访问:代理对象可以控制对原始对象的访问,从而可以提供一些额外的安全性和保护性。
- 增加性能:代理对象可以缓存原始对象的访问结果,以提高访问效率。
- 简化客户端:代理对象可以隐藏原始对象的复杂性,从而简化客户端的操作。
- 功能增强
代理模式可以用来增强被代理对象的功能,实现功能的动态扩展和修改。代理对象可以在不修改被代理对象的前提下,对其方法进行增强,包括但不限于以下几种方式:
- 记录日志:在代理对象的方法执行前后记录日志,方便问题排查和系统监控。
- 缓存数据:在代理对象的方法执行前先查询缓存,如果缓存中已经存在所需数据,则直接返回缓存数据,避免重复查询数据库等资源。
- 增强安全性:在代理对象的方法执行前进行权限验证,确保只有有权限的用户才能调用特定的方法。
- 远程调用:通过代理对象实现远程调用,将需要执行的方法序列化传输给远程服务器执行,并将结果反序列化返回。
- 延迟加载:在代理对象的方法执行前先判断是否需要加载数据,如果不需要则不进行数据加载,从而提高系统性能。
总之,代理模式可以通过动态生成代理对象来对被代理对象的方法进行增强,从而实现各种各样的功能扩展和修改。
但代理模式也可能带来一些缺点,例如:
- 增加复杂性:代理模式可能增加系统的复杂性,因为需要引入额外的代理对象来控制访问。
- 增加开销:代理对象可能会带来额外的开销,例如网络通信、对象的实例化等。
2 对象之间的关系
在通过UML设计类图的时候,常见关系,参考地址在文章最后3,4,这里不在赘述。
3 静态代理
静态代理是指在编译期就已经确定代理类和被代理类的关系。静态代理需要手动创建代理类,实现被代理接口,并在代理类中调用被代理类的方法。静态代理的优点是简单易懂,缺点是代理类数量增多,维护成本高。
在静态代理中,代理类和被代理类实现同一个接口,代理类持有一个被代理类的引用,在代理类的方法中调用被代理类的方法,并可以在调用前后做一些额外的操作,比如记录日志、统计时间、校验参数等。静态代理的优点是代码结构清晰,易于理解和维护,缺点是当被代理类的方法发生改变时,代理类的代码也需要相应修改。
下面我们通过简单的订单服务,(静态代理)UML类图如下:
订单接口OrderService源代码2-1如下:
package com.gaogzhen.proxy.service;
/**
* 订单服务接口
* @author gaogzhen
*/
public interface OrderService {
/**
* 生成订单
*/
void generate();
/**
* 修改订单
*/
void modify();
/**
* 检索订单
*/
void retrieve();
}
订单服务实现类OrderServiceImpl类代码2-2如下所示:
package com.gaogzhen.proxy.service.impl;
import com.gaogzhen.proxy.service.OrderService;
import java.util.concurrent.TimeUnit;
/**
* 订单服务
* @author gaogzhen
*/
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
// 模拟生成订单耗时
try {
TimeUnit.MILLISECONDS.sleep(1234);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("生成订单");
}
@Override
public void modify() {
// 模拟修改订单耗时
try {
TimeUnit.MILLISECONDS.sleep(556);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("修改订单");
}
@Override
public void retrieve() {
// 模拟检索订单耗时
try {
TimeUnit.MILLISECONDS.sleep(224);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("检索订单");
}
}
应用场景:假设系统运行一段时间,甲方感觉服务运行太慢,想要我们优化。现在我们需要统计每个业务接口的业务方法的耗时,有以下可选方案:
-
解决方案一:硬编码,在业务方法中直接添加统计耗时的程序。
- 缺点
- 违背OCP(开闭原则)原则;
- 大量冗余,开发效率低,代码不能复用。
- 缺点
-
解决方案二:编写业务类的子类,重写业务方法。
-
子类源代码2-3如下
package com.gaogzhen.proxy.service.impl; /** * 业务类子类 * @author gaogzhen */ public class OrderServiceImplSub extends OrderServiceImpl{ @Override public void generate() { long start = System.currentTimeMillis(); super.generate(); long end = System.currentTimeMillis(); System.out.println("generate 耗时:" + (end - start)); } @Override public void modify() { long start = System.currentTimeMillis(); super.modify(); long end = System.currentTimeMillis(); System.out.println("generate 耗时:" + (end - start)); } @Override public void retrieve() { long start = System.currentTimeMillis(); super.retrieve(); long end = System.currentTimeMillis(); System.out.println("generate 耗时:" + (end - start)); } }
-
缺点
- 耦合度高
- 大量冗余,代码不能复用
-
-
解决方案三:静态代理
-
代理类代码2-4如下所示:
package com.gaogzhen.proxy.service.proxy; import com.gaogzhen.proxy.service.OrderService; /** * OrderService代理类 * @author gaogzhen */ public class OrderServiceProxy implements OrderService { private OrderService orderService; public OrderServiceProxy(OrderService orderService) { this.orderService = orderService; } @Override public void generate() { long start = System.currentTimeMillis(); orderService.generate(); long end = System.currentTimeMillis(); System.out.println("generate 耗时:" + (end - start)); } @Override public void modify() { long start = System.currentTimeMillis(); orderService.modify(); long end = System.currentTimeMillis(); System.out.println("modify 耗时:" + (end - start)); } @Override public void retrieve() { long start = System.currentTimeMillis(); orderService.retrieve(); long end = System.currentTimeMillis(); System.out.println("retrieve 耗时:" + (end - start)); } } // 测试类 package com.gaogzhen.proxy.client; import com.gaogzhen.proxy.service.OrderService; import com.gaogzhen.proxy.service.impl.OrderServiceImpl; import com.gaogzhen.proxy.service.proxy.OrderServiceProxy; /** * 模拟服务客户端 * @author gaogzhen */ public class ServiceClient { public static void main(String[] args) { OrderService orderServiceImpl = new OrderServiceImpl(); OrderService orderServiceProxy = new OrderServiceProxy(orderServiceImpl); orderServiceProxy.generate(); orderServiceProxy.modify(); orderServiceProxy.retrieve(); } }
-
优点
- 通过关联关系解耦合
-
缺点
- 代码冗余,不能复用
- 一个目标对象需要对应的一个代理类对象,如果目标对象很多,程序运行需要消耗大量内存。
-
4 动态代理
动态代理是指在运行时根据接口动态生成代理类。Java中提供了两种方式实现动态代理:JDK动态代理、CGLIB动态代理和Javassist动态代理。
- Javassist是一种Java字节码编辑库,可以在运行时动态地修改字节码,包括修改已有的类、创建新的类和接口等。通过Javassist,我们可以实现动态代理。相比于Java原生的动态代理,Javassist动态代理具有更好的性能和更灵活的功能。
我们主要讲解JDK动态代理和CGLIB动态代理。
3.1 JDK动态代理
JDK动态代理是基于接口的代理模式。被代理类必须实现接口,代理类动态生成的过程由Java API实现。使用动态代理的方式,客户端调用代理对象的方法时,实际执行的是被代理对象的方法。JDK动态代理的优点是不需要手动创建代理类,缺点是只能代理实现了接口的类。
3.1.1 测试用例
测试用例同静态代理,根据测试需要随时添加代码:
- OrderService接口
- OrderServiceImpl目标类
3.1.1 JDK动态代理实现步骤
-
创建目标对象
-
创建代理对象:核心类
java.lang.reflect.Proxy
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
- ClassLoader loader:类加载器,spring会在内存中动态生成代理类的字节码,通过类加载器加载。JDK要求,目标类的类加载器必须和代理类的类加载器使用同一个
- Class<?>[]interfaces:代理类和目标类要实现同一个接口或者一些接口,生成代理类的时候,需要我们告诉它实现那些接口。
- InvocationHandler h:
InvocationHandler
是Java中动态代理机制中的一个接口,它定义了一个方法invoke
,该方法在代理对象方法被调用时被调用。在invoke
方法中,我们可以实现对被代理对象方法的增强,或者在代理对象方法被调用前后执行一些其他操作。
-
使用代理对象的代理方法
-
问题
-
我们需要手写InvocationHandler接口的实现类,会不会形成大量内存占用呢?
-
调用处理器的实现类我们只需要写一次,不会造成内存的大量占用。
-
3.1.2 InvocationHandler#invoke()
3.1.2.1 概述
- 为什么强行要求我们必须实现InvocationHandler接口的invoke()方法?
- 因为JDK底层调用invoke()方法的程序已经提前写好,所以这个方法必须是invoke。invoke()方法不是由我们程序员负责调用,而是JDK负责调用。
- invoke方法什么时候被调用?
- 当代理对象调用代理方法的时候,注册在InvocationHandler中的invoke()方法被调用。
invoke()方法测试,现在我们给测试用例添加计算时间的代理功能TimerInvocationHandler代码如下3.1.3-1所示:
package com.gaogzhen.proxy.service.handler;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* OrderService代理类
* @author gaogzhen
*/
public class TimerInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("invoke 被执行");
return null;
}
}
测试方法代码如下3.1.3-2所示:
package com.gaogzhen.proxy.client;
import com.gaogzhen.proxy.service.OrderService;
import com.gaogzhen.proxy.service.handler.TimerInvocationHandler;
import com.gaogzhen.proxy.service.impl.OrderServiceImpl;
import java.lang.reflect.Proxy;
/**
* 模拟服务客户端
* @author gaogzhen
*/
public class ServiceClient {
public static void main(String[] args) {
// 创建目标对象
OrderService target = new OrderServiceImpl();
// 创建代理对象
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TimerInvocationHandler());
}
}
测试结果:没有输出
结论:
- 不执行代理对象的代理方法,invoke()方法不会被调用。
下面我们来执行代理对象的代理方法,添加代码如下:
// 创建代理对象
OrderService proxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TimerInvocationHandler());
// 调用代理对象的代理方法
proxy.generate();
proxy.modify();
proxy.retrieve();
输出:
invoke 被执行
invoke 被执行
invoke 被执行
结论:
- 没调用一次代理方法,invoke()相应的执行一次。
- 因为我们代理类实现了和目标类相同的接口,所以可以转型为接口类型。
3.1.2.2 invoke()方法详解
invoke(Object proxy, Method method, Object[] args)方法的三个参数:
- invoke()方法由JDK负责调用,会自动传递给我们这3个参数。
- Object proxy:代理对象,参数较少使用。
- Method method:目标对象上的目标方法,要执行的目标方法。
- Object[] args:目标方法上的参数。
方法执行四要素:
- 对象
- 方法
- 参数
- 返回结果
继续测试invoke()方法,添加增强代码逻辑,添加代码如下:
package com.gaogzhen.proxy.service.handler;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* OrderService代理类
* @author gaogzhen
*/
public class TimerInvocationHandler implements InvocationHandler {
private Object target;
public TimerInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("===方法执行前增强===");
// 目标方法执行
Object val = method.invoke(target, args);
System.out.println("====方法执行====");
System.out.println("====方法执行后增强====");
return val;
}
}
// 接口添加带返回值的方法
/**
* 根据订单id获取订单名称
* @param id
* @return
*/
String getById(String id);
// 目标类实现
@Override
public String getById(String id) {
return "学习JDK动态代理";
}
// 测试类输出返回结果
// 调用代理对象的代理方法
System.out.println(proxy.getById("xxx"));
测试结果:
===方法执行前增强===
====方法执行====
====方法执行后增强====
学习JDK动态代理
3.1.2.3 JDK动态代理工具类封装
在以后使用中,需要经常使用Proxy#newProxyInstance()生成代理对象。为了简化开发,我们把该方法简单封装,工具类代码3.1.2.3-1如下所示:
package com.gaogzhen.proxy.utils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
/**
* 动态代理工具类
* @author gaogzhen
*/
public class ProxyUtils {
/**
* JDK动态代理生成代理对象
* @param target 目标对象
* @return 代理对象
*/
public static Object newProxyInstance(Object target, InvocationHandler h) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), h);
}
}
3.2 CGLIB动态代理
CGLIB动态代理是基于继承的代理模式。被代理类不需要实现接口,代理类动态生成的过程由第三方库实现。使用动态代理的方式,客户端调用代理对象的方法时,实际执行的是被代理对象的方法。CGLIB动态代理的优点是可以代理没有实现接口的类,缺点是生成代理类的过程比较耗时,会影响性能。
- 被代理目标类不能使用final修饰
测试用例UserService用户服务类,模拟登陆退出,代码3.2-1如下所示:
package com.gaogzhen.proxy.service;
/**
* 用户服务类
* @author gaogzhen
*/
public class UserService {
/**
* 模拟登陆
* @param username 用户名
* @param password 密码
* @return {@code true} 登陆成功;{@code false} 否则
*/
public boolean login(String username, String password) {
System.out.println("身份验证");
if ("admin".equals(username) && "123".equals(password)) {
System.out.println("====登陆成功====");
return true;
}
System.out.println("登陆失败,用户名或者密码错误");
return false;
}
/**
* 退出登陆
*/
public void logout() {
System.out.println("===退出登陆====");
}
}
TimerMethodInterceptor计算耗时功能增强,代码3.2-2如下所示:
package com.gaogzhen.proxy.interceptor;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 时间处理
* @author gaogzhen
*/
public class TimerMethodInterceptor implements MethodInterceptor {
/**
* 代理方法
* @param o 目标对象
* @param method
* @param objects 目标方法参数
* @param methodProxy 目标方法
* @return 执行结果
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("====目标方法调用之前增强====");
long start = System.currentTimeMillis();
// 调用目标方法
Object val = methodProxy.invokeSuper(o, objects);
System.out.println("====目标方法调用之后增强====");
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
return val;
}
}
测试类代码3.2-3如下所示:
package com.gaogzhen.proxy.client;
import com.gaogzhen.proxy.interceptor.TimerMethodInterceptor;
import com.gaogzhen.proxy.service.UserService;
import net.sf.cglib.proxy.Enhancer;
/**
* 模拟客户端
* @author gaogzhen
*/
public class Client {
public static void main(String[] args) {
// 1创建字节码增强对象
// 该对象上CGLIB库中的核心对象,需要该类生成代理类
Enhancer enhancer = new Enhancer();
// 1.1 指定父类,即目标类
enhancer.setSuperclass(UserService.class);
// 1.2 设置回调
enhancer.setCallback(new TimerMethodInterceptor());
// 2 创建代理对象
// 2.1 在内存中生成目标类的子类,实际上代理类的字节码
// 2.2 创建代理对象
UserService userServiceProxy = (UserService) enhancer.create();
System.out.println(userServiceProxy);
// 3 调用代理对象的代理方法
boolean success = userServiceProxy.login("admin", "123");
userServiceProxy.logout();
}
}
CGLIB在JDK版本1.8之后,想要正常运行,需要在启动项添加两个启动参数
- VM options添加:
--add-opens java.base/java.lang=ALL-UNNAMED
- 程序运行参数添加:
--add-opens java.base/sun.net.util=ALL-UNNAMED
如下图3.2-1所示:
测试结果:
====目标方法调用之前增强====
====目标方法调用之前增强====
====目标方法调用之后增强====
耗时:0
====目标方法调用之后增强====
耗时:6
com.gaogzhen.proxy.service.UserService$$EnhancerByCGLIB$$6f97ca11@65ae6ba4
====目标方法调用之前增强====
身份验证
====登陆成功====
====目标方法调用之后增强====
耗时:1
====目标方法调用之前增强====
===退出登陆====
====目标方法调用之后增强====
耗时:0
- UserService$$EnhancerByCGLIB$$6f97ca11为生成的代理子类,有点印象,目标类名$$EnhancerByCGLIB$$xxxx这种的底层就是使用CGLIB动态代理生成的。
结语
如果小伙伴什么问题或者指教,欢迎交流。
❓QQ:806797785
⭐️源代码仓库地址:https://gitee.com/gaogzhen/spring6-study
参考:
[1]Spring框架视频教程[CP/OL].P69-75.
[2]ChatGPT