一、AOP简介
AOP(Aspect Oriented Programming) ,面向切面思想,是Spring的三大核心思想之一(其余两个:IOC - 控制反转 、DI - 依赖注入 )。
那么AOP为何那么重要呢?
在我们的程序中,经常存在一些系统性的需求,比如 权限校验 、日志记录 、统计 等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护,那么面向切面编程往往让我们的开发更加低耦合,也大大减少了代码量,同时呢让我们更专注于业务模块的开发,把那些与业务无关的东西提取出去,便于后期的维护和迭代。
二、AOP体系与概念
简单地去理解,其实AOP要做三类事:
AOP的体系图: 一些概念:
概念 | 说明 |
---|
Pointcut | 切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为execution 方式和 annotation 方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。 | Advice | 处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。 | Aspect | 切面,即 Pointcut 和 Advice 。 | Joint point | 连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。 | Weaving | 织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。 |
三、AOP实例
1、创建SpringBoot工程
如何创建详见:IDEA 创建 SpringBoot 项目
2、添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3、AOP相关注解
package com.cw.tsb.app.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class ControllerAspect {
@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
public void pointCut() {
}
@Around("pointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
System.out.println("------------- doAround.");
Object obj = null;
try {
obj = joinPoint.proceed();
} catch (Throwable t){
t.printStackTrace();
}
return obj;
}
@After("pointCut()")
public void doAfter(JoinPoint joinPoint){
System.out.println("------------- doAfter.");
}
@Before("pointCut()")
public void doBefore(JoinPoint joinPoint){
System.out.println("------------- doBefore.");
}
@AfterReturning(value = "pointCut()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, String result){
System.out.println("doAfterReturning result = " + result);
}
@AfterThrowing(value = "pointCut()", throwing = "t")
public void doAfterThrowing(JoinPoint joinPoint, Throwable t){
System.out.println("------------- doAfterThrowing throwable = " + t.toString());
}
}
3.1、@Aspect
该注解要添加在类上,声明这是一个切面类,使用时需要与@Component注解一起用,表明同时将该类交给spring管理。
@Component
@Aspect
public class ControllerAspect {
}
3.2、@Pointcut
用来定义一个切点,即上文中所关注的某件事情的入口,切入点定义了事件触发时机。
该注解需要添加在方法上,该方法签名必须是 public void 类型,可以将@Pointcut 中的方法看作是一个用来引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此 @Pointcut 中的方法只需要方法签名,而不需要在方法体内编写实际代码 。
该注解有两个常用的表达式:execution() 和 annotation() 。
3.2.1、execution()
@Aspect
@Component
public class ControllerAspect {
@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
public void pointCut() {
}
}
表达式为:
execution(* com.cw.tsb.app.controller..*.*(..))
3.2.2、annotation()
annotation() 方式是针对某个注解来定义切点,比如我们对具有 @PostMapping 注解的方法做切面,可以如下定义切面:
@Aspect
@Component
public class ControllerAspect {
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void pointCut() {
}
}
然后使用该切面的话,就会切入注解是 @PostMapping 的所有方法。这种方式很适合处理 @GetMapping 、@PostMapping 、@DeleteMapping 不同注解有各种特定处理逻辑的场景。
还有就是如上面案例所示,针对自定义注解来定义切面。
@Aspect
@Component
public class ControllerAspect {
@Pointcut("@annotation(com.cw.tsb.app.annotation.PermissionsAnnotation)")
private void permissionCheck() {
}
}
3.3、@Around
@Around 注解用于修饰 Around 增强处理,Around 增强处理非常强大,表现在:
Around 增强处理有以下特点:
-
当定义一个 Around 增强处理方法时,该方法的第一个形参必须是 ProceedingJoinPoint 类型(至少一个形参)。在增强处理方法体内,调用 ProceedingJoinPoint 的 proceed 方法才会执行目标方法:这就是 @Around 增强处理可以完全控制目标方法执行时机、如何执行的关键;如果程序没有调用 ProceedingJoinPoint 的 proceed 方法,则目标方法不会执行。 -
调用 ProceedingJoinPoint 的 proceed 方法时,还可以传入一个 Object[] 对象,该数组中的值将被传入目标方法作为实参 —— 这就是 Around 增强处理方法可以改变目标方法参数值的关键。这就是如果传入的 Object[] 数组长度与目标方法所需要的参数个数不相等,或者 Object[] 数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。
@Around 功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的@Before 、@AfterReturning 就能解决的问题,就没有必要使用 Around 了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用 Around 。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用 Around 增强处理了。
3.4、@Before
@Before 注解指定的方法在切面切入目标方法之前执行,可以做一些 Log 处理,也可以做一些信息的统计,比如 获取用户的请求 URL 以及 用户的 IP 地址等等,这个在做个人站点的时候都能用得到,都是常用的方法。例如下面代码:
@Aspect
@Component
public class ControllerAspect {
@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
public void pointCut() {
}
@Before("pointCut()")
public void doBefore(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String declaringTypeName = signature.getDeclaringTypeName();
String funcName = signature.getName();
log.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String url = request.getRequestURL().toString();
String ip = request.getRemoteAddr();
}
}
JointPoint 对象很有用,可以用它来获取一个签名,利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs() 获取)等。
3.5、@After
@After 注解和 @Before 注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。
@Aspect
@Component
public class ControllerAspect {
@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
public void pointCut() {
}
@After("pointCut()")
public void doAfter(JoinPoint joinPoint) {
log.info("==== doAfter 方法进入了====");
Signature signature = joinPoint.getSignature();
String method = signature.getName();
log.info("方法{}已经执行完", method);
}
}
到这里,我们来写个 Controller 测试一下执行结果,新建一个 AopController 如下:
@RestController
@RequestMapping("/aop")
public class AopController {
@GetMapping("/{name}")
public String testAop(@PathVariable String name) {
return "Hello " + name;
}
}
启动项目,在浏览器中输入:http://localhost:8080/aop/csdn,观察一下控制台的输出信息:
====doBefore 方法进入了====
即将执行方法为: testAop,属于com.itcodai.mutest.AopController包
用户请求的 url 为:http://localhost:8080/aop/name,ip地址为:0:0:0:0:0:0:0:1
==== doAfter 方法进入了====
方法 testAop 已经执行完
从打印出来的 Log 中可以看出程序执行的逻辑与顺序,可以很直观的掌握 @Before 和 @After 两个注解的实际作用。
3.6、@AfterReturning
@AfterReturning 注解和 @After 有些类似,区别在于 @AfterReturning 注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理,例如:
@Aspect
@Component
public class ControllerAspect {
@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
public void pointCut() {
}
@AfterReturning(value = "pointCut()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, String result){
}
}
需要注意的是,在 @AfterReturning 注解 中,属性 returning 的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning 方法中可以对返回值进行增强,可以根据业务需要做相应的封装。
3.7、@AfterThrowing
当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing 属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。
@Aspect
@Component
public class ControllerAspect {
@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
public void pointCut() {
}
@AfterThrowing(value = "pointCut()", throwing = "t")
public void doAfterThrowing(JoinPoint joinPoint, Throwable t){
System.out.println("------------- doAfterThrowing throwable = " + t.toString());
}
}
|