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动态代理的内容
- jdk动态代理需要代理对象实现InvocationHandler接口,并重写invoke方法
- 提供服务的真实对象必须要实现一个或者多个接口
- 调用者通过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();}
上面的这段代码,我们需要在使用到事务的地方使用,这样大量与业务无关的代码,会导致我们的方法越来越臃肿,难以维护,如果使用动态代理,我们只需要在方法执行前后,对异常进行捕捉,从而实现事务的回滚,而不需要每个地方都写这样的样板代码。