SpringBoot AOP切面实现

文章目录

  • 一、AOP简介
  • 二、AOP体系与概念
  • 三、AOP实例
    • 1、创建SpringBoot工程
    • 2、添加依赖
    • 3、AOP相关注解
      • 3.1、@Aspect
      • 3.2、@Pointcut
        • 3.2.1、execution()
        • 3.2.2、annotation()
      • 3.3、@Around
      • 3.4、@Before
      • 3.5、@After
      • 3.6、@AfterReturning
      • 3.7、@AfterThrowing

一、AOP简介

AOP(Aspect Oriented Programming),面向切面思想,是Spring的三大核心思想之一(其余两个:IOC - 控制反转DI - 依赖注入)。


那么AOP为何那么重要呢?

在我们的程序中,经常存在一些系统性的需求,比如 权限校验日志记录统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护,那么面向切面编程往往让我们的开发更加低耦合,也大大减少了代码量,同时呢让我们更专注于业务模块的开发,把那些与业务无关的东西提取出去,便于后期的维护和迭代。


二、AOP体系与概念

简单地去理解,其实AOP要做三类事:

  • 在哪里切入,也就是权限校验等非业务操作在哪些业务代码中执行。

  • 在什么时候切入,是业务代码执行前还是执行后。

  • 切入后做什么事,比如做权限校验、日志记录等。

AOP的体系图:
请添加图片描述
一些概念:

概念说明
Pointcut切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为execution方式和 annotation 方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
Advice处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
Aspect切面,即 PointcutAdvice
Joint point连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
Weaving织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。

三、AOP实例

1、创建SpringBoot工程

如何创建详见:IDEA 创建 SpringBoot 项目


2、添加依赖

<!-- springboot-aop包,AOP切面注解,Aspectd等相关注解 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3、AOP相关注解

package com.cw.tsb.app.aspect;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Component
@Aspect
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。}@Around("pointCut()")public Object doAround(ProceedingJoinPoint joinPoint) {System.out.println("------------- doAround.");Object obj = null;try {obj = joinPoint.proceed();} catch (Throwable t){t.printStackTrace();}return obj;}@After("pointCut()")public void doAfter(JoinPoint joinPoint){System.out.println("------------- doAfter.");}@Before("pointCut()")public void doBefore(JoinPoint joinPoint){System.out.println("------------- doBefore.");}/*** 后置返回*      如果第一个参数为JoinPoint,则第二个参数为返回值的信息*      如果第一个参数不为JoinPoint,则第一个参数为returning中对应的参数* returning:限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知,否则不执行,*      参数为Object类型将匹配任何目标返回值*/@AfterReturning(value = "pointCut()", returning = "result")public void doAfterReturning(JoinPoint joinPoint, String result){System.out.println("doAfterReturning result = " + result);}@AfterThrowing(value = "pointCut()", throwing = "t")public void doAfterThrowing(JoinPoint joinPoint, Throwable t){System.out.println("------------- doAfterThrowing throwable = " + t.toString());}
}

3.1、@Aspect

该注解要添加在类上,声明这是一个切面类,使用时需要与@Component注解一起用,表明同时将该类交给spring管理。

@Component
@Aspect
public class ControllerAspect {
}

3.2、@Pointcut

用来定义一个切点,即上文中所关注的某件事情的入口,切入点定义了事件触发时机。

该注解需要添加在方法上,该方法签名必须是 public void 类型,可以将@Pointcut 中的方法看作是一个用来引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此 @Pointcut 中的方法只需要方法签名,而不需要在方法体内编写实际代码

该注解有两个常用的表达式:execution()annotation()


3.2.1、execution()

@Aspect
@Component
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。}
}

表达式为:

execution(* com.cw.tsb.app.controller..*.*(..))
  • 第一个 * :表示返回值类型,* 表示所有类型;

  • 包名:标识需要拦截的包名;

  • 包名后的 ..:表示当前包和当前包的所有子包,在本例中指 com.cw.tsb.app.controller 包、子包下所有类;

  • 第二个 * :表示类名,* 表示所有类;

  • 最后的 *(..) :星号表示方法名,* 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。


3.2.2、annotation()

annotation() 方式是针对某个注解来定义切点,比如我们对具有 @PostMapping 注解的方法做切面,可以如下定义切面:

@Aspect
@Component
public class ControllerAspect {@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")public void pointCut() {//该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。}
}

然后使用该切面的话,就会切入注解是 @PostMapping 的所有方法。这种方式很适合处理 @GetMapping@PostMapping@DeleteMapping不同注解有各种特定处理逻辑的场景。

还有就是如上面案例所示,针对自定义注解来定义切面。

@Aspect
@Component
public class ControllerAspect {@Pointcut("@annotation(com.cw.tsb.app.annotation.PermissionsAnnotation)")private void permissionCheck() {//该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。}
}

3.3、@Around

@Around 注解用于修饰 Around 增强处理,Around增强处理非常强大,表现在:

  • @Around 可以自由选择增强动作与目标方法的执行顺序,也就是说可以在增强动作前后,甚至过程中执行目标方法。这个特性的实现在于,调用 ProceedingJoinPoint 参数的 procedd() 方法才会执行目标方法。

  • @Around 可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。

Around 增强处理有以下特点:

  • 当定义一个 Around 增强处理方法时,该方法的第一个形参必须是 ProceedingJoinPoint 类型(至少一个形参)。在增强处理方法体内,调用 ProceedingJoinPointproceed 方法才会执行目标方法:这就是 @Around 增强处理可以完全控制目标方法执行时机、如何执行的关键;如果程序没有调用 ProceedingJoinPointproceed 方法,则目标方法不会执行。

  • 调用 ProceedingJoinPointproceed 方法时,还可以传入一个 Object[] 对象,该数组中的值将被传入目标方法作为实参 —— 这就是 Around 增强处理方法可以改变目标方法参数值的关键。这就是如果传入的 Object[] 数组长度与目标方法所需要的参数个数不相等,或者 Object[] 数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。

@Around 功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的@Before@AfterReturning 就能解决的问题,就没有必要使用 Around 了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用 Around 。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用 Around 增强处理了。


3.4、@Before

@Before 注解指定的方法在切面切入目标方法之前执行,可以做一些 Log 处理,也可以做一些信息的统计,比如 获取用户的请求 URL 以及 用户的 IP 地址等等,这个在做个人站点的时候都能用得到,都是常用的方法。例如下面代码:

@Aspect
@Component
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。}/*** 在上面定义的切面方法之前执行该方法* @param joinPoint jointPoint*/@Before("pointCut()")public void doBefore(JoinPoint joinPoint) {// 获取签名Signature signature = joinPoint.getSignature();// 获取切入的包名String declaringTypeName = signature.getDeclaringTypeName();// 获取即将执行的方法名String funcName = signature.getName();log.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);// 也可以用来记录一些信息,比如获取请求的 URL 和 IPServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 获取请求 URLString url = request.getRequestURL().toString();// 获取请求 IPString ip = request.getRemoteAddr();}
}

JointPoint 对象很有用,可以用它来获取一个签名,利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs() 获取)等。


3.5、@After

@After 注解和 @Before 注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。

@Aspect
@Component
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。}/*** 在上面定义的切面方法之后执行该方法* @param joinPoint jointPoint*/@After("pointCut()")public void doAfter(JoinPoint joinPoint) {log.info("==== doAfter 方法进入了====");Signature signature = joinPoint.getSignature();String method = signature.getName();log.info("方法{}已经执行完", method);}
}

到这里,我们来写个 Controller 测试一下执行结果,新建一个 AopController 如下:

@RestController
@RequestMapping("/aop")
public class AopController {@GetMapping("/{name}")public String testAop(@PathVariable String name) {return "Hello " + name;}
}

启动项目,在浏览器中输入:http://localhost:8080/aop/csdn,观察一下控制台的输出信息:

====doBefore 方法进入了====  
即将执行方法为: testAop,属于com.itcodai.mutest.AopController包  
用户请求的 url 为:http://localhost:8080/aop/name,ip地址为:0:0:0:0:0:0:0:1  
==== doAfter 方法进入了====  
方法 testAop 已经执行完

从打印出来的 Log 中可以看出程序执行的逻辑与顺序,可以很直观的掌握 @Before@After 两个注解的实际作用。


3.6、@AfterReturning

@AfterReturning 注解和 @After 有些类似,区别在于 @AfterReturning 注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理,例如:

@Aspect
@Component
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。}/*** 后置返回*      如果第一个参数为JoinPoint,则第二个参数为返回值的信息*      如果第一个参数不为JoinPoint,则第一个参数为returning中对应的参数* returning:限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知,否则不执行,*      参数为Object类型将匹配任何目标返回值*/@AfterReturning(value = "pointCut()", returning = "result")public void doAfterReturning(JoinPoint joinPoint, String result){// 实际项目中可以根据业务做具体的返回值增强}
}

需要注意的是,在 @AfterReturning 注解 中,属性 returning 的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning 方法中可以对返回值进行增强,可以根据业务需要做相应的封装。


3.7、@AfterThrowing

当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing 属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。

@Aspect
@Component
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。}@AfterThrowing(value = "pointCut()", throwing = "t")public void doAfterThrowing(JoinPoint joinPoint, Throwable t){System.out.println("------------- doAfterThrowing throwable = " + t.toString());// 处理异常的逻辑}
}



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/446667.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

英语口语-文章朗读Week8 Friday

文章 It is a phenomenon that people are losing trust in each other in today’s society. Some people become selfish,and for interest, they are likely to betray their colleagues,friends, and even their relatives. They tend to cater to those who can benefit …

C++primer 第 3 章 字符串、向量和数组 3 . 4 迭代器介绍

3.4迭代器介绍 我们已经知道可以使用下标运算符来访问string对象的字符或vector对象的元素&#xff0c;还有另外一种更通用的机制也可以实现同样的目的&#xff0c;这就是迭代器&#xff08;iterator&#xff09;。在第II部分中将要介绍&#xff0c;除了vector之外&#xff0c…

英语口语-文章朗读Week9 TuesDay

朗读文章 People living in ancient times had no alternative but to do housework manually. They fire the wood when they cook,they hand wash clothes with hands; they sweep the floor with brooms. Now, modern inventions come as a great relief to people. We co…

C++primer 第 3 章 字符串、向量和数组 3 . 5 数组

3.5数组 数组是一种类似于标准库类型vector&#xff08;参见3.3节&#xff0c;第86页&#xff09;的数据结构&#xff0c;但是在性能和灵活性的权衡上又与vector有所不同。与vector相似的地方是&#xff0c;数组也是存放类型相同的对象的容器&#xff0c;这些对象本身没有名字…

C++primer 第 4 章 表达式 4.1基础 4 . 2 算术运算符 4 .3 逻辑和关系运算符 4 . 4 赋值运算符 4 .5 递增和递减运算符 4.6成员访问运算符

表达式由一个或多个运算对象(operand)组成&#xff0c;对表达式求值将得到一个结果(result)字面值和变量是最简单的表达式(expression),其结果就是字面值和变量的值。把一个运算符(operator)和一个或多个运算对象组合起来可以生成较复杂的表达式 4.1基础 有几个基础概念对表达…

codeforces 266B-C语言解题报告

266B题目网址 题目解析 输入n,t,排队情况s,输出第t次循环后,排队情况 举例: 输入: 5 1 BGGBG 输出: GBGGB 2.输入的n代表排队的人数,t代表整个循环t次之后再输出结果 3.注意点: 使用while()大循环去控制t次的循环,使用for()内层循环去遍历整个字符串 如果if(s[j]‘B’&…

英语口语-文章朗读Week9 Wednesday

英语文章 Birds of the same species flock together&#xff0c; People tend to look for someone like themselves to be friends. But having the same interests is not the only standard when we are seeking friends. In most cases, especially for adults, people l…

C++primer 第 4 章 表达式 4.7条件运算符 4.8位运算符 4.9 sizeof运算符 4.10逗号运算符 4.11类型转换 4 . 1 2 运算符优先级表

4.7条件运算符 条件运算符(?&#xff1a;)允许我们把简单的if else逻辑嵌入到单个表达式当中&#xff0c;条件运算符按照如下形式使用&#xff1a;cond ? expr1 : expr2;其中cond是判断条件的表达式&#xff0c;而expr1和expr2是两个类型相同或可能转换为某个公共类型的表达…

Git 之 git tag标签使用

目录一、简介二、本地tag操作1、创建tag标签&#xff08;1&#xff09;创建轻量标签&#xff08;2&#xff09;创建附注标签2、查看tag标签&#xff08;1&#xff09;查看标签列表&#xff08;2&#xff09;查看标签提交信息&#xff08;3&#xff09;在提交历史中查看标签3、删…

C++primer 第 5 章语句 5.2语句作用域 5.3条件语句 5 . 4 迭代语句 5.5跳转语句 5.6 try语句块和异常处理

5 . 1 简单语句 C语言中的大多数语句都以分号结束&#xff0c;一个表达式&#xff0c;比如ival 5 , 末尾加上分号就变成了表达式语句(expression statement)。表达式语句的作用是执行表达式并丢弃掉求值结果&#xff1a;ival 5&#xff1b; // 一条没什么实际用处的表达式语…

英语口语-文章朗读Week9Thursday

英语文章 Everyone has his or her own dreams. Some people wants to be millionaires so they can give many generous donations later; some people want to be scientists so they can bring many conveniences to the world; some people only want to be bus-drivers s…

操作系统 内存管理相关知识

cpu执行程序的基本过程 译码器 输入为n管脚&#xff0c;输出为2^n根管脚&#xff0c;编号为从0到2^(n-1)&#xff0c;用少的输入端控制更多的输出端最常用的是三八译码器AD(Address bus)地址总线: 选中一行数据每一行 8bit 组成8吧B cpu输入端32根线&#xff0c;输出端就可以控…

Chrome浏览器必装插件!尤其程序猿!

Chrome 浏览器有一个好处&#xff0c;就是插件极其丰富&#xff0c;只有你想不到的&#xff0c;没有你找不到的&#xff0c;这恐怕是 Chrome 浏览器被众多爱好者钟爱的原因吧。 言归正传&#xff0c;今天来给大家推荐 10 款我自己珍藏的 Chrome 浏览器插件。 1、crxMouse Ch…

英语口语-文章朗读Week10 Monday

英语文章 Here are some valuable suggestions which may assist you in landing good job First, make your resume clear and associate it with the position you are applying for. Try to add details like your temporary jobs at college or your former jobs Second, …

英语口语-文章朗读Week10 Wednesday

英语文章 Everyone needs sleep for survival, but how much? It is said that eight hours of sleep is fundamental to a healthy person. But today, many people are sacrificing their sleep time。 Modern people have so many alternatives: cell phones, PCs, TVs, g…

嵌入式Linux多任务编程 进程 管道 命名管道

进程 进程是一个可并发执行的具有独立功能的程序关于某个数据集合的一次执行过程&#xff0c;也是操作系统执行资源分配和保护的基本单位。程序的一次执行就是一个进程一个程序可以派生多个进程多个不同程序运行的时候&#xff0c;也会有多个相对应的进程与其相互对应进程是动…

英语口语-文章朗读Week10 Thursday

英语文章 There are many customs and traditions in Chinese civilization. Here, we will talk about the development of the way people greet each other: In ancient times, people had to kneel to those who were superior to them. This custom remained until the …

Linux进程之间通信 信号

2) SIGINT 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出&#xff0c;用于通知前台进程组终止进程。 3) SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是Ctrl-\)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。 15)…

Linux进程之间通信 消息队列

使用命令 ipcs -q 查看对应的消息队列代码 文件接收者 #include <sys/types.h> #include <stdio.h> #include <unistd.h> #include <string> #include <signal.h> #include <wait.h> #include <sys/msg.h> #include <cstring&g…