Spring Boot 使用AOP
一、为什么需要面向切面编程?
面向对象编程(OOP)的好处是显而易见的,缺点也同样明显。当需要为多个不具有继承关系的对象添加一个公共的方法的时候,例如日志记录、性能监控等,如果采用面向对象编程的方法,需要在每个对象里面都添加相同的方法,这样就产生了较大的重复工作量和大量的重复代码,不利于维护。面向切面编程(AOP)是面向对象编程的补充,简单来说就是统一处理某一“切面”的问题的编程思想。如果使用AOP的方式进行日志的记录和处理,所有的日志代码都集中于一处,不需要再每个方法里面都去添加,极大减少了重复代码。
二、Spring AOP术语
Spring AOP术语
- 通知(Advice)包含了需要用于多个应用对象的横切行为,完全听不懂,没关系,通俗一点说就是定义了“什么时候”和“做什么”。
- 连接点(Join Point)是程序执行过程中能够应用通知的所有点。
- 切点(Poincut)是定义了在“什么地方”进行切入,哪些连接点会得到通知。显然,切点一定是连接点。
- 切面(Aspect)是通知和切点的结合。通知和切点共同定义了切面的全部内容——是什么,何时,何地完成功能。
- 引入(Introduction)允许我们向现有的类中添加新方法或者属性。
- 织入(Weaving)是把切面应用到目标对象并创建新的代理对象的过程,分为编译期织入、类加载期织入和运行期织入。
- 目标对象(target):即被代理对象。
三、Spring Boot AOP实战
3.1 引入依赖
Spring Boot使用AOP需要添加spring-boot-starter-aop依赖,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2 编写用于拦截的bean
直接定义一个controller,代码如下:
@RestController
public class AopController {
@RequestMapping("/hello")
public String sayHello(){
System.out.println("hello");
return "hello";
}
}
3.3 定义切面
Spring采用@AspectJ注解对POJO进行标注,该注解表明该类不仅仅是一个POJO,还是一个切面。切面是切点和通知的结合,那么定义一个切面就需要编写切点和通知。在代码中,只需要添加@Aspect注解即可。
3.3.1 定义切点
切点是通过@Pointcut注解和切点表达式定义的。
@Pointcut注解可以在一个切面内定义可重用的切点。
由于Spring切面粒度最小是达到方法级别,而execution表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的部件,并且实际中,大部分需要使用AOP的业务场景也只需要达到方法级别即可,因而execution表达式的使用是最为广泛的。
PointCut的定义包括两个部分:Pointcut表示式(expression)和Pointcut签名(signature)。
//Pointcut表示式
@Pointcut("execution(public * com.test.TestController.testFunc(..))")
//Pointcut签名
public void pointCut() {}
execution表示式的格式:
execution(
modifier-pattern?
ret-type-pattern
declaring-type-pattern?
name-pattern(param-pattern)
throws-pattern?)
括号中各个pattern分别表示:
- 修饰符匹配(modifier-pattern?)
- 返回值匹配(ret-type-pattern)
- 类路径匹配(declaring-type-pattern?)
- 方法名匹配(name-pattern)
- 参数匹配((param-pattern))
- 异常类型匹配(throws-pattern?),其中后面跟着“?”的是可选项。
如图为表达式实例:
execution表示在方法执行的时候触发。以“*”开头,表明方法返回值类型为任意类型。然后是全限定的类名和方法名,“*”可以表示任意类和任意方法。对于方法参数列表,可以使用“..”表示参数为任意类型。如果需要多个表达式,可以使用“&&”、“||”和“!”完成与、或、非的操作。
3.3.2 定义通知
通知有五种类型,分别是:
- 前置通知(@Before):在目标方法调用之前调用通知
- 后置通知(@After):在目标方法完成之后调用通知
- 环绕通知(@Around):在被通知的方法调用之前和调用之后执行自定义的方法
- 返回通知(@AfterReturning):在目标方法成功执行之后调用通知
- 异常通知(@AfterThrowing):在目标方法抛出异常之后调用通知
代码中定义了三种类型的通知,使用@Before注解标识前置通知,打印“beforeAdvice...”,使用@After注解标识后置通知,打印“AfterAdvice...”,使用@Around注解标识环绕通知,在方法执行前和执行之后分别打印“before”和“after”。这样一个切面就定义好了,代码如下:
@Aspect
@Component
public class AopAdvice {
@Pointcut("execution (* com.shangguan.aop.controller.*.*(..))")
public void test() {
}
@Before("test()")
public void beforeAdvice() {
System.out.println("beforeAdvice...");
}
@After("test()")
public void afterAdvice() {
System.out.println("afterAdvice...");
}
@Around("test()")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("before");
try {
proceedingJoinPoint.proceed();
} catch (Throwable t) {
t.printStackTrace();
}
System.out.println("after");
}
}
3.3.3 定义具有执行优先级切面
- 通过注解@Order来配置执行优先级,order数字越小越优先执行。
@Aspect
@Component
@Order(-1)
public class MyAspect2 {
//和上面例子一样,省略
}
- 通过实现org.springframework.core.Ordered接口
@Component
@Aspect
public class MyAspect2 implements Ordered {
@Override
public int getOrder() {
//do something to gen order.
return 2;
}
}
3.3.4 定义处理参数的切面
Spring AOP提供使用org.aspectj.lang.JoinPoint类型获取连接点数据,任何通知方法的第一个参数都可以是JoinPoint(环绕通知是ProceedingJoinPoint,JoinPoint子类)。
- JoinPoint:提供访问当前被通知方法的目标对象、代理对象、方法参数等数据
- ProceedingJoinPoint:只用于环绕通知,使用proceed()方法来执行目标方法
如参数类型是JoinPoint、ProceedingJoinPoint类型,可以从“argNames”属性省略掉该参数名(可选,写上也对),这些类型对象会自动传入的,但必须作为第一个参数。
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(public * com.test.TestController.testFunc(..))")
public void pointCut() {}
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
log.info("MyAspect before Method:{}::{}", joinPoint.getSignature().getDeclaringTypeName(), method);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
log.info("ClientIP:{}", request.getRemoteAddr());
}
@After("pointCut()")
public void after(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
log.info("MyAspect after Method:{}::{}", joinPoint.getSignature().getDeclaringTypeName(), method);
}
@AfterReturning("pointCut()")
public void afterReturning(JoinPoint joinPoint) {
String method = joinPoint.getSignature().getName();
log.info("MyAspect after returning Method:{}::{}", joinPoint.getSignature().getDeclaringTypeName(), method);
}
@AfterThrowing("pointCut()")
public void afterThrowing(JoinPoint joinPoint) {
log.info("MyAspect after throwing ...");
}
@Around("pointCut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("MyAspect around before ...");
joinPoint.proceed();
log.info("MyAspect around after ...");
}
}
Comments NOTHING