什么是代理模式
代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能.
这里使用到编程中的一个思想:对扩展开放,对修改关闭。也就是不要随意去修改已经写好的代码或者方法,如果需改修改,可以通过代理的方式来扩展该方法
举个例子来说明代理的作用:假设我们想邀请一位明星,那么并不是直接连接明星,而是联系明星的经纪人,来达到同样的目的.明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决.这就是代理思想在现实中的一个例子
代理分几种
代理分为静态代理和动态代理,静态代理是在编译时就将接口、实现类、代理类一股脑儿全部手动完成,但如果我们需要很多的代理,每一个都这么手动的去创建实属浪费时间,而且会有大量的重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例,来完成具体的功能,主要用的是JAVA的反射机制。
小结
- 静态代理
- 动态代理
为什么要用代理模式(代理模式优点)?
- 解耦:让某些方法之关心自己的功能。其他一些非核心功能可以通过代理来实现,比如日志的记录,鉴权操作等等
- 权限控制:用户并不是直接访问目标对象,而是通过代理。这时候代理就相当于一个门卫,可以控制对象的访问。
代理分类,按功能分
- 远程代理,也就是为一个对象在不同的地址空间提供局部代表,这样可以隐藏一个对象存在于不同地址空间的事实。比如说 WebService,当我们在应用程序的项目中加入一个 Web 引用,引用一个 WebService,此时会在项目中声称一个 WebReference 的文件夹和一些文件,这个就是起代理作用的,这样可以让那个客户端程序调用代理解决远程访问的问题;
- 虚拟代理,是根据需要创建开销很大的对象,通过它来存放实例化需要很长时间的真实对象。这样就可以达到性能的最优化,比如打开一个网页,这个网页里面包含了大量的文字和图片,但我们可以很快看到文字,但是图片却是一张一张地下载后才能看到,那些未打开的图片框,就是通过虚拟代里来替换了真实的图片,此时代理存储了真实图片的路径和尺寸;
- 安全代理,用来控制真实对象访问时的权限。一般用于对象应该有不同的访问权限的时候;
- 指针引用,是指当调用真实的对象时,代理处理另外一些事。比如计算真实对象的引用次数,这样当该对象没有引用时,可以自动释放它,或当第一次引用一个持久对象时,将它装入内存,或是在访问一个实际对象前,检查是否已经释放它,以确保其他对象不能改变它。这些都是通过代理在访问一个对象时附加一些内务处理;
- 延迟加载,用代理模式实现延迟加载的一个经典应用就在 Hibernate 框架里面。当 Hibernate 加载实体 bean 时,并不会一次性将数据库所有的数据都装载。默认情况下,它会采取延迟加载的机制,以提高系统的性能。Hibernate 中的延迟加载主要分为属性的延迟加载和关联表的延时加载两类。实现原理是使用代理拦截原有的 getter 方法,在真正使用对象数据时才去数据库或者其他第三方组件加载实际的数据,从而提升系统性能。
静态代理
所谓静态也就是在程序运行前就已经存在代理类>的字节码文件,代理类和委托类的关系在运行前就确定了。Java编译完成后代理类是一个实际的 class 文件。
代码
代码结构
代理中的三种角色
- 抽象角色Subject:IWorker
- 代理角色Proxy:StaticProxy
- 真实角色RealSubject:Worker
IWorker.java,Worker.java
public interface IWorker {
void doSomething();
}
public class Worker implements IWorker {
@Override
public void doSomething() {
System.out.println("doSomething");
}
}
StaticProxy.java
public class StaticProxy implements IWorker {
private Worker worker;
// 参数类型必须是Worker
public StaticProxy(Worker worker) {
this.worker = worker;
}
@Override
public void doSomething() {
System.out.println("开始前搞点事");
worker.doSomething();
System.out.println("结束后搞点事");
}
}
Main.java
public class Main {
public static void main(String[] args) {
Worker worker = new Worker();
StaticProxy staticProxy = new StaticProxy(worker);
staticProxy.doSomething();
}
}
运行结果
有了静态代理,为什么还需要动态代理?
静态代理类优缺点
优点:业务类只需要关注业务逻辑本身,保证了业务类的重用性。这是代理的共有优点。
缺点:
1)代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了。
2)如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。
另外,如果要按照上述的方法使用代理模式,那么真实角色(委托类)必须是事先已经存在的,并将其作为代理对象的内部属性。但是实际使用时,一个真实角色必须对应一个代理角色,如果大量使用会导致类的急剧膨胀;此外,如果事先并不知道真实角色(委托类),该如何使用代理呢?这个问题可以通过Java的动态代理类来解决。
动态代理
动态代理利用了JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。
静态代理与动态代理的区别主要在:
- 静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件
- 动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中
- 静态代理必须实现和被代理类一样的接口,动态代理对象不需要实现接口
实现方式
- 使用JDK实现动态代理
- 使用CGLIB实现动态代理
1、JDK动态代理代码
代码结构
代理中的三种角色
- 抽象角色Subject:IWorker
- 代理角色Proxy:DynamicProxy
- 真实角色RealSubject:Worker
IWorker.java,Worker.java
public interface IWorker {
void doSomething();
}
public class Worker implements IWorker {
@Override
public void doSomething() {
System.out.println("doSomething");
}
}
DynamicProxy.java
public class DynamicProxy {
private Object worker;
// 参数类型为Object
public DynamicProxy(Object worker) {
this.worker = worker;
}
//给目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(
worker.getClass().getClassLoader(),
worker.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始前搞点事");
Object value = method.invoke(worker, args);
System.out.println("结束后搞点事");
return value;
}
}
);
}
}
Main.java
public class Main {
public static void main(String[] args) {
Worker worker = new Worker();
IWorker proxy = (IWorker) new DynamicProxy(worker).getProxyInstance();
proxy.doSomething();
}
}
运行结果
参数解释
Proxy.newProxyInstance
代理类所在包:java.lang.reflect.Proxy
JDK实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )
注意该方法是在Proxy类中是静态方法,且接收的三个参数依次为:
- ClassLoader loader,:指定当前目标对象使用类加载器,获取加载器的方法是固定的
- Class<?>[] interfaces,:目标对象实现的接口的类型,使用泛型方式确认类型
- InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入
InvocationHandler(Interface)
InvocationHandler是负责连接代理类和委托类的中间类必须实现的接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。
InvocationHandler 的核心方法
Object invoke(Object proxy, Method method, Object[] args)
- proxy 代理对象
- method 代理对象调用的方法
- args 调用的方法中的参数
该方法也是InvocationHandler接口所定义的唯一的一个方法,该方法负责集中处理动态代理类上的所有方法的调用。调用处理器根据这三个参数进行预处理或分派到委托类实例上执行。
2、CGLib代理
上面的静态代理和动态代理模式都是要求目标对象实现一个接口或者多个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用构建目标对象子类的方式实现代理,这种方法就叫做:CGLib代理
一句话总结:使用动态代理的对象必须实现一个或多个接口,使用CGLib代理的对象则无需实现接口,达到代理类无侵入。
Tips:CGLib不属于JDK源码包中,需额外引入jar包。但是目前spring中已默认引了CGLib包,所以无需再次引入。
如果需要手动导入请看:https://mvnrepository.com/artifact/cglib/cglib
CGLib特点
- CGLib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截)
- CGLib包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的子类.
- 代理的类不能为final,否则报错;目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.
代码
代码结构
代理中的三种角色
- 抽象角色Subject:没有
- 代理角色Proxy:CGLibProxy
- 真实角色RealSubject:Worker
Worker.java
// 不需要实现接口
public class Worker {
public void doSomething() {
System.out.println("doSomething");
}
}
CGLibProxy.java
public class CGLibProxy implements MethodInterceptor {
// 维护目标对象
private Object target;
public CGLibProxy(Object target) {
this.target = target;
}
// 给目标对象创建一个代理对象
public Object getProxyInstance() {
// 1.工具类
Enhancer enhancer = new Enhancer();
// 2.设置父类
enhancer.setSuperclass(target.getClass());
// 3.设置回调函数
enhancer.setCallback(this);
//4.创建子类(代理对象)
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("开始前搞点事");
// 执行目标对象的方法
Object value = method.invoke(target, objects);
System.out.println("结束后搞点事");
return value;
}
}
Main.java
public class Main {
public static void main(String[] args) {
// 目标对象
Worker worker = new Worker();
// 代理对象
Worker proxy = (Worker) new CGLibProxy(worker).getProxyInstance();
// 执行代理对象方法
proxy.doSomething();
}
}
运行结果
CGLib问题
优点
实用 CGLib 动态代理的优势很明显,有了它,我们就可以为没有接口的类包装前置和后置方法了。从这点来说,它比无论是 JDK 动态代理还是静态代理都灵活的多。
缺点
既然它比 JDK 动态代理还要灵活,那么我为什么还要在前面花那么多篇幅去介绍 JDK 动态代理呢?这就不得不提它的一个很大的缺点了。
我们想想,JDK 动态代理 和它在调用阶段有什么不同?对,少了接口信息。那么JDK动态代理为什么需要接口信息呢?就是因为要根据接口信息来拦截特定的方法,而CGLib动态代理并没接收接口信息,那么它又是如何拦截指定的方法呢?答案是没有做拦截。。。它拦截了被代理的所有方法(各位读者可以自己试试)
总结
通过上述介绍我们可以看到,代理是一种非常有意思的模式。本文具体介绍了三种代理实现方式,静态代理、JDK动态代理 以及 CGLib动态代理。
这三种代理方式各有优劣,它们的优点在于:
- 我们通过在原有的调用逻辑过程中,再抽一个代理类的方式,使调用逻辑的变化尽可能的封装再代理类的内部中,达到不去改动原有被代理类的方法的情况下,增加新的动作的效果。
- 这就使得即便在未来的使用场景中有更多的拓展,改变也依然很难波及被代理类,我们也就可以放心的对被代理类的特定方法进行复用了
从缺点来看:
- 静态代理和JDK动态代理都需要被代理类的接口信息以确定特定的方法进行拦截和包装。
- CGLib动态代理虽然不需要接口信息,但是它拦截并包装被代理类的所有方法。
代理技术在实际项目中有非常多的应用,比如Spring 的AOP技术。
参考
https://design-patterns.readthedocs.io/zh_CN/latest/structural_patterns/proxy.html
https://blog.csdn.net/lhl1124281072/article/details/79780494
https://www.zybuluo.com/pastqing/note/174679