GoF之代理模式
概述
代理模式是GoF23种设计模式之一,属于结构型设计模式,本质就是通过引入代理对象间接实现对真实对象的操作
业务场景: 系统中有A、B、C
三个模块,使用这些模块的前提是需要用户登录
- 此时就可以为A、B、C三个模块提供一个代理,代理的逻辑请求来了之后先判断用户是否登录了,如果登录了则执行对应的目标,如果没有则跳转到登录页面
Java中的两种代理模式
静态代理
: 在编译期就生成代理对象动态代理
: 在Java运行时动态生成代理对象,动态代理又有JDK代理和CGLib代理两种
代理模式的作用
- 当一个对象需要受到保护的时候即不想让客户看到一些内容和服务,可以考虑使用代理对象去完成某个行为
- 需要给某个对象的功能进行增强的时候,可以考虑找一个代理进行增强如添加客户需要的额外服务
- 在程序中对象A和对象B无法直接交互时也可以使用代理模式来解决
代理模式分为三种角色
目标对象(Real Subject)
: 实现最终核心的业务,是代理对象所代表的真实对象即最终要引用的对象代理对象(Proxy Subject)
: 其内部含有对目标对象的引用,它可以访问、控制或扩展目标对象的功能- 目标对象和代理对象的公共接口(Abstract Subject): 目标对象和代理对象需要具有相同的行为和动作,让客户端在使用代理对象时就像在使用目标对象一样
静态代理
准备对象和接口
OrderService接口
是代理类和目标类实现的共同接口
public interface OrderService {
/**
* 生成订单
*/
void generate();
/**
* 查看订单详情
*/
void detail();
/**
* 修改订单
*/
void modify();
}
OrderServiceImpl
是目标类
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
try {
// 模拟生成订单的耗时
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成");
}
@Override
public void detail() {
try {
// 模拟查看订单的耗时
Thread.sleep(2541);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单信息如下:******");
}
@Override
public void modify() {
try {
// 模拟修改订单的耗时
Thread.sleep(1010);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改");
}
}
统计每个业务方法耗费时长
第一种方案: 直接修改Java源代码在每个业务方法中添加统计耗时的逻辑
- 缺点: 违背了OCP开闭原则另外代码没有得到复用
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
long begin = System.currentTimeMillis();
try {
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成");
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
@Override
public void detail() {
long begin = System.currentTimeMillis();
try {
Thread.sleep(2541);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单信息如下:******");
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
@Override
public void modify() {
long begin = System.currentTimeMillis();
try {
Thread.sleep(1010);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改");
long end = System.currentTimeMillis();
System.out.println("耗费时长"+(end - begin)+"毫秒");
}
}
第二种方案: 编写一个业务类的子类OrderServiceImpl
,在子类中重写父类的每个业务方法同时调用父类的业务方法(CGLIB动态代理实现的原理
)
- 缺点: 采用了
继承(满足is a的关系)
的方式会导致父类和子类的代码之间耦合度非常高,另外代码也没有得到复用
public class OrderServiceImplSub extends OrderServiceImpl{
@Override
public void generate() {
long begin = System.currentTimeMillis();
super.generate();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
@Override
public void detail() {
long begin = System.currentTimeMillis();
super.detail();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
@Override
public void modify() {
long begin = System.currentTimeMillis();
super.modify();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
}
第三种方案: 使用静态代理模式,编写一个代理类OrderServiceProxy
实现OrderService
接口,然后在代理对象中调用目标对象的目标方法
- 优点: 符合OCP开闭原则,同时采用的是
关联关系(满足has a的关系)
所以程序的耦合度较低 - 缺点: 每个目标对象都需要一个代理对象,随着目标对象越来越多就会导致类爆炸不好维护
// 代理类和目标类需要实现相同的接口,让客户端感觉使用代理对象就像在使用目标对象一样
public class OrderServiceProxy implements OrderService{ // 代理对象
// 关联目标对象即将目标对象作为代理对象的一个属性,使用OrderService公共接口接收目标对象才能解耦合(目标对象一定实现了这个接口)
private OrderService orderService;
// 通过构造方法将目标对象传递给代理对象
public OrderServiceProxy(OrderService orderService) {
this.orderService = orderService;
}
@Override
public void generate() {// 代理方法
// 对目标对象的方法进行增强
long begin = System.currentTimeMillis();
// 执行目标对象的目标方法
orderService.generate();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
@Override
public void detail() {// 代理方法
// 对目标对象的方法进行增强
long begin = System.currentTimeMillis();
// 执行目标对象的目标方法
orderService.detail();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
@Override
public void modify() {// 代理方法
// 对目标对象的方法进行增强
long begin = System.currentTimeMillis();
// 执行目标对象的目标方法
orderService.modify();
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
}
}
测试OrderServiceImpl
业务类中每个业务方法的耗时
public class Client {
public static void main(String[] args) {
// 创建目标对象
OrderService target = new OrderServiceImpl();
// 创建代理对象
OrderService proxy = new OrderServiceProxy(target);
// 调用代理对象的代理方法,底层是调用目标对象的目标方法
proxy.generate();
proxy.modify();
proxy.detail();
}
}
JDK动态代理
动态代理还是代理模式,只不过添加了字节码生成技术,可以在在程序运行时在内存中动态的生成字节码即代理类,有效减少代理类的数量同时解决代码复用的问题
在内存当中动态生成字节码
的技术常见的包括三种
代理技术 | 功能 |
---|---|
JDK动态代理技术 | 只能代理接口 |
CGLIB(Code Generation Library)动态代理技术 | 既可以代理接口又可以代理类,通过继承的方式实现代理(生成目标类的子类作为),性能比JDK动态代理要好 底层通过使用一个小而快的字节码处理框架ASM |
Javassist动态代理技术 | Javassist是一个开源的分析、编辑和创建Java字节码的类库 通过使用Javassist对字节码操作为JBoss应用服务器实现动态"AOP"框架 |
JDK动态代理原理
java.lang.reflect.Proxy
是JDK提供的一个动态代理类,通过这个类可以在内存中生成代理类的字节码
方法 | 功能 |
---|---|
newProxyInstance(类加载器,公共接口类型,调用处理器接口的实现类) | Proxy类提供的一个创建代理对象的静态方法 newProxyInstance方法执行结束后,首先在内存中动态生成 一个代理类的字节码文件(Xxx.class) 然后通过 类加载器 获取代理类的字节码对象,使用Class对象实例化一个代理对象并返回 该方法的返回值是Object类型,但由于 我们方法参数已经指定了代理对象实现的接口类型 ,所以可以放心的向下转型为对应的接口类型 |
invoke(代理对象,目标方法,目标方法调用时要传的参数) | 每次调用代理对象的任何方法最终都会先执行一次invoke方法(调用一次执行一次),调用代理对象的方法不同,invoke方法参数中的Method 也对象不同,最终调用对应的目标类的方法该方法的返回值由调用的目标对象的目标方法的执行结果决定,如果调用代理对象方法时需要接收返回值,那么就需要将目标对象的目标方法的执行结果返回 |
关于newProxyInstance()
方法的三个重要的参数的含义
类加载器
: 要想执行JDK在内存中生成的字节码文件需要先把它通过类加载器加载到内存当中,并且JDK要求目标类的类加载器必须和代理类的类加载器是同一个公共接口类型
: 告诉JDK动态代理生成的代理类要实现哪些接口,由于目标对象和代理对象实现的是同一个接口,所以可以直接通过目标对象获取接口类型调用处理器
: JDK动态代理规定的java.lang.reflect.InvocationHandler是一个回调接口,调用这个接口中方法的程序已经写好了(自动调用 ),我们只要编写增强代码
InvocationHandler
接口中invoke
方法上的三个参数及调用
Object proxy
: 设计这个参数只是为了后期需要在invoke方法中使用代理对象Method method
: 目标对象上的目标方法,即我们最终要执行的方法Object[] args
: 目标方法调用时需要传递的参数- 当你每调用一次代理对象的代理方法的时候,InvocationHandler接口的实现类中重写的invoke()方法就会被JDK调用一次,同时会携带方法需要的三个参数
- 因为InvocationHandler接口的实现类存储的是我们需要增强的代码,所以需要手写,这样并不会造成类爆炸,因为这种实现类只需要写一次就好,代码可以复用
JDK动态生成的代理对象和目标对象的唯一联系就是它们都实现了同一个接口,可以在创建代理对象的时候向下转型
代理类($Proxy0)
将我们在Proxy.newProxyInstance方法参数中的匿名内部类对象传递给了父类Proxy
JDK动态代理实现
第一步: 提供OrderService
接口及其实现类(目标类)OrderServiceImpl
,UserServiceProxy
代理类通过从在程序中动态生成
public interface OrderService {
/**
* 获取姓名
*/
Srting getName();
/**
* 生成订单
*/
void generate();
/**
* 查看订单详情
*/
void detail();
/**
* 修改订单
*/
void modify();
}
public class OrderServiceImpl implements OrderService {
@Override
public String getName() {
System.out.println("执行getName方法");
return "张三";
}
@Override
public void generate() {
try {
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成");
}
@Override
public void detail() {
try {
Thread.sleep(2541);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单信息如下:******");
}
@Override
public void modify() {
try {
Thread.sleep(1010);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改");
}
}
第二步: 编写调用处理器接口的实现类TimerInvocationHandler
同时实现接口中的invoke
方法用来调用目标对象的目标方法并对其功能增强
- 重写的方法名必须是invoke,因为invoke方法是JDK负责调用的,它已经把调用方法的程序写好了并且方法名就是invoke
- 给TimerInvocationHandler提供一个构造方法,通过这个构造方法接收目标对象,然后在invoke方法执行过程中使用传递的
method
参数来调用目标对象的目标方法
/*
专门负责计时的一个调用处理器对象,在这个调用处理器当中编写计时相关的增强代码,这个调用处理器只需要写一个就行了
*/
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 {
long begin = System.currentTimeMillis();
// 调用目标对象上的目标方法,方法四要素:哪个对象,哪个方法,传什么参数,返回什么值
Object retValue = method.invoke(target, args);
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
// 如果我们调用的目标对象的目标方法有返回值的话,invoke方法就必须将目标对象的目标方法执行结果继续返回
return retValue;
}
}
第三步: 测试调用代理对象的方法,底层JDK会调用invoke方法然后通过method
参数去执行目标对象的目标方法
public class Client {
public static void main(String[] args) {
// 第一步:创建目标对象
OrderService target = new OrderServiceImpl();
// 第二步:创建代理对象,由于我们方法参数已经指定了代理对象实现的接口类型,所以可以放心的向下转型为对应的接口类型
OrderService orderServiceProxy = (OrderService)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TimerInvocationHandler(target));
// 第三步:调用代理对象的代理方法,调用代理对象的代理方法的时候,如果你要做增强的话目标对象的目标方法得保证执行
orderServiceProxy.detail();
orderServiceProxy.modify();
orderServiceProxy.generate();
// 调用有返回值的代理对象的代理方法
String name = orderServiceProxy.getName();
System.out.println(name);
}
}
封装创建代理对象的工具类
在工具类ProxyUtil
封装方法,实现只要传递一个目标对象就可以通过这个方法获取代理对象
public class ProxyUtil {
public static Object newProxyInstance(Object target){
// 底层调用的还是JDK的动态代理
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TimerInvocationHandler(target));
}
}
// 进一步封装,使用匿名内部类的方式简化方法参数
public class ProxyUtil {
public static Object newProxyInstance(Object target){
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TimerInvocationHandler(target){
// 目标对象
private Object target;
public TimerInvocationHandler(Object target) {
// 赋值给成员变量。
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 这个接口的目的就是为了让你有地方写增强代码。
long begin = System.currentTimeMillis();
// 调用目标对象上的目标方法
// 方法四要素:哪个对象,哪个方法,传什么参数,返回什么值。
Object retValue = method.invoke(target, args);
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
//如果我们调用的代理对象的代理方法有返回值的话,invoke方法必须将目标对象的目标方法执行结果继续返回。
return retValue;
}
});
}
}
使用工具类封装的newProxyInstance
方法简化客户端代码
public class Client {
public static void main(String[] args) {
// 创建目标对象
OrderService target = new OrderServiceImpl();
// 使用工具类创建代理对象
OrderService orderServiceProxy = (OrderService) ProxyUtil.newProxyInstance(target);
// 调用代理对象的代理方法
orderServiceProxy.detail();
orderServiceProxy.modify();
orderServiceProxy.generate();
}
}
CGLIB动态代理原理
CGLIB动态代理既可以代理接口又可以代理类,底层通过继承的方式
实现对目标类的代理,所以被代理的目标类不能使用fianl
修饰
// CGLIB创建的代理对象的格式,可以根据这个名字推测框架底层是否使用了CGLIB动态代理
class UserService$$EnhancerByCGLIB$$82cb55e3 extends UserService{}
net.sf.cglib.proxy.MethodInterceptor
是CGLIB中提供的方法拦截器接口,和JDK动态代理的调用处理器接口一样,我们也需要提供该接口实现类并重写方法
CGLIB中的Enhancer
和JDK的Proxy
类都是用来创建代理对象的类
- 使用Proxy类是直接调用静态方法newProxyInstance()创建代理对象,在方法中设置目标对象相关参数和调用处理器接口实现类,直接返回代理对象
- 使用Enhancer类需要先创建字节码增强对象,然后通过Enhancer对象的不同方法设置目标类相关参数和方法拦截器接口的实现类,最后再调用方法创建代理对象
方法名 | 功能 |
---|---|
intercept(目标对象,目标方法,目标方法调用时的实参,调用目标方法时需要用到的MethodProxy类) | 负责调用目标类的目标方法以及添加增强的代码的方法 |
invokeSuper(目标对象,目标方法调用时的实参) | 最中调用目标对象中目标方法的方法 |
setSuperclass(Class clazz) | 设置代理类的父类即目标类 |
setCallback() | 设置回调为拦截器接口的实现类,等同于JDK动态代理当中的调用处理器接口的实现类 |
create() | 创建代理对象 先在内存中生成目标类的子类即代理类的字节码,然后创建代理对象 返回值默认是Object类型,但由于我们已经设置了代理类的父类,所以可以放心强转 |
CGLIB动态代理实现
第一步: 引入CGLIB
的依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
第二步: 准备一个目标类UserService
,这个类不需要实现任何接口
public class UserService {
public void login(String username, String password){
System.out.printn("系统正在验证身份...");
if ("admin".equals(username) && "123".equals(password)) {
return true;
}
return false;
}
public void logout(){
System.out.println("用户正在退出系统....");
}
}
第三步: 编写MethodInterceptor
接口的实现类TimerMethodInterceptor
并重写intercept()
方法负责调用目标类的目标方法以及添加增强代码
public class TimerMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 前面增强
long begin = System.currentTimeMillis();
// 调用目标对象的目标方法
Object retValue = methodProxy.invokeSuper(target, objects);
// 后面增强
long end = System.currentTimeMillis();
System.out.println("耗时"+(end - begin)+"毫秒");
return retValue;
}
}
第四步: 测试使用CGLIB在内存中为UserService
类生成的代理类
并创建代理对象,然后通过调用代理对象的方法去执行目标对象的目标方法
public class Client {
public static void main(String[] args) {
// 创建字节码增强器对象,这个对象是CGLIB库当中的核心对象,就是依靠它来生成代理类
Enhancer enhancer = new Enhancer();
// 告诉CGLIB生成的代理类的父类是谁即目标类是谁
enhancer.setSuperclass(UserService.class);
// 设置回调为方法拦截器接口MethodInterceptor的实现类,等同于JDK动态代理当中的调用处理器接口的实现类
enhancer.setCallback(new TimerMethodInterceptor());
// 创建代理对象,代理类的父类是UserService
UserService userServiceProxy = (UserService) enhancer.create();
// 查看CGLIB动态代理生成的代理对象
System.out.println(userServiceProxy);
// 调用代理对象的代理方法
boolean success = userServiceProxy.login("admin", "123");
System.out.println(success ? "登录成功" : "登录失败");
userServiceProxy.logout();
}
}
JDK高版本问题
如果高版本的JDK想要使用CGLIB
就需要在启动项中添加两个启动参数
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/sun.net.util=ALL-UNNAMED
动态代理与静态代理比较
优缺点
动态代理与静态代理相比较的优点是在接口方法数量比较多的时候可以进行灵活处理
-
接口中声明的所有方法都被转移到
调用处理器InvocationHandler
的一个集中方法invoke
中处理,而不需要像静态代理那样在每一个方法进行中转 -
不管你有多少个Service接口,由于我们的代理对象是在程序运行中动态生成的 , 所以可以实现任何接口代码不会写死