一、AOP术语
1)、通知(Advice):织入目标类连接点上的一段程序代码
Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
- 返回通知(After-returning):在目标方法成功执行之后调用通知
- 异常通知(After-throwing):在目标方法抛出异常后调用通知
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
2)、连接点(Join point):一个类或一段程序代码拥有一些具有边界性质的特定点,Spring仅支持方法的连接点
3)、切点(Poincut):通过切点定位特定的连接点
4)、切面(Aspect):通知和切点的结合
5)、引入(Introduction):一种特殊的通知,它为类添加一些属性和方法
6)、织入(Weaving):把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面的
二、SpringBoot中使用AOP
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、实现一个简单的Web请求入口
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello world";
}
}
3、直接使用切面
1)、切入点表达式
execution([可见性] 返回类型 [声明类型].方法名(参数) [异常])
其中[]中的为可选,其他的还支持通配符的使用:
*
:匹配所有字符..
:一般用于匹配多个包,多个参数+
:表示类及其子类
运算符有:&&
、||
、!
//匹配com.hand.aop包及其子包中所有类中的所有方法(返回类型任意,方法参数任意)
@Pointcut("execution(* com.hand.aop..*(..))")
public void pointcut(){
}
2)、前置通知
@Before:在某连接点之前执行的通知,除非抛出一个异常,否则这个通知不能阻止连接点之前的执行流程
@Aspect
@Component
public class WebLogAspect {
private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
//匹配com.hand.aop包及其子包中所有类中的所有方法(返回类型任意,方法参数任意)
@Pointcut("execution(* com.hand.aop..*(..))")
public void pointcut() {
}
//前置通知
@Before("pointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
logger.info("前置通知");
logger.info("获取目标方法的参数信息:{}", Arrays.toString(joinPoint.getArgs()));
logger.info("代理的目标对象:{}", joinPoint.getTarget());
//通知的签名
Signature signature = joinPoint.getSignature();
logger.info("被代理的方法:{}", signature.getName());
logger.info("被代理的类:{}", signature.getDeclaringTypeName());
//获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
logger.info("URL:" + request.getRequestURL().toString());
logger.info("HTTP_METHOD:" + request.getMethod());
logger.info("IP:" + request.getRemoteAddr());
}
}
3)、后置通知
@After:当某连接点退出时执行的通知(不论是正常返回还是异常退出)
@After("pointcut()")
public void doAfterAdvice(JoinPoint joinPoint) {
logger.info("执行后置通知");
}
4)、返回通知
@AfterReturning:在某连接点之后执行的通知,通常在一个匹配的方法返回的时候执行
//returning:限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行,对于returning对应的通知方法参数为Object类型将匹配任何目标返回值
@AfterReturning(value = "pointcut()", returning = "keys")
public void doAfterReturningAdvice1(JoinPoint joinPoint, Object keys) {
logger.info("返回值为" + keys);
}
控制台输出日志如下:
2019-07-27 10:39:27.877 INFO 19320 --- [nio-8080-exec-1] com.hand.aop.aspect.WebLogAspect : 返回值为hello world
5)、异常通知
@AfterThrowing:在方法抛出异常退出时执行的通知
//throwing:限定了只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知,否则不执行,对于throwing对应的通知方法参数为Throwable类型将匹配任何异常
@AfterThrowing(value = "pointcut()", throwing = "exception")
public void doAfterThrowingAdvice(JoinPoint joinPoint, Throwable exception) {
logger.info("被代理的方法:{}", joinPoint.getSignature().getName());
if (exception instanceof NullPointerException) {
logger.info("发生了空指针异常");
}
}
6)、环绕通知
@Around:包围一个连接点的通知,如方法调用等。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为,它也会选择是否继续执行连接点或者直接返回它自己的返回值或抛出异常来结束执行
环绕通知是一个对方法的环绕,具体方法会通过代理传递到切面中去,切面中可选择执行方法与否,执行几次方法等。环绕通知使用一个代理ProceedingJoinPoint类型的对象来管理目标对象,所以此通知的第一个参数必须是ProceedingJoinPoint类型。在通知体内调用ProceedingJoinPoint的proceed()方法会导致后台的连接点方法执行。proceed()方法也可能会被调用并且传入一个Object[]对象,该数组中的值将被作为方法执行时的入参
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Signature signature = joinPoint.getSignature();
logger.info("方法{}的开始时间是{}", signature.getName(), new Date());
Object object = joinPoint.proceed();
logger.info("方法{}的结束时间是{}", signature.getName(), new Date());
return object;
}
4、创建自定义注解对应切面
枚举:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DoneTime {
String name() default "";
}
切面:
@Aspect
@Component
public class DoneTimeAspect {
private final static Logger logger = LoggerFactory.getLogger(DoneTimeAspect.class);
@Around("@annotation(doneTime)")
public Object around(ProceedingJoinPoint joinPoint, DoneTime doneTime) throws Throwable {
Signature signature = joinPoint.getSignature();
logger.info("方法{}的开始时间是{}", signature.getName(), new Date());
Object object = joinPoint.proceed();
logger.info("方法{}的结束时间是{}", signature.getName(), new Date());
return object;
}
}
使用注解@DoneTime标注方法:
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
@DoneTime
public String hello() {
return "hello world";
}
}
控制台输出日志如下:
2019-07-27 09:12:54.467 INFO 2224 --- [nio-8080-exec-2] com.hand.aop.aspect.DoneTimeAspect : 方法hello的开始时间是Sat Jul 27 09:12:54 CST 2019
2019-07-27 09:12:54.475 INFO 2224 --- [nio-8080-exec-2] com.hand.aop.aspect.DoneTimeAspect : 方法hello的结束时间是Sat Jul 27 09:12:54 CST 2019