【Java EE进阶

2024-07-06

前言前面学习过Spring的第一大核心Spring-Ioc,受到众多读者访问,而今天要了解的AOP甚至比它更加抽象,难以理解。

AOP思想AOP全称:Aspect Oriented Programming(面向切面编程);切面:指某一类特定问题(后面会系统学习)。所以:AOP也可以理解为面向特定方法编程的思想。

核心思想:将业务逻辑与横切关注点分离,集中解决某一类问题,也就是上篇博客写的统一处理问题,就是AOP思想的具体实现。

Spring AOP:AOP是一种思想,而Spring框架实现了这种思想。

初学AOP我们会通过一个例子来体验一下Spring AOP的使用:实现一个功能记录各个接口方法的执行时间。

正常情况下,我们首先考虑的就是在方法运行前和运行后,记录下开始时间和结束时间,计算二者之差即可。

代码语言:javascript复制 long startTime=System.currentTimeMillis();

//执行方法

long endTime=System.currentTimeMillis();

log.info("方法耗时:"+(endTime-startTime)+" ms");这个方法可以,但是如果我在一个项目中要测超级多的接口方法耗时,那意味着我要去给每一个方法写这段代码,工作量太大。

那这时就需要AOP来解决:AOP在程序运行期间在不修改源代码的基础上对已有方法进行增强。

准备工作引入AOP依赖:

在项目中引入Spring AOP的依赖,在pom.xml中添加:

代码语言:javascript复制

org.springframework.boot

spring-boot-starter-aop

编写记录每个方法耗时的程序:

代码语言:javascript复制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;

import javax.naming.event.ObjectChangeListener;

@Slf4j

@Component

@Aspect

public class TimeAspect {

/**

* 记录方法耗时

*/

@Around("execution(* com.zc.blog.controller.*.*(..))")

public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {

long startTime=System.currentTimeMillis();

Object result=pjp.proceed();

long endTime=System.currentTimeMillis();

log.info("方法耗时:"+(endTime-startTime)+" ms");

return result;

}

}在这里插入图片描述

@Aspect:标识这是⼀个切⾯类

@Around: 环绕通知,在⽬标⽅法的前后都会被执⾏ . 后⾯的表达式表⽰对哪些⽅法进⾏增强

ProceedingJoinPoint.proceed() 让原始⽅法执⾏

总结:通过上面的实例,我们发现AOP编程的优点

1.代码解耦性强:无入侵性,在不修改原始代码的基础上就可以对原始方法进行增强

2.减少代码的重复率

3.维护方便

Spring AOP详解切点(Pointcut)切点,也称为"切入点":提供一组规则(切点表达式语言),告诉程序对哪些方法进行增强。也就是告诉程序被增强方法的路径

在这里插入图片描述连接点(Join Point)满足切点表达式规则的方法,就是连接点,也就是能被AOP控制的方法。

实例中,controller包中的所有方法都是连接点。

比如:

切点表达式:计算机学院所有学生。 连接点:张三 、小王、小赵、小段等学生

通知(Advice)通知不是我们理解的”通知“,这里指:具体要完成的工作,那些重复的逻辑,也就是功能增强部分(记录耗时时间的方法)

在这里插入图片描述

在AOP面向切面编程中,这部分重复的逻辑代码单独定义,这部分代码就是通知的内容。

切面(Aspect)切面(Aspect)=切点(Pointcut)+ 通知(Advice)

通过公式就知道,切面是通过切点完成通知;也是当前AOP程序针对哪些方法(切点),在什么时候执行什么操作(通知)

在这里插入图片描述注意:切⾯所在的类,我们⼀般称为切⾯类(被@Aspect注解标识的类)

通知类型Spring AOP提供了五种通知类型

前置通知(@Before):在目标方法执行之前执行后置通知(@After):在目标方法执行之后执行,无论方法是否正常返回返回通知(@AfterReturning):在目标方法正常返回之后执行异常通知(@AfterThrowing):在目标方法抛出异常时执行环绕通知(@Around):围绕目标方法执行,可以在方法执行前后自定义逻辑我们通过代码来显式的观察这几种类型:

代码语言:javascript复制@Slf4j

@Component

@Aspect

public class AspectDemo {

//前置通知

@Before("execution(* com.zc.blog.controller.*.*(..))")

public void doBefore(){

log.info("执行Before方法");

}

//后置通知

@After("execution(* com.zc.blog.controller.*.*(..))")

public void doAfter(){

log.info("执行After方法");

}

//返回后通知

@AfterReturning("execution(* com.zc.blog.controller.*.*(..))")

public void doAfterReturning(){

log.info("执行AfterReturning方法");

}

//抛出异常后通知

@AfterThrowing("execution(* com.zc.blog.controller.*.*(..))")

public void doAfterThrowing(){

log.info("执行AfterThrowing方法");

}

//添加环绕通知

@Around("execution(* com.zc.blog.controller.*.*(..))")

public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

log.info("方法执行前执行");

Object result=joinPoint.proceed();

log.info("方法结束后执行");

return result;

}

}在这里插入图片描述

程序运行逻辑:

在这里插入图片描述

方法抛出异常时,表示方法逻辑有误,通知中的环绕后的代码逻辑也不会执行,因为@Around环绕通知后需要调用joinPoint.proceed()方法。

注意:观察代码发现,一个切面类可以有多个切点

@PointCut我们发现上面代码的切点全部相同,这样代码中存在大量重复,Spring提供的@PointCut注解可以把公共的切点提取出来,需要时引入该切点即可。(类似于数学中的乘法分配律)

代码语言:javascript复制@Slf4j

@Component

@Aspect

public class AspectDemo {

@Pointcut("execution(* com.zc.blog.controller.*.*(..))")

private void pt(){};

//前置通知

@Before("pt()")

public void doBefore(){

log.info("执行Before方法");

}

//后置通知

@After("pt()")

public void doAfter(){

log.info("执行After方法");

}

...同时目前切点定义只能在当前切面类中使用,如果在其他切面类中使用,就需要把private改为public ,引用方式为:全限定类名.方法名()

代码语言:javascript复制@Before("com.zc.blog.Demo.AspectDemo.pt()")

public void doBefore(){

log.info("执行Before方法");

}切面优先级@Order当我们在项目中定义多个切面类时,并且这些切面类中有多个切点都匹配到同一个方法,当这个方法运行时,这些切面类中的通知都会运行,那么它们的运行顺序是什么呢?

多创建几个切面类测试:

在这里插入图片描述

发现规律:

@Before通知:排名靠前的先执行@After通知:排名靠后的后执行但是这种情况不受我们程序员掌控,为了提高我们对代码的控制,使用@Order注解:现在我们按照默认的倒序运行

代码语言:javascript复制@Slf4j

@Component

@Aspect

@Order(1)

public class AspectDemo4 {

@Pointcut("execution(* com.zc.blog.controller.*.*(..))")

private void pt(){};

//.....

}

@Slf4j

@Component

@Aspect

@Order(2)

public class AspectDemo3 {

@Pointcut("execution(* com.zc.blog.controller.*.*(..))")

private void pt(){};

//.....

}

@Slf4j

@Component

@Aspect

@Order(3)

public class AspectDemo2 {

@Pointcut("execution(* com.zc.blog.controller.*.*(..))")

private void pt(){};

//.....

}在这里插入图片描述

使用@Order注解执行顺序是:

@Before通知:数字越小先执行@After通知:数字越大后执行总结:@Order控制切⾯的优先级,先执⾏优先级较⾼的切⾯,再执⾏优先级较低的切⾯,最终执⾏⽬标⽅法

切点表达式常见的表达式有两种:

execution(......): 根据方法的签名来匹配@annotation(...): 根据注解匹配execution表达式 语法规则:

execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)

在这里插入图片描述@annotation匹配多个类的方法,execution表达式实现比较复杂,我们借助自定义注解的方式以及 切点表达式@annotation 来描述这个场景

编写自定义注解@MyAspect代码语言:javascript复制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 {

}简单解释一下两个注解:

@Target 标识了Annotation 所修饰的对象范围,即该注解可以⽤在什么地⽅

ElementType.TYPE: ⽤于描述类、接⼝(包括注解类型)或enum声明

ElementType.METHOD:描述⽅法

ElementType.PARAMETER:描述参数

ElementType.TYPE_USE: 可以标注任意类型

@Retention指Annotation被保留的时间长短,表示注解的生命周期

RetentionPolicy.RUNTIME:运⾏时注解.表⽰注解存在于源代码,字节码和运⾏时中.这意味着在编译时,字节码中和实际运⾏时都可以通过反射获取到该注解的信息.通常⽤于⼀些需要在运⾏时处理的注解

RetentionPolicy.CLASS:编译时注解.表⽰注解存在于源代码和字节码中,但在运⾏时会被丢弃.这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运⾏时⽆法获取.通常⽤于⼀些框架和⼯具的注解

RetentionPolicy.SOURCE:表⽰注解仅存在于源代码中,编译成字节码后会被丢弃.这意味着在运⾏时⽆法获取到该注解的信息,只能在编译时使⽤

使用@annotation表达式使用@annotation表达式定义切点时,只对@MyAspect生效

代码语言:javascript复制@RestController

@RequestMapping("/test")

public class TestController {

@MyAspect

@RequestMapping("/t1")

public String t1(){

return "t1";

}

}完结撒花!🎉