SpringBoot 整合 AOP
一、前言
AOP(Aspect Oriented Programming, 面向切面编程),是 Spring 的核心思想之一,即纵向重复,横向抽取,它在 Spring 中应用广泛,例如 拦截器、日志、事务等等,在 SpringBoot 中使用 AOP 之前,我们先复习下 AOP 的理论知识。
二、AOP理论
2.1 术语解释
为了方便解释,给出一个例子:
public interface UserService {
void save();
void delete();
void update();
void query();
}
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"); }
}
若我们想要对这四个方法进行增强,假设增强如下:
...
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动态代理
:针对实现了接口的类进行代理,代理对象和目标对象实现了共同的接口,拦截器必须实现InvocationHanlder接口。Cglib动态代理
:针对没有实现接口的类进行代理,底层是字节码增强技术,生成当前类的子类对象。代理对象是目标对象的子类,拦截器必须实现 MethodInterceptor 接口。
关于使用 JDK 动态代理 和 Cglib 动态代理实现上述例子,这里就不再赘述了,可以参考文章:JDK动态代理与Cglib动态代理。
三、与 SpringBoot 整合
3.1 依赖配置
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
相关配置只有一项:
# 当值为 true 时,使用 Cglib 动态代理,否则使用 JDK 动态代理,默认值为 true
spring.aop.proxy-target-class=true
3.2 入门程序
源码地址:https://github.com/jitwxs/blog\_sample
3.2.1 环境准备
(1)编写 UserService 和 UserServiceImpl:
public interface UserService {
void delete(String name);
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void delete(String name) {
System.out.println("delete:" + name);
}
}
(2)编写UserController
@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 类位于 jit.wxs.aop.controller 包下,那么如果我们为其 restDelete() 方法 配置切入点,如下:
public void jit.wxs.aop.controller.UserController.restDelete()
也就是 restDelete() 方法的 reference 路径加上访问修饰符和返回值,一般来说我们不对非 public 方法添加切入点,也不限制其返回值,因此省略掉访问修饰符(默认值即为 public),并将返回值修改为 *(即任何返回值均可):
* jit.wxs.aop.controller.UserController.restDelete()
再将切面方法扩展到 UserController 下的所有方法:
* jit.wxs.aop.controller.UserController.*()
此时只能对 UserController 下的所有无参 public 方法添加切入点,将其修改为不限制参数:
// 将 () 修改为 (..) ,不限制参数
* jit.wxs.aop.controller.UserController.*(..)
如果我们想要 controller 包下的所有以 Controller 结尾的类添加切入点,修改为:
// 将 controller包下所有以 Controller 结尾的类添加切入点
* jit.wxs.aop.controller.*Controller.*(..)
一般这样就可以了,如果你想为 controller 的子包也添加切入点,修改为:
// 在 controller 后多添加一个 . 表示包含它的子包
* jit.wxs.aop.controller..*Controller.*(..)
3.2.3 切面类
- 使用
@Aspect
注解将一个类标记为切面类,@Component
将该类加入 Spring 容器中。 - 使用
@Pointcut
注解定义切面表达式,为 controller 包下所有 Controller 结尾的类的 public 方法添加切入点。 - 通过
@Before
、@AfterReturning
、@AfterThrowing
、@After
、@Around
注解定义通知类型。 - 在
doBefore()
方法中,通过joinPoint
参数获取通知的签名信息,如目标方法名、目标方法参数信息等。、、 在
doAroundAdvice()
方法中,处理环绕通知。切面中可选择执行方法与否,执行几次方法等。环绕通知使用一个代理ProceedingJoinPoint类型的对象来管理目标对象。@Component
@Aspect
public class TestAspect {private Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("execution(* jit.wxs.aop.controller.*Controller.*(..))")
public void pointCunt(){ }
@Before("pointCunt()")
public void doBefore(JoinPoint joinPoint) {
logger.info("---前置通知---");
// 获取目标方法的参数信息
logger.info("目标对象参数信息: {}", Arrays.toString(joinPoint.getArgs()));
// AOP代理类的信息
logger.info("AOP代理类: {}", joinPoint.getThis());
// 代理的目标对象
logger.info("代理的目标对象: {}", joinPoint.getTarget());
// 通知的签名
Signature signature = joinPoint.getSignature();
// 代理的是哪一个方法
logger.info("代理的方法名: {}", signature.getName());
// AOP代理类的名字
logger.info("AOP代理类的名字: {}", signature.getDeclaringTypeName());
// AOP代理类的类信息
logger.info("AOP代理类的类信息: {}", signature.getDeclaringType());
// RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 获取 HttpServletRequest
// HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 获取 Session
// HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
}
@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 实现资源同步,例如统计切入点执行时间:
@Component
@Aspect
public class TestAspect {
private Logger logger = LoggerFactory.getLogger(getClass());
private ThreadLocal<Long> threadLocal = new ThreadLocal<>();
@Pointcut("execution(* jit.wxs.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),那么:
- 在
@Before
中优先执行 @Order(5) 的内容,再执行 @Order(10) 的内容 - 在
@After
和@AfterReturning
中优先执行 @Order(10) 的内容,再执行 @Order(5) 的内容
所以我们可以这样总结:
- 在切入点前的操作,按 order 的值由小到大执行
- 在切入点后的操作,按 order 的值由大到小执行
还没有评论,来说两句吧...