本文提供相关源码,请放心食用,详见网页侧边栏或底部,有疑问请评论或 Issue
一、前言
AOP(Aspect Oriented Programming, 面向切面编程),是 Spring 的核心思想之一,即纵向重复,横向抽取,它在 Spring 中应用广泛,例如 拦截器、日志、事务等等,在 SpringBoot 中使用 AOP 之前,我们先复习下 AOP 的理论知识。
二、AOP 理论
2.1 术语解释
为了方便解释,给出一个例子:
1 2 3 4 5 6
| public interface UserService { void save(); void delete(); void update(); void query(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class UserServiceImpl implements UserService { @Override public void save() { System.out.println("save"); }
@Override public void delete() { System.out.println("delete"); }
@Override public void update() { System.out.println("update"); }
@Override public void query() { System.out.println("query"); } }
|
若我们想要对这四个方法进行增强,假设增强如下:
1 2 3 4 5 6 7
| ... public void delete() { System.out.println("开启事务"); System.out.println("delete"); System.out.println("提交事务"); } ...
|
(1)连接点(JoinPoint)
解释:目标对象中,所有可以增强的方法。
例子:即 save()、delete()、update()、query() 这四个方法(因为我们可以使用动态代理技术对这些方法进行增强)。
(2)切入点(PointCut)
解释:目标对象中,已经增强的方法。
例子:若 delete() 方法在动态代理中被增强了,则 delete() 方法是切入点。
(3)通知/增强(Advice)
解释:增强的代码
例子:即 System.out.println(“开启事务”); 和 System.out.println(“提交事务”);
(4)目标对象(Target)
解释:被代理对象
例子:即 UserServiceImpl 类
(5)织入(Weaving)
解释:将通知应用到切入点的过程
(6)代理(Proxy)
解释:将通知织入到切入点后,形成代理对象
(7)切面(aspect)
解释:切入点和通知合称为切面
2.2 通知类型
通知类型 |
描述 |
前置通知 Before advice |
在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常 |
正常返回通知 After returning advice |
在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行 |
异常返回通知 After throwing advice |
在连接点抛出异常后执行 |
返回通知 After (finally) advice |
在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容 |
环绕通知 Around advice |
环绕着在切入点选择的连接点处的方法所执行的通知,环绕通知可以在方法调用之前和之后自定义任何行为,并且可以决定是否执行连接点处的方法、替换返回值、抛出异常等等。 |
2.3 底层实现
AOP 的实现是基于动态代理
技术,一般有基于 JDK 的动态代理和基于 Cglib 的动态代理两种实现。
这两种实现的区别是:
关于使用 JDK 动态代理 和 Cglib 动态代理实现上述例子,这里就不再赘述了,可以参考文章:《JDK动态代理与Cglib动态代理》。
三、与 SpringBoot 整合
3.1 依赖配置
导入依赖:
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
|
相关配置只有一项:
1 2 3
| spring: aop: proxy-target-class: true
|
3.2 入门程序
3.2.1 环境准备
(1)编写 UserService 和 UserServiceImpl:
1 2 3
| public interface UserService { void delete(String name); }
|
1 2 3 4 5 6 7
| @Service public class UserServiceImpl implements UserService { @Override public void delete(String name) { System.out.println("delete:" + name); } }
|
(2)编写 UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @RestController public class UserController { @Autowired private UserService userService;
@GetMapping("/delete") public String restDelete(String name) { userService.delete(name); return "success"; }
@GetMapping("/exception") public void restException() { int i = 1 / 0; } }
|
3.2.2 切面表达式
首先复习下 AOP 中的切面表达式,假设 UserController 类位于 com.github.jitwxs.sample.aop.controller 包下,那么如果我们为其 restDelete() 方法 配置切入点,如下:
1
| public void com.github.jitwxs.sample.aop.controller.UserController.restDelete()
|
也就是 restDelete() 方法的 reference 路径加上访问修饰符和返回值,一般来说我们不对非 public 方法添加切入点,也不限制其返回值,因此省略掉访问修饰符(默认值即为 public),并将返回值修改为 *(即任何返回值均可):
1
| * com.github.jitwxs.sample.aop.controller.UserController.restDelete()
|
再将切面方法扩展到 UserController 下的所有方法:
1
| * com.github.jitwxs.sample.aop.controller.UserController.*()
|
此时只能对 UserController 下的所有无参 public 方法添加切入点,将其修改为不限制参数:
1 2
| * com.github.jitwxs.sample.aop.controller.UserController.*(..)
|
如果我们想要 controller 包下的所有以 Controller 结尾的类添加切入点,修改为:
1 2
| * com.github.jitwxs.sample.aop.controller.*Controller.*(..)
|
一般这样就可以了,如果你想为 controller 的子包也添加切入点,修改为:
1 2
| * com.github.jitwxs.sample.aop.controller..*Controller.*(..)
|
3.2.3 切面类
-
使用 @Aspect
注解将一个类标记为切面类,@Component
将该类加入 Spring 容器中。
-
使用 @Pointcut
注解定义切面表达式,为 controller 包下所有 Controller 结尾的类的 public 方法添加切入点。
-
通过 @Before
、@AfterReturning
、@AfterThrowing
、@After
、@Around
注解定义通知类型。
-
在 doBefore()
方法中,通过 joinPoint
参数获取通知的签名信息,如目标方法名、目标方法参数信息等。、、
-
在 doAroundAdvice()
方法中,处理环绕通知。切面中可选择执行方法与否,执行几次方法等。环绕通知使用一个代理ProceedingJoinPoint类型的对象来管理目标对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| @Component @Aspect public class TestAspect { private Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("execution(* com.github.jitwxs.sample.aop.controller.*Controller.*(..))") public void pointCunt(){}
@Before("pointCunt()") public void doBefore(JoinPoint joinPoint) { logger.info("---前置通知---");
logger.info("目标对象参数信息: {}", Arrays.toString(joinPoint.getArgs()));
logger.info("AOP代理类: {}", joinPoint.getThis());
logger.info("代理的目标对象: {}", joinPoint.getTarget());
Signature signature = joinPoint.getSignature(); logger.info("代理的方法名: {}", signature.getName());
logger.info("AOP代理类的名字: {}", signature.getDeclaringTypeName());
logger.info("AOP代理类的类信息: {}", signature.getDeclaringType());
}
@AfterReturning(returning = "ret", pointcut = "pointCunt()") public void doAfterReturning(JoinPoint joinPoint, Object ret) { logger.info("---后置返回通知---"); logger.info("代理方法: {}, 返回参数: {} ", joinPoint.getSignature().getName(), ret); }
@AfterThrowing(pointcut = "pointCunt()", throwing = "e") public void doAfterThrowingAdvice(JoinPoint joinPoint, Throwable e) { logger.info("---后置异常通知---"); logger.info("代理方法: {}, 异常信息: {} ", joinPoint.getSignature().getName(), e.getMessage()); }
@After("pointCunt()") public void doAfterAdvice(JoinPoint joinPoint) { logger.info("---后置最终通知---"); }
@Around("pointCunt()") public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) { logger.info("---环绕通知开始---"); logger.info("代理方法: {}", proceedingJoinPoint.getSignature().getName()); try { Object obj = proceedingJoinPoint.proceed(); logger.info("---环绕通知结束---"); return obj; } catch (Throwable throwable) { logger.info("---环绕通知异常---"); throwable.printStackTrace(); } return null; } }
|
3.2.4 执行程序
当我们执行 “/delete” 接口时,访问 /delete?name=jitwxs
,运行结果如下:
当我们执行 “/exception” 接口时,访问 /exception
,运行结果如下:
3.3 资源同步
使用 ThreadLocal 实现资源同步,例如统计切入点执行时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Component @Aspect public class TestAspect { private Logger logger = LoggerFactory.getLogger(getClass());
private ThreadLocal<Long> threadLocal = new ThreadLocal<>();
@Pointcut("execution(* com.github.jitwxs.sample.aop.controller.*Controller.*(..))") public void pointCunt(){}
@Before("pointCunt()") public void doBefore(JoinPoint joinPoint) { logger.info("---前置通知---"); threadLocal.set(System.currentTimeMillis()); }
@AfterReturning(returning = "ret", pointcut = "pointCunt()") public void doAfterReturning(JoinPoint joinPoint, Object ret) { logger.info("---后置返回通知---"); logger.info("共执行时间:{} ", System.currentTimeMillis() - threadLocal.get()); } }
|
3.4 优先级
当我们对同一个切入点设置了多个切面后,我么你可以定义切面的优先级,来达到预想的执行顺序。
@Order()
注解来标识切面的优先级,i的值越小,优先级越高。假如存在两个切面,定义了 @Order(5) 和 @Order(10),那么:
所以我们可以这样总结: