AOP 全称 Aspect Oriented Programming ,即面向切面编程。通俗来说,我们可以在一个方法执行之前做一些操作,比如修改一下参数,在一个方法之后做一些操作,比如修改一下返回值。我们可以把这些要做的操作,放到一个类,或者是方法中,这个类或者方法,可以称作一个切面。
面向切面编程:即我们需要针对这些切面编程。编程的关注点不在于方法的逻辑而在于方法的执行前,执行后的逻辑。
一个切面大体包括两个部分,切点和在切点处要做的操作(官方叫:通知。一直没有理解这个叫法的含义 ^_^)。
切点可以以是一个或者一些具体的方法。
在切点处的操作又分为下面几种情况:
创建一个 AopService 接口,实现类的接口如下:
@Service
public class AopServiceImpl implements AopService {
@Override
public AccountInfo aopHello(AccountInfo accountInfo) {
accountInfo.setPwd("123");
return accountInfo;
}
}
为了方便测试,我们为这个 Service 创建一个 Controller,代码如下:
@RestController
public class AopController {
private static Logger logger = LoggerFactory.getLogger(AopController.class);
@Autowired
private AopService aopService;
@GetMapping("/helloAop/{name}")
public AccountInfo helloAop(@PathVariable("name") String name) {
logger.info("AOP 接口入参:{}", name);
AccountInfo accountInfo = new AccountInfo();
accountInfo.setName(name);
accountInfo = aopService.aopHello(accountInfo);
logger.info("AOP 接口出参:{}", accountInfo);
return accountInfo;
}
}
下面我们会为 aopHello 这个方法添加切面。对方法的执行前,执行后做一些处理。
首先需要添加 AOP 的依赖:
<!-- 引入 aop 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
然后创建一个切面。新建 AopAspect 类,代码如下:
import org.aspectj.lang.annotation.*;
@Aspect
@Component
public class AopAspect {
private static Logger logger = LoggerFactory.getLogger(AopAspect.class);
}
好了,这个切面是创建好了,现在我们还没有为其指定具体的切点以及切点处的操作
我们把这个切点指定到上面创建的 AopServiceImpl 的 aopHello 方法上:
/**
* 类 AopServiceImpl 下的 aopHello 方法为切入点
*/
@Pointcut("execution(public * com.zdran.springboot.service.impl.AopServiceImpl.aopHello(..))")
public void pointCut() {}
切点表达式。切点表达式可以清晰的表示一个或者一些方法。
格式: execution([可见性] 返回类型 [声明类型].方法名(参数) [异常])
支持以下通配符:
现在我们为这个切面指定了切点。下面我们定义一些在这个切点处的操作。
我们在这个方法执行前做一些操作,国际惯例,先打印个 hello aop
/**
* 在方法执行之前执行
*
* @param joinPoint
*/
@Before(value = "pointCut()")
public void doBefore(JoinPoint joinPoint) {
logger.info("doBefore run: hello aop");
}
你也可以将 Before 注解里的 value 值换成 execution 表达式。 意思是这个操作指定在某个切点上。
代码里的这种做法是把切点抽出来了,你可以理解为把这个切点定义成了一个变量,而不是每次使用的时候直接使用字符串了。
下面我们对请求参数进行修改。
/**
* 在方法执行之前执行
*
* @param joinPoint
*/
@Before(value = "pointCut()")
public void doBefore(JoinPoint joinPoint) {
logger.info("doBefore run");
AccountInfo accountInfo = (AccountInfo) joinPoint.getArgs()[0];
logger.info("AOP:{}", accountInfo.toString());
accountInfo.setName("aop");
}
joinPoint.getArgs() 返回的是一个数组,我们取第一个参数,强转成 AccountInfo 类型,并且修改参数值。
可以访问我们之前写的 Controller 测试一下。
后置通知是指在方法执行后做的一些操作,代码如下:
/**
* 在方法之后执行,可以对方法返回值进行修改
*
* @param point
* @param returnValue
*/
@AfterReturning(value = "pointCut()", returning = "returnValue")
public void doAfterReturning(JoinPoint point, AccountInfo returnValue) {
logger.info("doAfterReturning:{}", returnValue);
returnValue.setPwd("doAfterReturning");
}
AfterReturning 注解的 returning 属性不是必须的,如果你不需要对方法的返回值进行操作,的话,可以不要这个属性。
同样的,方法签名里的第二个参数也不是必须的。但是,方法签名里的第二个参数,是与 returning 属性绑定的,所以属性的值和参数名称必须保持一致。
方法中的第二个参数就是返回值,我们可以直接修改。
这个通知是在 方法之外 的 finally 块中的操作,所以这个操作的执行顺序是在 AfterReturning 之后执行的。
/**
* 在方法执行之后执行
*
* @param joinPoint
*/
@After(value = "pointCut()")
public void doAfter(JoinPoint joinPoint) {
logger.info("doAfter run");
}
与 AfterReturning 的最大区别可能就是这个通知不能修改返回值。
异常通知,是指当在执行方法时,抛出异常后的操作,或者说是 方法之外 的 catche 块中的操作。代码如下:
/**
* 在方法抛出异常时执行,执行顺序在 After 之后
*
* @param ex
*/
@AfterThrowing(value = "pointCut()", throwing = "ex")
public void doAfterThrowing(Throwable ex) {
logger.info("doAfterThrowing run");
logger.error("doAfterThrowing:", ex);
}
需要注意的是,这个注解的方法是在 After 之后执行的。异常通知不能对返回值做任何操作。
环绕通知相比于上面几个通知是最强大的一个通知。它不仅可以修改参数,修改返回值,还可以决定要不要调用切点处的方法。
需要注意的是,这个环绕通知是在 前置通知之前执行的。
/**
* 环绕通知
*
* @param joinPoint
*/
@Around(value = "pointCut()")
public AccountInfo doAround(ProceedingJoinPoint joinPoint) {
logger.info("doAround run");
AccountInfo accountInfo = (AccountInfo) joinPoint.getArgs()[0];
//在方法被执行前,修改参数
accountInfo.setBalance(123);
try {
//执行的实际方法
joinPoint.proceed();
} catch (Throwable throwable) {
return null;
}
//在方法执行后修改返回值
accountInfo.setName("around");
return accountInfo;
}
joinPoint.proceed(); 是实际要执行的方法,即我们 AopServiceImpl.aopHello() 方法,如果你不调用 proceed() 方法就不会执行 aopHello(),这样我们就可以控制到底要不要执行切点处的方法。甚至,我们可以在切点处执行别的方法。
通过上面的一些实例我们可以简单的整理一下这些通知的执行顺序:
//@Around
try {
try {
//@Before
method.invoke(..);
} finally {
//@After
}
//@AfterReturning
} catch (Exception e) {
//@AfterThrowing
}
完整代码见: Spring Boot 学习笔记 源码地址