设计模式——代理模式(超级详细的代理模式讲解)

本文首发于cdream的个人博客,点击获得更好的阅读体验!

欢迎转载,转载请注明出处。

本文主要对设计模式中的代理模式进行讲解,包括静态代理举例,动态代理中的jdk动态代理、cglib动态代理原理分析等几个方面。

image-20190101191050422

一、概念

定义:代理模式(Proxy Pattern)代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的访问。代理对象在客户端了和目标对象之间起到中介作用。

二、结构

UMLimage-20181225224431115

主要角色

真实主题类:客户端真正想调用的的主题类。

代理类:保存一个真实主题类的引用,使得代理对象可以访问真实主体对象的实体。真实主题类和代理对象都会继承相同的接口:在用到真实主题类的地方都可以使用代理类来完成。

抽象主体:定义真实主题类和代理类的接口。

三、静态代理

虚拟代理

虚拟代理作为创建开销大的对象的代表。直到我们真正使用对象时才会创建它,当对象在创建前和创建中,由虚拟代理来扮演对象的替身。对象创建后,代理会将请求直接委托给对象。

抽象主题接口

public interface LargeObject {
    /**
     * 干了一个老大的事了,所以我这个类老大了!
     */
    public void doBigThing();
}

具体主题角色,实现了抽象主题接口

public class RealLargeObject implements LargeObject {
    @Override
    public void doBigThing() {
        System.out.println("做了老大的一个事了");
    }
}

现在我们要对类进行访问控制,对"巨型对象"进行延迟创建。

public class ProxyLargeObject implements LargeObject {
    private LargeObject largeObject;
    @Override
    public void doBigThing() {
        if (largeObject==null){
            largeObject = new RealLargeObject();
        }
        largeObject.doBigThing();
    }
}

当时使用代理类时,只有当客户端调用doBigThing方法的时候才会创建LargeObject对象。

当我们需要对开销大的对象进行延迟创建或隐藏其创建过程时可以使用虚拟代理模式。

远程代理

概述

RMI

image-20181226203446138

远程代理RMI允许一个jvm上的对象调用另一个jvm上的对象,流程类似于上面这个图。

客户对象直接调用辅助对象 stub的方法,stub打包调用信息,通过网络把他运送给服务端辅助对象 skeleton,服务端辅助对象进行解包,调用真正服务对象的真正方法。

然后服务对象将结果返回给服务辅助对象,服务辅助对象将结果打包,然后客户服务对象将返回结果解包交给真正客户对象。

由registry来作为注册中心,服务端将对象注册到其中,客户端通过相关方法调用。

源代码

下面来介绍RMI步骤

1.制作远程接口

public interface MyRemote extends Remote {
 	// 这里由于是网络调用,肯定要面远程调用风险
    public String sayHello() throws RemoteException;

}

2.制作远程实现

public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {

    private static final long serialVersionUID = 6780156706603775814L;

    protected MyRemoteImpl() throws RemoteException {
        super();
    }

    @Override
    public String sayHello() throws RemoteException {
        return "hahahahaha";//真实的服务
    }
    public static void main(String[] args) {
        try {
            // 创建远程服务对象
            MyRemote myRemote = new MyRemoteImpl();
            // 绑定远程服务对象到 rmiregistry
            Naming.rebind("RemoteServer", myRemote);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3.产生Stub和Skeleton

对远程实现类执行,到classes目录下找到远程实现类,然后使用如下命令rmic com.rmidemo.MyRemoteImpl 产生stub和skeleton文件。

4.启动registry

依然是在classes 目录下运行 remiregistry 命令启动registry

5.启动服务

运行远程服务实现的 main() 方法 —> MyRemoteImpl

public class MyRemoteClient {
    public static void main(String[] args) {
        new MyRemoteClient().go();
    }
    public void go() {
        try {
            MyRemote myRemote = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteServer”);// RemoteServer 就是注册(rebind)时的 key
            String hello = myRemote.sayHello();
            System.out.println(hello);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这样就完成了RMI的调用,这种方法其实已经过时了,但是这是一个标准的远程代理模式,客户端调用的是注册中心中的远程实现类的代理,但就像是调用本地的方法一样,正常使用。

以上介绍了,两种静态代理模式,在编写代码时就被创建好了,代理类和被代理类实现了相同的接口,但这样带来的问题就是一旦接口添加新的方法,就需要对代理类和被代理类进行修改。此外,如果要针对不同类的不同方法实现相同增强,则需要创建多个代理类。就要为了解决这些问题,java引入了动态代理!

四、动态代理

动态代理有两种,一种是jdk动态代理,另一种是cglib动态代理,前者是根据继承当前类的接口,而cglib是继承当前类。所以jdk动态代理无法对没有实现接口的类进行代理。

jdk动态代理

UML:

image-20190101185629594

主题对象

// 定义一个人的接口,用来创建代理~
public interface Person {
    // 移动
    void move();
    // 获取名字
    String getName();
}

真正主题对象,就是我Cderam,移动只能靠走路

public class Cdream implements Person {
    private String name;
    private String desc;

    public Cdream() {
    }

    public Cdream(String name, String desc, String state) {
        this.name = name;
        this.desc = desc;
    }

    @Override
    public void move(){
        System.out.println("我现在移动只能靠走路!");
    }


    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }
    
}

现在我要开始通过动态代理来改装自己了!!

// 搞一个handler
public class CdreamInvocationHandler implements InvocationHandler {
    private Cdream cdream;

    public CdreamInvocationHandler(Cdream cdream) {
        this.cdream = cdream;
    }
	// proxy 代理对象
    // method 调用方法
    // 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		// 调用方法呢
        Object invoke = method.invoke(cdream, args);
        // 这个看起来太像Spring的Aop了,有木有?
        if (method.getName().startsWith("move")) {
            System.out.println("我要通过代理类给自己加技能了!");
            System.out.println("穿上钢铁战衣,化身钢铁侠");
            System.out.println("老子会飞了!哼!");
        }
        return invoke;

    }
}
invoke方法中的proxy的用途
1.可以使用反射获取代理对象的信息(也就是proxy.getClass().getName())
2.可以将代理对象返回以进行连续调用,这就是proxy存在的目的,因为this并不是代理对象。

测试类

public class Test {
    public static void main(String[] args) {
        Cdream cdream = new Cdream("在水一方","java的粉丝");
        // 创建一个handler
        CdreamInvocationHandler handler = new CdreamInvocationHandler(cdream);
		// 创建代理类
        Person o = (Person)Proxy.newProxyInstance(Cdream.class.getClassLoader(), cdream.getClass().getInterfaces(),
                handler);
        cdream.move();
        System.out.println("---------");
        o.move();
        System.out.println("---------");
        System.out.println(cdream.getName());
        System.out.println(o.getName());
    }
}

******输出结果*********
我现在移动只能靠走路!
---------
我现在移动只能靠走路!
我要通过代理类给自己加技能了!
穿上钢铁战衣,化身钢铁侠
老子会飞了!哼!
---------
在水一方
在水一方

看,我就这样穿上了钢铁战衣,化身钢铁侠,能飞上天与太阳肩并肩了!

这就是动态代理的功能,其实是把我加强了啊~~~~本来代理模式的目的是用来控制访问的,结果让我弄成加强自己了……有点像装饰者模式了,不过大家对付看吧,用法基本就是这个样子的。

看到这里一定有人要提问题了:“等等等等,我好像在哪里见过这个模式啊,哇!!!!这不是装饰者模式吗?小样,别以为你穿上马甲我就不认识你了!“

唉,其实我也觉得这两个模式太像了!然后我就谷歌啊百度,努力找到了这两个模式的不同,开始做笔记吧!

代理模式与装饰者模式的不同:
  • 目的不同:代理模式是为了控制对象的访问,而装饰者模式是装饰对象改变对象行为(不过将道理,spring与动态代理狼狈为奸搞出来的aop明显是在对类进行增强嘛,装饰者模式😠不屑的说到。)
  • 用法不同:装饰模式往往可以对被装饰类装饰多次,而代理模式只对被代理对象进行一次代理。
  • 被增强类地位不同:装饰者模式中的被装饰类是会正常创建并且可以进行包装,而代理模式的完全不会关注被代理类的,甚至可以自己在代理对象中创建被代理类
  • 然后来看看代理类的具体信息,对jdk动态代理进行深入研究!

    public class Test {
        public static void main(String[] args) {
            Cdream cdream = new Cdream("在水一方","java的粉丝","狂热");
            CdreamInvocationHandler handler = new CdreamInvocationHandler(cdream);
    
            Person o = (Person)Proxy.newProxyInstance(Cdream.class.getClassLoader(), cdream.getClass().getInterfaces(),
                    handler);
            System.out.println("o是Proxy的实例:"+(o instanceof Proxy));
            System.out.println("o真正的类名:"+o.getClass().toString());
            System.out.println("o实现了哪些接口:"+Arrays.toString(o.getClass().getInterfaces()));
            System.out.println("o的超类是:"+o.getClass().getSuperclass());
    
            System.out.print("o中的属性有:");
            Field[] field=o.getClass().getDeclaredFields();
            for(Field f:field){
                System.out.print(f.getName()+", ");
            }
            System.out.print("\n"+"o中的方法有:");
            Method[] method=o.getClass().getDeclaredMethods();
            for(Method m:method){
                System.out.print(m.getName()+", ");
            }
        }
    }
    ------------
    o是Proxy的实例:true
    o真正的类名:class com.sun.proxy.$Proxy0
    o实现了哪些接口:[interface design.proxy_pattern.dynamic_proxy.Person]
    o的超类是:class java.lang.reflect.Proxy
    o中的属性有:m1, m4, m3, m2, m0, 
    o中的方法有:equals, toString, hashCode, getName, move, 
    

    以上得出结论,创建出来的类是$Proxy0(动态生成的!),父类是Proxy,实现了接口Person,有四个看不懂的属性,方法除了我Person接口里的move、getName还多了几个方法。然后我们一头扎进newProxyInstance看看到地方发生了什么事。

    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
    	// handler非空判断
        Objects.requireNonNull(h);
    	// 对接口进行克隆
        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }
    
        /*
         * Look up or generate the designated proxy class.
         * 这个东西生成了一个新的Class,也就是我们刚看到的$Proxy0这个类
         */
        Class<?> cl = getProxyClass0(loader, intfs);
    
        /*
         * Invoke its constructor with the designated invocation handler.
         * 开始创建类了
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
    
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            // 创建了$Proxy0的对象,并传入h为Proxy中的Handler赋值($Proxy继承了Proxy)
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }
    

    这个方法干了两件事:

    1. 根据参数loader和interfaces调用方法 getProxyClass(loader, interfaces)创建代理类$Proxy0类 实现了interfaces的接口,并继承了Proxy类。
    2. 实例化$Proxy0并在构造方法中把Handler传过去,接着$Proxy0调用父类Proxy的构造器,为h赋值。

    然后看看生成代理类$Proxy0

    public final class Proxy0 extends Proxy implements Person {
        private static Method m1;
        private static Method m4;
        private static Method m3;
        private static Method m2;
        private static Method m0;
    
        public Proxy0(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);
            }
        }
    	// 就是在这里调用的invoke啊!!!!
        public final void move() throws  {
            try {
                super.h.invoke(this, m4, (Object[])null);
            } catch (RuntimeException | Error var2) {
                throw var2;
            } catch (Throwable var3) {
                throw new UndeclaredThrowableException(var3);
            }
        }
    
        public final String getName() throws  {
            try {
                return (String)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"));
                m4 = Class.forName("design.proxy_pattern.dynamic_proxy.Person").getMethod("move");
                m3 = Class.forName("design.proxy_pattern.dynamic_proxy.Person").getMethod("getName");
                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());
            }
        }
    }
    

    我们把这个$Proxy转成了Person,然后Person就调用这里面的move方法,move方法触发了CdreamInvocationHandler 中的invoke方法,所以我这次才有性穿上钢铁战衣化身钢铁侠!!!

    真是不禁会想到,我看个原理都看的这么费劲,写这东西的大佬得多可怕啊!!!

    jdk动态代理的缺陷,大家也知道,就是这个只能对有接口的类进行代理,并且只有接口里的方法能被增强,如果木有接口的类我们怎么处理啊?emmmmmm……下面我们介绍cglib动态代理。

    cglib动态代理

    image-20190101190450122

    沿用上面的Cdream类

    public class Cdream {
        private String name;
        private String desc;
    
        public Cdream() {
        }
    
        public Cdream(String name, String desc, String state) {
            this.name = name;
            this.desc = desc;
        }
    
        @Override
        public void move(){
            System.out.println("我现在移动只能靠走路!");
        }
    
    
        @Override
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getDesc() {
            return desc;
        }
    
        public void setDesc(String desc) {
            this.desc = desc;
        }
    }
    

    创建一个方法拦截器,类似于jdk中的handler

    public class CdreamInterceptor implements MethodInterceptor {
        private Cdream cdream;
    
        public CdreamInterceptor(Cdream cdream) {
            this.cdream = cdream;
        }
    
        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            // 这类注意参数里一个method,一个methodProxy,下面我进行解释
            Object invoke = methodProxy.invokeProxy(o, args);
            if (method.getName().startsWith("move")){
                System.out.println("穿上钢铁战衣,化身钢铁狗");
            }
            return invoke;
        }
    }
    

    测试类

    public class test2 {
        public static void main(String[] args) {
    		// 这个是cglib里的增强类
            Enhancer enhancer = new Enhancer();
            // 设置超类
            enhancer.setSuperclass(Cdream.class);
            // 创建被增强类
            Cdream cdream = new Cdream();
            cdream.setName("dog");
            cdream.setDesc("喜欢吃骨头肉");
            // 设置回调函数,类似于jdk中的Handler
            enhancer.setCallback(new CdreamInterceptor(cdream));
            Cdream o = (Cdream)enhancer.create();
            System.out.println(o.getName());
    
            cdream.move();
            System.out.println("---------");
            o.move();
        }
    }
    --------
    dog
    我现在移动只能靠走路!
    ---------
    我现在移动只能靠走路!
    穿上钢铁战衣,化身钢铁狗
    

    这个是cglib动态代理的基本流程,然后我们对cglib进行深入了解一下

    public class test2 {
        public static void main(String[] args) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Cdream.class);
            Cdream cdream = new Cdream();
            cdream.setName("dog");
            enhancer.setCallback(new CdreamInterceptor(cdream));
            Cdream o = (Cdream)enhancer.create();
    
            System.out.println("o真正的类名:"+o.getClass().toString());
            System.out.println("o实现为了哪些接口:"+ Arrays.toString(o.getClass().getInterfaces()));
            System.out.println("o的超类是:"+o.getClass().getSuperclass());
    
            System.out.print("o中的属性有:");
            Field[] field=o.getClass().getDeclaredFields();
            for(Field f:field){
                System.out.print(f.getName()+", ");
            }
            System.out.print("\n"+"o中的方法有:");
            Method[] method=o.getClass().getDeclaredMethods();
            for(Method m:method){
                System.out.print(m.getName()+", ");
            }
    
        }
    }
    
    --------
    o真正的类名:class design.proxy_pattern.dynamic_proxy.Cdream$$EnhancerByCGLIB$$b100d497
    o实现为了哪些接口:[interface net.sf.cglib.proxy.Factory]
    o的超类是:class design.proxy_pattern.dynamic_proxy.Cdream
    
    o中的属性有:CGLIB$BOUND, CGLIB$THREAD_CALLBACKS, CGLIB$STATIC_CALLBACKS, CGLIB$CALLBACK_0, CGLIB$getName$0$Method, CGLIB$getName$0$Proxy, CGLIB$emptyArgs, CGLIB$setName$1$Method, CGLIB$setName$1$Proxy, CGLIB$move$2$Method, CGLIB$move$2$Proxy, CGLIB$setDesc$3$Method, CGLIB$setDesc$3$Proxy, CGLIB$getDesc$4$Method, CGLIB$getDesc$4$Proxy, CGLIB$finalize$5$Method, CGLIB$finalize$5$Proxy, CGLIB$equals$6$Method, CGLIB$equals$6$Proxy, CGLIB$toString$7$Method, CGLIB$toString$7$Proxy, CGLIB$hashCode$8$Method, CGLIB$hashCode$8$Proxy, CGLIB$clone$9$Method, CGLIB$clone$9$Proxy, 
    
    o中的方法有:finalize, equals, toString, hashCode, clone, getName, newInstance, newInstance, newInstance, setName, move, setCallback, setCallbacks, CGLIB$SET_STATIC_CALLBACKS, CGLIB$SET_THREAD_CALLBACKS, getCallback, getCallbacks, setDesc, CGLIB$findMethodProxy, getDesc, CGLIB$STATICHOOK1, CGLIB$getName$0, CGLIB$BIND_CALLBACKS, CGLIB$setName$1, CGLIB$move$2, CGLIB$setDesc$3, CGLIB$getDesc$4, CGLIB$finalize$5, CGLIB$equals$6, CGLIB$toString$7, CGLIB$hashCode$8, CGLIB$clone$9, 
    

    我竟后悔把方法和属性都打印出来了,这谁受的了啊!不管了,看重点,cglib的代理类确实继承了被增强类,这样就不必在意是否有接口了,但带来的问题就是final方法无法增强,然后代理类又实现了Factory,用来创建实例使用。然后代理了好多好多方法~

    然后我们来看看代理类

    在调用方法创建代理类前把这个段代码加上去,后面是输出的位置

    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"/Users/cdream/IdeaProjects/Project1/out");
    

    下面是代理类里的move方法

     final void CGLIB$move$2() {
            super.move();
        }
    
        public final void move() {
            MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
            // 这个就是CallBack,上面创建实例Enhancer时设置进去了
            if (this.CGLIB$CALLBACK_0 == null) {
                CGLIB$BIND_CALLBACKS(this);
                var10000 = this.CGLIB$CALLBACK_0;
            }
    
            if (var10000 != null) {
                var10000.intercept(this, CGLIB$move$2$Method, CGLIB$emptyArgs, CGLIB$move$2$Proxy);
            } else {
                super.move();
            }
        }
    

    代理类为每个方法都生成了一个CGLIB$move$2这样的代理方法

    下面这个move方法是被我们强转后,会调用,如果CallBack为空的情况下,会调用父类中的方法,如果设置后就会调用方法拦截器中的方法(CdreamInterceptor.intercept)。

    然后我们来说一下method和proxyMethod,下面这个是源代码中的注释

    public interface MethodInterceptor
    extends Callback
    {
        /**
         * All generated proxied methods call this method instead of the original method.
         * The original method may either be invoked by normal reflection using the Method object,
         * or by using the MethodProxy (faster).
         * @param obj "this", the enhanced object
         * @param method intercepted Method
         * @param args argument array; primitive types are wrapped
         * @param proxy used to invoke super (non-intercepted method); may be called
         * as many times as needed
         * @throws Throwable any exception may be thrown; if so, super method will not be invoked
         * @return any value compatible with the signature of the proxied method. Method returning void will ignore this value.
         * @see MethodProxy
         */    
        public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                                   MethodProxy proxy) throws Throwable;
    
    }
    

    method是被拦截的方法,如果进行调用,则需要使用method.invoke(cdream,args),利用反射机制进行调用,而methodProxy则是利用fastClass机制,调用时利用一个hash算法快速定位代理类中的方法,调用时使用如下的方式methodProxy.invokeSuper(o,args)methodProxy.invoke(cdream,args)前者是调用代理类中的CGLIB$move$2()方法,后者是调用代理类中的move()方法。注意invokeSuper的第一个参数。

    五、优缺点

    优点

    1. 代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度;

    2. 代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了保护目标对象的作用。

    缺点

    1. 由于在客户端和真实对象之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢;
    2. 实现代理模式需要额外的工作,有些代理模式的实现非常复杂;

    六、总结

    本文对代理模式简要进行了介绍,在日常开发中,代理模式经常会被用到。

    静态代理模式与装饰者模式类似,但目的是不同的,装饰者模式目的是对类进行包装,而代理模式则是为了控制对象的方法访问权限。

    动态代理是静态代理的增强,避免创建过多的代理类。动态代理的代理类是在运行中生成的,而静态代理的代理类需要我们事先编写好。我们经常会用到的动态代理包括jdk动态代理和cglib动态代理,jdk动态代理需要被代理类的接口,cglib则是继承被代理类,由于cglib的fastClass机制,cglib的效率会高于利用反射的jdk动态代理。

    Spring中的AOP,微服务的RPC都是代理模式的一种形式。


    参考资料

    1. Head First 设计模式,Eric Freeman &Elisabeth Freeman with Kathy Sierra & Bert Bates
    2. Cglib源码分析 invoke和invokeSuper的差别
    3. cglib源码分析(四):cglib 动态代理原理分析
    4. Factory proxy pattern,wiki
    5. 说说cglib动态代理,占小狼
    posted @ 2019-01-01 19:21  cdream  阅读(1408)  评论(0编辑  收藏  举报