JAVA代理技术:JDK技术动态代理和CGLIB技术动态代理代码示例

本文介绍了JAVA动态代理技术,包括JDK和CGLIB两种实现方式。通过代码示例展示了如何创建JDK动态代理和CGLIB动态代理,详细解释了代理模式的概念和作用,并分析了JDK动态代理的技术细节,如InvocationHandler的工作原理。同时,文章还探讨了CGLIB动态代理的实现机制,利用继承生成代理类并利用MethodInterceptor进行方法增强。最后,对比了JDK与CGLIB在创建速度和执行效率上的差异,以及在Spring框架中的应用情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JDK技术动态代理和CGLIB技术动态代理代码示例

一、JDK技术动态代理代码示例

  • 创建接口
public interface DreamCgOne {
    void sing(String name);
}
  • 创建接口实现类
public class DreamCgOneImpl implements DreamCgOne {
    @Override
    public void sing(String name) {
        System.out.println("正在唱歌!");
    }
}
  • 创建分发器
public class DreamProxyCgOne implements InvocationHandler {

    private Object object;

    public void setObject(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("唱歌前准备工作!");
        Object invoke = method.invoke(object, args);
        return invoke;
    }
}
  • 测试
@Test
public void test96() {
    //todo:创建一个目标对象,也称为:被代理对象
    DreamCgOne dreamCgOne = new DreamCgOneImpl();
    //创建一个分发器对象,这个对象的主要用途是对重建的代理对象中的方法进行分发
    DreamProxyCgOne dreamProxyCgOne = new DreamProxyCgOne();
    //todo:将目标对象传入到分发器中
    dreamProxyCgOne.setObject(dreamCgOne);
    //todo:通过字节码操作工具,将三个参数:类加载器,目标对象的接口结构信息和分发器传入其中,通过字节码操作工具类,重建一个代理类对象字节码,再通过类加载器对其进行加载,形成一个数据空间,并返回!
    DreamCgOne dreamCgOneProxy = (DreamCgOne) Proxy.newProxyInstance(dreamCgOne.getClass().getClassLoader(), dreamCgOne.getClass().getInterfaces(), dreamProxyCgOne);
    //todo:通过代理对象调用
    dreamCgOneProxy.sing("hello");
}

二、CGLIB技术动态代理代码示例

  • 创建实例类
public class DreamEnhandlerImpl {
    public void sing(){
        System.out.println("开始唱歌!");
    }
}
  • 创建分发类
public class DreamEnhanderProxy implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("正在增加新的内容!");
        Object o = proxy.invokeSuper(obj, args);
        return o;
    }
}
  • 测试
@Test
public void test109() {
    //todo:字节码操作工具
    Enhancer enhancer = new Enhancer();
    //todo:传入父类的结构信息
    enhancer.setSuperclass(DreamEnhandlerImpl.class);
    //todo:创建一个分发器,用于向重建的方法中进行分发
    DreamEnhanderProxy dreamEnhanderProxy = new DreamEnhanderProxy();
    //todo:将分发器传入到字节码操作工具中
    enhancer.setCallback(dreamEnhanderProxy);
    //todo:通过字节码操作工具创那家一个代理类对象并返回
    DreamEnhandlerImpl dreamEnhandler = (DreamEnhandlerImpl) enhancer.create();
    //todo:调用代理类对象的方法
    dreamEnhandler.sing();
}

三、了解代理模式

代理模式是23种设计模式的一种,他是指一个对象A通过持有另一个对象B,可以具有B同样的行为的模式。为了对外开放协议,B往往实现了一个接口,A也会去实现接口。但是B是“真正”实现类,A则比较“虚”,他借用了B的方法去实现接口的方法。A虽然是“伪军”,但它可以增强B,在调用B的方法前后都做些其他的事情。Spring AOP就是使用了动态代理完成了代码的动态“织入”。

使用代理好处还不止这些,一个工程如果依赖另一个工程给的接口,但是另一个工程的接口不稳定,经常变更协议,就可以使用一个代理,接口变更时,只需要修改代理,不需要一一修改业务代码。从这个意义上说,所有调外界的接口,我们都可以这么做,不让外界的代码对我们的代码有侵入,这叫防御式编程。代理其他的应用可能还有很多。

上述例子中,类A写死持有B,就是B的静态代理。如果A代理的对象是不确定的,就是动态代理。动态代理目前有两种常见的实现,jdk动态代理和cglib动态代理。

四、JDK动态代理技术分析

jdk动态代理是jre提供给我们的类库,可以直接使用,不依赖第三方。先看下jdk动态代理的使用代码,再理解原理。

  • 首先有个“明星”接口类,有唱、跳两个功能:
package proxy;
 
public interface Star
{
    String sing(String name);
    
    String dance(String name);
}
  • 再有个明星实现类“刘德华”:
package proxy;
 
public class LiuDeHua implements Star
{   
    @Override
    public String sing(String name)
    {
         System.out.println("给我一杯忘情水");
 
        return "唱完" ;
    }
    
    @Override
    public String dance(String name)
    {
        System.out.println("开心的马骝");
 
        return "跳完" ;
    }
}
  • 明星演出前需要有人收钱,由于要准备演出,自己不做这个工作,一般交给一个经纪人。便于理解,它的名字以Proxy结尾,但他不是代理类,原因是它没有实现我们的明星接口,无法对外服务,它仅仅是一个wrapper。
package proxy;
 
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
 
public class StarProxy implements InvocationHandler
{
    // 目标类,也就是被代理对象
    private Object target;
    
    public void setTarget(Object target)
    {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    {
        // 这里可以做增强
        System.out.println("收钱");
        
        Object result = method.invoke(target, args);
        
        return result;
    }
    
    // 生成代理类
    public Object CreatProxyedObj()
    {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }  
   
}

上述例子中,方法CreatProxyedObj返回的对象才是我们的代理类,它需要三个参数,前两个参数的意思是在同一个classloader下通过接口创建出一个对象,该对象需要一个属性,也就是第三个参数,它是一个InvocationHandler。需要注意的是这个CreatProxyedObj方法不一定非得在我们的StarProxy类中,往往放在一个工厂类中。上述代理的代码使用过程一般如下:

  1. new一个目标对象

  2. new一个InvocationHandler,将目标对象set进去

  3. 通过CreatProxyedObj创建代理对象,强转为目标对象的接口类型即可使用,实际上生成的代理对象实现了目标接口。

        Star ldh = new LiuDeHua();
 
        StarProxy proxy = new StarProxy();
 
        proxy.setTarget(ldh); 
  
        Object obj = proxy.CreatProxyedObj();
        
        Star star = (Star)obj;

整个JDK动态代理的秘密也就这些,简单一句话,动态代理就是要生成一个包装类对象,由于代理的对象是动态的,所以叫动态代理。由于我们需要增强,这个增强是需要留给开发人员开发代码的,因此代理类不能直接包含被代理对象,而是一个InvocationHandler,该InvocationHandler包含被代理对象,并负责分发请求给被代理对象,分发前后均可以做增强。从原理可以看出,JDK动态代理是“对象”的代理。

五、CGLIB动态代理技术分析

我们了解到,“代理”的目的是构造一个和被代理的对象有同样行为的对象,一个对象的行为是在类中定义的,对象只是类的实例。所以构造代理,不一定非得通过持有、包装对象这一种方式。

通过“继承”可以继承父类所有的公开方法,然后可以重写这些方法,在重写时对这些方法增强,这就是cglib的思想。根据里氏代换原则(LSP),父类需要出现的地方,子类可以出现,所以cglib实现的代理也是可以被正常使用的。

package proxy;
 
import java.lang.reflect.Method;
 
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
 
public class CglibProxy implements MethodInterceptor
{
    // 根据一个类型产生代理类,此方法不要求一定放在MethodInterceptor中
    public Object CreatProxyedObj(Class<?> clazz)
    {
        Enhancer enhancer = new Enhancer();
        
        enhancer.setSuperclass(clazz);
        
        enhancer.setCallback(this);
        
        return enhancer.create();
    }
    
    @Override
    public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable
    {
        // 这里增强
        System.out.println("收钱");
        
        return arg3.invokeSuper(arg0, arg2);
    } 
}

从代码可以看出,它和jdk动态代理有所不同,对外表现上看CreatProxyedObj,它只需要一个类型clazz就可以产生一个代理对象, 所以说是“类的代理”,且创造的对象通过打印类型发现也是一个新的类型。不同于jdk动态代理,jdk动态代理要求对象必须实现接口(三个参数的第二个参数),cglib对此没有要求。

cglib的原理是这样,它生成一个继承B的类型C(代理类),这个代理类持有一个MethodInterceptor,我们setCallback时传入的。 C重写所有B中的方法(方法名一致),然后在C中,构建名叫“CGLIB”+“ 父 类 方 法 名 父类方法名 ”的方法(下面叫cglib方法,所有非private的方法都会被构建),方法体里只有一句话super.方法名(),可以简单的认为保持了对父类方法的一个引用,方便调用。

这样的话,C中就有了重写方法、cglib方法、父类方法(不可见),还有一个统一的拦截方法(增强方法intercept)。其中重写方法和cglib方法肯定是有映射关系的。

C的重写方法是外界调用的入口(LSP原则),它调用MethodInterceptor的intercept方法,调用时会传递四个参数,第一个参数传递的是this,代表代理类本身,第二个参数标示拦截的方法,第三个参数是入参,第四个参数是cglib方法,intercept方法完成增强后,我们调用cglib方法间接调用父类方法完成整个方法链的调用。

这里有个疑问就是intercept的四个参数,为什么我们使用的是arg3而不是arg1?

    @Override
    public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable
    {
        System.out.println("收钱");
        
        return arg3.invokeSuper(arg0, arg2);
    }

因为如果我们通过反射 arg1.invoke(arg0, …)这种方式是无法调用到父类的方法的,子类有方法重写,隐藏了父类的方法,父类的方法已经不可见,如果硬调arg1.invoke(arg0, …)很明显会死循环。

所以调用的是cglib开头的方法,但是,我们使用arg3也不是简单的invoke,而是用的invokeSuper方法,这是因为cglib采用了fastclass机制,不仅巧妙的避开了调不到父类方法的问题,还加速了方法的调用。

fastclass基本原理是,给每个方法编号,通过编号找到方法执行避免了通过反射调用。

对比JDK动态代理,cglib依然需要一个第三者分发请求,只不过jdk动态代理分发给了目标对象,cglib最终分发给了自己,通过给method编号完成调用。cglib是继承的极致发挥,本身还是很简单的,只是fastclass需要另行理解。

  • 测试:
   public static void main(String[] args)
    {
        int times = 1000000;
        
        Star ldh = new LiuDeHua();
        StarProxy proxy = new StarProxy();
        proxy.setTarget(ldh);
        
        long time1 = System.currentTimeMillis();
        Star star = (Star)proxy.CreatProxyedObj();
        long time2 = System.currentTimeMillis();
        System.out.println("jdk创建时间:" + (time2 - time1));
        
        CglibProxy proxy2 = new CglibProxy();
        long time5 = System.currentTimeMillis();
        Star star2 = (Star)proxy2.CreatProxyedObj(LiuDeHua.class);
        long time6 = System.currentTimeMillis();
        System.out.println("cglib创建时间:" + (time6 - time5));
        
        long time3 = System.currentTimeMillis();
        for (int i = 1; i <= times; i++)
        {
            star.sing("ss");
            
            star.dance("ss");
        }
        long time4 = System.currentTimeMillis();
        System.out.println("jdk执行时间" + (time4 - time3));
        
        long time7 = System.currentTimeMillis();
        for (int i = 1; i <= times; i++)
        {
            star2.sing("ss");
            
            star2.dance("ss");
        }
        
        long time8 = System.currentTimeMillis();
        
        System.out.println("cglib执行时间" + (time8 - time7));   
    }

经测试,jdk创建对象的速度远大于cglib,这是由于cglib创建对象时需要操作字节码。cglib执行速度略大于jdk,所以比较适合单例模式。另外由于CGLIB的大部分类是直接对Java字节码进行操作,这样生成的类会在Java的永久堆中。如果动态代理操作过多,容易造成永久堆满,触发OutOfMemory异常。spring默认使用jdk动态代理,如果类没有接口,则使用cglib。

————————————————
版权声明:本文为优快云博主「围子先生」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/flyfeifei66/article/details/81481222

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值