目录
前言
前一篇文章讲了一些关于动态代理和拦截器链的内容,Spring-动态代理和拦截器。因为动态代理就是 Spring AOP 的基础,所以可以先看下上篇文章,这样能更好的理解本篇文章。我们通过动态代理可以实现增强被代理对象的功能,Spring AOP 的作用也是为了增强被代理对象的,只不过它使得被代理对象的增强变得简单,更有利于开发使用。
AOP 的几个概念
我首先把 Spring 官网的解释拿过来,然后再尝试用我的理解进行一番解释。
- 通知(Advice)
其实就是我们需要增强的功能。比如标记了 @Before, @After 等注解的方法就是一个通知,它定义了在要在切点进行的一些行为,即定义了我们要如何增强。
- 连接点(JoinPoint)
连接点就是程序允许通知增强的地方,比如在执行方法前,执行方法后或者是跑出异常的时候。Spring AOP 只支持方法连接点,即只能以方法的维度进行增强。而不像其他的框架还能在构造器或者属性赋值的时候进行增强。
- 切点(Pointcut)
连接点是一个概念性的东西,比如连接点的概念告诉你 Spring AOP 可以以方法的维度进行增强。但是可以做并不是一定会做。因为我们不可能对所有的方法都进行增强,而切点就是要说明具体要在哪个连接点,即要对哪个方法进行增强。
- 切面(Aspect)
在 Spring AOP 中,如果一个类上面标注了 @Aspect 注解,那么它就是一个切面。其实想很好的理解切面,首先得理解切点和通知,切面是切点和通知的结合,其实如果看了上篇文章的同学应该意识到,它跟 Spring AOP 中的 Advisor 很像, 只不过 Advisor 只包含一个切点和通知,而切面可以定义很多切点和通知,不过最后 Spring 最后都会将其解析成一个个 Advisor, 其实它像是一个概念性的东西,比如说是一个公司,公司本身不能干活,是公司里的员工在干活。切面负责管理,真正做事情的是它包含的切点和通知。
-
引入 (Introduction)
引入是可以在现有的接口中引入其他的接口,以增强当前接口的功能
-
目标(target)
即被代理对象,需要被增强的对象
- 代理(proxy)
目标对象被代理后就会成为一个代理对象。相信看了上篇文章的同学都能理解
- 织入(Weaving)
织入可以理解为把切面应用到目标对象创建代理的过程,有些框架是编译时织入,即直接在被代理对象的代码附近修改源代码,有的框架是在运行时织入,比如Spring AOP 的代理动态代理方式,它不会改变被代理对象的代码。
代码样例
- 引入需要的依赖
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.6</version> </dependency>
- 测试接口和实现类
public interface IOrder { Integer query(String type); } @Service public class OrderService implements IOrder { @Override public Integer query(String type) throws Exception { System.out.println("查询类型为" + type + "订单数量"); //模拟查询过程中出错 try { //int a = 1 / 0; } catch (Exception ex) { throw new Exception("我错了"); } return 1; } }
- 添加切面类
@Aspect public class QueryAspect { @Pointcut("execution(* com.proxy.IOrder.query(..))") public void pointCut(){}; @Before(value = "pointCut()") public void methodBefore(JoinPoint joinPoint) throws Exception { System.out.println("调用目标方法前 @Before 可以提前获取到参数信息:" + Arrays.toString(joinPoint.getArgs())); //模拟 before 出错 //int a = 1 / 0; } @After(value = "pointCut()") public void methodAfter(JoinPoint joinPoint) { System.out.println("调用目标方法后 @After"); // 模拟 After 异常 int a = 1 / 0; } @AfterReturning(value = "pointCut()", returning = "result") public void methodAfterReturning(JoinPoint joinPoint, Object result) { System.out.println("目标方法返回后 @AfterReturning, 此时可以获取到返回结果 " + result); //模拟 AfterReturning 出错 //int a = 1 / 0; } @AfterThrowing(value = "pointCut()", throwing = "ex") public void methodAfterThrowing(JoinPoint joinPoint, Exception ex) { System.out.println("抛出异常后, @AfterThrowing " + ex); } //@Around(value = "pointCut()") public Object methodAround(ProceedingJoinPoint pdj) { Object result = null; System.out.println("调用目标方法前 @Around "); try { //调用目标方法 result = pdj.proceed(); } catch (Throwable ex) { System.out.println("捕捉到异常信息:" + ex); } System.out.println("调用目标方法后 @Around "); return result; } }
- 添加配置类,配置切面 bean
@Configuration @EnableAspectJAutoProxy @ComponentScan(value = {"com.proxy"}) public class AopConfig { @Bean public QueryAspect queryAspect() { return new QueryAspect(); } }
- 编写测试类
public class ProxyMain { public static void main(String[] args) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AopConfig.class); IOrder proxy = (IOrder) applicationContext.getBeanFactory().getBean("orderService"); try { Object result = proxy.query("鸿星尔克"); System.out.println("查询结果为:" + result); } catch (Exception e) { System.out.println("测试类捕捉到错误:" + e); } } }
- 先注释掉@Around的通知,查看运行结果
-
1:当没有异常的时候 调用目标方法前 @Before 可以提前获取到参数信息:[鸿星尔克] 执行目标方法,查询类型为鸿星尔克订单数量 目标方法返回后 @AfterReturning, 此时可以获取到返回结果 1 调用目标方法后 @After 查询结果为:1 2:当目标方法有异常的时候 调用目标方法前 @Before 可以提前获取到参数信息:[鸿星尔克] 执行目标方法,查询类型为鸿星尔克订单数量 抛出异常后, @AfterThrowing java.lang.Exception: 我错了 调用目标方法后 @After 测试类捕捉到错误:java.lang.Exception: 我错了 可见,当目标方法有异常的时候,@AfterReturning 的通知就不会执行了,但是 @After 的通知还会继 续执行 3: 当 @Before 通知异常的时候 调用目标方法前 @Before 可以提前获取到参数信息:[鸿星尔克] 测试类捕捉到错误:java.lang.ArithmeticException: / by zero 可见,@AfterThrowing 没有捕获 @Before 通知的异常,@After 和 @AfterReturning 的通知都没有执行 4: 当 @AfterReturning 通知异常的时候 调用目标方法前 @Before 可以提前获取到参数信息:[鸿星尔克] 执行目标方法,查询类型为鸿星尔克订单数量 目标方法返回后 @AfterReturning, 此时可以获取到返回结果 1 调用目标方法后 @After 测试类捕捉到错误:java.lang.ArithmeticException: / by zero 可见,@AfterThrowing 没有捕获 @AfterReturning 通知的异常,且 @After 通知会继续执行 5: 当 @After 通知异常的时候 调用目标方法前 @Before 可以提前获取到参数信息:[鸿星尔克] 执行目标方法,查询类型为鸿星尔克订单数量 目标方法返回后 @AfterReturning, 此时可以获取到返回结果 1 调用目标方法后 @After 测试类捕捉到错误:java.lang.ArithmeticException: / by zero 可见,三个通知都会执行
- 由上面的执行结果,我们可以发现
- @AfterThrowing 只可以捕捉到切点的异常。
- @Before 通知一定会执行,且如果执行过程中有异常,就不会再进行下面的步骤了。
- @After 通知在 @AfterReturning 通知之后再执行,且如果切点方法抛出异常的话,那么@AfterReturning 通知就不会执行了。
- 这三个通知的执行过程大概如下所示
// @Before 通知先执行 try { try { //@Before 通知如果没有异常,就会在此时执行目标方法 } catch (Throwable throwable) { //目标方法发生异常,会在此时执行 @AfterThrowing 通知 throw throwable; } //如果目标方法没有跑出异常,此时执行 @AfterReturn 的通知 } catch (Exception exception) { } finally { //只要@Before 不抛出异常,就会执行 @After 通知 }
- 上述三个通知,想必大家都知道怎么使用了,那么 @Around 通知呢?我们把其他三个通知都注释掉,只保留@Around通知观察一下执行结果
@Around(value = "pointCut()") public Object methodAround(ProceedingJoinPoint pdj) { Object result = null; System.out.println("调用目标方法前 @Around "); try { //调用目标方法 result = pdj.proceed(); } catch (Throwable ex) { System.out.println("捕捉到异常信息:" + ex); } System.out.println("调用目标方法后 @Around "); return result; }
-
执行结果
1:当切点方法没有异常的时候 调用目标方法前 @Around 执行目标方法,查询类型为鸿星尔克订单数量 调用目标方法后 @Around 查询结果为:1 2: 当切点方法有异常的时候 调用目标方法前 @Around 执行目标方法,查询类型为鸿星尔克订单数量 捕捉到异常信息:java.lang.Exception: 我错了 调用目标方法后 @Around 查询结果为:null
-
可以看到,其实 @Around 通知的功能比另外三个强大一些,它把切点方法都切入到通知中,可以在切点方法的各个地方进行增强。可以说是具备上述三个通知的所有功能。而上面三个通知各司其职,只能完成某一个增强功能。所以说。如果增强的需求很简单,只需要在某个点增强。那么选择上面的通知就好了。其优点在于简单高效。如果需要增强的功能很复杂需要在多个连接点进行增强,那么就需要使用 @Around 了大展身手了。其优点在于功能性强。
几种通知的常用场景
- @Before 通知可以提前获取到目标方法的参数信息,故可以用作权限认证(某些参数可以接着往下走,而有些就到此为止)。也可以用作保存接口访问纪录(因为这个通知一定会执行,其他的如果出了异常可能就保存不了这个访问纪录了)
- @AfterReturning 通知可以获取到目标方法的执行结果,故它可以对获取到的结果进行增强,比如可以对结果添加一些信息或者删除一些信息。也可以将目标方法的结果保存一份或者发送给其他系统一份。主要是为了对结果进行增强。
- @After 通知,因为无论目标方法无论失败与否都会执行该通知,所以可以用它执行一些关闭资源的操作。
- @AfterThrowing 通知,主要是对异常情况进行一些处理,比如可以进行数据备份与还原
- @Around 通知,主要在一些需要比较复杂的增强的时候使用。
关于@Pointcut
其实从上面我们也应该已经知道了,它是定义一个切点的,如上例中
@Pointcut("execution(* com.proxy.IOrder.query(..))")
public void pointCut(){};
其中 @Pointcut 指定了切点方法,pointCut() 方法是个签名,签名的作用只是为了使用切点时候的简化,不用一遍一遍写 execution(* com.proxy.IOrder.query(..))
@Pointcut 中除了execution 还有其它的可选项,比如 within, target, args, annotation 等,而execution 是最常用的匹配方法的标签,本文只介绍这一个。
excecution()的语法如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
- modifier-pattern: 表示方法的修饰符,如 public, private等。
- ret-type-pattern: 表示方法的返回值类型,* 表示任何类型返回值
- declaring-type-pattern: 表示方法所在的类的路径。
- name-pattern: 表示方法名,可以用完整方法名,也可以用query*表示匹配所有query开头的方法
- param-pattern: 表示参数类型,可以指定多个参数类型,用逗号隔开,也可以用 * 匹配任意类型的参数,比如 (int, *) 表示匹配第一个参数类型为 int,第二个为任意类型的方法。也可以用(..) 来匹配零个或多个任意的方法的参数。
- throws-pattern: 表示方法抛出的异常
其中带?的修饰符不是必填项,多个execution之间还可以使用 &&(与),|| (或),! (非表达式)
- @Pointcut("execution(Integer com.proxy.IOrder.query(..))") : 表示匹配 Integer 类型的query方法,但是方法的参数不限制。
- @Pointcut("execution(* com.proxy.IOrder.query(int))"): 表示匹配任意类型的query方法,但是参数列表中只能有一个 int 类型的参数。
- @Pointcut("execution(Integer com.proxy.IOrder.query(String)) || execution(Integer com.proxy.IOrder.query(int))"):表示匹配参数类型为 String 或 int 的 query 方法
总结
本文介绍了AOP的一些概念,且用实例介绍了 Spring AOP 定义切面,切点和通知的方式,还介绍了不同通知的使用场景。本文作为 Spring AOP 的入门篇,主要侧重于代码使用。旨在看了该文章之后知道怎么使用 Spring AOP。