【Spring】Spring AOP #### 文章目录* 前言* 1. 什么是 AOP* 2. 什么是 Spring AOP* 3. Spring AOP 的使用* * 引入 AOP 依赖 * 编写 AOP 程序* 4. Spring AOP 详解* * 4.1 Spring AOP 的概念 * * 4.1.1 切点 * 4.1.2 连接点 * 4.1.3 通知 * 4.1.4 切面 * 4.2 通知类型 * 4.3 切点 * 4.4 切面优先级 Order注解 * 4.5 切点表达式 * * 4.5.1 execution 切点表达式 * 4.5.2 annotation * * 4.5.2.1 自定义注解 * 4.5.2.2 切面类 * 4.5.2.3 添加自定义注解前言–前面我们学习了 SpringBoot 统一功能处理这篇文章我将为大家分享 Spring 框架的第二大核心——AOP第一大核心是 IOC1. 什么是 AOP-----------AOPAspect Oriented Programming是一种编程范型意为面向切面编程什么是向切面编程呢切面就是指某类特定问题所以AOP也可以理解为面向特定法编程它通过预编译和运行期动态代理的方式实现程序功能的统一维护。AOP可以看作是OOP面向对象编程的延续是软件开发中的一个热点也是Spring框架中的一个重要内容同时也是函数式编程的一种衍生范型。AOP的目标是实现对业务逻辑的各个部分进行隔离从而降低业务逻辑各部分之间的耦合度提高程序的可重用性并提升开发效率。在AOP中目标类代表核心代码和业务逻辑而额外功能也即AOP的功能则包括日志处理、事务处理、异常处理、性能分析等。通过将目标类与额外功能结合可以生成代理类。AOP的原理基于Java的动态代理机制。通过预编译方式和运行期动态代理AOP能够实现程序功能的统一维护使得开发者能够更加专注于业务逻辑的实现而无需过多关注其他非核心功能。AOP 是面向某一特定问题的编程那前面的统一功能处理什么呢其实前面学习的 统一功能处理是 AOP 的具体实现和应用。AOP是种思想拦截器是AOP思想的种实现。Spring框架实现了这种思想提供了拦截器技术的相关接口。2. 什么是 Spring AOP------------------知道了什么是 AOP那么什么是 Spring AOP 呢AOP 是一种思想而 Spring AOP、AspectJ、GGLIB等叫做 AOP 的实现。那么我们前面学习的拦截器、统一数据返回格式、统一异常处理这些 AOP 的实现不够吗其实是不够的拦截器作的维度是URL(次请求和响应)ControllerAdvice 应场景主要是全局异常处理(配合定义异常效果更佳)数据绑定数据预处理。AOP作的维度更加细致(可以根据包、类、法名、参数等进拦截)能够实现更加复杂的业务逻辑。假设一个项目中开发了很多业务功能但是呢由于一些业务的执行效率比较低耗时较长所以我们就需要对接口进行优化。首先需要做的就是找到耗时较长的业务方法那么我们如何知道每个业务方法执行的时间呢一个简单的方法就是可以通过获取到业务方法刚执行时候的时间 start再记录这个业务方法刚结束时候的时间 end用 end - statr就得到了该业务方法的执行时间那么是否意味着我们需要在每个方法中都加上这段代码呢 public void function1() { long startTime System.currentTimeMillis(); test(); long endTime System.currentTimeMillis(); log.info(“function1执行耗时” (endTime - startTime)); } 如果每个方法都加上这样的逻辑的话如果业务中的方法很多的话那么这也是一个很大的工作量所以这时候就可以用到我们的 AOP 了AOP可以做到在不改动这些原始法的基础上针对特定的法进行功能的增强。3. Spring AOP 的使用------------------### 引入 AOP 依赖AOP 属于第三方库要想使用的话就需要在 pom.xml 文件中引入对应的 AOP 依赖 org.springframework.boot spring-boot-starter-aop ### 编写 AOP 程序首先我们需要在类上加上Aspect注解表明这个类是一个 AOP 类import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; Slf4j Aspect Component public class TimeAspect { Around(“execution(* com.example.springbootbook2.controller..(…))”) public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { long begin System.currentTimeMillis(); //执行原始方法 Object result pjp.proceed(); long end System.currentTimeMillis(); log.info(pjp.getSignature() “执行耗时{}ms”,(end - begin)); return result; } } * Aspect标识这是个切面类* Around环绕通知在目标法的前后都会被执。后的表达式表示对哪些方法进行增强* pjp.proceed()让原始法执行因为Around注解的作用这个代码被分为了三个部分看看执行效果可以看到使用 AOP 可以在不更改原来业务代码的基础上做出一些额外的工作。AOP 面向切面编程的优势* 代码无侵入不修改原始的业务方法就可以对原始的业务方法进行功能的增强或是功能的改变* 减少重复代码* 提高开发效率* 维护方便既然 AOP 面向切面编程有这么多优势拿那么接下来我们来详细的学习一下 Spring AOP。4. Spring AOP 详解-----------------### 4.1 Spring AOP 的概念Spring AOP 需要了解的概念主要有1. 切点2. 连接点3. 通知4. 切面#### 4.1.1 切点切点PointCur也成为切入点作用是提供一组规则告诉程序哪些方法来进行功能增强。也就类似前面拦截器中的配置拦截路径。也就是这个execution(* com.example.springbootbook2.controller.*.*(..))叫做切点表达式这里后面为大家详细介绍。#### 4.1.2 连接点满足切点表达式规则的方法就是连接点也就是可以被 AOP 控制的方法参考上面的例子也就是com.example.springboot2.controller包下的所有类中的所有方法都叫做连接点。在我们上面的例子中主要体现在 pjp 参数中通过这个参数的proceed()方法可以执行目标方法。#### 4.1.3 通知通知就是具体要做的工作指那些重复的逻辑也就是共性功能最终体现为一个方法在AOP面向切面编程中我们把这部分重复的代码逻辑抽取出来单独定义这部分代码就是通知的内容。#### 4.1.4 切面切面(Aspect)就是 切点(PointCut) 通知(Advice)。通过切面就能够描述当前 AOP 程序需要针对于哪些法在什么时候执什么样的操作。切面既包含了通知逻辑的定义也包括了连接点的定义。一个切面类可以存在多个切面。### 4.2 通知类型Spring AOP 中的通知类型有以下几种1. Around环绕通知此注解标注的通知方法在目标方法前都被执行2. Before前置通知此注解标注的通知方法在目标方法前被执行3. After后置通过此注解标注的通知方法在目标方法后被执行论是否有异常都会执行4. AfterReturning返回后通知此注解标注的通知方法在目标方法后被执行有异常不会被执行5. AfterThrowing异常后通知此注解标注的通知方法发生异常后执行接下来我们通过一个例子来了解这几种通知类型 import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.; import org.springframework.stereotype.Component; Slf4j Aspect Component public class AspectDemo { Before(execution(com.example.springaop.controller..(…))“) public void doBefore() { log.info(“执行 Before 方法”); } After(“execution(* com.example.springaop.controller..(…))”) public void doAfter() { log.info(“执行 After 方法”); } AfterReturning(“execution(* com.example.springaop.controller..(…))”) public void doAfterReturning() { log.info(“执行 AfterReturning 方法”); } AfterThrowing(“execution(* com.example.springaop.controller..(…))”) public void doAfterThrowing() { log.info(“执行 AfterThrowing 方法”); } Around(“execution(* com.example.springaop.controller..(…))”) public Object doAround(ProceedingJoinPoint pjp) throws Throwable { log.info(“Around 方法开始执行”); Object result pjp.proceed(); log.info(“Around 方法执行后”); return result; } } import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; Slf4j RestController RequestMapping(“test”) public class TestController { RequestMapping(”/t1) public String t1() { log.info(“目标方法执行”); return “hello”; } }这里少了一个日志就是AfterThrowing方法打印的日志这是因为我们这个方法没有出现错误所以我们重新定义一个内部会剖出异常的方法 RequestMapping(“/t2”) public Integer t2() { log.info(“目标方法2执行”); Integer result 10/0; return result; }可以看到这几个通知类型的执行顺序目标方法中不出现异常Around通知类型的目标方法执行前的逻辑——Before通知类型——目标方法——AfterReturning通知类型——After通知类型——Around通知类型的目标方法执行之后的逻辑目标方法中出现异常Around通知类型的目标方法执行前的逻辑——Before通知类型——目标方法——AfterThrowing通知类型——After通知类型注意* Around 环绕通知需要调用ProceedingJoinPoint.proceed()来让原始方法执行其他通过不需要考虑目标方法的执行* Around 环绕通知方法的返回值必须指定为 Object来接收原始方法的返回值否则原始方法执行完毕是获取不到返回值的* 个切面类可以有多个切点### 4.3 切点通过前面的例子我们可以发现同样的切点表达式写了很多那么是否有一种方法可以节省相同切点表达式的书写呢当然是可以的我们程序员可是以“懒”著称的。我们可以使用PointCut注解将相同的切点表达式提取出来需要用到时引用该切点表达式即可。 Pointcut(“切点表达式”) 修饰限定词 返回值 方法名(){} public class AspectDemo { Pointcut(“execution(* com.example.springaop.controller..(…))”) private void pt(){} Before(“pt()”) public void doBefore() { log.info(“执行 Before 方法”); } After(“pt()”) public void doAfter() { log.info(“执行 After 方法”); } AfterReturning(“pt()”) public void doAfterReturning() { log.info(“执行 AfterReturning 方法”); } AfterThrowing(“pt()”) public void doAfterThrowing() { log.info(“执行 AfterThrowing 方法”); } Around(“pt()”) public Object doAround(ProceedingJoinPoint pjp) throws Throwable { log.info(“Around 方法开始执行”); Object result pjp.proceed(); log.info(“Around 方法执行后”); return result; } }我们这里PointCut注解的方法的修饰限定词是 private该切点表达式只能在当前类中使用如果我们想要在当前项目的其他切面类中使用这个提取出来的切点表达式的话需要将方法的修饰限定词改为 public并且引用这个切点表达式的方法需要使用 全限定类名.方法名()。 Pointcut(“execution(* com.example.springaop.controller..(…))”) public void pt(){} Slf4j Component Aspect public class AspectDemo2 { Before(“com.example.springaop.aspect.AspectDemo.pt()”) public void deBefore() { log.info(“执行 AspectDemo2 中的 Before 方法”); } }### 4.4 切面优先级 Order注解当存在多个切面的时候并且这些切面类的多个切入点都匹配到了同一个目标方法当目标方法运行的时候这些切面类中的通知方法都会执行这些通知方法的执行顺序会遵守两个原则1. 前面通知类型的先后顺序2. 切面类的类名排序我们创建出三个切面类来看多个切面类匹配到了同一个目标方法之后的通知方法的执行顺序通过这个运行结果可以知道当存在多个切面类时默认按照切面类名字母排序* Before 通知字母排名靠前的先执行* After 通知字母排名靠前的后执行就可以形象的看成这个模型但是根据切面类的类名字母排序的话很不方便所以为了切面类执行顺序更好的控制就出现了一个注解Order()用来控制多个切面类的执行顺序。可以看到通过使用Order注解可以控制多个切面类的执行顺序。### 4.5 切点表达式前面我们使用切点表达式来描述切点接下来我们来详细介绍一下切点表达式的语法。切点表达式常见的有两种表达方式1. execution(…)根据方法的签名来匹配2. annotation(…)根据注解匹配#### 4.5.1 execution 切点表达式execution() 是最常用的切点表达式用来匹配方法它的语法为execution(访问修饰符 返回类型 包名.类名.方法(方法参数) 异常)其中访问修饰符和异常可以省略。切点表达式支持通配符表达1. *匹配任意字符只匹配一个元素返回类型包类名方法或者方法参数* 包名使用 * 表示任意包一层包使用一个** 类名使用 * 表示任意类* 返回值使用 * 表示任意返回值类型* 法名使用 * 表示任意法* 参数使用 * 表示个任意类型的参数2. … 匹配多个连续的任意符号可以通配任意层级的包或任意类型任意个数的参数* 使用 … 配置包名表示此包以及此包下的所有子包* 可以使用 … 配置参数任意个任意类型的参数示例TestController 下的 public修饰返回类型为 String 方法名为 t1参法execution(public String com.example.demo.controller.TestController.t1())省略访问修饰符execution(String com.example.demo.controller.TestController.t1())匹配所有返回类型execution(* com.example.demo.controller.TestController.t1())匹配 TestController 下的所有无参方法execution(* com.example.demo.controller.TestController.*())匹配 TestController 下的所有方法execution(* com.example.demo.controller.TestController.*(..))匹配 controller 包下所有的类的所有方法execution(* com.example.demo.controller.*.*(..))匹配所有包下的 TestControllerexecution(* com..TestController.*(..))匹配 com.example.demo 包下子孙包下的所有类的所有法execution(* com.example.demo..*(..))#### 4.5.2 annotationexecution 表达式适用于更符合规则的如果我们要匹配更多无规则的方法的话使用 execution 表达式就会很吃力。假设我需要捕获到 t2 和 t3 方法使用 execution 表达式该如何写呢因为这两个方法的返回值不同使用 execution 表达式无法同时表示出这两个方法。所以这时候就需要使用annotation注解来捕获到更多无规则的方法。##### 4.5.2.1 自定义注解如何使用annotaion呢首先我们需要自定义出注解。在新建 Java class 的时候选择 Annotation然后里面的内容我们就实现一个最简单的注解参考Component注解来构造一个自定义的注解Target 标识了 Annotation 所修饰的对象范围即该注解可以用在什么地方* ElementType.TYPE于描述类、接包括注解类型或enum声明* ElementType.METHOD描述方法* ElementType.PARAMETER描述参数* ElementType.TYPE_USE可以标注任意类型Retention 指Annotation 被保留的时间长短标明注解的命周期1. RetentionPolicy.SOURCE表示注解仅存在于源代码中编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息只能在编译时使。比如 SuppressWarnings 以及lombok提供的注解 Data Slf4j2. RetentionPolicy.CLASS编译时注解。表示注解存在于源代码和字节码中但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息但在实际运行时法获取。通常于些框架和具的注解3. RetentionPolicy.RUNTIME运行时注解。表示注解存在于源代码字节码和运行时中。这意味着在编译时字节码中和实际运行时都可以通过反射获取到该注解的信息。通常于些需要在运行时处理的注解如Spring的 Controller ResponseBody我们一般就使用 RUNTIME 来指明注解的存在时间。 package com.example.springaop.aspect; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target({ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) public interface MyAspect { } ##### 4.5.2.2 切面类使用 annotation 切点表达式定义切点只对 MyAspect 效annotation 中需要填入我们自定义注解的全限定名。 Slf4j Component Aspect public class MyAspectDemo { Before(“annotation(com.example.springaop.aspect.MyAspect)”) public void before() { log.info(“执行 MyAspectDemo 的 Before 方法”); } After(“annotation(com.example.springaop.aspect.MyAspect)”) public void after() { log.info(“执行 MyAspect 的 After 方法”); } } ##### 4.5.2.3 添加自定义注解 Slf4j RestController RequestMapping(“test”) public class TestController { RequestMapping(“/t1”) public String t1() { log.info(“目标方法执行”); return “hello”; } MyAspect RequestMapping(“/t2”) public String t2() { log.info(“我爱Java我要成为Java高手”); return “hello world”; } MyAspect RequestMapping(“/t3”) public Integer t3() { log.info(“目标方法2执行”); Integer result 10/0; return result; } }可以看到通过使用annotation可以更加灵活的捕获到无规则的方法。