代理模式

代理模式

代理模式是很常见的一种设计模式,代理一词拆开来看就是代为受理,那显然是要涉及到请求被代理的委托方,提供代理的代理方,以及想要通过代理来实际联系委托方的客户三个角色。举个生活中常见的例子,房东都是通过中介来处置自己的房屋,并不与租客直接接触,这种场景下,房东本身是委托方,中介是代理方,房东把自己的房屋委托给中介进行房屋出租,这样当租客想要租房的时候,只能通过中介来咨询办理。这样房东自身不用暴露身份,不用和租客费时沟通,带领租客看房等一系列问题,这样都转由中介来解决。当然,中介也可以给多个房东提供服务,这样租客只接触一个中介,就可以看到不同的房源,最终找到符合心意的房间。

通过上面的例子,代理模式有以下两个优点:

  1. 可以隐藏委托类的实现;
  2. 可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。

字节码处理

Java 程序员都应该知道,Java 通过 Javac 编译器将.java 源文件编译成能被 JVM 虚拟机识别的.class 字节码文件, 当 .class字节码文件通过 JVM 转为机器可以执行的二进制机器码时,JVM 类加载器首先加载字节码文件,然后通过解释器逐行进行解释执行 ,生成对应的 Class 对象,进而使 Class 对象创建类的具体实例来进行调用实现具体的功能。

上图说明了 Java 加载字节码的流程,但是 Java 的强大在于不仅仅可以加载在编译器生成好的字节码,还可以在运行期系统中,遵循 Java 编译系统组织.class 文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类,这样就完成了在代码动态创建一个类的能力,如下图流程。

关于动态生成类的技术目前有两种,一种是自己动手,从零开始创建字节码,理论上可行,实际上很难;第二种是,使用已有的一些能操作字节码的库,帮助我们创建 class。目前,能够操作字节码的常用的工具/库有:

Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库, 接下来我们使用 Javasisst 工具在运行时动态创建字节码并加载类,如下代码:

package com.msdn.classLoad;

public class JavasisstLearn {

    /**
     * 动态生成一个新类
     * @return
     */
    public static Class<?> createNewClass(){
        try {
            //获取ClassPool
            ClassPool pool = ClassPool.getDefault();
            //创建User类
            CtClass ctClass = pool.makeClass("com.msdn.bean.User");
            //创建User类成员变量name
            CtField name = new CtField(pool.get("java.lang.String"),"name",ctClass);
            //设置name为私有
            name.setModifiers(Modifier.PRIVATE);
            //将name写入class,并初始化为空
            ctClass.addField(name,CtField.Initializer.constant(""));
            //增加set方法,名字为“setName”
            ctClass.addMethod(CtNewMethod.setter("setName",name));
            //增加get方法,名称为“getName”
            ctClass.addMethod(CtNewMethod.getter("getName",name));
            //添加无参构造函数
            CtConstructor constructor = new CtConstructor(new CtClass[] {},ctClass);
            constructor.setBody("{name = \"hresh\";}");//相当于public Sclass(){this.name = "hresh";}
            ctClass.addConstructor(constructor);
            //添加有参构造函数
            constructor = new CtConstructor(new CtClass[]{pool.get("java.lang.String")},ctClass);
            constructor.setBody("{$0.name = $1;}");//第一个传入的形参$1,第二个传入的形参$2,相当于public Sclass(String s){this.name = s;}
            ctClass.addConstructor(constructor);
            System.out.println(ctClass.getDeclaredField("name"));

            //反射调用新创建的类
            Class<?> uClass = ctClass.toClass();
            Object user = uClass.newInstance();
            Method getter = null;
            getter = user.getClass().getMethod("getName");
            System.out.println(getter.invoke(user));
        }catch (Exception ex){
            ex.printStackTrace();
        }

        return null;
    }

    public static void main(String[] args) {
        createNewClass();
    }
}
复制代码

这里只是介绍了 Javasisst 工具的部分功能,感兴趣的朋友可以参考这两篇文章: Java动态编程初探——JavassistJavassist中文技术文档

在 Java 学习的过程中,我们多是默认使用静态加载字节码,对于动态加载字节码没有过多接触,但是在某些技术场景下显得尤为必要。关于动态生成类的场景的介绍大家可以参考 Java动态生成类的场景的考察

介绍静态和动态加载字节码的两种方式,是为了引出下面关于两种代理方式的介绍,代理机制通过代理类创建时间的不同分为了静态代理和动态代理:

  • 静态代理 :代理类在编译阶段生成,程序运行前就已经存在,那么这种代理方式被称为静态代理,这种情况下的代理类通常都是我们在 Java 代码中定义的。
  • 动态代理 :代理类在程序运行时创建,也就是说,这种情况下,代理类并不是在 Java 代码中定义的,而是在运行时根据我们在 Java 代码中的“提示”动态生成的。

目前,静态代理主要有 AspectJ 静态代理、JDK 静态代理技术 ,而动态 代理有 JDK 动态代理、Cglib 动态代理技术,而 Spring AOP 是整合使用了 JDK 动态代理和 Cglib 动态代理两种技术。

关于 Java 编程的动态性有一系列的文章,有兴趣的朋友可以去阅读一番: Java动态化

静态代理

AspectJ 静态代理

关于 AspectJ 在 IDEA 中的配置,以及简单的案例学习,可以参看 AspectJ入门及在IDEA中的配置 一文。

将租房抽象为一个 Rent 接口,如下:

public interface Rent {
    public void rent();
}
复制代码

房东实现了 Rent 接口:

public class Host implements Rent {
    public void rent() {
        System.out.println("房屋出租");
    }

}
复制代码

用 AspectJ 语法实现一个代理 IntermediaryAspectJ:

public aspect IntermediaryAspectJ {
    //定义切点
    pointcut rentPointCut():call(void com.msdn.bean.Host.rent());


    /**
     * 定义前置通知
     * befor(参数):连接点函数{
     *     函数体
     * }
     */
    before():rentPointCut(){
        seeHouse();
    }

    /**
     * 定义后置通知
     * after(参数):连接点函数{
     *     函数体
     * }
     */
    after():rentPointCut(){
        fare();
    }

    private void seeHouse(){
        System.out.println("带租客看房");
    }

    private void fare(){
        System.out.println("收中介费");
    }
}
复制代码

测试代码如下:

public class AspectJTest {

    public static void main(String[] args) {
        Host host = new Host();
        host.rent();
    }
}
复制代码

执行结果为:

带租客看房
房屋出租
收中介费
复制代码

可以看到 Host 的 rent()方法前后输出了我们在 IntermediaryAspectJ 中定义的前置和后置通知,该类充当代理的功能。具体的 AspectJ 语法我们不深究,只需要知道 pointcut 是定义要代理的切入点,这里是定义了一个 pointcut,代理 Host 类中的 rent()方法。而 before()和 after()分别可以定义具体在切入点前后需要的额外操作。

总结一下,AspctJ 就是用特定的编译器和语法,对类实现编译期增强,实现静态代理技术,下面我们看 JDK 静态代理。

JDK静态代理

JDK 静态代理更多的是一种设计模式,JDK 静态代理的代理类和委托类会实现同一接口或是派生自相同的父类,代理类对客户对象是可见的,其结果图如下:

继续套用上面租房的例子,改写代码实现一个 JDK 静态代理模式。

中介也实现 Rent 接口,持有一个房东对象来提供租房服务,包括带领租客看房,解答相关疑问等:

public class Intermediary implements Rent {
    private Host host;

    public Intermediary() {
    }

    public Intermediary(Host host) {
        this.host = host;
    }

    @Override
    public void rent() {
        seeHouse();
        host.rent();
        fare();
    }

    public void seeHouse(){
        System.out.println("带租客看房");
    }

    public void fare(){
        System.out.println("收中介费");
    }
}
复制代码

通过中介来替房东完成租房行为:

public class JdkTest {

    public static void main(String[] args) {
        Host host = new Host();
        Intermediary proxy = new Intermediary(host);
        proxy.rent();
    }
}
复制代码

执行结果同上。

以上就是一个典型的静态代理的实例,很简单但是也能说明问题,我们可以分析一下静态代理的优缺点:

优点:静态代理在编译时产生 class 文件,运行时无需产生,可直接使用,效率好。业务类可以只关注自身逻辑,可以重用,通过代理类来增加通用的逻辑处理;

缺点:静态代理要为每个目标类创建一个代理类,当需要代理的对象太多,那么代理类也变得很多。同时代理类违背了可重复代理只写一次的原则。 另外当接口中增加方法时,所有实现类都需要实现该方法,增加代码维护的复杂度。

关于静态代理存在的缺陷,可以通过动态代理来解决。

动态代理

动态代理中,代理类并不是在 Java 代码中实现,而是在运行时期生成,相比静态代理,动态代理可以很方便的对委托类的方法进行统一处理,如添加方法调用次数、添加日志功能等等,动态代理分为 JDK动态代理和 cglib 动态代理。

JDK动态代理

JDK 提供了动态代理,其原理图如下:

还以中介和房东的模型为例,将租房抽象为一个接口:

public interface Rent {
    public void rent();
}
复制代码

房东实现了 Rent 接口:

public class Host implements Rent {
    public void rent() {
        System.out.println("房屋出租");
    }

}
复制代码

实现一个代理类的请求处理器,处理对具体类的所有方法的调用:

public class InvocationHandlerImpl implements InvocationHandler {
    Rent rent;

    public InvocationHandlerImpl(Rent rent) {
        this.rent = rent;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        seeHouse();
        //租房
        Object object = method.invoke(rent,objects);
        fare();
        return object;
    }

    public void seeHouse(){
        System.out.println("带租客看房");
    }

    public void fare(){
        System.out.println("收中介费");
    }
}
复制代码

通过 JDK 动态代理机制实现一个动态代理:

public class JdkProxy {

    public static void main(String[] args) {
        //创建被代理的具体类
        Host host = new Host();
        //获取对应的ClassLoader
        ClassLoader classLoader = host.getClass().getClassLoader();
        //获取被代理对象实现的所有接口
        Class[] interfaces = host.getClass().getInterfaces();
        //设置请求处理器,处理所有方法调用
        InvocationHandler invocationHandler = new InvocationHandlerImpl(host);

        /**
         * 5.根据上面提供的信息,创建代理对象 在这个过程中,
         *   a.JDK会通过根据传入的参数信息动态地在内存中创建和.class文件等同的字节码
         *   b.然后根据相应的字节码转换成对应的class,
         *   c.然后调用newInstance()创建实例
         */
        Object o = Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
        Rent rent = (Rent) o;
        rent.rent();
    }
}
复制代码

我们从代理的创建入手,看看 JDK 的动态代理都做了什么:

Proxy.newProxyInstance(classLoader,interfaces,this);
复制代码
  1. Proxy.newProxyInstance()获取 Host 类的所有接口列表(第二个参数:interfaces);
  2. 确定要生成的代理类的类名,默认为:com.sun.proxy.$ProxyXXXX;
  3. 根据需要实现的接口信息,在代码中动态创建该 Proxy 类的字节码;
  4. 将对应的字节码转换为对应的 class 对象;
  5. 创建 InvocationHandler 实例 handler,用来处理 Proxy 所有方法调用;
  6. Proxy 的 class 对象以创建的 handler 对象为参数(第三个参数: invocationHandler ),实例化一个 Proxy 对象。

而对于 InvocationHandler,我们需要重写其 invoke 方法:

public Object invoke(Object proxy, Method method, Object[] args) 
复制代码

在调用代理对象中的每一个方法时,在代码内部,都是直接调用了 InvocationHandler 的 invoke 方法,而 invoke 方法根据代理类传递给自己的 method 参数来区分是什么方法。

可以看出, Proxy.newProxyInstance() 方法生成的对象也是实现了 Rent 接口的,所以可以在代码中将其强制转换为 Rent 来使用,和静态代理达到了同样的效果。我们可以用下面代码把生成的代理类的字节码保存到磁盘里,然后反编译看看 JDK 生成的动态代理类的结构。

public class ProxyUtils {

    public static void main(String[] args) {
        Host host = new Host();
        genereateClassFile(host.getClass(),"HostProxy");
    }

    public static void genereateClassFile(Class clazz,String proxyName){
        //根据类信息和提供的代理类名称,生成字节码
        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName,clazz.getInterfaces());
        String paths = clazz.getResource(".").getPath();
        System.out.println(paths);
        FileOutputStream out = null;

        try {
            //保存到硬盘中
            out = new FileOutputStream(paths + proxyName + ".class");
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}
复制代码

编译后的 HostProrxy.class 文件结果如下:

//动态代理类HostProrxy实现了Rent接口
public final class HostProxy extends Proxy implements Rent {
    //加载接口中定义的所有方法
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    //构造函数接入InvocationHandler,也就是持有了InvocationHandler对象
    public HostProxy(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    //自动生成的rent方法,实际调用InvocationHandler对象的invoke方法,传入m3参数对象代表rent()方法
    public final void rent() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    //加载接口中定义的所有方法
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.msdn.bean.Rent").getMethod("rent");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}
复制代码

通过查看编译后的代码可以看出,JDK 生成的动态代理类实现和具体类相同的接口,并持有 InvocationHandler 对象(InvocationHandler对象又持有具体类),调用动态代理类中的方法,会触发传入 InvocationHandler 的 invoke()方法 ,通过 method 参数,来区分调用的是什么具体的方法。

注意:JDK 动态代理会根据被代理对象生成一个继承了 Proxy 类,并实现了该业务接口的 JDK 代理类,该类的字节码会被传进去的 ClassLoader 加载,创建了 JDK 代理对象实例。

JDK 代理对象实例创建时,业务代理对象实例会被赋值给 Proxy 类,JDK 代理对象实例也就有了业务代理对象实例,同时 JDK 代理对象实例通过反射根据被代理类的业务方法创建了相应的 Method 对象m(可能有多个)。当 JDK 代理对象实例调用业务方法,如 proxy.rent();此时会先把对应的m对象作为参数传给 invoke()方法(就是 invoke 方法的第二个参数),调用了 JDK 代理对象实例的 invoke()回调方法,在 invoke 方法里面再通过反射调用被代理对象的方法,即 Object object = method.invoke(rent,objects);

CGLIB动态代理

JDK 中提供的生成动态代理类的机制有个鲜明的特点是:

某个类必须有实现的接口,而生成的代理类也只能代理某个类接口定义的方法,比如:如果上述例子的 Host 类实现了继承自 Rent 接口的方法外,自身另外实现了方法 buyHouse(),则在产生的动态代理类中不会有这个方法。更极端的情况是:如果某个类没有实现接口,那么这个类就不能用 JDK 产生动态代理。

那么怎么解决上述存在的问题呢?这里介绍一种新的代理方法——CGLIB,“CGLIB(Code Generation Library),是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展 Java 类与实现J ava接口。

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

  1. 查找A上的所有非 final 的 public 类型的方法定义;
  2. 将上述步骤中的方法的定义转换成字节码;
  3. 将组成的字节码转换成相应的代理的 Class 对象;
  4. 实现 MethodInterceptor 接口,用来处理对代理类上的所有方法的请求(该接口和 JDK 动态代理 InvocationHandler 的功能和角色是一样的)

有了上述 JDK 动态代理的例子,CGLIB 就比较容易理解了,接下里用案例进行分析,复用上述的 Rent 接口和 Host 类。

Maven 导入 CGLIB 相关包,格式如下:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.6</version>
</dependency>
复制代码

实现 MethodInterceptor 接口:

public class MethodInterceptorImpl implements MethodInterceptor {
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        seeHouse();
        Object object = methodProxy.invokeSuper(o,objects);
        fare();
        return object;
    }

    public void seeHouse(){
        System.out.println("带租客看房");
    }

    public void fare(){
        System.out.println("收中介费");
    }
}
复制代码

创建动态代理:

public class CglibProxyDemo {

    public static void main(String[] args) {
        Host host = new Host();
        MethodInterceptor methodInterceptor = new MethodInterceptorImpl();

        //cglib中加强器,用来创建动态代理
        Enhancer enhancer = new Enhancer();
        //设置要创建动态代理的类
        enhancer.setSuperclass(host.getClass());
        //设置回调,这里相当于是对于代理类上所有方法的调用,都会调用Callback,而Callback则需要实行intercept()方法进行拦截
        enhancer.setCallback(methodInterceptor);

        Rent rent = (Rent) enhancer.create();
        rent.rent();
    }
}
复制代码

通过以上实例可以看出,Cglib 通过继承实现动态代理,具体类不需要实现特定的接口,而且代理类可以调用具体类的非接口方法,更加灵活。

关于 JDK 动态代理和 CGLIB动态代理适用范围的不同,需要修改一下 Host 类,然后查看动态代理执行的结果。

Host.java

public class Host implements Rent {
    public void rent() {
        System.out.println("房屋出租");
    }

    public void buyHouse(){
        System.out.println("买房后续出租");
    }

    public final void travel(){
        System.out.println("旅游!!!");
    }
}
复制代码

JdkProxy.java

public class JdkProxy {

    public static void main(String[] args) {
        //创建被代理的具体类
        Host host = new Host();
        //获取对应的ClassLoader
        ClassLoader classLoader = host.getClass().getClassLoader();
        //获取被代理对象实现的所有接口
        Class[] interfaces = host.getClass().getInterfaces();
        //设置请求处理器,处理所有方法调用
        InvocationHandler invocationHandler = new InvocationHandlerImpl(host);

        /**
         * 5.根据上面提供的信息,创建代理对象 在这个过程中,
         *   a.JDK会通过根据传入的参数信息动态地在内存中创建和.class文件等同的字节码
         *   b.然后根据相应的字节码转换成对应的class,
         *   c.然后调用newInstance()创建实例
         */
        Object o = Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
        Rent rent = (Rent) o;
//        Rent rent = (Rent) new InvocationHandlerImpl().newInstance(host);
        rent.rent();
        Host host1 = (Host) o;
        host1.buyHouse();
        host1.travel();
    }
}
复制代码

执行结果为:

带租客看房
房屋出租
收中介费
Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to com.msdn.bean.Host
    at com.msdn.dynamic.JdkProxy.main(JdkProxy.java:36)
复制代码

CglibProxyDemo.java

public class CglibProxyDemo {

    public static void main(String[] args) {
        Host host = new Host();
        MethodInterceptor methodInterceptor = new MethodInterceptorImpl();

        //cglib中加强器,用来创建动态代理
        Enhancer enhancer = new Enhancer();
        //设置要创建动态代理的类
        enhancer.setSuperclass(host.getClass());
        //设置回调,这里相当于是对于代理类上所有方法的调用,都会调用Callback,而Callback则需要实行intercept()方法进行拦截
        enhancer.setCallback(methodInterceptor);

        Rent rent = (Rent) enhancer.create();
        rent.rent();
        Host host1 = (Host) enhancer.create();
        host1.buyHouse();
        host1.travel();
    }
}
复制代码

执行结果为:

带租客看房
房屋出租
收中介费
带租客看房
买房后续出租
收中介费
旅游!!!
复制代码

总结:JDK 动态代理必须实现接口,通过反射来动态生成代理方法,消耗系统性能。Cglib 动态代理无需实现接口,通过生成子类字节码来实现,比反射快一点,没有性能问题。但是由于 Cglib 会继承被代理类,需要重写被代理方法,所以被代理类不能是 final 类,被代理方法不能是 final 标识。因此,Cglib 应用更加广泛一些。

参考文献

从代理机制到Spring AOP

jdk静态代理,jdk动态代理,cglib动态代理

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章