类型信息动态代理
代理模式
代理模式的英文叫做Proxy或Surrogate,中文都可译为“代理”,所谓代理,就是一个人或者一个机构代表另一个人或者另一个机构采取行动。在一些情况下,一个客户不想或者不能够直接引用一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用 。代理就是为其他对象提供一个代理以控制对某个对象的访问。比如火车票代售点就是一个代理,它控制要买火车票的人(其他对象)对火车站售票点(某个对象)的访问,由它向火车票售票点买票。
代理模式简要图示如下:
代理模式的优点
- 优点一:可以隐藏真实目标类的实现;
- 优点二:可以实现客户与真实目标类间的解耦,在不修改真实目标类代码的情况下能够做一些额外的处理。
代理模式:代理类和被代理类实现共同的接口(或继承),代理类中存有指向被代理类的索引,实际执行时通过调用代理类的方法、实际执行的是被代理类的方法。
如上图所示,Client类就是客户端,Proxy类就是代理类,RealSubject类是真实目标类,Proxy和RealSubject类为客户提供的服务能力都体现在DoAction()方法中,Proxy的DoAction()实际上是在调用RealSubject的DoAction方法,当然Proxy类的DoAction()在调用RealSubject的DoAction方法前后也可以有一些自定义的操作,比如在打印日志等,这样一个流程就是体现了简单的代理思想。
代理又可以分为静态代理和动态代理。
- 静态代理:由程序员创建或特定工具自动生成源代码,再对其编译。在程序运行前,代理类的.class文件就已经存在了。
- 动态代理:在程序运行时,运用反射机制动态创建而成,动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。
举一个实际的例子,考虑一个字体提供功能,字体库可能源自本地磁盘、网络或者系统。 先考虑从本地磁盘中获取字体,和上面的例子一样,采用代理的方式实现,定义一个提供字体的接口FontProvider:
public interface FontProvider {
Font getFont(String name);
}
真正提供获取磁盘字体库的类:
class FontProviderFromDisk implements FontProvider {
@Override
Font getFont(String name){
System.out.println("磁盘上的字体库");
return null;
}
}
代理类ProxyForFont:
class ProxyForFont implements FontProvider {
private FontProvider fontProvider;
ProxyForFont(FontProvider fontProvider) {
this.fontProvider=fontProvider;
}
@Override
Font getFont(String name) {
System.out.println("调用代理方法前可以做点事情");
Font font = fontProvider.getFont(name);
System.out.println("调用代理方法后可以再做点事情");
return font;
}
}
当我们需要从磁盘获取字体库时,直接调用ProxyForFont就可以了:
public class MyFontProvider {
public static void main(String[] args) {
FontProvider fp = new ProxyForFont(new FontProviderFromDisk());
fp.getFont("字体库名");
}
}
这样实现的好处在哪儿呢?比如,每次从磁盘获取字体库的时候,磁盘的I/O比较耗时,想通过缓存将读到的字体库暂存一份。此时,我们直接修改ProxyForFont类而不用去修改真正的目标类FontProviderFromDisk:
class ProxyForFont implements FontProvider {
private FontProvider fontProvider;
ProxyForFont(FontProvider fontProvider) {
this.fontProvider = fontProvider;
}
@Override
Font getFont(String name){
System.out.println("检查磁盘缓存中是否存在字体库");
if (exist) {
Font font = fontProvider.getFont(name);
System.out.println("将磁盘读到的字体库保存到缓存");
return font;
} else {
System.out.println("如果存在直接从缓存中获取");
return null;
}
}
}
直接修改FontProviderFromDisk类的getFont方法,也能达到相同的效果,但是这样会有一个问题,上文中我们提到,字体库的获取源除了磁盘还有系统和网络等,所以还存在FontProviderFromSystem和FontProviderFromNet两个类,如果这两个类也需要缓存功能的时候,还得再继续动这两个类的getFont实现,而如果使用了代理模式,不仅能实现客户与目标的解耦,还可以在不修改真实目标类代码的情况下能够做一些额外的处理,即只需要在代理类ProxyForFont中修改即可。
从静态代理到动态代理
以上都是静态代理的实现方式,是不是感觉静态代理已经无所不能了呢?我们再来看一个需求。
以上都是获取字体库,如果想获取图片、音乐等其他资源呢?这个时候一个FontProvider接口就不够用了,还得提供ImageProvider和MusicProvider接口,实现对应的两个功能类以及两个代理类ProxyForImage和ProxyForMusic。当要给获取图片和获取音乐等都加上缓存功能的时候,两个代理类ProxyForImage和ProxyForMusic都需要改动,而缓存的逻辑三个类又是相同的,如此写代码就会出现重复和代理类爆炸。当你要代理的方法越多时,你需要重复的逻辑就越多,假设你的目标类有100个方法,那么你的代理类就需要对这100个方法进行委托,但是又可能他们前后需要执行的逻辑时一样的,这样就会产生很多冗余。 这样,就有个更好的动态代理的方法出现了。
动态代理也分为两类:基于接口的代理和基于继承的代理;两类实现的代表分别是:JDK代理与CGlib代理。
JDK代理
JDK动态代理主要涉及java.lang.reflect包下的Proxy类和InvocationHandler接口。 JDK代理实现的三个要点:
- 通过java.lang.reflect.Proxy类来动态生成代理类;
- 代理类要实现InvocationHandler接口;
- JDK代理只能基于接口进行动态代理;
每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。我们来看看InvocationHandler这个接口的唯一一个方法 invoke 方法:
/**
* proxy: 指代我们所代理的那个真实对象
* method: 指代的是我们所要调用真实对象的某个方法的Method对象
* args: 指代的是调用真实对象某个方法时使用的参数
*/
Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
Proxy这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法,这个方法的作用就是得到一个动态的代理对象,其接收三个参数,我们来看看这三个参数所代表的含义。
/**
* loader:
* 一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象
* 进行加载
* interfaces:
* 一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么
* 接口,如果我提供了一组接口给它,这样我就能调用这组接口中的方法了
* h:
* 一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的
* 时候,会关联到哪一个InvocationHandler对象上
*
* /
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException;
JDK动态代理实例。
//subject接口,这个是jdk动态代理必须的前提。
public interface Subject {
void request();
void hello();
}
定义业务类,实现该接口
//目标对象RealSubject
public class RealSubject implements Subject {
@Override
public void request() {
System.out.println("real subject execute request");
}
@Override
public void hello() {
System.out.println("hello");
}
}
定义代理类
//代理类JdkProxySubject
public class JdkProxySubject implements InvocationHandler {
//这个就是我们要代理的真实对象
private Object subject;
//构造方法,给我们要代理的真实对象赋初值
public JdkProxySubject(Object subject) {
this.subject = subject;
}
/*
*invoke方法方法参数解析
*Object proxy:指被代理的对象。
*Method method:要调用的方法
*Object[] args:方法调用时所需要的参数
*InvocationHandler接口的子类可以看成代理的最终操作类。
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before");
Object result = null;
try {
//利用反射动态的来反射方法,这就是动态代理和静态代理的区别
result = method.invoke(subject,args);
} catch (Exception e) {
System.out.println("ex:"+e.getMessage());
throw e;
} finally {
System.out.println("after");
}
return result;
}
}
定义客户端类,执行业务逻辑时使用代理类
//客户端Client
public class Client {
/*
*newProxyInstance方法参数解析
*ClassLoader loader:类加载器
*Class<?>[] interfaces:得到全部的接口
*InvocationHandler h:得到InvocationHandler接口的子类实例
*/
public static void main(String[] args) {
//我们要代理的真实对象
Subject realSubject = new RealSubject();
//我们要代理哪个真实对象,就将该对象传进去
InvocationHandler handler = new JdkProxySubject(realSubject);
Subject subject = (Subject) Proxy.newProxyInstance(Client.class.getClassLoader(),new Class[]{Subject.class}, handler);
subject.hello();
subject.request();
}
}
输出:
before
hello
after
before
real subject execute request
after
因为利用JdkProxySubject生成的代理类实现了接口,所以目标类中所有的方法在代理类中都有。现在,如果我们在subject接口中新增加了一个goodBye()方法,然后再RealSubject中对goodBye()方法进行实现,但是在代理类中,我们不需要再去为goodBye()方法再去写一个代理方法,而是通过反射调用目标对象的方法,来动态的生成代理类。
代理的本质其实就是一种对行为的监听,对代理对象($proxy InvocationHandler)的一种监听行为。
CGlib代理模式
CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,为一个类创建子类,并在子类中采用方法拦截的技术拦截所有对父类方法的调用,并顺势加入横切逻辑。CGlib是针对类来实现代理的,原理是对指定的业务类生成一个子类,并覆盖其中业务方法实现代理,因为采用的是继承,所以不能对final修饰的类进行代理。CGlib和JDK的原理类似,也是通过方法去反射调用目标对象的方法。
//目标对象RealSubject,cglib不需要为目标类定义接口,当然目标类实现了接口也不影响
public class RealSubject {
public void request() {
System.out.println("real subject execute request");
}
public void hello() {
System.out.println("hello");
}
}
//实现MethodInterceptor方法代理接口,创建代理类
public class DemoCglibProxy implements MethodInterceptor {
private Object target;//业务类对象,供代理方法中进行真正的业务方法调用
//相当于JDK动态代理中的绑定
public Object getInstance(Object target) {
//给业务对象赋值
this.target = target;
//创建加强器,用来创建动态代理类
Enhancer enhancer = new Enhancer();
//为加强器指定要代理的业务类(即:为下面生成的代理类指定父类)
enhancer.setSuperclass(this.target.getClass());
//设置回调:对于代理类上所有方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦
enhancer.setCallback(this);
// 创建动态代理类对象并返回
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("before in cglib");
Object result = null;
try{
result = proxy.invokeSuper(obj, args);
}catch (Exception e){
System.out.println("get ex:"+e.getMessage());
throw e;
}finally {
System.out.println("after in cglib");
}
return result;
}
}
创建业务类和代理类对象,然后通过代理类对象的getInstance(业务类对象) 返回一个动态代理类对象(它是业务类的子类,可以用业务类引用指向它),最后通过动态代理类对象进行方法调用。
//客户端
public class Client {
public static void main(String[] args) {
// 此刻,realSubject不是单纯的目标类,而是增强过的目标类
RealSubject realSubject = ((RealSubject))new DemoCglibProxy().getInstance(new RealSubject());
realSubject.hello();
realSubject.request()
}
}
输出:
before in cglib
hello
after in cglib
before in cglib
real subject execute request
after in cglib
Cglib是无需通过接口来实现,它是通过实现子类的方式来完成调用的。Enhancer对象把代理对象设置为被代理类的子类来实现动态代理的。因为是采用继承方式,所以代理类不能加final修饰,否则会报错。被final修饰的类不能被继承,内部的方法和变量都变成final类型。
Spring如何选择用JDK还是CGLib
- 当Bean实现接口时,Spring就会用JDK的动态代理;
- 当Bean没有实现接口时,Spring使用CGlib的代理实现;
- 可以通过修改配置文件强制使用CGlib;
CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类和方法进行代理,因为CGLib原理是动态生成被代理类的子类。代理模式也是我们必须要理解的一种模式,因为学习好代理模式有助于我们去读一些源码,排查一些更深层次的问题,或者面对一些业务场景问题,也能有一个很大的提升,设计模式本身也就是为了解决问题而创建出来的。
- 点赞
- 收藏
- 关注作者
评论(0)