对于初学者,建议先阅读《Spring框架汇总(Spring AOP——理论基础)》与《Spring框架汇总(Spring AOP——基于XML的简单案例)》。
首先我们要知道Spring AOP支出五种通知的类型:
1.前置通知(before):先于目标方法执行,如果前置通知抛出异常则不执行目标方法;
2.返回通知(after-returning):目标方法执行结束,返回后执行,若前置通知或目标方法抛出异常则不执行;
3.异常通知(after-throwing):如果执行过程中抛出了可被异常通知捕捉的异常,则执行异常通知;
4.后置通知(after):在返回通知或异常通知之后执行,无论执行过程是否抛出过异常都会执行;
5.环绕通知(around):环绕目标方法执行,起点在前置通知之后,终点在后置通知之前,一般在需要对目标方法等参数进行修改或决定目标方法是否执行时使用,并且与以上四种通知分开使用,如果一起使用会打乱以上四种通知的联系,影响在有异常抛出的情况下的执行情况。
开始编写测试程序前,我们需要先引入jar包依赖(Maven项目):
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>
<!-- spring底层AOP的实现,需要依赖于AspectJ框架(不要引入1.8.5版本,会有版本冲突问题) -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
<!-- 引入单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
</dependency>
</dependencies>
编写业务接口和实现类:
package com.test.service;
/**
* 核心业务接口
* @author Elio
*/
public interface HelloService {
void sayHello(String msg);
void sayBye(String msg);
}
package com.test.service;
import org.springframework.stereotype.Service;
/**
* 核心业务实现
* @author Elio
*/
@Service
public class HelloServiceImpl implements HelloService {
public void sayHello(String msg) {
System.out.println("hello "+msg);
}
public void sayBye(String msg) {
System.out.println("bye "+msg);
}
}
一、为书写方便,我们先来测试环绕通知与其他四种通知混用的情况(实际上这种情况很少使用)
编写一个高级切面:
package com.test.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
public class AdvencedAspect {
/**
* 前置通知,希望此方法在核心业务方法执行之前执行
*/
public void beforeMethod() {
System.out.println("before");
}
/**
* 后置通知,希望此方法在核心业务方法执行之后执行
*/
public void afterMethod() {
System.out.println("after");
}
/**
* 返回通知,希望此方法在核心业务方法返回结果之后执行
*/
public void returnMethod() {
System.out.println("returned");
}
/**
* 异常通知,希望此方法在核心业务方法抛出异常之后执行
*/
public void thrownMethod() {
System.out.println("thrown");
}
/**
* 环绕通知
* @param joinPoint 连接点对象,由Spring自行创建并注入
*/
public void aroundMethod(ProceedingJoinPoint joinPoint) {
System.out.println("around begin");
try {
joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("around stop");
}
}
在XML配置文件中完成配置:
<!-- 开启扫描包创建实例 -->
<context:component-scan base-package="com.test.service"/>
<!-- 创建用于执行扩展业务的实例 -->
<bean id="advencedAspect" class="com.test.aspect.AdvencedAspect" />
<!-- 配置AOP方案 -->
<aop:config>
<!-- Spring AOP支持方法级别的精细切入点,且支持通配符 -->
<aop:pointcut expression="execution(* com.test.service.HelloServiceImpl.say*(..))"
id="helloPt"/>
<!-- 定义一个或多个切面,这里要注意切点的定义要在切面之前 -->
<aop:aspect ref="advencedAspect">
<!-- 定义前置通知(切入点指向的业务方法之前执行,如果抛出异常则不执行核心业务方法) -->
<aop:before method="beforeMethod" pointcut-ref="helloPt" />
<!-- 定义返回通知(切入点指向的业务方法返回结果之后执行,如果前置通知或核心业务抛出异常则不执行) -->
<aop:after-returning method="returnMethod" pointcut-ref="helloPt" />
<!-- 定义异常通知(切入点指向的业务方法抛出异常之后执行) -->
<aop:after-throwing method="thrownMethod" pointcut-ref="helloPt"/>
<!-- 定义后置通知 (切入点指向的业务方法之后,无论是否抛出异常都会执行) -->
<aop:after method="afterMethod" pointcut-ref="helloPt" />
<!-- 定义环绕通知,参数由Spring容器自行创建并传递 -->
<aop:around method="aroundMethod" pointcut-ref="helloPt"/>
</aop:aspect>
</aop:config>
切入点表达式expression有三种:
bean | 实例级别的粗粒度控制:bean(helloServiseImpl) bean(*ServiceImpl) |
within | 类级别的粗粒度控制:within(com.test.service.HelloServiceImpl) within(com.test.service.*) within(com.test.service..*) |
execution | 方法级别的细粒度控制:execution(void com.test.service.HelloServiceImpl.sayHello()) execution(* com.test.service..*.*(..)) |
编写测试类的测试方法:
/**
* 测试具体业务应用
*/
@Test
public void testSayHello(){
//1.获取对象
HelloService hService=ctx.getBean("helloServiceImpl",HelloService.class);
//2.执行业务
hService.sayHello("handsome boy");
}
输出结果:
before
around begin
hello handsome boy
around stop
after
returned
这里“after”在“return”前输出,我一开始的理解是,可能和try-catch-finally块的运行顺序有关,先执行finally块中的语句,后执行try块中的return关键字后的语句,但是后来的测试又出现后置通知在异常通知之前执行的现象,这又有悖于try-catch-finally块的运行顺序。关于这一处的现象,我不太理解,欢迎各位同学和大神留言赐教。
接下来测试抛出异常的情况。
1.先将被拦截的方法修改,添加能够抛出异常的语句,例如:int i=4/0。
public void sayHello(String msg) {
System.out.println("hello "+msg);
//会抛出运行时异常ArithmeticException
int i=4/0;
}
输出结果:
before
around begin
hello handsome boy
java.lang.ArithmeticException: / by zero ......
around stop
after
returned
输出结果显示,异常通知并没有捕获到目标方法中的抛出异常,异常由环绕通知自行处理了。
2.前置通知抛出异常:
public void beforeMethod() {
System.out.println("before");
int i=4/0;
}
输出结果:
before
前置通知抛出的异常不会被异常通知捕捉。
3.返回通知抛出异常:
输出结果:
before
around begin
hello handsome boy
around stop
after
returned
返回通知抛出的异常不会被异常通知捕捉。
4.后置通知抛出异常:
输出结果: before
around begin
hello handsome boy
around stop
after
thrown
后置通知抛出的异常被异常通知捕捉,从抛出异常处直接进入异常通知处理异常,跳过了返回通知。
5.环绕通知抛出异常:
before
around begin
after
thrown
环绕通知抛出的异常被异常通知捕捉,从抛出异常处直接进入异常通知处理异常。
可见如果将环绕通知与其他四种通知混用,在正常执行的情况下没有问题,可是如果执行过程中有异常抛出,异常处理的方式和结果有些混乱。
二、接下来测试只使用前四种通知的情况
先将XML配置文件中定义环绕通知的代码注释即可:
<!-- 开启扫描包创建实例 -->
<context:component-scan base-package="com.test.service"/>
<!-- 创建用于执行扩展业务的实例 -->
<bean id="advencedAspect" class="com.test.aspect.AdvencedAspect" />
<!-- 配置AOP方案 -->
<aop:config>
<!-- Spring AOP支持方法级别的精细切入点,且支持通配符 -->
<aop:pointcut expression="execution(* com.test.service.HelloServiceImpl.say*(..))"
id="helloPt"/>
<!-- 定义一个或多个切面,这里要注意切点的定义要在切面之前 -->
<aop:aspect ref="advencedAspect">
<!-- 定义前置通知(切入点指向的业务方法之前执行,如果抛出异常则不执行核心业务方法) -->
<aop:before method="beforeMethod" pointcut-ref="helloPt" />
<!-- 定义返回通知(切入点指向的业务方法返回结果之后执行,如果前置通知或核心业务抛出异常则不执行) -->
<aop:after-returning method="returnMethod" pointcut-ref="helloPt" />
<!-- 定义异常通知(切入点指向的业务方法抛出异常之后执行) -->
<aop:after-throwing method="thrownMethod" pointcut-ref="helloPt"/>
<!-- 定义后置通知 (切入点指向的业务方法之后,无论是否抛出异常都会执行) -->
<aop:after method="afterMethod" pointcut-ref="helloPt" />
<!-- 定义环绕通知,参数由Spring容器自行创建并传递 -->
<!-- <aop:around method="aroundMethod" pointcut-ref="helloPt"/> -->
</aop:aspect>
</aop:config>
输出结果:
before
hello handsome boy
returned
after
这里的“after”和“returned”又恢复了定义中指定的顺序。
接下来测试不同异常抛出位置的输出情况:
1.被拦截的方法抛出异常:
before
hello handsome boy
thrown
after
2.前置通知抛出异常:
before
thrown
after
3.返回通知抛出异常:
before
hello handsome boy
returned
thrown
after
1、2、3三种情况,异常都被异常通知捕捉,从抛出异常处直接进入异常通知处理异常,并执行后置通知。
4.后置通知抛出异常:
before
hello handsome boy
returned
after
后置通知抛出的异常不会被异常通知捕捉。
这里单独使用前四种通知时,“after”语句每次都是最后才会输出,这也与第一种情况不同,希望知道原因的同学能给点指点。对比一、二两种情况,我们一般不会将环绕通知和其他四种混用,环绕通知完全可以实现其他四种通知的所有功能,并且还要更加强大。
关于环绕通知的具体使用将在《Spring框架汇总(Spring AOP——基于注解的进阶案例)》中讲解。