Spring之面向切面编程

目录

1 一个简单的约定游戏

1.1 约定规则

1.2 读者的代码

1.3 笔者的代码

2 Spring AOP的基本概念

2.1 AOP的概念和使用原因

2.2 面向切面编程的术语

2.2.1 切面(Aspect)

2.2.2 通知(Advice)

2.2.3 引入(Introduction)

2.2.4 切点(Pointcut)

2.2.5 连接点(join point)

2.2.6 织入(Weaving)

2.3 Spring对AOP的支持

3 使用@AspectJ注解开发Spring AOP

3.1 选择连接点

3.2 创建切面

3.3 定义切点

3.4 测试AOP

3.5 环绕通知

3.6 织入

3.7 给通知传递参数

3.8 引入

4 使用XML配置开发Spring AOP

4.1 前置通知、后置通知、返回通知和异常通知

4.2 环绕通知

4.3 给通知传递参数

4.4 引入

5 经典Spring AOP应用程序

6 多个切面


之前介绍了Spring装配Bean到IoC容器中的相关知识,感兴趣可以查看《Spring之装配Bean》。本文讲解Spring的另一大特性——AOP(面向切面编程)。

如果说IoC是Spring的核心,那么面向切面编程就是Spring最为重要的功能之一了,在数据库事务中切面编程被广泛使用。一切要从Spring AOP的底层技术——动态代理开始。理解了动态代理的例子,你就能对Spring AOP豁然开朗。为了能更通俗易懂地阐述AOP,先和读者玩一个简单的约定游戏。


1 一个简单的约定游戏

应该说AOP原理是Spring技术中最难理解的一个部分,而这个约定游戏也许会给你很多的帮助,通过这个约定游戏,就可以理解Spring AOP的含义和实现方法,也能帮助读者更好地运用Spring AOP到实际的编程当中,这对于正确理解Spring AOP是十分重要的。

1.1 约定规则

首先提供一个Interceptor接口,其定义如下:

package com.hys.spring.example4.aop;

public interface Interceptor {

    public void before(Object obj);

    public void after(Object obj);

    public void afterReturning(Object obj);

    public void afterThrowing(Object obj);
}

 这里是一个拦截接口,可以对它创建实现类。如果使用过Spring AOP,你就会发现笔者的定义和Spring AOP定义的消息是如此相近。如果你没有使用过,那么也无关紧要,这只是一个很简单的接口定义,理解它很容易。

此时笔者要求读者生成对象的时候都用这样的一个类来生成对应的对象,代码如下:

package com.hys.spring.example4.aop;

public class ProxyBeanFactory {

    @SuppressWarnings("unchecked")
    public static <T> T getBean(T obj, Interceptor interceptor) {
        return (T) ProxyBeanUtil.getBean(obj, interceptor);
    }
}

具体类ProxyBeanUtil的getBean方法的逻辑不需要去理会,因为这是笔者需要来完成的内容。但是作为读者,你要知道当使用了这个方法后存在如下约定(这里不讨论obj对象为空或者拦截器interceptor为空的情况,因为这些并不具备很大的讨论价值,只需要很简单的判断就可以了)。

当一个对象通过ProxyBeanFactory的getBean方法定义后,拥有这样的约定:

  1. Bean必须是一个实现了某一个接口的对象。
  2. 最先会执行拦截器的before方法。
  3. 其次执行Bean的方法(通过反射的形式)。
  4. 执行Bean方法时,无论是否产生异常,都会执行after方法。
  5. 执行Bean方法时,如果不产生异常,则执行afterReturning方法;如果产生异常,则执行afterThrowing方法。

这个约定实际已经十分接近Spring AOP对我们的约定,所以这个约定十分重要,其流程如下图所示:

上图是笔者和读者的约定流程,这里有一个判断,即是否存在Bean方法的异常。如果存在异常,则会在结束前调用afterThrowing方法,否则就做正常返回,那么就调用afterReturning方法。

1.2 读者的代码

上面笔者给出了接口和获取Bean的方式,同时也给出了具体的约定,这个时候读者可以根据约定编写代码,比如打印一个角色信息。由于约定服务对象必须实现接口,于是可以自己定义一个RoleService接口,代码如下:

package com.hys.spring.example4.service;

import com.hys.spring.example4.pojo.Role;

public interface RoleService {

    public void printRole(Role role);
}

然后就可以编写它的实现类了,假设下述代码就是读者编写的RoleService的实现类,它提供了printRole方法的具体实现:

package com.hys.spring.example4.impl;

import com.hys.spring.example4.pojo.Role;
import com.hys.spring.example4.service.RoleService;

public class RoleServiceImpl implements RoleService {

    @Override
    public void printRole(Role role) {
        System.out.println("{id=" + role.getId() + ",roleName=" + role.getRoleName() + ",note=" + role.getNote() + "}");
    }
}

然后是Role类:

package com.hys.spring.example4.pojo;

public class Role {

    private Long   id;
    private String roleName;
    private String note;

    public Role() {}

    public Role(Long id, String roleName, String note) {
        this.id = id;
        this.roleName = roleName;
        this.note = note;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

显然这也没什么难度,只是还欠缺一个拦截器,它只需要实现上述Interceptor接口而已,也十分简单,假设下面是读者自己写的实现类,代码如下:

package com.hys.spring.example4.interceptor;

import com.hys.spring.example4.aop.Interceptor;

public class RoleInterceptor implements Interceptor {

    @Override
    public void before(Object obj) {
        System.out.println("准备打印角色信息");
    }

    @Override
    public void after(Object obj) {
        System.out.println("已经完成角色信息的打印处理");
    }

    @Override
    public void afterReturning(Object obj) {
        System.out.println("刚刚完成打印功能,一切正常");
    }

    @Override
    public void afterThrowing(Object obj) {
        System.out.println("打印功能执行异常,查看一下角色对象是否为空");
    }
}

它编写了上述活动图中描述流程的各个方法,这个时候你可以清楚地知道代码将按照活动图的流程执行。注意,你并不需要知道笔者如何实现,你只需要知道我们之间的约定即可,使用下述代码测试约定流程:

package com.hys.spring.example4.test;

import com.hys.spring.example4.aop.Interceptor;
import com.hys.spring.example4.aop.ProxyBeanFactory;
import com.hys.spring.example4.impl.RoleServiceImpl;
import com.hys.spring.example4.interceptor.RoleInterceptor;
import com.hys.spring.example4.pojo.Role;
import com.hys.spring.example4.service.RoleService;

public class Test {

    public static void main(String[] args) {
        RoleService roleService = new RoleServiceImpl();
        Interceptor interceptor = new RoleInterceptor();
        RoleService proxy = ProxyBeanFactory.getBean(roleService, interceptor);
        Role role = new Role(1L, "role_name_1", "role_note_1");
        proxy.printRole(role);
        System.out.println("----------------------测试afterThrowing方法----------------------");
        role = null;
        proxy.printRole(role);
    }
}

第15行代码是笔者和读者约定的获取Bean的方法,而到了后面为了测试afterThrowing方法,笔者将角色对象role设置为空,这样便能使得原有的打印方法发生异常。此时运行这段代码,就可以得到下面的结果:

准备打印角色信息
{id=1,roleName=role_name_1,note=role_note_1}
已经完成角色信息的打印处理
刚刚完成打印功能,一切正常
----------------------测试afterThrowing方法----------------------
准备打印角色信息
已经完成角色信息的打印处理
打印功能执行异常,查看一下角色对象是否为空

可见底层已经处理了这个流程,使用者只需要懂得上述活动图中的约定,实现接口中的方法即可。这些都是笔者对你的约定,而你不需要知道笔者是如何实现的。也许你会好奇,笔者是如何做到这些的?下节笔者将展示如何做到这个约定游戏的。

1.3 笔者的代码

1.1节笔者只是和读者进行了约定,而没有展示代码,本节将展示代码。上面的代码都基于动态代理模式。不熟悉动态代码的读者可以参阅笔者的另一篇博客《动态代理概述》

下面展示通过JDK动态代码实现上述流程的代码,代码如下:

package com.hys.spring.example4.aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyBeanUtil implements InvocationHandler {

    //被代理对象
    private Object      obj;
    //拦截器
    private Interceptor interceptor = null;

    /**
     * 
    * <p>Title: getBean </p>
    * <p>Description: 获取动态代理对象 </p>
    * @param obj 被代理对象
    * @param interceptor 拦截器
    * @return    动态代理对象
    * @author houyishuang
    * @date 2018年8月19日
     */
    public static Object getBean(Object obj, Interceptor interceptor) {
        //使用当前类,作为代理方法,此时被代理对象执行方法的时候,会进入当前类的invoke方法里
        ProxyBeanUtil _this = new ProxyBeanUtil();
        //保存被代理对象
        _this.obj = obj;
        //保存拦截器
        _this.interceptor = interceptor;
        //生成代理对象,并绑定代理方法
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), _this);
    }

    /**
     * 
    * <p>Title: invoke</p>
    * <p>Description: 代理方法</p>
    * @param proxy 代理对象
    * @param method 当前调度方法
    * @param args 参数
    * @return 方法返回
    * @throws Throwable 异常
    * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
    * @author houyishuang
    * @date 2018年8月19日
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object retObj = null;
        //是否产生异常
        boolean exceptionFlag = false;
        //before方法
        interceptor.before(obj);
        try {
            //反射原有方法
            retObj = method.invoke(obj, args);
        } catch (Exception e) {
            exceptionFlag = true;
        } finally {
            //after方法
            interceptor.after(obj);
        }
        if (exceptionFlag) {
            //afterThrowing方法
            interceptor.afterThrowing(obj);
        } else {
            //afterReturning方法
            interceptor.afterReturning(obj);
        }
        return retObj;
    }
}

上面的代码使用了动态代理,由于这段代码的重要性,这里有必要讨论其实现过程。

首先,通过getBean方法保存了被代理对象、拦截器(interceptor)和参数(args),为之后的调用奠定了基础。然后,生成了JDK动态代理对象(proxy),同时绑定了ProxyBeanUtil的invoke方法中,于是焦点又到了invoke方法上。

在invoke方法中,笔者将拦截器的方法按照上述活动图实现了一遍,其中设置了异常标志(exceptionFlag),通过这个标志就能判断反射原有对象方法的时候是否发生了异常,这就是读者的代码能够按照流程打印的原因。但是,由于动态代理和反射的代码会比较抽象,更多的时候大部分的框架只会告诉你流程图和具体的流程方法的配置,就像笔者之前只是给出约定而已,相信有心的读者已经明白这句话的意思了,这就是说Spring框架也是这样做的。

这个例子告诉大家,笔者完全可以将所编写的代码按照一定的流程去织入到约定的流程中。同样,Spring框架也是可以的,而且Spring框架提供的方式更多也更为强大,只要我们抓住了约定的内容,就不难理解Spring的应用了。


2 Spring AOP的基本概念

上节展示了动态代理使程序运行时,可以按照设计者约定的流程运行,那么这有什么意义呢?这是本节要讨论的问题。

2.1 AOP的概念和使用原因

现实中有一些内容并不是面向对象(OOP)可以解决的,比如数据库事务,它对于企业级的Java EE应用而言是十分重要的,又如在电商网站购物需要经过交易系统、财务系统,对于交易系统存在一个交易记录的对象,而财务系统则存在账户的信息对象。从这个角度而言,我们需要对交易记录和账户操作形成一个统一的事务管理。交易和账户的事务,要么全部成功,要么全部失败。交易记录和账户记录都是对象,这两个对象需要在同一个事务中控制,这就不是面向对象可以解决的问题,而需要用到面向切面的编程,这里的切面环境就是数据库事务。

AOP编程有着重要的意义,首先它可以拦截一些方法,然后把各个对象组织成一个整体,比如网站的交易记录需要记录日志,如果我们约定好了动态的流程,那么就可以在交易前后、交易正常完成后或者交易异常发生时,通过这些约定记录相关的日志了。

也许到现在你也没能理解AOP的重要性,不过不要紧。回到JDBC的代码中,令人讨厌和最折腾的问题永远是无穷无尽的try...catch...finally...语句和数据库资源的关闭问题,而且这些代码会存在大量重复,加上开发者水平参差不齐。Spring出现前,在Java EE的开发中,try...catch...finally...语句常常被严重滥用,使得Java EE的开发存在着许多问题,虽然MyBatis对JDBC做了良好的封装,但是还是不足的。先看一个MyBatis的例子,它的作用是扣减一个产品的库存,然后新增一笔交易记录,代码如下:

    /**
     * 
    * <p>Title: savePurchaseRecord </p>
    * <p>Description: 记录购买记录 </p>
    * @param productId 产品编号
    * @param record    购买记录
    * @author houyishuang
    * @date 2018年8月23日
     */
    public void savePurchaseRecord(Long productId, PurchaseRecord record) {
        SqlSession sqlSession = null;
        try {
            sqlSession = SqlSessionFactoryUtils.openSqlSession();
            ProductMapper productMapper = sqlSession.getMapper(ProductMapper.class);
            Product product = productMapper.getRole(productId);
            //判断库存是否大于购买数量
            if (product.getStock() >= record.getQuantity()) {
                //减库存,并更新数据库记录
                product.setStock(product.getStock() - record.getQuantity());
                productMapper.update(product);
                //保存交易记录
                PurchaseRecordMapper purchaseRecordMapper = sqlSession.getMapper(PurchaseRecordMapper.class);
                purchaseRecordMapper.save(record);
                sqlSession.commit();
            }
        } catch (Exception e) {
            //异常回滚事务
            e.printStackTrace();
            sqlSession.rollback();
        } finally {
            //关闭资源
            if (sqlSession != null) {
                sqlSession.close();
            }
        }
    }

这里购买交易的产品和购买记录都在try...catch...finally...语句中,首先需要自己去获取对应的映射器,而业务流程中穿插着事务的提交和回滚,也就是如果交易可以成功,那么就会提交事务,交易如果发生异常,那么就回滚事务,最后在finally语句中会关闭SqlSession所持有的功能。

但是这并不是一个很好的设计,按照Spring的AOP设计思维,它希望写成下述代码的样子:

    @Autowired
    private ProductMapper        productMapper        = null;
    @Autowired
    private PurchaseRecordMapper purchaseRecordMapper = null;

    //do something...
    @Transactional
    public void updateRoleNote(Long productId, PurchaseRecord record) {
        Product product = productMapper.getRole(productId);
        //判断库存是否大于购买数量
        if (product.getStock() >= record.getQuantity()) {
            //减库存,并更新数据库记录
            product.setStock(product.getStock() - record.getQuantity());
            productMapper.update(product);
            //保存交易记录
            purchaseRecordMapper.save(record);
        }
    }

这段代码除了一个注解@Transactional,没有任何关于打开或者关闭数据库资源的代码,更没有任何提交或者回滚数据库事务的代码,但是它却能够完成savePurchaseRecord方法的全部功能。注意,这段代码更简洁,也更容易维护,主要都集中在业务处理上,而不是数据库事务和资源管控上,这就是AOP的魅力。到这步初学者可能会有一个疑问,AOP是怎么做到这点的?

为了回答这个问题,首先来了解正常执行SQL的逻辑步骤,一个正常的SQL是:

  1. 打开通过数据库连接池获得数据库连接资源,并做一定的设置工作。
  2. 执行对应的SQL语句,对数据进行操作。
  3. 如果SQL执行过程中发生异常,回滚事务。
  4. 如果SQL执行过程中没有发生异常,最后提交事务。
  5. 到最后的阶段,需要关闭一些连接资源。

于是我们得到这样的一个流程图,如下图所示:

细心的读者会发现,这个图实际和约定游戏中的流程十分接近,也就是说作为AOP,完全可以根据这个流程做一定的封装,然后通过动态代理技术,将代码织入到对应的流程环节中。换句话说,类似于这样的流程,参考约定游戏中的例子,我们完全可以设计成这样:

  1. 打开获取数据连接在before方法中完成。
  2. 执行SQL,按照读者的逻辑会采用反射的机制调用。
  3. 如果发生异常,则回滚事务;如果没有发生异常,则提交事务,然后关闭数据库连接资源。

如果一个AOP框架不需要我们去实现流程中的方法,而是在流程中提供一些通用的方法,并可以通过一定的配置满足各种功能,比如AOP框架帮助你完成了获取数据库,你就不需要知道如何获取数据库连接功能了,此外再增加一些关于事务的重要约定:

  • 当方法标注为@Transactional时,则方法启用数据库事务功能。
  • 在默认的情况下(注意是默认情况下,可以通过配置改变),如果原有方法出现异常,则回滚事务;如果没有发生异常,那么就提交事务,这样整个事务管理AOP就完成了整个流程,无须开发者编写任何代码去实现。
  • 最后关闭数据库资源,这点也比较通用,这里AOP框架也帮你完成它。

有了上面的约定,我们可以得到AOP框架约定SQL流程图,如下图所示:

这是使用最广的执行流程,符合约定优于配置的开发原则。这些约定的方法加入默认实现后,你要做的只是执行SQL这步而已。于是你看到了上述updateRoleNote方法的代码,没有数据库资源的获取和关闭,也没有事务提交和回滚的相关代码。这些AOP框架依据约定的流程默认实现了,在大部分的情况下,只需要使用默认的约定即可,或者进行一些特定的配置,来完成你所需要的功能,这样对于开发者而言就更为关注业务开发,而不是资源控制、事务异常处理,这些AOP框架都可以完成。

以上只讨论了事务同时成功或者同时失败的情况,比如信用卡还款存在一个批量任务,总的任务按照一定的顺序调度各张信用卡,进行还款处理,这个时候不能把所有的卡都视为同一个事务。如果这样,只要有一张卡出现异常,那么所有卡的事务都会失败,这样就会导致有些用户正常还款也出现了问题,这显然不符合真实场景的需要。这个时候必须要允许存在部分成功、部分失败的场景,这时候各个对象在事务的管控就更为复杂了,不过通过AOP的手段也可以比较容易地控制它们,这就是Spring AOP的魅力所在。

AOP是通过动态代理模式,带来管控各个对象操作的切面环境,管理包括日志、数据库事务等操作,让我们拥有可以在反射原有对象方法之前正常返回、异常返回事后,插入自己的逻辑代码的能力,有时候甚至取代原有方法。在一些常用的流程中,比如数据库事务,AOP会提供默认的实现逻辑,也会提供一些简单的配置,程序员就能比较方便地修改默认的实现,达到符合真实应用的效果,这样就可以大大降低开发的工作量,提高代码的可读性和可维护性,将开发集中在业务逻辑上。

2.2 面向切面编程的术语

上节涉及了AOP对数据库的设计,这里需要进一步地明确AOP的抽象概念,有了对约定游戏的理解,虽然真正的AOP框架要比笔者的游戏更加复杂,但是二者的原理是一样的,有了这样的类比,解释AOP的原理就容易得多了。

2.2.1 切面(Aspect)

切面就是在一个怎么样的环境中工作。比如在上述updateRoleNote方法的代码中,数据库的事务直接贯穿了整个代码层面,这就是一个切面,它可以定义后面需要介绍的各类通知、切点和引入等内容,然后Spring AOP会将其定义的内容织入到约定的流程中,在动态代理中可以把它理解成一个拦截器,比如约定游戏中的RoleInterceptor类就是一个切面类。

2.2.2 通知(Advice)

通知是切面开启后,切面的方法。它根据在代理对象真实方法调用前、后的顺序和逻辑区分,它和约定游戏的例子里的拦截器的方法十分接近。

  • 前置通知(before):在动态代理反射原有对象方法或者执行环绕通知前执行的通知功能。
  • 后置通知(after):在动态代理反射原有对象方法或者执行环绕通知后执行的通知功能。无论是否抛出异常,它都会被执行。
  • 返回通知(afterReturning):在动态代理反射原有对象方法或者执行环绕通知后正常返回(无异常)执行的通知功能。
  • 异常通知(afterThrowing):在动态代理反射原有对象方法或者执行环绕通知产生异常后执行的通知功能。
  • 环绕通知(around):在动态代理中,它可以取代当前被拦截对象的方法,提供回调原有被拦截对象的方法。

2.2.3 引入(Introduction)

引入允许我们在现有的类里添加自定义的类和方法。

2.2.4 切点(Pointcut)

这是一个告诉Spring AOP在什么时候启动拦截并织入对应的流程,因为并不是所有的开发都需要启动AOP的,它往往通过正则表达式进行限定。

2.2.5 连接点(join point)

连接点对应的是具体需要拦截的东西,比如通过切点的正则表达式去判断哪些方法是连接点,从而织入对应的通知,比如约定游戏中的printRole方法就是一个连接点。

2.2.6 织入(Weaving)

织入是一个生成代理对象并将切面内容放入到流程中的过程。实际代理的方法分为静态代理和动态代理(详见笔者之前的文章《动态代理概述》)。静态代理是在编译class文件时生成的代码逻辑,但是在Spring中并不使用这样的方式。一种是通过ClassLoader也就是在类加载的时候生成的代码逻辑,但是它在应用程序代码运行前就生成对应的逻辑。还有一种是在运行期动态生成代码的方式,这是Spring AOP所采用的方式,Spring是以JDK和CGLIB动态代理来生成代理对象的,正如在约定游戏中的例子,笔者也是通过JDK动态代理来生成代理对象的。

AOP的概念比较生涩难懂,为了便于理解,笔者通过类比约定游戏中的代码,为读者画出流程图,如下图所示,相信AOP流程图对读者理解AOP术语会有很大的帮助:

2.3 Spring对AOP的支持

AOP并不是Spring框架特有的,Spring只是支持AOP编程的框架之一。每一个框架对AOP的支持各有特点,有些AOP能够对方法的参数进行拦截,有些AOP对方法进行拦截。而Spring AOP是一种基于方法拦截的AOP,话句话说Spring只能支持方法拦截的AOP。在Spring中有4种方式来实现AOP的拦截功能:

  • 使用ProxyFactoryBean和对应的接口实现AOP。
  • 使用XML配置AOP。
  • 使用@AspectJ注解驱动切面。
  • 使用AspectJ注入切面。

需要说明的一点是:第三种和第四种方式完全是两种不同的方式实现的拦截切面。AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。AOP大体上可以通过静态织入或动态代理两种方式来实现,AspectJ是通过静态织入的方式实现的AOP。而Spring AOP与AspectJ的目的是一致的,都是为了提供横切业务的功能。Spring也引入了和AspectJ框架相同的@Aspect注解,但是Spring只是使用了同样的注解而已,并没有使用AspectJ的编译器,Spring中@Aspect注解的底层依然是动态代理的方式实现的

在Spring AOP的拦截方式中,真正常用的是用@AspectJ注解的方式实现的切面,有时候XML配置也有一定的辅助作用,因此对这两种方式会详细讨论。对于ProxyFactoryBean和AspectJ注入切面的方法只会简单介绍,因为这两种方式已经很少用了。


3 使用@AspectJ注解开发Spring AOP

鉴于使用@AspectJ注解的方式已经成为了主流,所以先以@AspectJ注解的方式详细讨论Spring AOP的开发,有了对@AspectJ注解实现的理解,其他的方式其实也是大同小异。不过在此之前要先讨论一些关键的步骤,否则将难以理解一些重要的内容(AspectJ的jar包需要单独下载,Spring中并不包含。笔者所用的是aspectj-1.9.1的版本,不同版本可能会有所差异)。

3.1 选择连接点

Spring是方法级别的AOP框架,而我们主要也是以某个类的某个方法作为连接点,用动态代理的理论来说,就是要拦截哪个方法织入对应AOP通知。为了更好地测试,先建一个接口,代码如下:

package com.hys.spring.example5.service;

import com.hys.spring.example5.pojo.Role;

public interface RoleService {

    public void printRole(Role role);
}

这个接口很简单,接下来提供一个实现类,代码如下:

package com.hys.spring.example5.impl;

import org.springframework.stereotype.Component;

import com.hys.spring.example5.pojo.Role;
import com.hys.spring.example5.service.RoleService;

@Component
public class RoleServiceImpl implements RoleService {

    @Override
    public void printRole(Role role) {
        System.out.println("{id: " + role.getId() + ", " + "role_name: " + role.getRoleName() + ", " + "note: " + role.getNote() + "}");
    }
}

这个类没什么特别的,只是这个时候如果把printRole作为AOP的连接点,那么用动态代理的语言就是要为RoleServiceImpl类生成代理对象,然后拦截printRole方法,于是可以产生各种AOP通知方法。

POJO如下:

package com.hys.spring.example5.pojo;

public class Role {

    private Long   id;
    private String roleName;
    private String note;

    public Role() {}

    public Role(Long id, String roleName, String note) {
        this.id = id;
        this.roleName = roleName;
        this.note = note;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

3.2 创建切面

选择好了连接点就可以创建切面了,对于动态代理的概念而言,它就如同一个拦截器,在Spring中只要使用@Aspect注解一个类,那么Spring IoC容器就会认为这是一个切面了,代码如下:

package com.hys.spring.example5.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class RoleAspect {

    @Before("execution(* com.hys.spring.example5.impl.RoleServiceImpl.printRole(..))")
    public void before() {
        System.out.println("before...");
    }

    @After("execution(* com.hys.spring.example5.impl.RoleServiceImpl.printRole(..))")
    public void after() {
        System.out.println("after...");
    }

    @AfterReturning("execution(* com.hys.spring.example5.impl.RoleServiceImpl.printRole(..))")
    public void afterReturning() {
        System.out.println("afterReturning...");
    }

    @AfterThrowing("execution(* com.hys.spring.example5.impl.RoleServiceImpl.printRole(..))")
    public void afterThrowing() {
        System.out.println("afterThrowing...");
    }
}

这里的注解解释如下表所示:

注解 通知 备注
@Before 在被代理对象的方法前先调用 前置通知
@Around 将被代理对象的方法封装起来,并用环绕通知取代它 环绕通知,它将覆盖原有方法,但是允许你通过反射调用原有方法
@After 在被代理对象的方法后调用 后置通知
@AfterReturning 在被代理对象的方法正常返回后调用 返回通知,要求被代理对象的方法执行过程中没有发生异常
@AfterThrowing 在被代理对象的方法抛出异常后调用 异常通知,要求被代理对象的方法执行过程中产生异常

有了这个表,再参考上述的AOP的流程图,就知道各个方法执行的顺序了。这段代码中的注解使用了对应的正则式,这些正则式是切点的问题,也就是要告诉Spring AOP,需要拦截什么对象的什么方法,为此我们要学习切点的知识。

3.3 定义切点

上节讨论了切面的组成,但是并没有详细讨论Spring是如何判断是否需要拦截方法的,毕竟并不是所有的方法都需要使用AOP编程,这就是一个确定连接点的问题。上述创建切面的代码,在注解中定义了execution的正则表达式,Spring是通过这个正则表达式判断是否需要拦截你的方法的,这个表达式是:

execution(* com.hys.spring.example5.impl.RoleServiceImpl.printRole(..))

依次对这个表达式做出分析:

  • execution:代表执行方法的时候会触发。
  • *:代表任意返回类型的方法。
  • com.hys.spring.example5.impl.RoleServiceImpl:代表类的全限定名。
  • printRole:被拦截方法名称。
  • (..):任意的参数。

显然通过上面的描述,全限定名为com.hys.spring.example5.impl.RoleServiceImpl类的printRole方法被拦截了,这样它就按照AOP通知的规则把方法织入到流程中。只是上述的表达式还有些简单,我们需要进一步论述它们,它可以配置如下内容,如下表所示:

AspectJ指示器 描述
args() 限制连接点匹配参数为指定类型的方法
@args() 限制连接点匹配参数为指定注解标注的执行方法
execution 用于匹配连接点的执行方法,这是最常用的匹配,可以通过类似上面的的正则式进行匹配
this() 限制连接点匹配AOP代理的Bean,引用为指定类型的类
target 限制连接点匹配被代理对象为指定的类型
@target() 限制连接点匹配特定的执行对象,这些对象要符合指定的注解类型
within() 限制连接点匹配指定的包
@within() 限制连接点匹配指定的类型
@annotation 限定匹配带有指定注解的连接点

注意,Spring只能支持上表所列出的AspectJ的指示器。如果使用了非表格中所列举的指示器,那么它将会抛出IllegalArgumentException异常。

此外,Spring还根据自己的需求扩展了一个Bean()的指示器,使得我们可以根据bean id或者名称来定义对应的Bean,但是本文并不会谈及所有的指示器,因为有些指示器并不常用。我们只会对那些常用的指示器进行探讨,如果需要全部掌握,那么可以翻阅关于AspectJ框架的相关资料。

比如下面的例子,我们只需要对com.hys.spring.example5.impl包及下面的包的类进行匹配。因此要修改前置通知,这样指示器就可以编写成下述代码的样子:

    @Before("execution(* com.hys.spring.example5.*.*.printRole(..)) && within(com.hys.spring.example5.impl.*)")
    public void before() {
        System.out.println("before...");
    }

这里笔者使用了within来限定了execution定义的正则式下的包的匹配,从而达到了限制效果,这样Spring就只会拿到com.hys.spring.example5.impl包下面的类的printRole方法作为连接点了。&&表示并且的含义,如果使用XML方式引入,&在XML中具有特殊含义,因此可以用and代替它。运算符||可以用or代替,非运算符!可以用not代替。

上述切面代码中的正则表达式需要重复书写多次,比较麻烦,只要引入另一个注解@Pointcut定义一个切点就可以避免这个麻烦,代码如下:

package com.hys.spring.example5.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class RoleAspect {

    @Pointcut("execution(* com.hys.spring.example5.impl.RoleServiceImpl.printRole(..))")
    public void print() {}

    @Before("print()")
    public void before() {
        System.out.println("before...");
    }

    @After("print()")
    public void after() {
        System.out.println("after...");
    }

    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("afterReturning...");
    }

    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("afterThrowing...");
    }
}

这样我们就可以重复使用一个简易表达式来取代需要多次书写的复杂表达式了。

3.4 测试AOP

上述RoleServiceImpl类的代码给出了连接点的内容,而上述RoleAspect给出了切面的各个通知和切点的规则,这个时候可以通过编写测试代码来测试AOP的内容。首先要对Spring的Bean进行配置,采用Java注解配置,代码如下:

package com.hys.spring.example5.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import com.hys.spring.example5.aspect.RoleAspect;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.hys.spring.example5")
public class AopConfig {

    @Bean
    public RoleAspect getRoleAspect() {
        return new RoleAspect();
    }
}

@EnableAspectJAutoProxy注解代表着启用AspectJ框架的自动代理,这个时候Spring才会生成动态代理对象,进而可以使用AOP,而getRoleAspect方法,则生成一个切面实例。

也许你不喜欢使用注解的方式,Spring还提供了XML的方式,这里就需要使用AOP的命名空间了,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

	<aop:aspectj-autoproxy />
	<bean id="roleAspect"
		class="com.hys.spring.example5.aspect.RoleAspect" />
	<bean id="roleService"
		class="com.hys.spring.example5.impl.RoleServiceImpl" />

</beans>

其中<aop:aspectj-autoproxy />的代码如同注解@EnableAspectJAutoProxy,采用的也是自动代理的功能。

无论用XML还是用Java的配置,都能使Spring产生动态代理对象,从而组织切面,把各类通知织入到流程当中,下述代码是笔者测试的代码:

package com.hys.spring.example5.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.hys.spring.example5.config.AopConfig;
import com.hys.spring.example5.pojo.Role;
import com.hys.spring.example5.service.RoleService;

public class Test {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        //使用XML使用ClassPathXmlApplicationContext作为IoC容器
        //        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hys/spring/example5/config/spring-cfg.xml");
        RoleService roleService = ctx.getBean(RoleService.class);
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("role_name_1");
        role.setNote("role_note_1");
        roleService.printRole(role);
        System.out.println("######################");
        role = null;
        roleService.printRole(role);
    }
}

在第二次打印之前,笔者将role设置为null,这样是为了测试异常返回通知,通过运行这段代码,便可以得到下面的结果:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
before...
{id: 1, role_name: role_name_1, note: role_note_1}
after...
afterReturning...
######################
before...
after...
afterThrowing...
Exception in thread "main" java.lang.NullPointerException
	at com.hys.spring.example5.impl.RoleServiceImpl.printRole(RoleServiceImpl.java:13)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:197)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.aspectj.AspectJAfterAdvice.invoke(AspectJAfterAdvice.java:47)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:55)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
	at com.sun.proxy.$Proxy25.printRole(Unknown Source)
	at com.hys.spring.example5.test.Test.main(Test.java:24)

显然切面的通知已经通过AOP织入约定的流程当中了,这时可以使用AOP来处理一些需要切面的场景了。

3.5 环绕通知

环绕通知是Spring AOP中最强大的通知,它可以同时实现前置通知和后置通知。它保留了调度被代理对象原有方法的功能,所以它既强大,又灵活。但是由于强大,它的可控制性不那么强,如果不需要大量改变业务逻辑,一般而言并不需要使用它。让我们在RoleAspect切面类的代码中加入下面这个环绕通知的方法,代码如下:

    @Around("print()")
    public void around(ProceedingJoinPoint jp) {
        System.out.println("around before...");
        try {
            jp.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("around after...");
    }

这样在一个切面里通过@Around注解加入了切面的环绕通知,这个通知里有一个ProceedingJoinPoint参数。这个参数是Spring提供的,使用它可以反射连接点方法,在加入反射连接点方法后,对代码再次进行测试,可以得到如下结果:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
around before...
before...
{id: 1, role_name: role_name_1, note: role_note_1}
around after...
after...
afterReturning...
######################
around before...
before...
java.lang.NullPointerException
	at com.hys.spring.example5.impl.RoleServiceImpl.printRole(RoleServiceImpl.java:13)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:197)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:56)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
	at com.hys.spring.example5.aspect.RoleAspect.around(RoleAspect.java:42)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.aspectj.AspectJAfterAdvice.invoke(AspectJAfterAdvice.java:47)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:55)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:62)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
	at com.sun.proxy.$Proxy26.printRole(Unknown Source)
	at com.hys.spring.example5.test.Test.main(Test.java:24)
around after...
after...
afterReturning...

从上述结果中可以知道环绕通知使用jp.proceed()后会先调度前置通知(当笔者使用XML方式时,前置通知放在jp.proceed()之前调用,估计是版本问题),然后才会反射切点方法,最后才是后置通知和返回(或者异常)通知。ProceedingJoinPoint参数值得我们探究一下,为此先打入一个断点,监控这个参数,如下图所示:

从上图中可以看到动态代理对象,请注意这里使用的是JDK动态代理对象,原有对象、方法和参数都包含在内,这样Spring就可以组织对应的流程了。

3.6 织入

织入是生成代理对象并将切面内容放入约定流程的过程,在上述的代码中,连接点所在的类都是拥有接口的类,而事实上即使没有接口,Spring也能提供AOP的功能,所以是否拥有接口不是使用Spring AOP的一个强制要求。在笔者之前讲动态代理的文章中介绍过,使用JDK动态代理时,必须拥有接口,而使用CGLIB则不需要,于是Spring就提供了一个规则:当类的实现存在接口的时候,Spring将提供JDK动态代理,从而织入各个通知,就如上图所示的那样,可以看到明显的JDK动态代理的痕迹;而当类不存在接口的时候没有办法使用JDK动态代理,Spring会采用CGLIB来生成代理对象,这时可以删掉RoleService接口,然后其他代码修正错误后就可以通过断点进行调试,这样就可以监控到具体的对象了。下图是断点调试监控,可以看到CGLIB的动态代理技术:

 动态代理对象是由Spring IoC容器根据描述生成的,一般不需要修改它,对于使用者而言,只要知道AOP术语中的约定便可以使用AOP了,只是在Spring中建议使用接口编程,这样的好处是使定义和实现相分离,有利于实现变化和替换,更为灵活一些。

3.7 给通知传递参数

在Spring AOP各类通知中,除了环绕通知外,并没有讨论参数的传递,有时候我们还是希望能够传递参数的,为此本节介绍如何传递参数给AOP的各类通知。这里先修改连接点为一个多参数的方法,如下所示:

    @Before("print() && args(role, sort)")
    public void before(Role role, int sort) {
        System.out.println("before...");
    }

如上述代码所示,在切点的定义中,加入了参数的定义,这样Spring就会解析这个正则式,然后将参数传递给方法了,如下图所示:

从断点监控来看,传递给前置通知的参数是成功的,对于其他的通知,也是相同的,这样就可以把参数传递给通知了。

3.8 引入

Spring AOP只是通过动态代理技术,把各类通知织入到它所约定的流程当中,而事实上,有时候我们希望通过引入其他类的方法来得到更好的实现,这时就可以引入其他的方法了。

比如printRole方法要求,如果要求当角色为空时不再打印,那么要引入一个新的检测器对其进行检测。先定义一个RoleVerifier接口,代码如下:

package com.hys.spring.example5.service;

import com.hys.spring.example5.pojo.Role;

public interface RoleVerifier {

    public boolean verify(Role role);
}

verify方法检测对象role是否为空,如果它不为空才返回true,否则返回false。此时需要一个实现类——RoleVerifierImpl,代码如下:

package com.hys.spring.example5.impl;

import com.hys.spring.example5.pojo.Role;
import com.hys.spring.example5.service.RoleVerifier;

public class RoleVerifierImpl implements RoleVerifier {

    @Override
    public boolean verify(Role role) {
        return role != null;
    }
}

同样的,它也是十分简单,仅仅是检测role对象是否为空,那么要引入它就需要改写上述的RoleAspect切面类。我们在其中加入一个新的属性,代码如下:

    @DeclareParents(value = "com.hys.spring.example5.impl.RoleServiceImpl+", defaultImpl = RoleVerifierImpl.class)
    public RoleVerifier roleVerifier;

注解@DeclareParents的使用如上所示,这里讨论它的配置:

  • value = "com.hys.spring.example5.impl.RoleServiceImpl+":表示对RoleServiceImpl类进行增强,也就是在RoleServiceImpl中引入一个新的接口。
  • defaultImpl:代表其默认的实现类,这里是RoleVerifierImpl。

然后就可以使用这个方法了,现在要在打印角色(printRole)方法之前先检测角色是否为空,于是便可以得到下述的测试代码:

        ApplicationContext ctx = new AnnotationConfigApplicationContext(AopConfig.class);
        //使用XML使用ClassPathXmlApplicationContext作为IoC容器
        //        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hys/spring/example5/config/spring-cfg.xml");
        RoleService roleService = ctx.getBean(RoleService.class);
        RoleVerifier roleVerifier = (RoleVerifier) roleService;
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("role_name_1");
        role.setNote("role_note_1");
        if (roleVerifier.verify(role)) {
            roleService.printRole(role, 30);
        }
        System.out.println("######################");
        role = null;
        if (roleVerifier.verify(role)) {
            roleService.printRole(role, 30);
        }

使用强制转换之后就可以把roleService转化为RoleVerifier接口对象,然后就可以使用verify方法了。而RoleVerifier调用的方法verify,显然它就是通过RoleVerifierImpl来实现的。

这是再看看运行结果,如下:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
around before...
before...
{id: 1, role_name: role_name_1, note: role_note_1}
30
around after...
after...
afterReturning...
######################

由上可以看到,第二次并没有调用printRole方法,测试成功。

分析一下它的原理,我们知道Spring AOP依赖于动态代理来实现,生成动态代理对象是通过类似于下面这行代码来实现的:

        //生成代理对象,并绑定代理方法
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), _this);

obj.getClass().getInterfaces()意味着代理对象挂在多个接口之下,换句话说,只要Spring AOP让代理对象挂到RoleService和RoleVerifier两个接口之下,那么就可以把对应的Bean通过强制转换,让其在RoleService和RoleVerifier之间相互转换了。关于这点,我们可以对断点进行验证,如下图所示:

上图中动态代理下挂了两个接口,所以能够相互转换,进而可以调用引用的方法,这样就能够通过引入功能,在原有的基础上再次增强Bean的功能了。

同样的如果RoleServiceImpl没有接口,那么它也会使用CGLIB动态代理,使用增强类(Enhancer)也会有一个interfaces的属性,允许代理对象挂到对应的多个接口下,于是也可以按JDK动态代理那样使得对象可以在多个接口之间相互转换。


4 使用XML配置开发Spring AOP

上节基于注解详细讨论了AOP的开发,本节介绍使用XML方式开发AOP,其实它们的原理是相同的,所以这里主要介绍一些用法即可。这里需要在XML中引入AOP的命名空间,所以先来了解一下AOP可配置的元素,如下表所示:

AOP配置元素 用途 备注
aop:advisor 定义AOP的通知器 一种较老的方式,目前很少使用
aop:aspect 定义一个切面
aop:before 定义前置通知
aop:after 定义后置通知
aop:around 定义环绕通知
aop:after-returning 定义返回通知
aop:after-throwing 定义异常通知
aop:config 顶层的AOP配置元素 AOP的配置是以它为开始的
aop:declare-parents 给通知引入新的额外接口,增强功能
aop:pointcut 定义切点

有了@AspectJ注解驱动的切面开发,只要记住它是依据上述讲过的AOP流程图织入流程的,对于大部分元素相信读者都不会理解困难了。下面先定义要拦截的类和方法,尽管Spring并不强迫定义接口使用AOP(有接口使用JDK动态代理,没有接口则使用CGLIB动态代理),但是仍然建议使用接口,这样有利于实现和定义相分离,使得系统更为灵活,所以这里先给出一个新的接口,代码如下:

package com.hys.spring.example6.service;

import com.hys.spring.example6.pojo.Role;

public interface RoleService {

    public void printRole(Role role);
}

然后就可以给出实现类,代码如下:

package com.hys.spring.example6.impl;

import com.hys.spring.example6.pojo.Role;
import com.hys.spring.example6.service.RoleService;

public class RoleServiceImpl implements RoleService {

    public void printRole(Role role) {
        System.out.println("{id: " + role.getId() + ", " + "role_name: " + role.getRoleName() + ", " + "note: " + role.getNote() + "}");
    }
}

这里和普通编程的实现并没有太多不同。通过AOP来增强它的功能,为此需要一个切面类,代码如下:

package com.hys.spring.example6.aspect;

public class XmlAspect {

    public void before() {
        System.out.println("before...");
    }

    public void after() {
        System.out.println("after...");
    }

    public void afterReturning() {
        System.out.println("afterReturning...");
    }

    public void afterThrowing() {
        System.out.println("afterThrowing...");
    }
}

同样的也没有任何的注解,这就意味着需要我们使用XML去向Spring IoC容器描述它们。

4.1 前置通知、后置通知、返回通知和异常通知

由于前置通知、后置通知、返回通知和异常通知这4个通知都遵循上述AOP流程图中约定的流程,而且十分接近,所以放在一起讨论。下面进行配置,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

	<bean id="xmlAspect"
		class="com.hys.spring.example6.aspect.XmlAspect" />

	<bean id="roleService"
		class="com.hys.spring.example6.impl.RoleServiceImpl" />

	<aop:config>
		<!-- 引用xmlAspect作为切面 -->
		<aop:aspect ref="xmlAspect">
			<!-- 定义通知 -->
			<aop:before method="before"
				pointcut="execution(* com.hys.spring.example6.impl.RoleServiceImpl.printRole(..))" />
			<aop:after method="after"
				pointcut="execution(* com.hys.spring.example6.impl.RoleServiceImpl.printRole(..))" />
			<aop:after-returning method="afterReturning"
				pointcut="execution(* com.hys.spring.example6.impl.RoleServiceImpl.printRole(..))" />
			<aop:after-throwing method="afterThrowing"
				pointcut="execution(* com.hys.spring.example6.impl.RoleServiceImpl.printRole(..))" />
		</aop:aspect>
	</aop:config>

</beans>

这里首先通过引入的XML定义了AOP的命名空间,然后定义了一个roleService类和切面xmlAspect类,最后通过<aop:config>来定义AOP的内容信息。

  • <aop:aspect>:用于定义切面类,这里是xmlAspect。
  • <aop:before>:定义前置通知。
  • <aop:after>:定义后置通知。
  • <aop:after-returning>:定义返回通知。
  • <aop:after-throwing>:定义异常通知。

这些方法都会根据约定织入到流程中,但是这些通知拦截的方法都采用了同一个正则式来匹配,重写那么多的正则式显然有些冗余。和使用注解一样,也可以通过定义切点,然后引用到别的通知上,如下所示:

        <aop:config>
		<!-- 引用xmlAspect作为切面 -->
		<aop:aspect ref="xmlAspect">
			<!-- 定义切点 -->
			<aop:pointcut
				expression="execution(* com.hys.spring.example6.impl.RoleServiceImpl.printRole(..))"
				id="printRole" />
			<!-- 定义通知,引入切点 -->
			<aop:before method="before" pointcut-ref="printRole" />
			<aop:after method="after" pointcut-ref="printRole" />
			<aop:after-returning method="afterReturning"
				pointcut-ref="printRole" />
			<aop:after-throwing method="afterThrowing"
				pointcut-ref="printRole" />
		</aop:aspect>
	</aop:config>

通过这段代码就可以定义切点并进行引入,这样就可以避免多次书写同一正则式的麻烦,我们同样可以通过XML定义前置通知、后置通知、返回通知和异常通知。

4.2 环绕通知

和其他通知一样,环绕通知也可以织入到约定的流程当中,比如在上述XmlAspect切面类中加入一个新的方法,代码如下:

    public void around(ProceedingJoinPoint jp) {
        System.out.println("around before...");
        try {
            jp.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("around after...");
    }

ProceedingJoinPoint连接点在注解方式中讨论过,通过调度它的proceed方法就能够调用原有的方法了。这里沿用之前的配置,在XML中加入下面的配置即可使用这个环绕通知:

<aop:around method="around" pointcut-ref="printRole" />

这样,所有的通知都已经定义了,使用如下的代码对XML定义的切面进行测试:

package com.hys.spring.example6.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.hys.spring.example6.pojo.Role;
import com.hys.spring.example6.service.RoleService;

public class Test {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hys/spring/example6/config/spring-cfg.xml");
        RoleService roleService = ctx.getBean(RoleService.class);
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("role_name_1");
        role.setNote("role_note_1");
        roleService.printRole(role);
    }
}

这里读入了XML文件,然后通过容器获取了Bean,创建了角色类,然后打印角色,就能得到日志:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
before...
around before...
{id: 1, role_name: role_name_1, note: role_note_1}
around after...
afterReturning...
after...

显然所有的通知都被织入了AOP所约定的流程,但是请注意,环绕通知的before是在前置通知之后打印出来的,这说明了它符合上述的AOP流程图中关于AOP的约定。而使用@AspectJ注解方式要注意其执行的顺序,这点可能是版本更替留下的bug。

4.3 给通知传递参数

通过XML的配置,也可以引入参数到通知当中,下面以前置通知为例,来探讨它。

首先改写之前的XmlAspect切面类中的before方法:

    public void before(Role role) {
        System.out.println("role_id=" + role.getId() + " before...");
    }

此时带上了参数role,将XML中关于前置通知的配置修改为下面的代码:

<aop:before method="before"
				pointcut="execution(* com.hys.spring.example6.impl.RoleServiceImpl.printRole(..)) and args(role)" />

注意,和注解的方式有所不同的是,笔者使用and代替了&&,因为在XML中&有特殊的含义。再次运行测试代码,就可以通过断点监控参数了,如下图所示:

对其他通知也是通用的,这里不再过多赘述。

4.4 引入

在注解当中,我们探讨到了引入新的功能,也探讨了其实现原理,无论是使用JDK动态代理,还是使用CGLIB动态代理都可以将代理对象下挂到多个接口之下,这样就能够引入新的方法了,注解能做到的事情通过XML也可以。

这里在XmlAspect切面类中加入一个新的属性RoleVerifier类对象:

public RoleVerifier roleVerifier = null;

此时可以使用XML配置它,配置的内容和注解引入的方法相当,它是使用<aop:declare-parents>来引入的,下述代码就是通过使用它引入新方法的配置:

        <aop:declare-parents
				types-matching="com.hys.spring.example6.impl.RoleServiceImpl+"
				implement-interface="com.hys.spring.example6.service.RoleVerifier"
				default-impl="com.hys.spring.example6.impl.RoleVerifierImpl" />

显然它的配置和通过注解的方式十分接近,然后就可以参考之前的代码改造进行测试了,这里不再过多赘述。


5 经典Spring AOP应用程序

这是Spring早期所提供的AOP实现,在现实中几乎被废弃了,不过具有一定的讨论价值。它需要通过XML的方式来配置,例如要完成一个RoleServiceImpl类中printRole方法的切面前置通知的功能,这时可以把printRole称为AOP的连接点。先定义一个类来实现前置通知,它要求类实现MethodBeforeAdvice接口的before方法,代码如下:

package com.hys.spring.example6.aspect;

import java.lang.reflect.Method;

import org.springframework.aop.MethodBeforeAdvice;

public class ProxyFactoryBeanAspect implements MethodBeforeAdvice {

    /**
     * 
    * <p>Title: before</p>
    * <p>Description: 前置通知</p>
    * @param method 被拦截方法(连接点)
    * @param params 参数 数组[role]
    * @param roleService 被拦截对象
    * @throws Throwable
    * @see org.springframework.aop.MethodBeforeAdvice#before(java.lang.reflect.Method, java.lang.Object[], java.lang.Object)
    * @author houyishuang
    * @date 2018年8月26日
     */
    @Override
    public void before(Method method, Object[] params, Object roleService) throws Throwable {
        System.out.println("前置通知...");
    }
}

有了它还需要对Spring IoC容器描述对应的信息,这个时候需要一个XML文件来描述它,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

	<bean id="proxyFactoryBeanAspect"
		class="com.hys.spring.example6.aspect.ProxyFactoryBeanAspect" />

	<!-- 设定代理类 -->
	<bean id="roleService"
		class="org.springframework.aop.framework.ProxyFactoryBean">
		<!-- 这里代理的是接口 -->
		<property name="proxyInterfaces">
			<value>com.hys.spring.example6.service.RoleService</value>
		</property>

		<!-- 是ProxyFactoryBean要代理的目标类 -->
		<property name="target">
			<bean class="com.hys.spring.example6.impl.RoleServiceImpl" />
		</property>

		<!-- 定义通知 -->
		<property name="interceptorNames">
			<list>
				<!-- 引入定义好的spring bean -->
				<value>proxyFactoryBeanAspect</value>
			</list>
		</property>
	</bean>

</beans>

此时可以使用下述代码来测试这个前置通知:

package com.hys.spring.example6.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.hys.spring.example6.pojo.Role;
import com.hys.spring.example6.service.RoleService;

public class Test1 {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/hys/spring/example6/config/spring-cfg1.xml");
        RoleService roleService = ctx.getBean(RoleService.class);
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("role_name_1");
        role.setNote("role_note_1");
        roleService.printRole(role);
    }
}

通过运行这段代码,可以得到如下结果:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
前置通知...
{id: 1, role_name: role_name_1, note: role_note_1}

这样Spring AOP就被用起来了,它虽然很经典,但是已经不是主流方式了,所以不再进行更详细地讨论了。


6 多个切面

上面的例子讨论了一个方法只有一个切面的问题,而事实是Spring也能支持多个切面。在此之前要先定义一个连接点,为此新建一个接口——MultiBean,它十分简单,代码如下:

package com.hys.spring.example6.service;

public interface MultiBean {

    public void testMulti();
}

它的简单实现,代码如下:

package com.hys.spring.example6.impl;

import org.springframework.stereotype.Component;

import com.hys.spring.example6.service.MultiBean;

@Component
public class MultiBeanImpl implements MultiBean {

    @Override
    public void testMulti() {
        System.out.println("test multi aspects!!!");
    }
}

这样就定义好了连接点,那么现在需要切面:Aspect1、Aspect2和Aspect3进行AOP编程,这3个切面的定义,代码如下:

package com.hys.spring.example6.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Aspect1 {

    @Pointcut("execution(* com.hys.spring.example6.impl.MultiBeanImpl.testMulti(..))")
    public void print() {}

    @Before("print()")
    public void before() {
        System.out.println("before 1...");
    }

    @After("print()")
    public void after() {
        System.out.println("after 1...");
    }

    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("afterReturning 1...");
    }

    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("afterThrowing 1...");
    }
}

package com.hys.spring.example6.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Aspect2 {

    @Pointcut("execution(* com.hys.spring.example6.impl.MultiBeanImpl.testMulti(..))")
    public void print() {}

    @Before("print()")
    public void before() {
        System.out.println("before 2...");
    }

    @After("print()")
    public void after() {
        System.out.println("after 2...");
    }

    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("afterReturning 2...");
    }

    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("afterThrowing 2...");
    }
}

package com.hys.spring.example6.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class Aspect3 {

    @Pointcut("execution(* com.hys.spring.example6.impl.MultiBeanImpl.testMulti(..))")
    public void print() {}

    @Before("print()")
    public void before() {
        System.out.println("before 3...");
    }

    @After("print()")
    public void after() {
        System.out.println("after 3...");
    }

    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("afterReturning 3...");
    }

    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("afterThrowing 3...");
    }
}

这样3个切面就拦截了这个连接点,那么它的执行顺序是怎样的呢?为此我们来搭建运行的Java环境配置对此进行测试,代码如下:

package com.hys.spring.example6.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

import com.hys.spring.example6.aspect.Aspect1;
import com.hys.spring.example6.aspect.Aspect2;
import com.hys.spring.example6.aspect.Aspect3;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.hys.spring.example6")
public class MultiConfig {

    @Bean
    public Aspect1 getAspect1() {
        return new Aspect1();
    }

    @Bean
    public Aspect2 getAspect2() {
        return new Aspect2();
    }

    @Bean
    public Aspect3 getAspect3() {
        return new Aspect3();
    }
}

通过AnnotationConfigApplicationContext加载配置文件。在多次测试后发现其顺序是一定的,即按照MultiConfig配置类中切面从上到下的声明顺序进行AOP,得到如下的测试结果:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
before 1...
before 2...
before 3...
test multi aspects!!!
after 3...
afterReturning 3...
after 2...
afterReturning 2...
after 1...
afterReturning 1...

注意,在之前的版本中可能存在运行结果不唯一的情况,即切面执行顺序可能是随机的。

如果想自己控制切面的执行顺序,除了调整上述MultiConfig配置类中生成切面Bean的声明顺序之外,还可以给切面加入注解@Order,比如在Aspect1类中加入@Order(3):

package com.hys.spring.example6.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;

@Aspect
@Order(3)
public class Aspect1 {

    //do something...
}

给Aspect2类加入@Order(1),给Aspect3类加入@Order(2),再次对其进行测试,得到如下结果:

ERROR StatusLogger No log4j2 configuration file found. Using default configuration: logging only errors to the console.
before 2...
before 3...
before 1...
test multi aspects!!!
after 1...
afterReturning 1...
after 3...
afterReturning 3...
after 2...
afterReturning 2...

注意,@Order的顺序和MultiConfig配置类中生成切面Bean的声明顺序中,以@Order的顺序为准。也就是说,如果加上了@Order注解后,以该注解的顺序为准;没有配置@Order注解,则以MultiConfig配置类中生成切面Bean的声明顺序为准。

得到了预期结果,到了这个时候有必要对执行顺序进行更深层次的讨论。众所周知,Spring AOP的实现方法是动态代理,在多个代理的情况下,代理的顺序跟责任链模式的执行顺序相似。为了让读者更好地理解,这里画出该执行顺序,如下图所示:

上图展示了一条责任链,话句话说,Spring底层也是通过责任链模式来处理多个切面的,只要理解了这点,那么其执行的顺序也就很容易理解了。

上面只是一种实现多切面排序的方法,而事实是还有其他的方法,比如也可以让切面实现Ordered(org.springframework.core.Ordered)接口,它定义了一个getOrder方法。如果需要取代Aspect1中的@Order(1)的功能,那么将Aspect1改写为下述代码的样子:

 

package com.hys.spring.example6.aspect;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.Ordered;

@Aspect
public class Aspect1 implements Ordered {

    @Override
    public int getOrder() {
        return 1;
    }

    //do something...
}

也可以对Aspect2、Aspect3进行类似的改写,这样也可以指定切面的顺序。但这显然没有使用@Order注解方便,有时候也许你会想念XML,这也没有问题,你只需要在<aop:aspect>里增加一个属性order排序即可:

<aop:aspect ref="aspect1" order="1">
    //do something...
</aop:config>

到此关于多个切面的知识就讲解完了,读者还需要通过练习来掌握它。

 

参考资料:[1]杨开振 周吉文 梁华辉 谭茂华.Java EE 互联网轻量级框架整合开发:SSM框架(Spring MVC+Spring+MyBatis)和Redis实现.北京:电子工业出版社,2017.7

猜你喜欢

转载自blog.csdn.net/weixin_30342639/article/details/81837652