AOP 面向切面编程
面向切面编程(Aspect Oriented Programming)是一种编程的思维模式,已经在软件工程/设计模式
章节中有所介绍,关于什么是AOP这里就不多说了。
Spring框架中,自带了SpringAOP,此外还集成了AspectJ框架实现AOP,后者因为使用非常方便,性能也较好,因此使用较多。
AOP的一些术语
通知 Advice:切面的工作被称为通知。通俗的说,就是调用目标方法前或后,执行的代码(切面)。
- 前置通知:目标方法调用前调用通知功能
- 后置通知:目标方法调用后调用通知功能
- 返回通知:目标方法成功执行之后调用通知功能
- 异常通知:目标方法抛出异常时通知功能
- 环绕通知:目标方法调用前和调用后执行通知功能
连接点 Join Point:应用过程中,能够插入切面的点。
切点 Poincut:匹配通知所要织入的一个或多个连接点。
切面 Aspect:通知和切点的结合,即:在何时和何处完成其功能。
引入 Introduction:引入,允许我们向现有的类添加新方法或属性。
织入 Weaving:把切面应用到目标对象并创建新的代理对象的过程。
可以进行织入的阶段:
- 编译期织入:例如AspectJ的织入编译器
- 类加载期织入:需要特殊的类加载器,如AspectJ 5的LTW
- 运行时织入:使用Java语言提供的动态代理实现,SpringAOP就采用这种方式
总的来说,Spring的AOP是基于动态代理实现的,被代理的Bean载入类容器时,代理类就会被创建,同时,基于动态代理也导致了Spring只支持方法级别的连接点。
引入Maven依赖
启动AOP功能需要引入spring-aop
包和AspectJ框架相关依赖:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.20</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
切入点表达式
切点需要使用AspectJ的切入点表达式来定义,实际上由于Spring只支持方法级别的AOP,因此我们使用的只是AspectJ切入点表达式的一个子集。
有关如何编写切入点表达式,其实就是方法签名中间包含若干通配符,我们直接上一张图:
例子:
execution(public * * (..)) 任意公共方法
execution(* set* (..)) 任意以set开通的方法
execution(* com.gacfox.demo.MyService.* (..)) com.gacfox.demo.MyService下所有方法
execution(* com.gacfox.demo.*.* (..)) com.gacfox.demo包下所有类的方法
execution(* com.gacfox.demo..*.* (..)) com.gacfox.demo包及子包下所有类的方法
使用注解创建切面
可用的通知类型注解:
@After
目标方法返回或抛出异常调用@AfterRunning
目标方法返回后调用@AfterThrowing
目标方法抛出异常后调用@Around
环绕通知@Before
目标方法调用之前执行
在使用AOP注解前,applicationContext.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.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.gacfox.demo" />
<aop:aspectj-autoproxy />
</beans>
下面是一个使用AOP切面的例子:
MyAspect.java
package com.gacfox.demo;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
@Before("execution(* com.gacfox.demo.UtilA.doA(..))")
public void before() {
System.out.println("before");
}
@After("execution(* com.gacfox.demo.UtilA.doA(..))")
public void after() {
System.out.println("after");
}
}
UtilA.java
package com.gacfox.demo;
import org.springframework.stereotype.Component;
@Component
public class UtilA {
public void doA() {
System.out.println("utilA");
}
}
当我们调用UtilA.doA()方法时,会得到如下输出:
before
utilA
after
注意,切面类也必须使用@Component
托管在类容器中,不然切面是不起作用的。
改进切面的写法 @Pointcut
上面切面中,我们写了两次同样的切入点表达式,这个实际上可以用一个@Poincut
注解进行简化。
MyAspect.java
package com.gacfox.demo;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* com.gacfox.demo.UtilA.doA(..))")
public void doA() {
}
@Before("doA()")
public void before() {
System.out.println("before");
}
@After("doA()")
public void after() {
System.out.println("after");
}
}
环绕通知
我们看一个环绕通知的例子:
package com.gacfox.demo;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* com.gacfox.demo.UtilA.doA(..))")
public void doA() {
}
@Around("doA()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {
try {
System.out.println("before");
Object retValue = proceedingJoinPoint.proceed();
System.out.println("after");
return retValue;
} catch (Throwable throwable) {
throwable.printStackTrace();
throw new RuntimeException();
}
}
}
环绕通知切面方法,有一个参数ProceedingJoinPoint
,实际上proceedingJoinPoint.proceed()
就是目标方法的调用。
处理目标方法的参数
上面的例子中,我们的目标方法没有参数。实际上,切面也可以对参数进行拦截:
UtilA.java
package com.gacfox.demo;
import org.springframework.stereotype.Component;
@Component
public class UtilA {
public void doA(int i) {
System.out.println("utilA:" + i);
}
}
MyAspect.java
package com.gacfox.demo;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAspect
{
@Pointcut("execution(* com.gacfox.demo.UtilA.doA(int)) && args(i)")
public void doA(int i) {}
@Before(value = "doA(i)", argNames = "i")
public void before(int i)
{
System.out.println("aspect:" + i);
System.out.println("before");
}
}
输出结果:
aspect:1
before
utilA:1