文章目录

1. Spring AOP 是什么2. 为什么要用 AOP3. 怎么学 Spring AOP4. AOP 组成5. Spring AOP 实现5.1 添加 Spring AOP 框架支持5.2 定义切面和切点5.3 实现通知方法5.4 使⽤ AOP 统计 UserController 每个⽅法的执⾏时间 StopWatch5.4 切点表达式说明 AspectJ

6. Spring AOP 实现原理6.1 生成代理的时机 :织入(Weaving)6.2 JDK 动态代理实现6.3 CGLIB 动态代理实现6.4 JDK 和 CGLIB 实现的区别

1. Spring AOP 是什么

学习 Spring AOP 之前,先要了解 AOP 是什么

AOP(Aspect Oriented Programming):面向切面编程,它和 OOP(面向对象编程)类似。

它是一种思想,是对某一类事情的集中处理。

比如用户登录权限的效验,在学习 AOP 之前,在需要判断用户登录的页面,都要各自实现或调用用户验证的方法,学习 AOP 之后,我们只需要在某一处配置一下,那么所有需要判断用户登录的页面就全部可以实现用户登录验证了,不用在每个方法中都写用户登录验证了

AOP 是一种思想,而 Spring AOP 是实现(框架),这种关系和 IOC(思想)与 DI(实现)类似

2. 为什么要用 AOP

高频:对于这种功能统一,且使用地方较多的功能,可以考虑用 AOP 来处理(比如 用户登录验证)使⽤ AOP 可以扩充多个对象的某个能⼒,AOP 可以说是 OOP 的补充和完善(比如 现在要实现的业务和这个通用的功能没什么关系,但处于安全考虑,又必须进行登录的验证)

除了统一的用户登录判断外,AOP 还可以实现

统一日志处理统一方法执行时间统计统一的返回格式设置统一的异常处理事务的开启和提交等

3. 怎么学 Spring AOP

Spring AOP 学习主要分为3个部分

学习 AOP 是如何组成的学习 Spring AOP 使用学习 Spring AOP 实现原理

4. AOP 组成

(1)切面(Aspect)

定义 AOP 是针对某个统一的功能的,这个功能就叫做一个切面,比如用户登录功能或方法的统计日志,他们就各是一个切面。切面是由切点和通知组成的

(2)连接点(Join Point)

所有可能触发 AOP(拦截方法的点)就称为连接点

(3)切点(Pointcut)

切点的作用就是提供一组规则来匹配连接点,给满足规则的连接点添加通知,总的来说就是,定义 AOP 拦截的规则的

切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)

(4)通知(Advice)

切面的工作就是通知

通知:规定了 AOP 执行的时机和执行的方法

Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后悔通知本方法进行调用

前置通知 @Before:通知方法会在目标方法调用之前执行后置通知 @After:通知方法会在目标方法返回或者抛出异常后调用返回之后通知 @AfterReturning:通知方法会在目标方法返回后调用抛异常后通知:@AfterThrowing:通知方法会在目标方法爬出异常之后调用环绕通知:@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为

举个例子,在一个生产型公司中

通知相当于底层的执行者,切点是小领导制定规则,切面是大领导制定公司的发展方向,连接点是属于一个普通的消费者用户

以多个⻚⾯都要访问⽤户登录权限为例子,AOP 整个组成部分如图所示

5. Spring AOP 实现

Spring AOP 实现步骤

添加 Spring AOP 框架支持定义切面和切点实现通知

接下来我们使⽤ Spring AOP 来实现⼀下 AOP 的功能,完成的⽬标是拦截所有 UserController ⾥⾯的

⽅法,每次调⽤ UserController 中任意⼀个⽅法时,都执⾏相应的通知事件。

5.1 添加 Spring AOP 框架支持

在中央仓库中搜锁 Spring AOP Maven Repository: Search/Browse/Explore (mvnrepository.com)

在 pom.xml 中添加如下配置:

org.springframework.boot

spring-boot-starter-aop

5.2 定义切面和切点

@Aspect // 当前类是一个切面

@Component

public class UserAspect {

// 定义一个切点(设置拦截规则)

@Pointcut("execution(* com.example.springaop.controller.UserController.*(..))")

public void pointcut() {

}

}

5.3 实现通知方法

前置通知 @Before:通知方法会在目标方法调用之前执行后置通知 @After:通知方法会在目标方法返回或者抛出异常后调用返回之后通知 @AfterReturning:通知方法会在目标方法返回后调用抛异常后通知:@AfterThrowing:通知方法会在目标方法爬出异常之后调用环绕通知:@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为

实现通知方法也就是在什么时机执行什么方法

@Aspect // 当前类是一个切面

@Component

public class UserAspect {

// 定义一个切点(设置拦截规则)

@Pointcut("execution(* com.example.springaop.controller.UserController.*(..))")

public void pointcut() {

}

// 定义 pointcut 切点的前置通知

@Before("pointcut()")

public void doBefore() {

System.out.println("执行前置通知");

}

// 后置通知

@After("pointcut()")

public void doAfter() {

System.out.println("执行后置通知");

}

// 返回之后通知

@AfterReturning("pointcut()")

public void doAfterReturning() {

System.out.println("执行返回之后通知");

}

// 抛出异常之后通知

@AfterThrowing("pointcut()")

public void doAfterThrowing() {

System.out.println("执行抛出异常之后通知");

}

}

@RestController

@RequestMapping("/user")

public class UserController {

@RequestMapping("/sayhi")

public String sayHi() {

System.out.println("sayhi 方法被执行");

int num = 10/0;

return "你好,java";

}

@RequestMapping("/sayhi2")

public String sayHi2() {

System.out.println("sayhi2 方法被执行");

return "你好,java2";

}

}

环绕通知:@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为

// 添加环绕通知

@Around("pointcut()")

public Object doAround(ProceedingJoinPoint joinPoint) {

Object result = null;

System.out.println("环绕通知:前置方法");

try {

// 执行拦截方法

result = joinPoint.proceed();

} catch (Throwable e) {

e.printStackTrace();

}

System.out.println("环绕通知:后置方法");

return result;

}

5.4 使⽤ AOP 统计 UserController 每个⽅法的执⾏时间 StopWatch

Spring AOP 中统计时间用 StopWatch 对象

// 添加环绕通知

@Around("pointcut()")

public Object doAround(ProceedingJoinPoint joinPoint) {

// spring 中的时间统计对象

StopWatch stopWatch = new StopWatch();

Object result = null;

try {

stopWatch.start(); // 统计方法的执行时间,开始计时

// 执行目标方法,以及目标方法所对应的相应通知

result = joinPoint.proceed();

stopWatch.stop(); // 统计方法的执行时间,停止计时

} catch (Throwable e) {

e.printStackTrace();

}

System.out.println(joinPoint.getSignature().getDeclaringTypeName() + "." +

joinPoint.getSignature().getName() +

"执行花费的时间:" + stopWatch.getTotalTimeMillis() + "ms");

return result;

}

5.4 切点表达式说明 AspectJ

AspectJ 表达式语法:SpringAOP & AspectJ

@Pointcut("execution(* com.example.springaop.controller.UserController.*(..))")

AspectJ 语法(Spring AOP 切点的匹配语法):

切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:

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

AspectJ ⽀持三种通配符

* :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)

… :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。

+ :表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的所有⼦类包括本身

修饰符,一般省略

public 公共方法*任意

返回值,不能省略

void 返回没有值String 返回值字符串*任意

包,通常不省略,但可以省略

com.gyf.crm 固定包com.gyf.crm.*.service crm 包下面子包任意(例如:com.gyf.crm.staff.service)com.gyf.crm… crm 包下面的所有子包(含自己)com.gyf.crm.*service… crm 包下面任意子包,固定目录 service,service 目录任意包

类,通常不省略,但可以省略

UserServiceImpl 指定类

*Impl 以 Impl 结尾

User* 以 User 开头

*任意

方法名,不能省略

addUser 固定方法

add* 以 add 开头

*DO 以 DO 结尾

*任意

参数

() 无参

(int) 一个整形

(int,int)两个整型

(…) 参数任意

throws可省略,一般不写

表达式示例

execution(* com.cad.demo.User.*(…)) :匹配 User 类⾥的所有⽅法execution(* com.cad.demo.User+.*(…)) :匹配该类的⼦类包括该类的所有⽅法execution(* com.cad..(…)) :匹配 com.cad 包下的所有类的所有⽅法execution(* com.cad….(…)) :匹配 com.cad 包下、⼦孙包下所有类的所有⽅法execution(* addUser(String, int)) :匹配 addUser ⽅法,且第⼀个参数类型是 String,第⼆个参数类型是 int

6. Spring AOP 实现原理

Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截

Spring AOP 动态代理实现:

默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类

JDK Proxy(JDK 动态代理)

CGLIB Proxy:默认情况下 Spring AOP 都会采用 CGLIB 来实现动态代理,因为效率高

CGLIB 实现原理:通过继承代理对象来实现动态代理的(子类拥有父类的所有功能)

CGLIB 缺点:不能代理最终类(也就是被 final 修饰的类)

6.1 生成代理的时机 :织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中

在目标对象的生命周期中有多个点可以进行织入

编译期:切面在目标类编译时被织入,这种方法需要特殊的编译器,AspectJ 的织入编译器就是以这种方式织入切面的类加载期:切面在目标类加载到 JVM 时被织入,这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码,AspectJ5 的加载时织入 (load-time weaving. LTW)就支持以这种方式织入切面运行期:切面在应用运行的某一时刻被织入,一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP 就是以这种方式织入切面的

6.2 JDK 动态代理实现

JDK 动态代理就是依靠反射来实现的

//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被

代理类必须实现接⼝

public class PayServiceJDKInvocationHandler implements InvocationHandler {

//⽬标对象即就是被代理对象

private Object target;

public PayServiceJDKInvocationHandler( Object target) {

this.target = target;

}

//proxy代理对象

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

//1.安全检查

System.out.println("安全检查");

//2.记录⽇志

System.out.println("记录⽇志");

//3.时间统计开始

System.out.println("记录开始时间");

//通过反射调⽤被代理类的⽅法

Object retVal = method.invoke(target, args);

//4.时间统计结束

System.out.println("记录结束时间");

return retVal;

}

public static void main(String[] args) {

PayService target= new AliPayService();

//⽅法调⽤处理器

InvocationHandler handler = new PayServiceJDKInvocationHandler(target);

//创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建

PayService proxy = (PayService) Proxy.newProxyInstance(

target.getClass().getClassLoader(),new Class[]{PayService.class},handler);

proxy.pay();

}

}

6.3 CGLIB 动态代理实现

public class PayServiceCGLIBInterceptor implements MethodInterceptor {

//被代理对象

private Object target;

public PayServiceCGLIBInterceptor(Object target){

this.target = target;

}

@Override

public Object intercept(Object o, Method method, Object[] args, MethodProxymethodProxy)throws Throwable {

//1.安全检查

System.out.println("安全检查");

//2.记录⽇志

System.out.println("记录⽇志");

//3.时间统计开始

System.out.println("记录开始时间");

//通过cglib的代理⽅法调⽤

Object retVal = methodProxy.invoke(target, args);

//4.时间统计结束

System.out.println("记录结束时间");

return retVal;

}

public static void main(String[] args) {

PayService target= new AliPayService();

PayService proxy= (PayService) Enhancer.create(target.getClass(),

new PayServiceCGLIBInterceptor(target));

proxy.pay();

}

}

6.4 JDK 和 CGLIB 实现的区别

JDK 实现,要求被代理类必须实现接口,之后是通过 InvocationHander 及 Proxy,在运行时动态的在内存中生成了代理对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实现的方式),只是该代理类是在运行期时,动态的织入统一的业务逻辑字节码来完成的CGLIB 实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类对象,这种方式实现方式效率高