> 文档中心 > JDK动态代理与CGLIB动态代理

JDK动态代理与CGLIB动态代理


设计模式之代理模式


下面假设一个常见的场景,假如你有一个法国的朋友叫Alice,有一天你想要购买一瓶法国香奈儿的香水,这种香水只在法国销售。你找到你的朋友Alice,请求她帮忙购买一瓶香奈儿的香水,你的朋友Alice于是乎就找到香奈儿的销售店购买了一瓶法国香奈儿的香水,并将香水交给了你。

在上面这个例子当中,你想要购买法国香水,但是无法直接向香奈儿供应商购买,只能通过中间人(Alice)购买,这就是我们生活中比较常见的代理模式。我们假设有一天,你突然想要一瓶法国红酒,但是由于Alice本身并不具备购买红酒的能力,所以你需要让Alice也能帮你购买红酒,这在程序世界需要修改才能实现,我们都知道在软件设计当中有一个开放封闭原则,即对修改关闭,对拓展开放,而上面的例子显然违背了这个原则,所以我们需要学习一种新的代理模式来实现我们的需求。即对代码进行拓展从而实现类功能的增强。

下面我们就来学习一下在java当中的代理。代理共分为两者模式,它们分别是

静态代理


我们先从最简单的静态代理讲起,要实现静态代理,我们需要代理对象(Alice)和供应商都实现一套相同的接口。如下所示,我们先定义共同的接口:

public interface Store {  //定义一个接口    void provide(double price);  //表示提供商品的方法}

在定义了接口之后,我们需要让香奈儿供应商与代理人Alice都实现这个接口,代码如下:

public class ChanelSupplier implements Store {public void provide(double price){   System.out.println("已成功购买一瓶法国香奈儿香水,花费"+price+"元");    }}public class AliceAgent implements Store {  private Store store;    public AliceAgent(Store store){ this.store = store; //这里我们内部维护了一个提供服务的真实对象  }  public void provide(double price){//这里可以在执行代理方法前,做一些逻辑处理doSomethingBefore();store.provide(price); //执行真实提供的方法doSomethingAfter(); //我们可以在执行后做默写逻辑操作  }  public void doSomethingBefore(){System.out.println("执行方法前置通知________________________"); }  public void doSomethingAfter(){System.out.println("执行方法后置通知________________________"); }}//最后是你,也就是客户端,需要调用服务的一方public class ClientPoint{   public static void main(String[] args){//我们需要实例化代理对象与香奈儿供应商对象ChanelSupplier  chanelSupplier = new ChanelSupplier();AliceAgent agent = new AliceAgent(channelSupplier); //将香奈儿供应商传入//实现代理agent.provice(1998.99); //实现了方法增强   }}

在上面的例子当中,我们分别实现了香奈儿供应商、代理人对象,并且演示了如何在客户端发起调用的过程。再回到我们之前那个例子,假如你现在想要从代理人处购买一瓶红酒,想象一下,是不是也需要先定义接口,然后让代理人和红酒供应商共同实现该接口,这么做最大的问题是,需要一直修改代理人Alice的类,让它去实现更过的接口来应对更多的需求变化,这样显然违背了设计模式当中的开放封闭原则,而动态代理就是为了解决此类问题而生的。

动态代理


我们上面讲了静态代理,也搞清楚了它最大的短板,就是每次需求的变化,可能需要修改源码才能完成需求,有没有一种既不需要修改源码,而动态实现让Alice买红酒的例子呢?这就需要我们用到今天所使用的动态代理,动态代理的出现就是为了弥补静态代理的不足。而动态代理,目前主要有两大类的实现,他们分别是:

  • JDK动态代理
  • CGLIB动态代理

我们先从JDK动态代理讲起,要实现jdk动态代理,我们需要了解JAVA反射的相关知识点,因为动态代理底层就是用反射实现的。使用JDK动态代理我们需要关注InvocationHandler接口与Proxy类,具体操作如下:

//首先我们让代理对象(Alice)实现我们的InvocationHandler接口public class AliceAgent implements InvocationHandler{  private Object realObj; //代理人内部维护了一个提供商品的真实对象   public AliceAgent(Object realObj){      this.realObj = realObj; //在构造代理对象的时候,传入提供服务的真实对象  }//我们需要实现InvocationHandler接口的invoke方法@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throwsThrowable {doSomethingBefore();Object obj = method.invoke(realObj, args); //反射调用我们的真实对象提供服务doSomethingAfter();return obj;}    public void doSomethingBefore(){System.out.println("执行方法前置通知________________________"); } public void doSomethingAfter(){System.out.println("执行方法后置通知________________________"); }}//这里我们定义供应商接口public interface Store {  //定义一个接口    void provide(double price);  //表示提供商品的方法}//让供应商实现这个接口public class ChanelSupplier implements Store {public void provide(double price){   System.out.println("已成功购买一瓶法国香奈儿香水,花费"+price+"元");    }}---------------------------------------------------------------------------//接下来是我们的客户端,也就是实际需要购买东西的那个人public class ClientPoint {   public static void main(String[] args){//创建一个提供服务的供应商对象ChanelSupplier suplier = new ChanelSupplier();//创建一个代理人对象AliceAgent agent = new AliceAgent(suplier); //将提供对象传入//使用Proxy实现动态代理      Store  store = (Store)Proxy.newProxyInstance(ChanelSupplier.class.getClassLoader(),ChanelSupplier.class.getClass().getInterfaces(),agent);store.provide(8848.8); //调用我们的服务对象   }}

以上便完成我们实现jdk动态代理的过程,执行后,可以发现会从代理对象当中执行invoke方法。从而实现方法的代理。
在Proxy.newProxyInstance()方法中,共有三个参数,它们分别是:

  • ClassLoader loader 这是加载动态代理类的类加载器
  • Class[] interfaces 指的是加载代理类实现的接口,可以传入多个接口
  • InvocationHandler h 指定代理类调用的程序,即在调用接口当中的方法时,会先找到该代理工厂,并调用invoke方法

我们最终实现了jdk动态代理,那么它能否实现我们之前提到过的需求呢?在不修改的情况下实现我们上面的需求?
仔细想一下,如果现在需要让Alice代理红酒,需要怎么做呢?只需要让红酒供应商提供一个接口,然后根据接口实现具体的供应方法,接着在调用处直接将对象安装上面的方法执行即可。这里我们可以明显发现区别,就是不需要修改代理类,也就是AliceAgent类,这也就满足我们的开放封闭原则。

最后我们总结一下关于jdk动态代理的内容
  1. jdk动态代理需要代理对象实现InvocationHandler接口,并重写invoke方法
  2. 提供服务的真实对象必须要实现一个或者多个接口
  3. 调用者通过Proxy.newProxyInstance() 方法调用对象

可能聪明的读者你也发现一个问题,也就是提供服务的对象必须要实现接口才能进行jdk动态代理,对于某些特殊的类来说,可能它没有实现接口,也想进行动态代理,该怎么办呢?下面就隆重介绍我们实现动态代理的第二种方案,它更为强大,易用。


CGLIB代理

CGLIB(Code generation Library)它不是JDK自带的动态代理,所以我们需要手动引入第三方依赖。他是一个字节码类生成库。能够动态生成Java类与Java接口。它与jdk动态代理最大的区别是,它不需要提供服务对象实现接口就可以对类进行代理,废话不多说,我们直接上代码。

首先我们需要引入jar,这里我使用maven引入cglib包

cglibcglib-nodep3.3.0

CGLIB实现动态代理的两个核心类分别是MethodInterceptor接口Enhancer类,前者是一个实现代理工厂的接口,可以根据调用者的需要,动态的帮我们生成对象的代理类,而后者帮我们最终实现对类的代理,废话不多说,我们直接看示例:

//首先让我们先定义提供商品服务的供应商对象public class ChanelSupplier {  public void provide(double price){    System.out.println("已成功出售一瓶香奈儿香水,价格"+price+"元。");  }}//紧接着我们实现代理工厂对象public class AliceAgentFactory implements MethodInterceptor{    public <T> T getProxyInstance(Class<T> tclzz){ Enhancer enhancer = new Enhancer(); //设置需要增强类的加载器对象 enhancer.setClassLoader(tclzz.getClassLoader()); //设置需要增强的类 enhancer.setClass(tclzz); //设置方法拦截器,代理工厂 enhancer.setCallback(this); //创建代理类 return (T)enhancer.create();    }@Overridepublic Object intercept(Object o, Method method, Object[] objects,MethodProxy methodProxy) throws Throwable {doSomethingBefore(); //前置方法Object object = methodProxy.invokeSuper(o, objects); //调用真实服务对象doSomethingAfter(); //后置方法return object;} public void doSomethingBefore(){  System.out.println("执行方法前置通知________________________");  }  public void doSomethingAfter(){ System.out.println("执行方法后置通知________________________");  }}//调用者对象public class ClientPoint {  public static void mian(String[] args){//创建代理工厂,获取被代理的对象AliceAgentFactory factory = new AliceAgentFactory();//获取增强之后的被代理对象ChanelSupplier supplier = factory.getProxyInstance(ChanelSupplier.class);//执行方法supplier.provide(1998.99);  }}

可以看到使用CGLIB动态代理后,代码更为简介,而且被代理对象不需要实现接口也能实现方法的增强,可以说是非常方便了。不过值得注意的是,之前的静态代理当中的代理人,是我们事先定义好的,因此在源码中可见,但是使用动态代理后,代理对象是通过代理工厂,动态被创建出来的,所以动态代理才能实现不需要修改代理对象即可实现类功能的增强。

在intercept()方法当中,共有四个参数:

  • Object o 被代理对象,在上例中就是我们的香奈儿香水供应商
  • Method method 表示被拦截到的方法
  • Object[] objects 表示被拦截方法的入参
  • MethodProxy methodProxy 表示调用原始方法

不过需要注意的是,我们在上例当中,使用的是methodProxy.invokeSuper()方法,我并没有使用invoke()方法,关于两者的区别,请参考这边文章:Cglib源码分析 invoke和invokeSuper的差别。主要是因为CGLIB使用了继承类的方式实现了原始类的方法增强,如果类或方法一旦被final修饰,那么将无法进行动态代理,因为final修饰类无法被继承,final修饰的方法无法被重写。感兴趣的同学可以把刚才的ChanelSupplier类加上final修饰,看看是否还能被代理成功。

最后我们总结一下CGLIB动态代理的内容

1)首先代理工厂需要实现MethodInterceptor接口,并重写intercept()方法
2)使用Enhancer类对被代理对象进行方法增强
3)CGLIB实现动态代理是使用继承类的方式,所以方法和类都不能被final修饰

我们对比一下CGLIB动态代理与JDK动态代理的差异

JDK动态代理 CGLIB动态代理
代理工厂实现接口 InvocationHandler MethodInterceptor
构造代理对象给client服务 Proxy Enhancer

动态代理的应用

SpringAop

我们在日常的业务处理中,经常需要对类进行日志记录,或者访问控制等,传统的OOP编程情况下,我们需要对每一个方法都使用log进行日志输出,但是这样产生了大量的样板代码,它们与业务无关,却大量充斥在业务代码当中,不利于维护,如果我们可以将这些代码抽离出来,放在一个地方处理,这样既不妨碍正常的方法执行,而且也可以处理正常的业务逻辑,实现了非业务代码与业务代码的分离,便于维护。

声明式事务

在Spring项目中,我们经常在方法或者类上面加上一个@Transcation的注解,这样,一旦方法抛出异常,那么事务就会回滚。在没有使声明式事务以前,我们通常需要这么做

SqlSession session = null;try{   //执行SQL代码    session = getSqlSessionFactory().openSession(false);session.update("...", new Object());// 提交事务session.commit();}catch(SQLException ex){  session.rollback(); //回滚事务}finly{// 关闭事务session.close();}

上面的这段代码,我们需要在使用到事务的地方使用,这样大量与业务无关的代码,会导致我们的方法越来越臃肿,难以维护,如果使用动态代理,我们只需要在方法执行前后,对异常进行捕捉,从而实现事务的回滚,而不需要每个地方都写这样的样板代码。