( 二十四 )SpringBoot中使用AOP
( 二十四 )SpringBoot中使用AOP
1、简介
前面我们在Spring中了解了 AOP简介 和 在Spring中使用Aop, 本章对SpringBoot中的使用做简单的介绍。AOP是通过动态代理实现的,动态代理又分为两个部分:JDK动态代理 和 CGLIB动态代理,以下两点需要记住:
动态代理模式。
2、AOP是
方法级别
的。 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
这里可以不用指定版本号, 在SpringBoot 的 parent 包里面有版本的控制。
3、切入点(@Pointcut)表达式(execution())的语法
execution表达式:用于匹配方法执行的连接点,属于方法级别, 语法: execution(修饰符 返回值类型 方法名(参数)异常)
语法参数 | 描述 |
---|---|
修饰符 | 可选,如public,protected,写在返回值前,任意修饰符填* 号就可以 |
返回值类型 | 必选 ,可以使用* 来代表任意返回值 |
方法名 | 必选 ,可以用* 来代表任意方法 |
参数 | 必选, () 代表是没有参数,(..) 代表是匹配任意数量,任意类型的参数,当然也可以指定类型的参数进行匹配,如要接受一个String类型的参数,则(java.lang.String) , 任意数量的String类型参数:(java.lang.String..) 等等。。。 |
异常 | 可选,语法:throws 异常 ,异常是完整带包名,可以是多个,用逗号分隔 |
execution()表达式案例:
- 拦截
com.dw.study
包下的所有子包里的任意类的任意方法
execution(* com.dw.study..*.*(..))
-
拦截
com.dw.study
.Test2Controller下的任意方法
execution(*
com.dw.study
.Test2Controller.*(..)) -
拦截任何修饰符为public的方法
execution(public * * (..))
-
拦截
com.dw.study
下的所有子包里的以ok开头的方法
execution(*
com.dw.study
..*.ok* (..))
4、JoinPoint
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象。 除 @Around 外,每个通知的方法里都可以加或者不加参数JoinPoint。JoinPoint包含了类名、被切面的方法名、参数等属性。@Around 参数必须为 ProceedingJoinPoint。
4.1、JoinPoint
方法名 | 功能 |
---|---|
Signature getSignature(); | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs(); | 获取传入目标方法的参数对象 |
Object getTarget(); | 获取被代理的对象 |
Object getThis(); | 获取代理对象 |
4.2、ProceedingJoinPoint 对象是 JoinPoint 的子接口, 该对象只用在@Around的切面方法中:
方法名 | 功能 |
---|---|
Object proceed() throws Throwable | 执行目标方法 |
Object proceed(Object[] var1) throws Throwable | 传入的新的参数去执行目标方法 |
5、使用示例:
5.1、使用包名匹配:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
* @Author dw
* @ClassName MyAop
* @Description AOP 各个通知使用示例:
* @EnableAspectJAutoProxy : 默认已开启不需要再添加响应的注解
* @Date 2021/8/15 0:21
* @Version 1.0
*/
@Aspect
@Component
public class MyAop {
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 定义切入点 (一个切面类中可以定义多个切入点)
* 拦截所有 com.dw.study.controller 包以及子包下所有类的所有方案
* 第一个 * 表示所有返回值
* 第二个 * 表示所有类
* 第三个 * 表示所有的方法名
* 两个 .. 表示当前包以及子包
**/
@Pointcut("execution(* com.dw.study.controller..*.*(..))")
public void aspect() {
}
/**
* 前置通知,目标方法调用前被调用
* 除@Around外,每个方法里都可以加或者不加参数JoinPoint。
* JoinPoint包含了类名、被切面的方法名、参数等属性。
**/
@Before(value = "aspect()")
public void before(JoinPoint joinPoint) {
log.info("AOP before 执行... :参数类型:{}", joinPoint.getArgs());
// // 接收到请求,记录请求内容
// ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
// HttpServletRequest request = attributes.getRequest();
// // 记录下请求内容
// System.out.println("URL : " + request.getRequestURL().toString());
// System.out.println("HTTP_METHOD : " + request.getMethod());
// System.out.println("IP : " + request.getRemoteAddr());
// System.out.println("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
// System.out.println("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
/**
* 最终通知,目标方法执行完之后执行
**/
@After(value = "aspect()")
public void after(JoinPoint joinPoint) {
log.info("AOP after 执行... :{}", joinPoint.toLongString());
}
/**
* 后置返回通知
* 如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
* 如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
* returning 只有目标方法返回值与通知方法具有相应参数类型时才能执行后置返回通知,否则不执行
* 除了使用上面定义好的切面aspect(), 也可以直接使用表达式。
**/
@AfterReturning(value = "execution(* com.dw.study.controller..*.*(..))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
log.info("AOP afterReturning 执行... :返回结果:{}", result);
}
/**
* 环绕通知
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
* 2:proceedingJoinPoint.proceed() 执行被代理的方法
**/
@Around(value = "aspect()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {
try {
log.info("AOP around开始... 执行方法... :{}", proceedingJoinPoint.getSignature().getName());
Object proceed = proceedingJoinPoint.proceed();
log.info("AOP around结束... 执行方法... :{}", proceedingJoinPoint.getSignature().getName());
return proceed;
} catch (Throwable throwable) {
log.info("AOP around 执行错误... error :{}", throwable.getMessage());
throwable.printStackTrace();
return "执行around出错。。。";
}
}
/**
* 后置异常通知
* 定义一个名字,该名字用于匹配通知实现方法的一个参数名,当目标方法抛出异常返回后,将把目标方法抛出的异常传给通知方法;
* throwing 只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知,否则不执行,
**/
@AfterThrowing(value = "aspect()", throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Throwable exception) {
log.error("AOP afterThrowing 执行... , msg : {}", exception.getMessage());
if (exception instanceof NullPointerException)
log.info("空指针异常");
}
}
1、没有出现异常的执行顺序:
正常: around开始 --》 before 执行--》afterReturning 执行--》after 执行--》around结束
2、异常时执行顺序
异常: around开始--》before 执行--》afterThrowing 执行--》after 执行--》around
5.2、自定义注解测试
(一)自定义注解无参数
1、定义切面:
@Aspect
@Component
public class MyAop {
private final Logger log = LoggerFactory.getLogger(this.getClass());/**
* 基于注解定义切入点 2
* 对于注解了 @MethodAspect的方法进行拦截
* 表示标注了特定注解的目标方法链接点。如@annotation(com.dw.study.TestAnnotation)表示任何标注了@TestAnnotation注解的目标类方法。
*/
@Pointcut("@annotation(com.dw.study.customAnnotation.MethodAspect)")
public void testMyAnnotationAspect() {}
/**
* 前置通知,目标方法调用前被调用
* 除@Around外,每个方法里都可以加或者不加参数JoinPoint。
* JoinPoint包含了类名、被切面的方法名、参数等属性。
**/
@Before(value = "testMyAnnotationAspect()")
public void before(JoinPoint joinPoint) {
log.info("AOP before 执行... :参数类型:{}", joinPoint.getArgs());
}
}
2、自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodAspect {
}
3、controller
@RequestMapping("hello")
@MethodAspect
public String hello(){
return "SpringBoot-HelloWorld";
}
(二)自定义注解带参数,并在切面中获取参数
1、修改上述自定义的注解如下:在自定义注解中增加几个属性,下面自定义的中有两个属性:value 和 description
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodAspect {
/**
* value
* @return
*/
String value();
/**
* 描述
* @return
*/
String description() default "default description";
}
2、修改MyAop 如下:
@Aspect
@Component
public class MyAop {
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* 基于注解定义切入点 2
* 对于注解了 @MethodAspect的方法进行拦截
* 表示标注了特定注解的目标方法链接点。如@annotation(com.dw.study.TestAnnotation)表示任何标注了@TestAnnotation注解的目标类方法。
*/
@Pointcut("@annotation(com.dw.study.customAnnotation.MethodAspect)")
public void testMyAnnotationAspect() {}
/**
* 前置通知,目标方法调用前被调用
* 除@Around外,每个方法里都可以加或者不加参数JoinPoint。
* JoinPoint包含了类名、被切面的方法名、参数等属性。
* @annotation中的值,需要和方法参数名相同
**/
@Before(value = "testMyAnnotationAspect() && @annotation(methodAspect)")
public void before(JoinPoint joinPoint, MethodAspect methodAspect) {
log.info("AOP before 执行... :参数类型:{}", joinPoint.getArgs());
log.info("before-value== "+ methodAspect.value() + "before-description== " + methodAspect.description());
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
System.out.println("URL : " + request.getRequestURL().toString());
System.out.println("HTTP_METHOD : " + request.getMethod());
System.out.println("IP : " + request.getRemoteAddr());
System.out.println("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
System.out.println("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
/**
* 环绕通知
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
* 2:proceedingJoinPoint.proceed() 执行被代理的方法
* @annotation中的值,需要和方法参数名相同
**/
@Around(value = "testMyAnnotationAspect() && @annotation(methodAspect)")
public Object around(ProceedingJoinPoint proceedingJoinPoint, MethodAspect methodAspect) {
try {
log.info("AOP around开始... 执行方法... :{}", proceedingJoinPoint.getSignature().getName());
log.info("around-value== "+ methodAspect.value() + "around-description== " + methodAspect.description());
Object proceed = proceedingJoinPoint.proceed();
log.info("AOP around结束... 执行方法... :{}", proceedingJoinPoint.getSignature().getName());
return proceed;
} catch (Throwable throwable) {
log.info("AOP around 执行错误... error :{}", throwable.getMessage());
throwable.printStackTrace();
return "执行around出错。。。";
}
}
}
3、controller如下:
@RequestMapping("hello")
@MethodAspect(value = "测试注解拦截", description = "测试自定义注解!!!")
public String hello(){
return "SpringBoot-HelloWorld";
}
4、执行结果如下: