SpringBoot AOP切面实现 AOP简介 AOP (Aspect Oriented Programming), 面向切面思想,是Spring的三大核心思想之一( 其余两个: IOC - 控制反转、DI - 依赖注入)
在我们的程序中,经常存在一些系统性的需求,比如 权限校验
、日志记录
、统计
等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护,那么面向切面编程往往让我们的开发更加低耦合,也大大减少了代码量,同时呢让我们更专注于业务模块的开发,把那些与业务无关的东西提取出去,便于后期的维护和迭代
AOP体系与概念 简单地去理解,其实AOP要做三类事:
在哪里切入
,也就是权限校验等非业务操作在哪些业务代码中执行 (Pointcut切点)
在什么时候切入
,是业务代码执行前还是执行后 (Advice处理时机)
切入后做什么事
,比如做权限校验、日志记录等 (Advice处理内容)
AOP的体系图
AOP相关名词概念
概念
说明
Pointcut
切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为execution
方式和 annotation
方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
Advice
处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
Aspect
切面,即 Pointcut
和 Advice
。
Joint point
连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
Weaving
织入,就是通过动态代理,在目标对象方法中执行处理内容的过程
Springboot中AOP的实现 AOP Maven依赖 1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
AOP相关注解 @Aspect 声明这是一个切面类
该注解要添加在类上,使用时需要与@Component
注解一起用,表明同时将该类交给spring管理
1 2 3 4 5 @Component @Aspect public class ControllerAspect { }
@Pointcut 用来定义一个切点, 定义Advice需要在何处切入业务代码(织入切面)中
该注解需要添加在方法上,该方法签名必须是 public void
类型,可以将@Pointcut
中的方法看作是一个用来引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此 @Pointcut 中的方法只需要方法签名,而不需要在方法体内编写实际代码
该注解有两个常用的表达式:execution()
和 @annotation()
, execution()
使用项目中的引用定位, 来获取切点所在的位置, @annotation
使用注解的方式,它会扫描自己方法体内的注解(自定义的注解或者当前框架现有的注解 如@RequestMapping等), 被这些注解定义的方法就会成为切点
execution() 1 2 3 4 5 6 7 8 9 10 @Aspect @Component public class MyAop { @Pointcut("execution(* com.crisp.controller..*.*(..))") public void pointCut () { } }
上述代码的@Pointcut注解中
第一个 *
:表示返回值类型,*
表示所有类型;
**包名
**:标识需要拦截的包名;
**包名后的 ..
**:表示当前包和当前包的所有子包,在本例中指 com.crisp.controller
包、子包下所有类;
第二个 *
:表示类名,*
表示所有类;
最后的 *(..)
:星号表示方法名,* 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
execution表达式
表达式语法:访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
标准的表达式写法范例:
1 2 public void com.crisp.controller.HelloController.helloAop()1
访问修饰符可以省略
1 void com.crisp.controller.HelloController.helloAop()
返回值可以使用通配符*
,表示任意返回值
1 * com.crisp.controller.HelloController.helloAop()
包名可以使用通配符,表示任意包,但是有几级包,就需要写几个*
.
1 * *.*.*.*.HelloController.helloAop()
包名可以使用...
表示当前包及其子包
1 * *...HelloController.helloAop()
类名和方法名都可以使用*
来实现通配
表达式中的参数列表,可以直接写数据类型:
基本类型直接写名称 :例如,int
引用类型写包名.类名的方式 :例如,java.lang.String
可以使用通配符*
表示任意类型,但是必须有参数
可以使用…
表示有无参数均可,有参数可以是任意类型
全通配写法:* *..*.*(...)
@annotation() 采用框架现有的注解
来声明切点
1 2 3 4 5 6 7 8 9 10 @Aspect @Component public class MyAop { @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") public void pointCut () { } }
然后使用该切面的话,就会切入注解是 @PostMapping
的所有方法。这种方式很适合处理 @GetMapping
、@PostMapping
、@DeleteMapping
不同注解有各种特定处理逻辑的场景
采用自定义的注解来声明切点
1.编写一个自定义注解
1 2 3 4 5 6 7 8 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String desc () default " " ; }
2.在某处方法上添加该注解
1 2 3 4 5 6 @GetMapping("/register") @MyAnnotation(desc = "wow") public ResponseJson register (@RequestParam("username") String username, @RequestParam("password") String password,) { }
3.切面类代码
1 2 3 4 5 6 7 8 9 10 @Aspect @Component public class MyAop { @Pointcut("@annotation(com.crisp.MyAnnotations.MyAnnotation)") public void pointCut () { } }
@Pointcut的关系运算符 1 2 3 4 5 6 7 8 9 10 11 12 13 @Pointcut("execution(public * com.aismall.testaop.controller.HelloController.*(..))") public void cutController () {} @Pointcut("execution(public * com.aismall.testaop.service.UserService.*(..))") public void cutService () {} @Pointcut("cutController() && cutService()") public void cutAll () {}
@Before 作用在用@Component
注解和@Aspect
作用的切面类的方法中, 标注了@Before的方法表示此方法是前置通知
前置通知, 顾名思义是在@Pointcut
注解标注的切入点类中的方法执行前被调用
以@Pointcut使用@annotation方法来标注切入点为例, 先创建一个自定义注解
1 2 3 4 5 6 7 8 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value () default " " ; }
将注解作用在要标注的切入点上
1 2 3 4 5 6 7 8 9 10 @RESTController @RequestMapping("/object") public class ObjectController { @GetMapping("/show") @MyAnnotation("wow") public void showObject (@RequestParam("bucketName") String bucketName) { System.out.println("ObjectController控制器的获取指定存储桶中所有对象的信息方法执行" ) } }
书写切入面代码, 在前置通知的方法上标注@Before注解, 参数为标注了@Pointcut注解的切入点方法名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component @Aspect public class MyAop { public final static Logger log = LoggerFactory.getLogger(MyAop.class); @Pointcut("@annotation(MyAnnotation)") public void beforeLog () { } public void doBefore () { log.info("业务方法执行!" ); } }
项目运行后, 当切入点方法执行时 (本例就是/object/show被访问后showObject方法被执行);
@Before
注解指定的方法在切面切入目标方法之前执行, 从而实现打印日志, 安全验证等功能
结果示例 (控制台打印日志)
1 2 2023 -03 -10T21:09:27 ,347 INFO [http-nio-9009 -exec-1 ] com.crisp.miniostorage.aop.MyAop: 业务方法执行!2023 -03 -10T21:09:27 ,484 INFO [http-nio-9009 -exec-1 ] com.crisp.miniostorage.controller.ObjectController: ObjectController控制器的获取指定存储桶中所有对象的信息方法执行
@After 作用在用@Component
注解和@Aspect
作用的切面类的方法中, 标注了@After的方法表示此方法是后置通知
后置通知, 顾名思义是在@Pointcut
注解标注的切入点类中的方法执行后被调用
以@Pointcut使用execution定位的方法来标注切入点为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Component @Aspect public class MyAop { public final static Logger log = LoggerFactory.getLogger(MyAop.class); @Pointcut("execution(* com.crisp.miniostorage.controller.*.*(..))") public void afterLog () { } @After("afterLog()") public void doAfter (JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); String name = signature.getName(); log.info(name+"方法执行完毕!" ); } }
项目运行后, 当切入点方法执行时 (本例子中是controller包下的任意方法时),
@After
注解指定的方法在切面切入目标方法之后执行
JoinPoint类可以获取切入点的信息, 在之后会详细介绍
结果示例:
1 2 2023 -03 -10T21:32 :40 ,079 INFO [http-nio-9009 -exec-1 ] com.crisp.miniostorage.controller.ObjectController: ObjectController控制器的获取指定存储桶中所有对象的信息方法执行2023 -03 -10T21:32 :40 ,081 INFO [http-nio-9009 -exec-1 ] com.crisp.miniostorage.aop.MyAop: showObject方法执行完毕!
@AfterReturning @AfterReturning
注解和 @After
有些类似,区别在于 @AfterReturning
注解可以用来捕获切入方法执行完之后的返回值 ,对返回值进行业务逻辑上的增强处理
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 @Component @Aspect public class MyAop { public final static Logger log = LoggerFactory.getLogger(MyAop.class); @Pointcut("execution(* com.crisp.miniostorage.controller.ObjectController.*(..))") public void afterReturningLog () { } @AfterReturning(value = "afterReturningLog()",returning = "resultMsg") public void doAfterReturning (JoinPoint joinPoint, ResponseJson resultMsg) { log.info(resultMsg.toString()); } }
后置返回的参数说明:
如果第一个参数为JoinPoint,则第二个参数为返回值的信息
如果第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
returning:限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知,否则不执行, 参数为Object类型将匹配任何目标返回值
结果示例:
1 2 2023 -03 -10T21:49 :47 ,685 INFO [http-nio-9009 -exec-1 ] com.crisp.miniostorage.controller.ObjectController: ObjectController控制器的校验桶内是否存在重复的数据方法执行2023 -03 -10T21:49 :47 ,885 INFO [http-nio-9009 -exec-1 ] com.crisp.miniostorage.aop.MyAop: ResponseJson{code=1 , msg='校验成功' , data=[ObjectVerifyInfo{objectName='XKCY(K}]~~[}AUT%FQ@S]A8.jpg' , size='10.72KB' , verifyMsg='文件1' }, ObjectVerifyInfo{objectName='XKCY(K}]~~[}AUT%FQ@S]A8.png' , size='10.72KB' , verifyMsg='文件1' }, ObjectVerifyInfo{objectName='bigImage.png' , size='1.21MB' , verifyMsg='文件2' }, ObjectVerifyInfo{objectName='bigImage02.png' , size='1.21MB' , verifyMsg='文件2' }]}
@AfterThrowing 当被切方法执行过程中抛出异常时,会进入 @AfterThrowing
注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing
属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Component @Aspect public class MyAop { public final static Logger log = LoggerFactory.getLogger(MyAop.class); @Pointcut("execution(* *..*.*())") public void pointCut () { } @AfterThrowing(value = "pointCut()",throwing = "t") public void doAfterExpetion (JoinPoint joinPoint,Throwable t) { Signature signature = joinPoint.getSignature(); String name = signature.getName(); log.error(name+"方法抛出异常!--" +t.toString()); } }
@Around spring的通知(Advice)中 一共有五种通知,之前已经介绍了四种,为什么不把环绕通知和它们放在一起说,因为环绕通知可以把前面的四种通知都表示出来
,而且环绕通知一般单独使用
环绕通知的使用:
Spring框架为我们提供了一个接口:ProceedingJoinPoint
,该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
该接口作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
增强代码写在调用proceed()方法之前为前置通知
,之后为返回通知
,写在catch中为异常通知
,写在finally中为后置通知
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 @Aspect @Component public class MyAroundAop { @Pointcut("execution("execution(* *..*.*())") //切入点签名 public void logAround() { System.out.println("pointCut签名。。。"); } //环绕通知,环绕增强,相当于MethodInterceptor @Around("logAround()") public Object aroundAdvice(ProceedingJoinPoint pjp) { Object rtValue = null; try { Object[] args = pjp.getArgs();//得到方法执行所需的参数 System.out.println("通知类中的aroundAdvice方法执行了[前置Advice]"); //可以对切入点方法的参数(args)在切面类中进行预处理,进行修改后把新的参数传入切入点方法 rtValue = pjp.proceed(args);//明确调用切入点方法(切入点方法) System.out.println("通知类中的aroundAdvice方法执行了[后置返回Advice]"); System.out.println("返回通知:"+rtValue); return rtValue; } catch (Throwable e) { System.out.println("通知类中的aroundAdvice方法执行了[异常Advice]"); throw new RuntimeException(e); } finally { System.out.println("通知类中的aroundAdvice方法执行了[后置Advice]"); } } }
AOP相关类 JoinPoint 对象 JoinPoint对象封装了SpringAop中切面方法的信息, 在切面方法中添加JoinPoint参数, 就可以获取到封装了该方法信息的JoinPoint对象
JoinPoint常用api
方法名
功能
Signature getSignature();
获取封装了署名信息的对象,在方法名功能Signature对象中可以获取到目标方法名,所属类的Class等信息
Object[] getArgs();
获取传入目标方法的参数对象,在@Around环绕通知中可以获取参数并且进行预处理, 然后再执行切入点方法proceed()
Object getTarget();
获取被代理的对象(标注的切入点所在的类的对象)
Object getThis();
获取代理对象
joinPoint.getSignature()示例
1 2 3 4 5 6 7 8 9 10 @Before("pointCut()") public void doBefore (JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); String declaringTypeName = signature.getDeclaringTypeName(); String funcName = signature.getName(); }
joinPoint.getArgs()示例
1 2 3 4 5 6 7 @Before("pointCut()") public void doBefore (JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); for (int i = 0 ; i < args.length; i++) { System.out.println("第" + (i+1 ) + "个参数为:" + args[i]); } }
ServletRequestAttributes对象 也可以用来记录一些信息,比如获取请求的 URL 和 IP, 可以在advice通知内容中使用, 获取查看请求信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest(); String contextPath = request.getContextPath();String servletPath = request.getServletPath();String realPath = request.getServletContext().getRealPath("/" );String requestURI = request.getRequestURI(); String requestURL = request.getRequestURL();
示例:
1 2 3 4 5 6 7 8 9 @Before("pointCut()") public void doBefore (JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String url = request.getRequestURL().toString(); String ip = request.getRemoteAddr(); }
ProceedingJoinPoint对象 ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中, 添加了
Object proceed() throws Throwable //执行目标方法
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法 两个方法
getArgs(); 获取切入点的参数
AOP 代码模板 笔记所用代码: 自定义注解类:
1 2 3 4 5 6 7 8 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value () default " " ; }
切面类:
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 @Component @Aspect public class MyAop { public final static Logger log = LoggerFactory.getLogger(MyAop.class); @Pointcut("@annotation(MyAnnotation)") public void beforeLog () { } @Pointcut("execution(* com.crisp.miniostorage.controller.BucketController.*(..))") public void afterLog () { } @Pointcut("execution(* com.crisp.miniostorage.controller.ObjectController.*(..))") public void afterReturningLog () { } @Pointcut("execution(* *..*.*())") public void pointCut () { } @Before("beforeLog()") public void doBefore () { log.info("业务方法执行!" ); } @After("afterLog()") public void doAfter (JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); String name = signature.getName(); log.info(name+"方法执行完毕!" ); } @AfterReturning(value = "afterReturningLog()",returning = "resultMsg") public void doAfterReturning (JoinPoint joinPoint, ResponseJson resultMsg) { log.info(resultMsg.toString()); } @AfterThrowing(value = "pointCut()",throwing = "t") public void doAfterExpetion (JoinPoint joinPoint,Throwable t) { Signature signature = joinPoint.getSignature(); String name = signature.getName(); log.error(name+"方法抛出异常!--" +t.toString()); } }
示例模板: 自定义注解类: CrispAnnotation:
1 2 3 4 5 6 7 8 9 10 import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface CrispAnnotation { String value () default " " ; }
自定义切面类:
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 73 import org.aspectj.lang.JoinPoint;import org.aspectj.lang.Signature;import org.aspectj.lang.annotation.*;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;@Component @Aspect public class CrispAop { public final static Logger log = LoggerFactory.getLogger(CrispAop.class); @Pointcut("execution(* com.crisp.项目包名.controller.*.*(..))") public void controllerPoint () { } @Pointcut("execution(* com.crisp.项目包名.service.*.*(..))") public void servicePoint () { } @Pointcut("controllerPoint()&&servicePoint()") public void globalPoint () { } @Pointcut("@annotation(CrispAnnotation)") public void annotationPoint () { } @Before("controllerPoint()") public void doBefore (JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); String packageName = signature.getDeclaringTypeName(); String name = signature.getName(); Object[] args = joinPoint.getArgs(); String parameter = "" ; for (int i = 0 ; i < args.length; i++) { parameter+=args[i].toString(); } log.info(packageName+"包下的" +name+"控制器方法执行[开始],所携带的参数为: " +parameter); } @AfterReturning(value = "controllerPoint()",returning = "object") public void doControllerAfter (JoinPoint joinPoint,Object object) { Signature signature = joinPoint.getSignature(); String packageName = signature.getDeclaringTypeName(); String name = signature.getName(); log.info(packageName+"包下的" +name+"控制器方法执行[结束],返回值为: " +object.toString()); } @AfterReturning(value = "servicePoint()",returning = "object") public void doServiceAfter (JoinPoint joinPoint,Object object) { Signature signature = joinPoint.getSignature(); String packageName = signature.getDeclaringTypeName(); String name = signature.getName(); log.info(packageName+"包下的" +name+"业务逻辑方法执行[结束],返回值为: " +object.toString()); } @AfterThrowing(value = "globalPoint()",throwing = "t") public void doAfterExpetion (JoinPoint joinPoint,Throwable t) { Signature signature = joinPoint.getSignature(); String packageName = signature.getDeclaringTypeName(); String name = signature.getName(); log.error(packageName+"包下的" +name+"方法抛出异常!!! 异常为: " +t.toString()); } }