Spring学习之Spring AOP的底层实现技术 -- 动态代理

AOP概述

AOP是软件开发思想的一个飞跃,AOP的引入将有效弥补OOP的不足,OOP和AOP分别从纵向和横向对软件进行抽象,有效地消除重复性的代码,使代码以更优雅的更有效的方式进行逻辑表达。

spring AOP 面向切面编程 Aspect Oriented Programming(适用于权限检查,日志记录,性能分析,事务管理,异常管理) 。

AOP有三种织入切面的方法:

  1. 其一是编译期织入,这要求使用特殊的Java编译器,AspectJ是其中的代表者;
  2. 其二是类装载期织入,而这要求使用特殊的类装载器,AspectJ和AspectWerkz是其中的代表者;
  3. 其三为动态代理织入,在运行期为目标类添加增强生成子类的方式,Spring AOP采用动态代理织入切面

Spring AOP使用了两种代理机制,一种是基于JDK的动态代理,另一种是基于CGLib的动态代理,之所以需要两种代理机制,很大程度上是因为JDK本身只提供基于接口的代理,不支持类的代理。

基于JDK的代理和基于CGLib的代理是Spring AOP的核心实现技术,认识这两代理技术,有助于探究Spring AOP的实现机理。只要你愿意,你甚至可以抛开Spring,提供自己的AOP实现。

带有横切逻辑的实例

首先,我们来看一个无法通过OOP进行抽象的重复代码逻辑,它们就是AOP改造的主要对象。
下面,我们通过一个业务方法事务管理的实例了解横切逻辑。

业务方法事务管理,在每一个业务方法调用之前开始事务,业务方法结束后提交事务:

包含事务管理的横切代码:

UserDaoImpl.java

public class UserDaoImpl implements UserDao {

    @Override
    public User saveUser(User user) {
        Session session = HibernateUtil.getSessionFactory().getCurrentSession();
        session.beginTransaction();//开始事务

        session.save(user);

        session.getTransaction().commit();//提交事务
        return user;
    }

    @Override
    public void updateUser(User user) {
        Session session = HibernateUtil.getSessionFactory().getCurrentSession();
        session.beginTransaction();//开始事务

        session.load(User.class, user.getUid());
        user.setUname("等风的草");
        user.setUsex("男");

        session.getTransaction().commit();//提交事务        
    }   
}

注释部分表示的代码就是具有横切特征的代码,需要进行事务管理的每个业务方法的前后都需要添加类似的事务管理语句。

通过下面代码测试这个拥有事务管理的业务方法:

@Test
public void nonProxyTest() {
    UserDao userDao = new UserDaoImpl();
    User user = new User();
    user.setUname("1号等风的草");
    user.setUsex("男");
    userDao.saveUser(user);
}

在控制台可以看到一条insert插入语句:

如实例所示,要对业务类进行CRUD,就必须在每个业务类方法的前后两处添加上重复性的开启事务和提交事务的代码。这些非业务逻辑的事务管理破坏了作为业务类UserDaoImpl的纯粹性。

下面,分别用JDK动态代理和CGLib动态代理技术,将业务方法中开启和提交事务的这些横切代码从业务类中完成移除。

基于JDK的动态代理

JDK的动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。

其中 InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,在并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起

Proxy为InvocationHandler实现类动态创建一个符合某一接口的代理实例

这样讲一定很抽象,我们马上着手动用Proxy和InvocationHandler这两个魔法戒,对以上事务管理代码进行AOP式的改造。

首先,我们从业务类UserDaoImpl中删除事务管理的横切代码,使UserDaoImpl只负责具体的业务逻辑,如下:

public class UserDaoImpl implements UserDao {

    @Override
    public User saveUser(User user) {
        Session session = HibernateUtil.getSessionFactory().getCurrentSession();        

        session.save(user);

        return user;
    }

    @Override
    public void updateUser(User user) {
        Session session = HibernateUtil.getSessionFactory().getCurrentSession();

        session.load(User.class, user.getUid());
        user.setUname("等风的草");
        user.setUsex("男");  
    }

}

可以看到原来的事务处理代码被移除了,只保留了真正的业务逻辑。

首先,提供了一个非常简单的事务管理实现类:

MyTransaction.java

public class MyTransaction {

    private Transaction transaction;

    //开始事务
    public void beginTransaction() {
        this.transaction = HibernateUtil.getSessionFactory().getCurrentSession().beginTransaction();
    }

    //提交事务
    public void commit() {
        this.transaction.commit();
    }
}

MyTransaction类提供两个事务管理的方法,开始事务和提交事务。

从业务类中移除的横切代码当然还得找到一个寄居之所,InvocationHandler就是横切代码的家园乐土,我们将事务管理的代码安置在UserDaoInterceptor中,如下所示

UserDaoInterceptor.java

public class UserDaoInterceptor implements InvocationHandler {

    private Object target;//1 目标对象类

    private MyTransaction transaction;//事务管理对象

    public UserDaoInterceptor(Object target,MyTransaction transaction) {
        this.target = target;
        this.transaction = transaction;
    }

    /**
     * proxy 目标对象的代理类实例 
     * method 这个Method实例对应于代理实例上调用的接口方法
     * args 传入到代理实例上方法参数值的对象数组
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {

        this.transaction.beginTransaction();//开始事务

        Object obj = method.invoke(target, args);//2 调用目标对象上的方法

        this.transaction.commit();//提交事务

        //方法返回值,没有时为空
        return obj;
    }

}

我们发现,横切代码只出现一次,而不是原来那样星洒各处。大家注意2 处的method.invoke(),该语句通过反射机制调用目标对象的方法,这样InvocationHandler的invoke(Object proxy, Method method, Object[] args)方法就将横切代码和目标业务类代码编织到一起了,所以我们可以将InvocationHandler看成是业务逻辑和横切逻辑的编织器。

下面,我们对这段代码做进一步的说明

首先,我们实现InvocationHandler接口,该接口定义了一个 invoke(Object proxy, Method method, Object[] args)的方法,

  1. proxy是代理实例,一般不会用到;
  2. method是代理实例上的方法,通过它可以发起对目标类的反射调用;
  3. args是通过代理类传入的方法参数,在反射调用时使用。

此外,我们在构造函数里通过target传入真实的目标对象,如1 处所示,在接口方法invoke(Object proxy, Method method, Object[] args)里,将目标类实例传给method.invoke(target, args)方法,通过反射调用目标类方法,如2 所示。

下面,我们通过Proxy结合UserDaoInterceptor创建UserDao接口的代理实例,如下代码:

@Test
public void jdkproxyTest() {
    Object target = new UserDaoImpl();//1 创建目标类实例
    MyTransaction transaction = new MyTransaction();//创建切面类实例

    //2 将目标类和带有横切代码的切面类编织在一起
    UserDaoInterceptor userDaoHandler = new UserDaoInterceptor(target, transaction);
    //3 为编织了目标业务类逻辑和事务管理横切逻辑的handler创建代理类
    UserDao userDaoProxy = (UserDao) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), userDaoHandler);

    User user = new User();
    user.setUname("2号等风的草");
    user.setUsex("男");
    userDaoProxy.saveUser(user);//4 被代理类调用的方法,该方法是由UserDao接口定义的

}

上面的代码完成了业务类代码和横切代码编织和接口代理实例生成的工作,其中在2 处,我们将UserDao实例编织为一个包含事务处理的UserDaoInterceptor实例,然后在3 处,通过Proxy的静态方法newProxyInstance()为融合了业务类逻辑和事务处理的handler创建一个UserDao接口的代理实例,该方法的第一个入参为类加载器,第二个入参为创建的代理实例所要实现的一组接口,第三个参数是整合了业务逻辑和横切逻辑的编织器对象。

按照3 处的设置方式,这个代理实例就实现了目标业务类的所有接口,也即UserDaoImpl的UserDao接口。这样,我们就可以按照调用UserDao接口的实例相同的方式调用代理实例,如4 所示。运行以上的代码,输出以下的信息:

同时USER表结果如下:

我们发现,程序的运行效果和直接在业务类中编写事务处理逻辑的效果一致,但是在这里,原来分散的横切逻辑代码已经被我们抽取到UserDaoInterceptor中。当其它业务类(如OrderDao、SystemDao等)的业务方法也需要使用事务处理时,我们只要按照以上的方式,分别为它们创建代理对象就可以了。

小结:

JDK的动态代理必须具备四个条件:

  1. 目标接口
  2. 目标类
  3. 拦截器(处理器)
  4. 代理类

1、因为利用JDKProxy生成的代理类实现了接口,所以目标类中所有的方法在代理类中都有

2、生成的代理类的所有的方法都拦截了目标类的所有的方法。而拦截器(处理器)中invoke方法的内容正好就是代理类的各个方法的组成体

3、利用JDKProxy方式必须有接口的存在。

4、invoke方法中的三个参数可以访问目标类的被调用方法的API、被调用方法的参数、被调用方法的返回类型。

基于CGLIB的动态代理

首先明白一个问题:使用JDK创建代理有一个限制,即JDK动态代理只能为接口创建代理,这一点我们从Proxy的接口方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)就看得很清楚,第三个入参interfaces就是为代理实例指定的实现接口。

现在的问题是:对于没有通过接口定义业务方法的类,如何动态创建代理实例呢?JDK的代理技术显然已经黔驴技穷,CGLib作为一个替代者,填补了这个空缺。

你可以从https://github.com/cglib/cglib获取CGLib的类包,也可以直接从Spring的关联类库lib/cglib中获取类包。

CGLib采用非常底层的字节码技术,可以为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,并在拦截方法相应地方织入横切逻辑。

下面,我们采用CGLib技术,编写一个可以为任何类创建织入事务管理横切逻辑的代理对象的代理器:

UserDaoInterceptor.java

public class UserDaoInterceptor implements MethodInterceptor {

    private Object target;//要拦截的目标对象

    private MyTransaction transaction;//事务管理对象

    public UserDaoInterceptor(Object target,MyTransaction transaction) {
        this.target = target;
        this.transaction = transaction;
    }

    //这个方法体好像是固定写法,方法名随意
    public Object createProxy() {
        Enhancer enhancer = new Enhancer();//实例化增强器用于创建代理
        enhancer.setSuperclass(this.target.getClass());//要设置子类的目标类
        enhancer.setCallback(this);//设置回调对象为本身
        return enhancer.create();//通过字节码技术动态创建子类实例
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args,
            MethodProxy methodProxy) throws Throwable {

        this.transaction.beginTransaction();//1 横切逻辑

        Object o = methodProxy.invokeSuper(obj, args);//通过代理类调用父类中的方法

        this.transaction.commit();// 1 横切逻辑
        return o;
    }   
}

在上面代码中,你可以通过createProxy()方法为一个类创建动态代理对象,该代理对象是指定类target的子类。在这个代理对象中,我们织入事务处理的横切逻辑(1 处)。

intercept(Object obj, Method method, Object[] args,MethodProxy methodProxy)是CGLib定义的Inerceptor接口的方法:

obj表示父类的实例,method为父类方法的反射对象,args为方法的动态入参,而proxy为代理类实例。

下面,我们通过UserDaoInterceptor为UserDao类创建代理对象,并测试代理对象的方法,

测试代码:

public static void main(String args[]) {
    Object target = new UserDao();
    MyTransaction transaction = new MyTransaction();
    UserDaoInterceptor userDaoInterceptor = new UserDaoInterceptor(target, transaction);
    //通过动态生成子类的方式创建代理对象
    UserDao userDao = (UserDao) userDaoInterceptor.createProxy();

    User user = new User();
    user.setUname("cglib");
    user.setUsex("男");
    userDao.saveUser(user);

}

USER表:

小结

1、 CGlib是一个强大的,高性能,高质量的Code生成类库。它可以在运行期扩展Java类与实现Java接口。

2、 用CGlib生成代理类是目标类的子类

3、 用CGlib生成代理类不需要接口。

4、 用CGLib生成的代理类重写了父类的各个方法

5、 拦截器中的intercept方法内容正好就是代理类中的方法体

总结

Spring AOP在底层就是利用JDK动态代理或CGLib动态代理技术为目标Bean织入横切逻辑。在这里,我们对以上两节动态创建代理对象做一个总结。

Spring AOP通过Pointcut(切点)指定在哪些类的哪些方法上施加横切逻辑,
通过Advice(增强)描述横切逻辑和方法的具体织入点(方法前、方法后、方法的两端等),
此外,Spring还通过Advisor(切面)组合Pointcut和Advice。有了Advisor的信息,Spring就可以利用JDK或CGLib的动态代理技术为目标Bean创建织入切面的代理对象了。

JDK动态代理所创建的代理对象,在JDK 1.3下,性能强差人意。虽然在高版本的JDK中,动态代理对象的性能得到了很大的提高,但是有研究表明:

CGLib所创建的动态代理对象的性能依旧比JDK的所创建的代理对象的性能高不少(大概10倍)。

而CGLib在创建代理对象时性能却比JDK动态代理慢很多(大概8倍),

所以对于singleton的代理对象或者具有实例池的代理,因为不需要频繁创建代理对象,所以比较适合用CGLib动态代理技术,反之适合用JDK动态代理技术。此外,由于CGLib采用生成子类的技术创建代理对象,所以不能对目标类中的final方法进行代理。


部分内容引用:
http://www.51cto.com/specbook/223/39480.htm

欢迎转载

如需转载请注明出处:
http://blog.csdn.net/u011726984

你可能感兴趣的:(Spring)