接上次博客: JavaEE进阶(10)SpringBoot统一功能处理:拦截器入门及详解、DispatcherServlet源码、统一数据返回格式、统一异常处理、@ControllerAdvice源码、案例代码补充-CSDN博客

目录

AOP概述

什么是AOP?

什么是Spring AOP?

Spring AOP快速入门

引入AOP依赖

编写AOP程序

Spring AOP 详解

Spring AOP中涉及的核心概念

切点(Pointcut)

连接点(Join Point)

通知(Advice)

切面(Aspect)

Spring AOP通知类型

多个AOP程序的执行顺序

@PointCut

切面优先级 @Order

切点表达式

execution表达式

@annotation

Spring AOP 原理(重要)

代理模式

静态代理 

动态代理

JDK动态代理

CGLIB动态代理

Spring AOP 源码剖析(了解)

AOP概述

学习完Spring的统一功能之后,我们进入Spring AOP的学习。

AOP 是 Spring 框架的第二大核心,而第一大核心是 IoC(控制反转)。

Spring Framework官方文档:Aspect Oriented Programming with Spring :: Spring Framework

Spring Boot中文文档:Spring Boot 中文文档

什么是AOP?

AOP—— Aspect Oriented Programming(面向切面编程)。

面向切面编程(AOP)是一种编程范式,它的核心思想是将横切关注点从核心业务逻辑中分离出来,然后通过特定的方式对这些关注点进行统一的处理。所谓的切面就是指某一类特定问题,而AOP可以理解为面向解决这类特定问题的编程。

举例来说,假设我们有一个常见的需求,需要在用户访问某个业务方法之前进行登录校验。这个登录校验是一个特定问题,即用户访问权限的验证。通过AOP,我们可以将这个特定问题抽象成一个切面,然后通过拦截器(Interceptor)来统一处理这个切面,实现对登录校验的统一处理。因此,拦截器是AOP思想的一种实现方式。

在Spring框架中,AOP思想得到了广泛的应用。比如,统一数据返回格式和统一异常处理也可以看作是AOP思想的一种实现。通过AOP,我们可以将这些通用的处理逻辑从业务代码中剥离出来,集中管理,提高代码的可维护性和复用性。

简单来说,AOP是一种思想,它通过对某一类特定问题的集中处理,提供了一种解决复杂系统中横切关注点的方法。

什么是Spring AOP?

Spring AOP(面向切面编程)是 Spring 框架提供的一种实现 AOP 思想的方式。AOP是一种编程思想,其实现方法有很多,包括Spring AOP、AspectJ、CGLIB等,而Spring AOP则是其中的一种实现方式。

学会了Spring框架中的统一功能并不意味着就掌握了Spring AOP。虽然它们都涉及到对横切关注点的处理,但拦截器和AOP的应用场景和作用维度是不同的。

拦截器作用的维度主要是请求和响应的URL级别,即一次请求和响应。拦截器可以通过配置拦截器来拦截特定的URL,并在请求处理前后进行一些预处理和后处理操作。常见的应用场景包括全局异常处理、数据绑定、数据预处理等。

而Spring AOP作用的维度更加细致,可以根据包、类、方法名、参数等进行拦截,可以实现更加复杂的业务逻辑。Spring AOP通过特定的注解或配置来实现对特定方法的拦截和增强,例如通过@Aspect注解和@Around、@Before、@After等注解来定义切面和通知,从而实现对特定方法的增强操作,例如日志记录、性能监控、事务管理等。

总的来说,拦截器和Spring AOP都是用来处理横切关注点的,但它们的作用维度和实现方式略有不同,适用于不同的场景和需求。

假设我们有一个电商项目,其中包含了订单管理、商品管理、用户管理等多个业务模块,每个模块下面又有多个接口或方法。现在我们发现在订单管理模块中有一个查询订单信息的接口执行效率较低,需要对其进行优化。

传统的方法是在查询订单信息的方法中添加代码来记录方法的开始时间和结束时间,然后计算方法的执行耗时。但是,这种方式需要在每个需要统计执行时间的方法中都添加相同的代码,这样会导致代码重复,增加维护成本。

通过使用AOP,我们可以在不修改原始方法的情况下,针对特定的方法进行功能的增强,例如记录方法的执行时间。我们可以编写一个切面,在切面中定义一个方法,在方法的前后分别记录方法的开始时间和结束时间,并计算执行耗时。然后将这个切面应用到需要统计执行时间的方法上,即可实现对这些方法的执行时间进行统计,而无需在每个方法中都添加相同的代码。

这样,我们就实现了在不改动原始方法的基础上,针对特定的方法进行功能的增强,达到了解耦和无侵入性的效果。

AOP(面向切面编程)的作用是在程序运行期间,在不修改源代码的基础上对已有方法进行增强。这种方式实现了无侵入性,即不需要直接修改原始方法的代码,而是通过在方法执行前、后或者环绕时,织入额外的功能。这种做法有助于解耦,即将原始业务逻辑与额外功能分离开来,使得代码更加清晰和易于维护。

还是刚刚那个例子,假设我们有一个方法用于处理订单的创建,在创建订单之前需要进行权限检查,创建订单之后需要发送邮件通知用户。传统的做法是在订单创建方法中添加权限检查和邮件发送的逻辑,这样会使得方法既处理业务逻辑又处理额外功能,导致代码耦合度高、可维护性差。

通过使用AOP,我们就可以将权限检查和邮件发送的逻辑封装在切面中,然后在订单创建方法执行前或执行后,将切面织入到订单创建方法中。这样,订单创建方法的原始逻辑不受影响,而权限检查和邮件发送的功能也得到了实现,达到了代码解耦和增强功能的效果。

接下来我们来看Spring AOP如何来实现。

Spring AOP快速入门

我们先通过下面的程序体验下AOP的开发,并掌握Spring中AOP的开发步骤。

需求: 统计图书系统各个接口方法的执行时间。

引入AOP依赖

在pom.xml文件中添加配置:

org.springframework.boot

spring-boot-starter-aop

编写AOP程序

记录Controller中每个方法的执行时间:

@Aspect注解是AspectJ框架的一部分,而Spring AOP借用了AspectJ的语法和注解,但其实现方式与AspectJ不同。在Spring AOP中,@Aspect注解用于定义切面类,其中可以定义切点和通知等内容。

虽然Spring AOP使用了@Aspect注解,但它的实现并不依赖于AspectJ框架。相反,Spring AOP是基于动态代理实现的,它使用了JDK动态代理和CGLIB代理来实现切面的织入。这意味着Spring AOP只能针对接口进行代理的情况下使用JDK动态代理,对于类则需要使用CGLIB代理。

所以,总的来说,虽然Spring AOP借鉴了AspectJ的注解,但其底层实现方式与AspectJ是不同的,它主要依赖于动态代理来实现AOP功能。

这就像加盟商使用了总部提供的品牌、标志和经营模式,但在具体的运营过程中可能会有所调整或自己添加特色一样,Spring AOP借鉴了AspectJ框架的注解和语法,但在实现方式上有所不同,并且在Spring框架的基础上添加了自己的特性和功能。

package com.example.librarysystem.book.aspect;

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

@Component

@Aspect

public class TimeRecordAspect {

@Around("execution(* com.example.librarysystem.book.controller.*.*(..))")

public Object record(ProceedingJoinPoint pj) throws Throwable {

/**

* 切面逻辑

* 1. 记录开始时间

* 2. 执行目标方法

* 3. 记录结束时间

* 4. 记录消耗时间

*/

long start = System.currentTimeMillis();

Object result = pj.proceed();

log.info(pj.getSignature()+"cost time:"+(System.currentTimeMillis()-start)+"ms");

log.info(pj.getSignature().toString());

log.info(pj.toLongString());

log.info(pj.toShortString());

log.info(String.valueOf(pj.getArgs()));

return result;

}

}

@Around("execution(* com.example.librarysystem.book.controller.*.*(..))"): 这是一个环绕通知(Around Advice),表示切面逻辑将围绕目标方法执行。

具体解释如下:

@Around: 表示这是一个环绕通知,能够在目标方法执行前后进行增强操作。"execution(* com.example.librarysystem.book.controller.*.*(..))": 这是切入点表达式(Pointcut Expression),定义了切入的目标方法。具体含义如下: execution: 表示匹配方法执行的连接点。*: 表示任意返回类型。com.example.librarysystem.book.controller.*: 表示匹配com.example.librarysystem.book.controller包下的所有类。*.*: 表示匹配所有方法。(..): 表示匹配任意参数的方法。 log.info(pj.getSignature()+"cost time:"+(System.currentTimeMillis()-start)+"ms");:这行代码记录了目标方法的签名(即方法名和参数列表)以及该方法的执行时间。它将打印出目标方法的签名,后跟字符串 "cost time:",然后是执行方法所消耗的时间(以毫秒为单位)。log.info(pj.getSignature().toString());:这行代码记录了目标方法的签名。它将打印出目标方法的签名,以字符串形式表示。log.info(pj.toLongString());:这行代码记录了目标方法的完整签名,包括方法的修饰符、返回类型、方法名和参数列表。它将打印出一个完整的字符串,表示目标方法的所有信息。log.info(pj.toShortString());:这行代码记录了目标方法的简略签名,只包括方法名和参数列表。它将打印出一个简略的字符串,表示目标方法的基本信息。log.info(String.valueOf(pj.getArgs()));:这行代码记录了目标方法的参数。它将打印出目标方法的参数列表,以字符串形式表示

总体来说,这个切面类通过@Around注解实现了环绕通知,针对指定的目标方法(在com.example.librarysystem.book.controller包下的所有方法)记录了方法的执行时间,并将记录输出到日志中。 

 

2024-03-03 19:38:14.291 INFO 10304 --- [io-8080-exec-10] c.e.l.book.aspect.TimeRecordAspect : Result com.example.librarysystem.book.controller.BookController.getListByPage(PageRequest,HttpSession)cost time:24ms

2024-03-03 19:38:14.291 INFO 10304 --- [io-8080-exec-10] c.e.l.book.aspect.TimeRecordAspect : Result com.example.librarysystem.book.controller.BookController.getListByPage(PageRequest,HttpSession)

2024-03-03 19:38:14.291 INFO 10304 --- [io-8080-exec-10] c.e.l.book.aspect.TimeRecordAspect : execution(public com.example.librarysystem.book.model.Result com.example.librarysystem.book.controller.BookController.getListByPage(com.example.librarysystem.book.model.PageRequest,javax.servlet.http.HttpSession))

2024-03-03 19:38:14.291 INFO 10304 --- [io-8080-exec-10] c.e.l.book.aspect.TimeRecordAspect : execution(BookController.getListByPage(..))

2024-03-03 19:38:14.291 INFO 10304 --- [io-8080-exec-10] c.e.l.book.aspect.TimeRecordAspect : [Ljava.lang.Object;@24a924af

2024-03-03 19:40:57.367 INFO 10304 --- [nio-8080-exec-5] c.e.l.book.aspect.TimeRecordAspect : Result com.example.librarysystem.book.controller.BookController.getListByPage(PageRequest,HttpSession)cost time:10ms

2024-03-03 19:40:57.367 INFO 10304 --- [nio-8080-exec-5] c.e.l.book.aspect.TimeRecordAspect : Result com.example.librarysystem.book.controller.BookController.getListByPage(PageRequest,HttpSession)

2024-03-03 19:40:57.367 INFO 10304 --- [nio-8080-exec-5] c.e.l.book.aspect.TimeRecordAspect : execution(public com.example.librarysystem.book.model.Result com.example.librarysystem.book.controller.BookController.getListByPage(com.example.librarysystem.book.model.PageRequest,javax.servlet.http.HttpSession))

2024-03-03 19:40:57.367 INFO 10304 --- [nio-8080-exec-5] c.e.l.book.aspect.TimeRecordAspect : execution(BookController.getListByPage(..))

2024-03-03 19:40:57.367 INFO 10304 --- [nio-8080-exec-5] c.e.l.book.aspect.TimeRecordAspect : [Ljava.lang.Object;@69bc0ebb

2024-03-03 19:41:06.493 INFO 10304 --- [nio-8080-exec-6] c.e.l.book.interceptor.LoginInterceptor : 目标方法执行前执行: LoginInterceptor.preHandle....

2024-03-03 19:41:06.493 INFO 10304 --- [nio-8080-exec-6] c.e.l.book.interceptor.LoginInterceptor : 目标方法执行前执行: LoginInterceptor.preHandle....

2024-03-03 19:41:06.499 INFO 10304 --- [nio-8080-exec-6] c.e.l.book.controller.BookController : 查询列表信息, pageRequest:PageRequest(currentPage=2, pageSize=10, offset=10)

2024-03-03 19:41:06.510 INFO 10304 --- [nio-8080-exec-6] c.e.l.book.aspect.TimeRecordAspect : Result com.example.librarysystem.book.controller.BookController.getListByPage(PageRequest,HttpSession)cost time:11ms

2024-03-03 19:41:06.513 INFO 10304 --- [nio-8080-exec-6] c.e.l.book.aspect.TimeRecordAspect : Result com.example.librarysystem.book.controller.BookController.getListByPage(PageRequest,HttpSession)

2024-03-03 19:41:06.513 INFO 10304 --- [nio-8080-exec-6] c.e.l.book.aspect.TimeRecordAspect : execution(public com.example.librarysystem.book.model.Result com.example.librarysystem.book.controller.BookController.getListByPage(com.example.librarysystem.book.model.PageRequest,javax.servlet.http.HttpSession))

2024-03-03 19:41:06.513 INFO 10304 --- [nio-8080-exec-6] c.e.l.book.aspect.TimeRecordAspect : execution(BookController.getListByPage(..))

2024-03-03 19:41:06.513 INFO 10304 --- [nio-8080-exec-6] c.e.l.book.aspect.TimeRecordAspect : [Ljava.lang.Object;@289aa2bf

2024-03-03 19:41:09.299 INFO 10304 --- [nio-8080-exec-8] c.e.l.book.interceptor.LoginInterceptor : 目标方法执行前执行: LoginInterceptor.preHandle....

2024-03-03 19:41:09.299 INFO 10304 --- [nio-8080-exec-8] c.e.l.book.interceptor.LoginInterceptor : 目标方法执行前执行: LoginInterceptor.preHandle....

2024-03-03 19:41:09.299 INFO 10304 --- [nio-8080-exec-8] c.e.l.book.controller.BookController : 查询图书信息, bookId:10

2024-03-03 19:41:14.151 INFO 10304 --- [io-8080-exec-10] c.e.l.book.aspect.TimeRecordAspect : Result com.example.librarysystem.book.controller.BookController.getListByPage(PageRequest,HttpSession)cost time:12ms

2024-03-03 19:41:14.151 INFO 10304 --- [io-8080-exec-10] c.e.l.book.aspect.TimeRecordAspect : Result com.example.librarysystem.book.controller.BookController.getListByPage(PageRequest,HttpSession)

2024-03-03 19:41:14.151 INFO 10304 --- [io-8080-exec-10] c.e.l.book.aspect.TimeRecordAspect : execution(public com.example.librarysystem.book.model.Result com.example.librarysystem.book.controller.BookController.getListByPage(com.example.librarysystem.book.model.PageRequest,javax.servlet.http.HttpSession))

2024-03-03 19:41:14.151 INFO 10304 --- [io-8080-exec-10] c.e.l.book.aspect.TimeRecordAspect : execution(BookController.getListByPage(..))

2024-03-03 19:41:14.151 INFO 10304 --- [io-8080-exec-10] c.e.l.book.aspect.TimeRecordAspect : [Ljava.lang.Object;@5ce9745c

通过AOP,我们可以实现代码的无侵入性,即不需要修改原始的业务方法代码,就能够对其进行功能的增强或改变。这为我们的项目带来了许多优势:

代码无侵入性: AOP可以在不修改原始代码的情况下,对现有的业务逻辑进行增强或改变,这样可以保持原有代码的纯净性和稳定性。 减少重复代码: AOP可以将一些横切关注点(如日志记录、性能统计、事务管理等)从业务逻辑中剥离出来,避免了在多个地方重复编写这些代码,提高了代码的复用性和可维护性。 提高开发效率: 通过AOP,可以将一些通用的功能封装成切面,直接应用到需要的地方,从而简化了开发流程,提高了开发效率。 便于维护: AOP将横切关注点集中到一个地方管理,当需要修改或调整这些功能时,只需要修改切面的逻辑即可,不会影响到业务逻辑代码,使得维护更加方便。

AOP的引入可以使项目的结构更加清晰,代码更加简洁,提高了项目的可维护性和扩展性,为我们开发人员带来了诸多便利。

Spring AOP 详解

Spring AOP中涉及的核心概念

切点(Pointcut)

切点(Pointcut)是AOP中的一个重要概念,也被称为"切入点"。它定义了一组规则,指导程序在哪些方法上应用通知,以实现功能增强。

在AOP中,切点通过使用AspectJ pointcut expression language来进行描述。这种语言允许我们精确地指定应该拦截的方法。通过指定包、类、方法名、参数等条件,我们可以定义出一组符合特定规则的方法集合,这就是切点的作用所在。

举例来说,一个切点可以定义为拦截某个特定包中的所有类的所有方法,或者拦截某个特定类中的所有方法,或者只拦截某个特定类中的特定方法,又或者根据方法的参数类型进行拦截等等。切点的作用就是告诉程序对哪些方法进行功能增强,从而实现AOP的切面功能。

连接点(Join Point)

连接点(Join Point)是AOP中另一个至关重要的概念,它代表了在程序执行过程中可以被AOP控制的特定执行点。换句话说,连接点就是符合切点表达式规则的方法或者代码片段。

切点和连接点之间存在着密切的联系,它们共同构成了AOP的核心。

切点可以被视为连接点的集合,它定义了一组满足特定条件的执行点,用于确定在何处应用横切关注点(cross-cutting concerns)。具体来说,切点决定了哪些连接点会被拦截以应用通知,而连接点则是指被切点所定义的具体方法或者代码段本身。

举例说明,如果我们定义了一个切点,它包含了某个特定包中的所有类的所有方法,那么这个切点就涵盖了许多连接点,即这些包中所有方法都成为连接点。同样地,如果我们定义了一个切点,它只包含了某个特定类中的特定方法,那么这个切点就只涵盖了这个类中的部分方法,而这些方法就是连接点。

连接点代表了AOP中实际被拦截的执行点,而切点则明确定义了哪些执行点应该被拦截。连接点是切点在实际应用中的具体体现,它们共同构成了AOP中的关键概念,为横切关注点的有效管理提供了基础。

通知(Advice)

通知(Advice)是AOP中的一个关键概念,用于定义在程序执行的特定切点(连接点)上需要执行的具体操作或逻辑。通知描述了在连接点处所执行的代码,通常代表了一种横切关注点,比如日志记录、性能监控、事务管理等。通知在特定时机对指定的连接点执行特定的操作,体现了AOP中对重复逻辑的抽取和模块化。

通知的作用是解决横切关注点的重复性问题,将这些重复的逻辑封装成通用的操作单元,以便在不同的连接点上重复利用。

在AOP编程中,通知的类型包括前置通知、后置通知、环绕通知、异常通知和最终通知等。每种类型的通知都有特定的执行时机和执行逻辑,以满足不同横切关注点的需求。例如,在方法执行前记录日志、在方法执行后处理返回结果、在方法执行前后进行性能监控等。(稍后详细介绍)

总的来说,通知是AOP中描述在特定连接点上执行的操作或逻辑的概念,实现了对横切关注点的抽象和模块化,提高了代码的可维护性和可重用性。

切面(Aspect)

切面(Aspect)是AOP的核心概念之一,它是切点(Pointcut)和通知(Advice)的完美结合,用于描述在程序执行过程中需要对哪些方法进行增强以及何时执行何种操作。

具体而言,一个切面定义了一组切点和与这些切点关联的通知。切面不仅包含了通知的逻辑定义,还包括了连接点的定义,从而决定了在哪些地方以及何时执行哪些操作。

在Spring AOP中,切面通常表示为一个带有@Aspect注解的类。这个类中包含了一组切点和与之相关联的通知。通过@Aspect注解,Spring能够识别出这个类是一个切面类,并在程序执行过程中自动应用其中定义的通知。

总的来说,切面是AOP中描述横切关注点的一个重要概念,它将切点和通知巧妙地结合起来,实现了对方法的增强。切面在AOP编程中扮演着不可或缺的角色,为应用程序提供了一种灵活而强大的方式来管理横切关注点,从而提高了代码的可维护性和可重用性。

Spring AOP通知类型

上面我们讲了什么是通知,接下来学习通知的类型。@Around 就是其中一种通知类型,表示环绕通知。

Spring中AOP的通知类型有以下几种:

@Around:环绕通知。此注解标注的通知方法在目标方法前后都被执行,可以在目标方法执行前后进行一些自定义的操作。 @Before:前置通知。此注解标注的通知方法在目标方法前被执行,用于在目标方法执行之前进行一些预处理操作。 @After:后置通知。此注解标注的通知方法在目标方法执行后被执行,无论是否有异常都会执行。通常用于释放资源或进行一些清理操作。 @AfterReturning:返回后通知。此注解标注的通知方法在目标方法正常返回后被执行,用于获取目标方法的返回值或进行其他后处理操作。 @AfterThrowing:异常后通知。此注解标注的通知方法在目标方法抛出异常后被执行,用于处理异常情况或进行异常信息记录等操作。

这些通知类型可以根据需求选择合适的方式来增强目标方法的功能,实现对方法执行过程中不同阶段的控制和处理。

接下来我们通过代码来加深对这几个通知的理解。

多个AOP程序的执行顺序

为方便学习,我们新建一个项目。

package com.example.aop.controller;

import lombok.extern.slf4j.Slf4j;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@Slf4j

@RequestMapping("/test")

@RestController

public class TestController {

// @TimeRecord

@RequestMapping("/t1")

public String t1(){

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

return "t1";

}

@RequestMapping("/t2")

public String t2(){

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

int a = 10/0;

return "t2";

}

}

切面类可以有多个切点:一个切面类可以包含多个切点,这样可以在不同的连接点上应用不同的通知,提高了代码的灵活性和可重用性。 

package com.example.aop.aspect;

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.aop.controller.*.*(..))")

public void doBefore(){

log.info("执行AspectDemo.before方法...");

}

@After("execution(* com.example.aop.controller.*.*(..))")

public void doAfter(){

log.info("执行AspectDemo.after方法...");

}

@AfterReturning("execution(* com.example.aop.controller.*.*(..))")

public void doAfterReturn(){

log.info("执行AspectDemo.doAfterReturn方法...");

}

// @AfterThrowing("execution(* com.example.aop.controller.*.*(..))")

// public void doAfterThrow(){

// log.info("执行AspectDemo.doAfterThrow方法...");

// }

@Around("execution(* com.example.aop.controller.*.*(..))")

public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

log.info("执行AspectDemo.doAround 目标方法前...");

Object result;

result = joinPoint.proceed();

log.info("执行AspectDemo.doAround 目标方法后...");

return result;

}

}

在程序正常运行的情况下,@AfterThrowing标识的通知方法不会执行。这意味着,如果目标方法没有抛出异常,那么与@AfterThrowing相关联的通知方法将不会被调用。

从图中可以看出,@Around标识的通知方法包含两部分:一个"前置逻辑"和一个"后置逻辑"。其中,"前置逻辑"会在@Before标识的通知方法之前执行,而"后置逻辑"会在@After标识的通知方法之后执行。这种执行顺序确保了在环绕通知中可以在目标方法执行前后进行一些自定义的操作,并且灵活地控制方法的执行过程。

 异常情况下:

package com.example.aop.aspect;

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.aop.controller.*.*(..))")

public void doBefore(){

log.info("执行AspectDemo.before方法...");

}

@After("execution(* com.example.aop.controller.*.*(..))")

public void doAfter(){

log.info("执行AspectDemo.after方法...");

}

@AfterReturning("execution(* com.example.aop.controller.*.*(..))")

public void doAfterReturn(){

log.info("执行AspectDemo.doAfterReturn方法...");

}

@AfterThrowing("execution(* com.example.aop.controller.*.*(..))")

public void doAfterThrow(){

log.info("执行AspectDemo.doAfterThrow方法...");

}

@Around("execution(* com.example.aop.controller.*.*(..))")

public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {

log.info("执行AspectDemo.doAround 目标方法前...");

Object result;

result = joinPoint.proceed();

log.info("执行AspectDemo.doAround 目标方法后...");

return result;

}

}

在程序发生异常的情况下,通知的执行情况如下:

@AfterReturning 标识的通知方法不会执行,因为目标方法未正常返回结果。 @AfterThrowing 标识的通知方法会执行,因为目标方法抛出了异常,这种通知专门用于处理方法执行过程中的异常情况。 对于 @Around 环绕通知,如果在通知中调用的原始方法发生异常,则通知中环绕后的代码逻辑也不会执行。这是因为环绕通知在执行过程中包含了对原始方法的调用,如果原始方法抛出异常,通知会捕获这个异常,并且不会继续执行环绕后的代码逻辑。

现在对doAround稍作修改:

package com.example.aop.aspect;

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.aop.controller.*.*(..))")

public void doBefore(){

log.info("执行AspectDemo.before方法...");

}

@After("execution(* com.example.aop.controller.*.*(..))")

public void doAfter(){

log.info("执行AspectDemo.after方法...");

}

@AfterReturning("execution(* com.example.aop.controller.*.*(..))")

public void doAfterReturn(){

log.info("执行AspectDemo.doAfterReturn方法...");

}

@AfterThrowing("execution(* com.example.aop.controller.*.*(..))")

public void doAfterThrow(){

log.info("执行AspectDemo.doAfterThrow方法...");

}

@Around("execution(* com.example.aop.controller.*.*(..))")

public Object doAround(ProceedingJoinPoint joinPoint) {

log.info("执行AspectDemo.doAround 目标方法前...");

Object result = null;

try {

result = joinPoint.proceed();

}catch (Throwable e) {

log.error(joinPoint.toShortString()+"发生异常, e:",e);

}

log.info("执行AspectDemo.doAround 目标方法后...");

return result;

}

}

在使用@Around环绕通知时,需要注意以下几点:

调用原始方法:在环绕通知方法内部需要显式调用ProceedingJoinPoint.proceed()来执行原始方法,否则原始方法将不会被执行。 返回值类型:环绕通知方法的返回值类型必须指定为Object类型,以接收原始方法的返回值。否则,原始方法执行完毕后,无法获取到返回值。

遵循以上注意事项,可以更有效地使用@Around环绕通知,并确保在AOP中的正确运行和预期的功能增强。

@PointCut

上面的代码存在⼀个问题,就是存在大量重复的切点表达式 ——("execution(* com.example.aop.controller.*.*(..))")。

Spring提供了 @PointCut 注解, 把公共的切点 表达式提取出来,需要用到时引用该切入点表达式即可。

上述代码就可以修改为:

package com.example.aop.aspect;

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 {

@Pointcut("execution(* com.example.aop.controller.*.*(..))")

public void pt(){};

@Before("pt()")

public void doBefore(){

log.info("执行AspectDemo.before方法...");

}

@After("pt()")

public void doAfter(){

log.info("执行AspectDemo.after方法...");

}

@AfterReturning("execution(* com.example.aop.controller.*.*(..))")

public void doAfterReturn(){

log.info("执行AspectDemo.doAfterReturn方法...");

}

@AfterThrowing("execution(* com.example.aop.controller.*.*(..))")

public void doAfterThrow(){

log.info("执行AspectDemo.doAfterThrow方法...");

}

@Around("execution(* com.example.aop.controller.*.*(..))")

public Object doAround(ProceedingJoinPoint joinPoint) {

log.info("执行AspectDemo.doAround 目标方法前...");

Object result = null;

try {

result = joinPoint.proceed();

}catch (Throwable e) {

log.error(joinPoint.toShortString()+"发生异常, e:",e);

}

log.info("执行AspectDemo.doAround 目标方法后...");

return result;

}

}

那如果别的类也想用这个切点呢?

直接这样写?

肯定不行,都已经报错了。

所以公共的切点应该如何实现跨类使用呢?

package com.example.aop.aspect;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.*;

import org.springframework.core.annotation.Order;

import org.springframework.stereotype.Component;

@Slf4j

@Component

@Aspect

@Order(4)

public class AspectDemo2 {

@Before("com.example.aop.aspect.AspectDemo.pt()")

public void doBefore(){

log.info("执行AspectDemo2.before方法...");

}

@After("com.example.aop.aspect.AspectDemo.pt()")

public void doAfter(){

log.info("执行AspectDemo2.after方法...");

}

}

当切点定义使用private修饰时,仅能在当前切面类中使用。如果其他切面类也需要使用当前切点定义,就需要将private改为public。在其他切面类中引用这个公共切点的方式为:全限定类名(路径+类名). 方法名()。

这样做的目的是为了提供切点的可访问性,使得其他切面类也能够使用这个切点,实现切面的复用和模块化。

切面优先级 @Order

当我们在⼀个项目中定义了多个切面类时,并且这些切面类的多个切入点都匹配到了同一个目标方法。当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的呢?

我们还是通过程序来求证:

为简单化,我们只看 @Before 和 @After 两个通知。

现在我们在多添加几个切面类,并且为了防止干扰,我们把AspectDemo这个切面先去掉(把@Component 注解去掉就可以)。 

package com.example.aop.aspect;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.annotation.After;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.springframework.core.annotation.Order;

import org.springframework.stereotype.Component;

@Slf4j

@Component

@Aspect

public class AspectDemo3 {

@Before("com.example.aop.aspect.AspectDemo.pt()")

public void doBefore(){

log.info("执行AspectDemo3.before方法...");

}

@After("com.example.aop.aspect.AspectDemo.pt()")

public void doAfter(){

log.info("执行AspectDemo3.after方法...");

}

}

package com.example.aop.aspect;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.annotation.After;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.springframework.core.annotation.Order;

import org.springframework.stereotype.Component;

@Slf4j

@Component

@Aspect

public class AspectDemo4 {

@Before("com.example.aop.aspect.AspectDemo.pt()")

public void doBefore(){

log.info("执行AspectDemo4.before方法...");

}

@After("com.example.aop.aspect.AspectDemo.pt()")

public void doAfter(){

log.info("执行AspectDemo4.after方法...");

}

}

通过上述程序的运行结果,我们可以观察到以下情况:

存在多个切面类时,默认按照切面类的类名字典序排序来确定通知方法的执行顺序:

@Before通知:类名字典序排名靠前的先执行。@After通知:类名字典序排名靠前的后执行。

然而,这种方式并不方便管理,因为我们的类名更多还是具备一定含义的。为了解决这个问题,Spring提供了一个新的注解,用于控制切面通知的执行顺序:@Order。

package com.example.aop.aspect;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.*;

import org.springframework.core.annotation.Order;

import org.springframework.stereotype.Component;

@Slf4j

@Component

@Aspect

@Order(3)

public class AspectDemo2 {

@Before("com.example.aop.aspect.AspectDemo.pt()")

public void doBefore(){

log.info("执行AspectDemo2.before方法...");

}

@After("com.example.aop.aspect.AspectDemo.pt()")

public void doAfter(){

log.info("执行AspectDemo2.after方法...");

}

}

package com.example.aop.aspect;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.annotation.After;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.springframework.core.annotation.Order;

import org.springframework.stereotype.Component;

@Slf4j

@Component

@Aspect

@Order(2)

public class AspectDemo3 {

@Before("com.example.aop.aspect.AspectDemo.pt()")

public void doBefore(){

log.info("执行AspectDemo3.before方法...");

}

@After("com.example.aop.aspect.AspectDemo.pt()")

public void doAfter(){

log.info("执行AspectDemo3.after方法...");

}

}

package com.example.aop.aspect;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.annotation.After;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.springframework.core.annotation.Order;

import org.springframework.stereotype.Component;

@Slf4j

@Component

@Aspect

@Order(1)

public class AspectDemo4 {

@Before("com.example.aop.aspect.AspectDemo.pt()")

public void doBefore(){

log.info("执行AspectDemo4.before方法...");

}

@After("com.example.aop.aspect.AspectDemo.pt()")

public void doAfter(){

log.info("执行AspectDemo4.after方法...");

}

}

通过上述程序的运行结果,我们得出以下结论:

@Order注解标识的切面类,其通知方法的执行顺序如下:

@Before通知:数字越小先执行。@After通知:数字越大先执行。默认级别最低。

因此,@Order注解可以用来控制切面的优先级,先执行优先级较高的切面,再执行优先级较低的切面,最终执行目标方法。这样可以更灵活地管理切面类的执行顺序,提高代码的可维护性和可读性。

所以Spring AOP通知的执行顺序可以总结如下:

当在一个项目中定义了多个切面类,并且这些切面类的多个切入点都匹配到了同一个目标方法时,这些切面类中的通知方法的执行顺序遵循以下规则:

切面优先级: Spring AOP允许使用@Order注解或实现Ordered接口来指定切面的执行顺序。具有较低优先级值的切面将先于具有较高优先级值的切面执行。 通知类型: 不同类型的通知会按照通知类型的顺序执行。通常情况下,环绕通知(@Around)首先执行,其次是前置通知(@Before),然后是目标方法本身,接着是后置通知(@After),最后是返回后通知(@AfterReturning)或异常后通知(@AfterThrowing)。 同一切面内方法的声明顺序: 如果同一个切面内有多个通知方法,那么这些方法的执行顺序与它们在切面类中的声明顺序一致。先声明的通知方法会先执行,后声明的通知方法会后执行。

总的来说,通知方法的执行顺序取决于切面的优先级、通知类型以及在同一个切面类中方法的声明顺序。通过合理设置切面优先级和控制通知方法的声明顺序,可以精确地控制通知方法的执行顺序,以满足需求。

切点表达式

切点表达式常见的两种方式如下:

execution(根据方法的签名来匹配):使用execution关键字,根据方法的签名来匹配方法的执行。@annotation(根据注解匹配):使用@annotation关键字根据方法上的注解进行匹配。

execution表达式

execution()是最常用的切点表达式,用来匹配方法的执行。其语法结构如下:

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

其中各个部分的含义如下:

访问修饰符(可选):表示方法的修饰符,如public、protected、private等。返回类型:表示方法的返回类型,可以使用通配符*表示任意返回类型。包名.类名.方法:表示方法所在的包名、类名以及方法名,使用点号(.)分隔。方法参数:表示方法的参数列表,使用逗号分隔,每个参数由参数类型和参数名称组成。异常(可选):表示方法可能抛出的异常类型,可以使用通配符*表示任意异常类型。

通过execution()表达式,我们可以精确地定义需要匹配的方法,从而实现对方法的精确增强。

注意:访问修饰符和异常部分在切点表达式中是可选的,可以根据实际情况省略。这使得切点表达式更加灵活,简化了切点的定义过程。

因此,可以将切点表达式的语法简化为:

execution(<返回类型> <包名.类名.方法(方法参数)>)

在这种简化的形式下,只需指定方法的返回类型、所在的包名、类名以及方法名,就可以匹配到相应的方法。 

切点表达式支持通配符表达,主要有以下两种形式:

*:匹配任意字符,只匹配一个元素。

包名:使用 * 表示任意包,例如 com.example.* 匹配 com.example 包下的所有类。类名:使用 * 表示任意类,例如 *Service 匹配所有以 Service 结尾的类。返回值:使用 * 表示任意返回值类型,例如 * 匹配任意返回值类型。方法名:使用 * 表示任意方法名,例如 do 匹配以 do 开头的所有方法。参数:使用 * 表示一个任意类型的参数,例如 () 匹配任意参数类型的方法。 ..:匹配多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数。

包名:使用 .. 配置包名,标识此包以及此包下的所有子包,例如 com.example.. 匹配 com.example 包及其所有子包。参数:使用 .. 配置参数,匹配任意个任意类型的参数,例如 *(..) 匹配任意个任意类型的参数。

这些通配符可以帮助我们精确地定义切点,匹配到需要增强的方法,实现对方法的精确增强。

切点表达式示例:

匹配 TestController 下的 public 修饰,返回类型为 String,方法名为 t1,无参数方法:execution(public String com.example.demo.controller.TestController.t1())  省略访问修饰符,匹配 TestController 下的 public 或非 public 方法,返回类型为 String,方法名为 t1,无参数方法:execution(String com.example.demo.controller.TestController.t1())  匹配 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.*.*(..))  匹配所有包下面的 TestController:execution(* com..TestController.*(..))  匹配 com.example.demo 包下,包括子孙包下的所有类的所有方法,无论有无参数:execution(* com.example.demo..*(..))

@annotation

当我们需要匹配多个无规则的方法时,如 TestController 中的 t1() 和 UserController 中的 u1(),使用 execution 这种切点表达式来描述可能不太方便。在这种情况下,我们可以借助自定义注解的方式以及另一种切点表达式 @annotation 来描述这一类的切点。

@Slf4j

@Component

@Aspect

public class AspectDemo5 {

@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")

public void doBefore(){

log.info("执行AspectDemo5.before方法...");

}

}

但是这样我们还是比较受限制,我们选择的这些注解大多是第三方实现的吗,有自己的功能。我们想要的注解应该是可以实现某个我们需要的功能,同时又不包含其他我们不需要的功能。

要想实现这个,我们可以使用自定义注解。

实现步骤如下:

编写自定义注解: 首先,我们需要编写一个自定义注解,例如 @MyMethodAnnotation,用于标记我们要匹配的方法。 import java.lang.annotation.*;

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public @interface MyMethodAnnotation {

// 可以根据需要添加属性

}

使用 @annotation 表达式描述切点: 然后,我们可以使用 @annotation 表达式来描述切点,以匹配所有使用 @MyMethodAnnotation 注解的方法。 @Aspect

public class MyAspect {

@Before("@annotation(com.example.demo.annotation.MyMethodAnnotation)")

public void beforeMyMethod() {

// 在使用了 @MyMethodAnnotation 注解的方法执行前执行的逻辑

}

}

在连接点的方法上添加自定义注解: 最后,我们在需要匹配的方法上添加 @MyMethodAnnotation 注解。 @RestController

public class TestController {

@GetMapping("/t1")

@MyMethodAnnotation

public String t1() {

// 方法逻辑

}

}

@RestController

public class UserController {

@GetMapping("/u1")

@MyMethodAnnotation

public String u1() {

// 方法逻辑

}

}

通过以上步骤,我们可以使用自定义注解和 @annotation 表达式来描述这一类的切点,实现对多个无规则方法的匹配和增强。

我们现在就来自主实现一个自定义注解:

首先我们定义一个注解:

package com.example.aop.aspect;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

/**

* 记录方法的执行时间

*/

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.METHOD})

public @interface TimeRecord {

}

@Target({ElementType.TYPE, ElementType.METHOD}):

该注解用于指定注解可以应用的目标元素类型。在我们的自定义注解中,它指定了注解可以应用到类和方法上。ElementType.TYPE 表示可以应用到类、接口(包括注解类型)、枚举等类型元素上。ElementType.METHOD 表示可以应用到方法上。

@Retention(RetentionPolicy.RUNTIME):

该注解用于指定注解的保留策略,即注解在何时生效。RetentionPolicy.RUNTIME 表示注解将在运行时被保留,因此可以通过反射机制读取并使用。在我们的代码中,指定了注解在运行时保留,这意味着可以在运行时通过反射机制访问被注解标记的类和方法。

具体来看: 

@Target 注解:

用于标识注解所修饰的对象范围,即该注解可以用在什么地方。常用取值包括:

ElementType.TYPE:用于描述类、接口(包括注解类型)或枚举声明。ElementType.METHOD:描述方法。ElementType.PARAMETER:描述参数。ElementType.TYPE_USE:可以标注任意类型。@Retention 注解:

用于指定注解的保留策略,即注解的生命周期。常用取值有三种:

RetentionPolicy.SOURCE:表示注解仅存在于源代码中,在编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息,只能在编译时使用。比如 @SuppressWarnings,以及一些 Lombok 提供的注解。RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时无法获取。通常用于一些框架和工具的注解。RetentionPolicy.RUNTIME:运行时注解。表示注解存在于源代码、字节码和运行时中。这意味着在编译时、字节码中和实际运行时都可以通过反射获取到该注解的信息。通常用于一些需要在运行时处理的注解,如 Spring 的 @Controller、@ResponseBody 等。

通过使用这两个注解和枚举,我们就可以在 Java 中定义具有不同作用范围和生命周期的注解,以满足不同的需求。

使用 @annotation 表达式描述切点,实现注解功能:

package com.example.aop.aspect;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.After;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Before;

import org.springframework.core.annotation.Order;

import org.springframework.stereotype.Component;

import org.springframework.web.bind.annotation.RequestMapping;

@Slf4j

@Component

@Aspect

public class AspectDemo5 {

@Around("@annotation(com.example.aop.aspect.TimeRecord)")

public Object timeRecord(ProceedingJoinPoint joinPoint) {

long start = System.currentTimeMillis();

Object result = null;

try {

result = joinPoint.proceed();

}catch (Throwable e){

log.error(joinPoint.toString()+"发生异常, e:",e);

}

log.info(joinPoint.toString()+"执行时间: "+ (System.currentTimeMillis()-start) + "ms");

return result;

}

}

在连接点的方法上添加自定义注解——准备测试代码:

package com.example.aop.controller;

import com.example.aop.aspect.TimeRecord;

import lombok.extern.slf4j.Slf4j;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@Slf4j

@RequestMapping("/test")

@RestController

public class TestController {

@TimeRecord

@RequestMapping("/t1")

public String t1(){

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

return "t1";

}

@RequestMapping("/t2")

public String t2(){

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

return "t2";

}

}

package com.example.aop.controller;

import com.example.aop.aspect.TimeRecord;

import lombok.extern.slf4j.Slf4j;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@Slf4j

@RequestMapping("/user")

@RestController

public class UserController {

@RequestMapping("/u1")

public String u1(){

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

return "u1";

}

@TimeRecord

@RequestMapping("/u2")

public String u2(){

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

return "u2";

}

}

Spring AOP 的常见的面试题,这里简要概述一下:

基于注解 @Aspect:

使用 @Aspect 注解标识切面类,结合其他注解如 @Before、@After、@Around 等,定义通知逻辑,并通过 @Pointcut 定义切点表达式。这是目前最常用也是推荐的方式。  

基于自定义注解:

开发者可以定义自己的注解,然后通过 @annotation 切点表达式来匹配使用了该注解的方法。这种方式适用于需要特定注解的情况,使得切面更加灵活。  

基于 Spring API:

通过 XML 配置文件的方式进行 AOP 的配置,使用 等标签进行定义切面和通知。这种方式在 Spring Boot 中使用较少,但在传统 Spring 项目中仍然可见。  

基于代理来实现(一会儿详谈):

这是较早期的实现方式,通过 JDK 动态代理或者 CGLIB 代理来实现 AOP 的功能。这种方式需要手动编写代理类,相比于现代的注解方式,代码更为冗长和繁琐,不建议使用。

面试官:谈谈你对IOC和AOP的理解及AOP四种实现方式[通俗易懂]-腾讯云开发者社区-腾讯云 (tencent.com)

Spring AOP 原理(重要)

上面我们主要学习了Spring AOP的应用,接下来我们将深入探讨Spring AOP的实现原理,也就是Spring是如何实现AOP的。Spring AOP的实现是基于动态代理的,我们的学习内容将主要分为以下两部分:

代理模式:我们将学习代理模式的基本概念,理解动态代理是如何工作的,以及在Spring AOP中如何应用代理模式来实现切面功能。 Spring AOP源码剖析:我们将深入分析Spring框架中关于AOP的源代码,探讨其中的关键实现细节和设计思路,以更深入地理解Spring AOP的工作原理。

代理模式

代理模式,也称为委托模式,是一种设计模式。

其定义如下:为其他对象提供一种代理以控制对这个对象的访问。其作用是通过提供一个代理类,让我们在调用目标方法时不再直接对目标方法进行调用,而是通过代理类间接调用。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

使用代理前:

使用代理后:

代理模式在生活中的应用:

艺人经纪人:在这个例子中,艺人是被代理的对象,而经纪人充当代理。广告商想要与艺人合作拍摄广告,但他们不直接与艺人联系,而是通过经纪人来安排和沟通。经纪人作为代理,负责处理与艺人相关的所有事务,如安排会面、商议合约条件等。这种方式使得广告商和艺人之间的交互变得更加高效和专业。 房屋中介:在房屋租赁过程中,房屋的卖方委托中介来代理租赁交易。租客不直接与房屋的卖方联系,而是通过中介来安排看房、咨询租赁条件等事宜。中介作为代理方负责处理租赁过程中的各种事务,从而简化了租赁交易的流程,并提供了更好的服务体验。 经销商:制造商委托经销商来代理销售产品。制造商不直接将产品销售给最终客户,而是通过经销商来完成销售。经销商作为代理,负责促销、销售、售后服务等工作。这种方式使得制造商可以专注于生产,而不必担心销售和分销的事务。 秘书/助理:在商务场合中,高级管理人员的秘书或助理经常充当代理。合作伙伴想要与高级管理人员进行会议或商务洽谈时,通常需要先与其秘书或助理联系。秘书或助理负责安排会议时间、地点,以及向高级管理人员传达相关信息。这种方式可以有效地管理高级管理人员的日程安排,并确保会议或商务洽谈的顺利进行。

在这些例子中,代理模式提供了一种灵活的解决方案,使得在不直接访问或操作目标对象的情况下,能够有效地管理和控制对其的访问。代理模式在各种场景下都能发挥重要作用,提高了系统的安全性、可维护性和可扩展性。

代理模式通常用于以下几种情况:

远程代理(Remote Proxy):代理类用于控制访问位于不同地址空间的对象,典型的例子是远程方法调用(RPC)。 虚拟代理(Virtual Proxy):代理类用于控制访问创建开销较大的对象,代理类在真正需要对象时才会实例化目标对象。 保护代理(Protection Proxy):代理类用于控制对目标对象的访问权限,根据客户端的身份或权限来限制对目标对象的访问。 缓存代理(Cache Proxy):代理类用于为频繁访问的目标对象提供缓存,减少访问时间。 智能引用代理(Smart Reference Proxy):代理类用于提供额外的操作,例如在访问目标对象之前进行一些预处理或后处理操作。

代理模式的优点包括:

增加安全性:代理类可以控制对目标对象的访问权限,增加安全性。 降低耦合度:代理类作为客户端和目标类之间的中介,降低了它们之间的耦合度。 延迟加载:虚拟代理可以延迟加载目标对象,减少了系统的开销。 提高性能:缓存代理可以通过缓存目标对象的结果来提高性能。

代理模式是一种结构型设计模式,用于提供对对象的间接访问。它允许我们创建一个代理类,代理类可以控制或封装对目标对象的访问,同时隐藏或添加一些额外的功能。

下面是代理模式的几个关键概念:

代理类(Proxy Class):代理类是目标对象的替代品,它实现了与目标对象相同的接口,从而使得客户端可以通过代理类间接地访问目标对象。代理类持有对目标对象的引用,可以控制对目标对象的访问。 目标类(Real Subject):目标类是代理类所代表的真实对象。客户端最终想要访问的对象就是目标类对象。代理类通过调用目标类的方法来完成客户端的请求。 客户端(Client):客户端是使用代理模式的代码。客户端不直接访问目标类,而是通过代理类来访问目标类。

代理模式是一种设计模式,它允许在不修改被代理对象的基础上,通过扩展代理类来实现一些功能的附加与增强。根据代理的创建时期,代理模式可以分为静态代理和动态代理。

静态代理是由程序员手动创建代理类或使用特定工具自动生成源代码,并对其进行编译。在程序运行之前,代理类的.class文件就已经存在了。动态代理则是在程序运行时利用反射机制动态创建。代理类不是预先定义好的,而是根据需要在运行时生成。

静态代理 

静态代理是一种代理模式,在程序运行之前就已经创建了代理类的.class文件。这种代理方式类似于在出租房子之前,中介已经做好了相关的工作,只等待租客来租房子一样。也就是说,在程序编译和打包阶段,代理类就已经存在了,而不是在运行时动态生成。

我们通过代码来加深理解,以房租租赁为例: 

package com.example.aop.proxy;

public interface HouseSubject {

void rentHouse();

void saleHouse();

}

package com.example.aop.proxy;

public class RealHouseSubject implements HouseSubject{

@Override

public void rentHouse() {

System.out.println("我是房东, 我出租房子");

}

@Override

public void saleHouse() {

System.out.println("我是房东, 我出售房子");

}

}

package com.example.aop.proxy;

public class HouseProxy implements HouseSubject{

//授权

private HouseSubject houseSubject;

public HouseProxy(HouseSubject houseSubject) {

this.houseSubject = houseSubject;

}

@Override

public void rentHouse() {

System.out.println("我是中介, 我开始代理");

houseSubject.rentHouse();

System.out.println("我是中介, 我结束代理");

}

@Override

public void saleHouse() {

System.out.println("我是中介, 我开始代理");

houseSubject.saleHouse();

System.out.println("我是中介, 我结束代理");

}

}

package com.example.aop.proxy;

import java.lang.reflect.Proxy;

public class Main {

public static void main(String[] args) {

//静态代理

HouseSubject houseSubject = new HouseProxy(new RealHouseSubject());

houseSubject.rentHouse();

houseSubject.saleHouse();

}

}

 

你会发现,尽管静态代理完成了对目标对象的代理,但由于代码被硬编码了,对目标对象的每个方法的增强都需要手动完成,这使得静态代理缺乏灵活性。因此,在日常开发中,我们很少见到静态代理的应用场景。 

从上述代码中我们可以观察到,当我们修改接口(Subject)和业务实现类(RealSubject)时,还需要修改代理类(Proxy)。同样地,如果有新增接口(Subject)和业务实现类(RealSubject),也需要对每一个业务实现类新增代理类(Proxy)。

由于代理的流程是相同的,是否存在一种方法能够通过一个代理类来实现呢? 

答案是肯定的,这就需要利用动态代理技术。动态代理技术允许我们在运行时动态创建代理对象,而无需提前知道被代理的类的具体信息。这样一来,无论是修改接口、业务实现类还是新增接口、业务实现类,都不需要修改代理类,只需实现一个通用的代理类来处理代理流程。这种灵活性使得动态代理在实际开发中得到了广泛的应用。

动态代理

相比于静态代理,动态代理提供了更大的灵活性。我们不需要为每个目标对象单独创建一个代理对象,而是将创建代理对象的工作推迟到程序运行时由JVM来实现。换句话说,动态代理在程序运行时,根据需要动态创建生成代理对象。就像房屋中介,我不需要提前预测都有哪些业务,而是在业务来了之后根据情况创建。

Java也对动态代理进行了实现,并给我们提供了一些API。常见的实现方式有两种:JDK动态代理和CGLIB动态代理。

虽然动态代理在我们日常开发中使用相对较少,但在框架中几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助。

JDK动态代理

当使用JDK动态代理时,通常需要遵循以下步骤:

定义接口及其实现类:首先,我们需要定义一个接口以及其对应的实现类。这个接口定义了被代理对象的方法列表,而实现类提供了这些方法的具体实现。例如,我们可以有一个名为HouseSubject的接口和一个名为RealHouseSubject的实现类,RealHouseSubject实现了HouseSubject接口中定义的方法。自定义InvocationHandler并重写invoke方法:接着,我们需要自定义一个实现了InvocationHandler接口的类,并在其中重写invoke方法。在invoke方法中,我们可以实现对目标方法的调用以及自定义的一些处理逻辑。在这个方法中,我们会接收到代理对象、目标方法和方法参数,并通过反射调用目标方法。通过Proxy.newProxyInstance方法创建代理对象:最后,我们使用Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)方法创建代理对象。这个方法需要传入一个类加载器(ClassLoader)、一个接口数组以及一个InvocationHandler对象。它会返回一个实现了指定接口的代理对象。在这个方法中,JVM会根据接口信息以及InvocationHandler对象动态地生成代理类的字节码,并通过类加载器加载这个代理类,最终返回一个代理对象。

总的来说,JDK动态代理的实现步骤包括定义接口及其实现类、自定义InvocationHandler并重写invoke方法以及通过Proxy.newProxyInstance方法创建代理对象。通过这些步骤,我们可以在运行时动态地生成代理对象,并实现对目标方法的调用以及自定义的处理逻辑。

定义JDK动态代理类

实现 InvocationHandler 接口:

invoke方法可以被理解为一个代理执行的流程模型,它定义了代理对象在调用目标方法时所遵循的流程,包括对目标方法的调用和增强逻辑的执行。

package com.example.aop.proxy;

import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

@Slf4j

public class JDKInvocationHandler implements InvocationHandler {

private Object target;

public JDKInvocationHandler(Object target) {

this.target = target;

}

/**

* 调用目标方法, 并对方法进行增强

* @param proxy 代理类

*

* @param method 目标方法

* @param args 参数

*/

@Override

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

log.info("JDK动态代理开始");

//调用目标函数

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

log.info("JDK动态代理结束");

return result;

}

}

invoke方法是InvocationHandler接口中的抽象方法,需要在实现类中进行重写。在该方法中,我们可以实现对目标方法的调用以及自定义的处理逻辑。这段代码中,首先输出了一条日志表示JDK动态代理开始,然后通过反射调用了目标方法method.invoke(target, args),并将返回值存储在result变量中。最后,再输出一条日志表示JDK动态代理结束,并返回result作为方法的返回值。

你可能觉得还是很难以理解,让我们说得更详细一些:

在JDK动态代理中,invoke方法的原理涉及了Java的反射机制。让我们逐步解释:

Object proxy参数: 在invoke方法的签名中,确实有一个Object proxy参数。这个参数表示生成的代理对象,即代理类的实例。通常情况下,在实现invoke方法时,我们并不需要直接使用这个参数。这个参数的主要作用是用于在invoke方法内部调用代理对象的方法,但通常情况下,我们使用target来调用目标对象的方法,因为target是真正的被代理对象。 调用目标方法: 在invoke方法中,通过method.invoke(target, args)调用了目标方法。method参数表示要调用的目标方法,target是被代理对象,args是方法的参数。这里使用了Java的反射机制,通过Method对象的invoke方法来调用目标方法。 增强逻辑: 在调用目标方法之前和之后,可以加入一些增强逻辑。比如在示例代码中,在调用目标方法之前输出了一条日志表示JDK动态代理开始,在调用之后输出了一条日志表示JDK动态代理结束。这种增强逻辑是在实际业务逻辑前后添加一些额外的处理,以实现更复杂的功能需求。 关于proxy参数的使用: 在一般的情况下,proxy参数并不会被直接使用,因为在invoke方法中,我们主要关注的是目标对象的调用以及增强逻辑的实现。proxy参数主要在特殊情况下使用,比如当需要对代理对象自身进行一些操作时才会用到。例如,我们可以使用proxy参数来获取代理对象的类信息或者执行一些特殊的操作。

invoke方法是JDK动态代理的核心,通过该方法,我们可以实现对目标方法的调用和增强逻辑的实现。

package com.example.aop.proxy;

import java.lang.reflect.Proxy;

public class Main {

public static void main(String[] args) {

// //静态代理

// HouseSubject houseSubject = new HouseProxy(new RealHouseSubject());

// houseSubject.rentHouse();

// houseSubject.saleHouse();

// JDK动态代理

// 目标类

HouseSubject target = new RealHouseSubject();

//生成代理对象

HouseSubject proxy = (HouseSubject) Proxy.newProxyInstance(

target.getClass().getClassLoader(),

new Class[]{HouseSubject.class},

new JDKInvocationHandler(target));

proxy.rentHouse();

proxy.saleHouse();

}

}

这个过程的原理大致如下:

参数说明:

ClassLoader loader:代理类的类加载器。这个类加载器用于加载生成的代理类。Class[] interfaces:代理类需要实现的接口列表。代理对象会实现这些接口,并且可以被当做这些接口类型的对象来使用。InvocationHandler h:用于处理代理对象的InvocationHandler实现。在代理对象的方法调用时,InvocationHandler的invoke方法会被调用。

生成代理对象:

在我们的代码中,RealHouseSubject是目标类,JDKInvocationHandler是用于处理代理对象的InvocationHandler实现。Proxy.newProxyInstance方法会根据传入的类加载器和接口列表动态地生成一个代理类的字节码,并使用类加载器加载这个代理类。这个代理类会实现传入的接口列表中的所有接口,并且在方法调用时会委托给InvocationHandler的invoke方法来处理。

代理对象的使用:

生成代理对象后,我们可以像使用普通对象一样使用它。当调用代理对象的方法时,实际上是调用了InvocationHandler的invoke方法。在invoke方法中,我们还可以添加额外的逻辑,在调用目标方法之前或之后执行一些操作。在我们的代码中,JDKInvocationHandler的invoke方法中输出了代理开始和代理结束的日志信息。

代理对象的调用:在上面我们的代码中,代理对象proxy的rentHouse()和saleHouse()方法会被调用。在调用这些方法时,实际上是调用了JDKInvocationHandler的invoke方法。在invoke方法中,首先会输出代理开始的日志信息,然后调用目标方法的实际逻辑,最后输出代理结束的日志信息。

总的来说,使用Proxy.newProxyInstance方法生成的代理对象会实现指定的接口,并在方法调用时委托给指定的InvocationHandler处理。这样可以实现对目标对象方法的增强和控制。

Proxy.newProxyInstance方法的第三个参数Class[] interfaces体现了JDK动态代理的一个局限性,即它只能代理接口而不能代理普通类。这是由于JDK动态代理的实现机制决定的。

JDK动态代理是基于接口的代理机制,它利用传入的接口列表来生成代理类,并在运行时动态地实现这些接口。由于Java的单继承机制,一个类只能继承一个父类,但可以实现多个接口。因此,JDK动态代理选择了基于接口而不是基于类来生成代理类,以确保代理对象可以实现多个接口的功能。

这种设计决定了JDK动态代理只能代理接口,而不能直接代理普通类。如果想要代理普通类,可以使用其他的代理方式,比如CGLIB动态代理。

CGLIB动态代理是基于继承的代理机制,它可以代理普通类,并在运行时生成目标类的子类来实现代理。这样就可以实现对普通类的代理和增强。

CGLIB动态代理

CGLIB(Code Generation Library)是一个基于Java字节码操作和分析框架ASM的字节码生成库。它提供了一种在运行时对Java字节码进行修改和动态生成的能力。CGLIB允许程序员通过编程方式创建和修改Java类,包括添加字段、方法和属性,以及修改现有类的行为。

主要特点包括:

动态生成字节码: CGLIB允许在运行时动态生成Java字节码,从而创建新的类或修改现有类。这种能力使得CGLIB成为实现代理、动态创建类以及AOP等功能的理想工具。 基于继承实现代理: CGLIB通常通过创建目标类的子类来实现代理。代理类继承了目标类,并且可以覆盖或添加目标类的方法,以实现额外的功能或行为。 广泛应用于开源框架: CGLIB被广泛应用于各种开源框架中,包括Spring框架的AOP模块。在Spring中,当目标对象没有实现接口时,默认采用CGLIB动态代理实现AOP代理。

综上,CGLIB是一个强大的字节码生成库,它使得在运行时对Java字节码进行修改和动态生成变得简单和灵活。通过CGLIB,程序员可以实现代理、AOP、动态创建类等功能,为Java应用程序增加了更多的动态性和灵活性。

很多知名的开源框架都使用到了CGLIB,比如Spring框架中的AOP(面向切面编程)模块使用了CGLIB动态代理来实现对目标对象的代理,我们正要学习的就是它。

具体来说,Spring在创建代理对象时会根据目标对象的类型来选择使用JDK动态代理还是CGLIB动态代理。

当目标对象实现了接口时,Spring会默认使用JDK动态代理。JDK动态代理要求目标对象必须实现接口,因此它只能代理实现了接口的类。在这种情况下,Spring会使用Java自带的Proxy类来创建代理对象,然后通过实现InvocationHandler接口来处理代理对象的方法调用。

当目标对象没有实现接口时,Spring会采用CGLIB动态代理。CGLIB动态代理不依赖于接口,它可以代理普通的Java类。在这种情况下,Spring会使用CGLIB库来动态生成目标类的子类,并重写其中的方法以实现代理功能。

通过这种方式,Spring框架可以根据目标对象的特点选择合适的代理方式,从而实现对目标对象的代理和增强。这种灵活的代理机制使得Spring AOP模块能够适用于更广泛的应用场景,并且能够在不同的情况下选择最合适的代理方式。

大致实现步骤如下:

1、定义被代理类: 首先,我们需要定义一个类作为被代理类。这个类是我们希望在运行时对其方法进行增强的类。

2、自定义MethodInterceptor: 接下来,我们需要自定义一个类实现MethodInterceptor接口,并重写intercept方法。intercept方法是CGLIB动态代理中用于增强目标方法的核心方法,类似于JDK动态代理中的invoke方法。在intercept方法中,我们可以对目标方法进行增强操作,比如在方法执行前后添加额外的逻辑、修改方法的参数或返回值等。

3、通过Enhancer创建代理类: 最后,我们可以使用Enhancer类的create()方法来创建代理类。

在create()方法中,我们需要传入被代理类的类加载器、被代理类的接口(如果有)、以及我们实现的MethodInterceptor对象。Enhancer类会根据传入的参数动态生成代理类的字节码,并使用类加载器加载这个代理类。生成的代理类继承了被代理类,并重写了其中的方法,以实现对目标方法的代理和增强。

总的来说,使用CGLIB动态代理时,我们需要定义被代理类,实现MethodInterceptor接口来自定义增强逻辑,并使用Enhancer类来动态生成代理类。通过这个代理类,我们可以实现对目标类方法的代理和增强。

接下来我们开始实现:

添加依赖

和JDK 动态代理不同,CGLIB(Code Generation Library) 实际是属于一个开源项目,如果你要使用它的话,需要手动添加相关依赖:

cglib

cglib

3.3.0

自定义 MethodInterceptor(方法拦截器)

实现MethodInterceptor接口:

package com.example.aop.proxy;

import lombok.extern.slf4j.Slf4j;

import net.sf.cglib.proxy.MethodInterceptor;

import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

@Slf4j

public class CGLibMethodInterceptor implements MethodInterceptor {

private Object target;

public CGLibMethodInterceptor(Object target) {

this.target = target;

}

/**

* 调用目标方法, 并对目标方法进行功能增强

* @param o 代理类

* @param method 目标方法

* @param objects 参数

* @param methodProxy

* @return

* @throws Throwable

*/

@Override

public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

log.info("CGLib动态代理开始");

//调用目标方法

Object result = method.invoke(target, objects);

log.info("CGLib 动态代理结束");

return result;

}

}

MethodInterceptor的作用:

MethodInterceptor是CGLIB库中的一个接口,用于在代理对象的方法调用前后执行额外的逻辑。它类似于JDK动态代理中的InvocationHandler接口。当代理对象的方法被调用时,CGLIB会将控制权转交给MethodInterceptor的intercept方法,在该方法中可以对目标方法进行增强操作。 intercept方法的参数: Object o:代理类的实例,即生成的代理对象。Method method:目标方法的反射对象,可以通过它调用目标方法。Object[] objects:目标方法的参数数组。MethodProxy methodProxy:用于调用父类方法的代理对象。 原理:

CGLIB通过继承的方式实现代理。在创建代理对象时,CGLIB会生成目标类的子类,并重写目标方法。在重写的方法中,CGLIB会调用MethodInterceptor的intercept方法,而不是直接调用目标方法。在intercept方法中,我们可以实现对目标方法的增强,比如在方法执行前后添加额外的逻辑。 MethodProxy对象是CGLIB中用于调用被代理类父类方法的代理对象。在CGLIB动态代理中,代理类是被代理类的子类,因此代理类可以调用被代理类的所有方法,包括父类中的方法。然而,在某些情况下,我们可能希望在代理类的方法中调用父类的方法,而不是被代理类的方法。这时就需要用到MethodProxy对象。 在MethodInterceptor的intercept方法中,第四个参数MethodProxy methodProxy表示被代理类方法的代理对象。通过这个MethodProxy对象,我们可以调用被代理类的方法,同时确保不会触发代理逻辑。也就是说,即使在代理类的方法中调用父类方法,也不会再次进入intercept方法,避免了无限循环的情况发生。 简而言之,MethodProxy对象允许我们在代理类的方法中调用被代理类的父类方法,同时绕过代理逻辑,确保方法调用的顺利进行。

这段代码实现了一个CGLIB动态代理的方法拦截器,用于在目标方法的执行前后添加额外的逻辑。这是CGLIB动态代理中非常重要的一部分,通过这种方式我们可以实现对目标方法的动态增强。

package com.example.aop.proxy;

import net.sf.cglib.proxy.Enhancer;

import java.lang.reflect.Proxy;

public class Main {

public static void main(String[] args) {

// CGlib动态代理

HouseSubject target = new RealHouseSubject();

HouseSubject proxy = (HouseSubject) Enhancer.create(

target.getClass(),

new CGLibMethodInterceptor(target));

proxy.rentHouse();

proxy.saleHouse();

}

}

Enhancer.create() 方法用于创建代理对象。第一个参数 target.getClass() 表示要创建代理对象的目标类的类对象。在这里,我们使用了目标类 RealHouseSubject 的类对象作为参数,这意味着我们希望创建的代理对象能够处理 RealHouseSubject 的方法调用。第二个参数是我们定义的方法拦截器 CGLibMethodInterceptor 的实例。这个拦截器会在代理对象的方法被调用时进行拦截,并在方法执行前后添加额外的逻辑。Enhancer.create() 方法会根据传入的参数动态生成代理类的字节码,并使用类加载器加载这个代理类。生成的代理类会继承目标类,并重写其中的方法,在重写的方法中调用拦截器的 intercept 方法,从而实现对目标方法的代理和增强。最后,返回的代理对象可以被用来调用目标类的方法,而这些方法的执行会被拦截器拦截,并执行拦截器中定义的增强逻辑。

CGLIB也可以对普通类进行动态代理: 

package com.example.aop.proxy;

public class RealHouseSubject2 {

public void rentHouse() {

System.out.println("我是房东, 我出租房子");

}

public void saleHouse() {

System.out.println("我是房东, 我出售房子");

}

}

package com.example.aop.proxy;

import net.sf.cglib.proxy.Enhancer;

import java.lang.reflect.Proxy;

public class Main {

public static void main(String[] args) {

//CGLib代理非接口类

RealHouseSubject2 target = new RealHouseSubject2();

RealHouseSubject2 proxy = (RealHouseSubject2) Enhancer.create(

target.getClass(),

new CGLibMethodInterceptor(target));

proxy.rentHouse();

proxy.saleHouse();

}

}

Spring AOP 源码剖析(了解)

Spring AOP 主要基于两种方式实现的: JDK 及 CGLIB 的方式。

Spring对于AOP的实现,基本上都是靠 AnnotationAwareAspectJAutoProxyCreator 去完成,生成代理对象的逻辑在父类 AbstractAutoProxyCreator 中。

AnnotationAwareAspectJAutoProxyCreator:

AnnotationAwareAspectJAutoProxyCreator是Spring框架中用于自动创建代理对象的类,它可以基于AspectJ注解自动创建代理对象,并将切面织入到目标对象中。这个类会自动扫描Spring容器中所有的Bean,检查是否需要对其进行代理,并生成相应的代理对象。 AbstractAutoProxyCreator:

AbstractAutoProxyCreator是AnnotationAwareAspectJAutoProxyCreator的父类,它提供了创建代理对象的基本逻辑和实现。这个类中定义了创建代理对象的方法,并且定义了代理对象的创建时机和条件。具体来说,它会根据一些条件来判断是否需要对目标对象进行代理,比如是否匹配AOP切点表达式、是否需要应用事务管理等。在满足代理条件的情况下,AbstractAutoProxyCreator会调用createProxy()方法来创建代理对象。这个方法会使用ProxyFactory工厂类来创建代理对象,并根据配置和条件来确定使用JDK动态代理还是CGLIB动态代理。

protected Object createProxy(Class beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) {

// 如果beanFactory是ConfigurableListableBeanFactory类型的,则将目标类暴露出来,方便AOP功能使用

if (this.beanFactory instanceof ConfigurableListableBeanFactory) {

AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory)this.beanFactory, beanName, beanClass);

}

// 创建代理工厂对象

ProxyFactory proxyFactory = new ProxyFactory();

// 从当前对象复制配置到代理工厂对象

proxyFactory.copyFrom(this);

/**

* 检查proxyTargetClass属性值,spring默认为false

* proxyTargetClass 检查接⼝是否对类代理, ⽽不是对接⼝代理

* 如果代理对象为类, 设置为true, 使⽤cglib代理

*/

// 判断是否应该使用代理目标类的类

if (proxyFactory.isProxyTargetClass()) {

// 如果需要使用代理目标类的类,且beanClass是代理类或Lambda表达式类,则将其所有接口添加到代理工厂对象中

if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) {

Class[] interfaces = beanClass.getInterfaces();

for (Class ifc : interfaces) {

proxyFactory.addInterface(ifc);

}

}

} else {

// 如果不使用代理目标类的类,则根据beanClass和beanName判断是否需要使用代理目标类的类

if (this.shouldProxyTargetClass(beanClass, beanName)) {

proxyFactory.setProxyTargetClass(true);

} else {

/**

* 如果beanClass实现了接⼝,且接⼝⾄少有⼀个⾃定义⽅法,则使⽤JDK代理

* 否则CGLIB代理(设置ProxyTargetClass为true )

* 即使我们配置了proxyTargetClass=false, 经过这⾥的⼀些判断还是可能会将其设为true

*/

// 否则,评估beanClass的接口,并将其添加到代理工厂对象中

this.evaluateProxyInterfaces(beanClass, proxyFactory);

}

}

// 构建Advisor(通知器)数组

Advisor[] advisors = this.buildAdvisors(beanName, specificInterceptors);

// 将Advisor数组添加到代理工厂对象中

proxyFactory.addAdvisors(advisors);

// 设置目标源

proxyFactory.setTargetSource(targetSource);

// 定制代理工厂对象

this.customizeProxyFactory(proxyFactory);

// 设置代理对象是否冻结

proxyFactory.setFrozen(this.freezeProxy);

// 如果通知器(Advisor)已经预过滤,则设置代理工厂对象预过滤为true

if (this.advisorsPreFiltered()) {

proxyFactory.setPreFiltered(true);

}

// 获取代理类加载器

ClassLoader classLoader = this.getProxyClassLoader();

// 如果类加载器是SmartClassLoader类型,并且不是beanClass的类加载器,则使用原始类加载器

if (classLoader instanceof SmartClassLoader && classLoader != beanClass.getClassLoader()) {

classLoader = ((SmartClassLoader)classLoader).getOriginalClassLoader();

}

// 返回代理对象

return proxyFactory.getProxy(classLoader);

}

这个方法的主要思路和流程是:

确定是否需要暴露目标类。如果beanFactory是ConfigurableListableBeanFactory类型的,则将目标类暴露出来,方便AOP功能使用。创建代理工厂对象,并从当前对象复制配置到代理工厂对象中。根据配置决定是否使用代理目标类的类,以及确定使用JDK动态代理还是CGLIB动态代理。根据给定的通知器(Advisor)构建Advisor数组,并将其添加到代理工厂对象中。设置代理对象的目标源、自定义代理工厂对象、是否冻结代理对象等属性。最后,使用代理工厂对象创建代理对象,并返回。

代理工厂有一个重要的属性:proxyTargetClass,其默认值为false,也可以通过程序设置。proxyTargetClass属性影响着Spring在创建代理对象时选择使用的代理方式。当proxyTargetClass为false时,Spring会优先考虑目标对象是否实现了接口来确定代理方式;而当proxyTargetClass为true时,Spring会直接使用CGLIB动态代理,不考虑目标对象是否实现了接口。

proxyTargetClass目标对象代理方式false实现了接口JDK代理false未实现接口 (只有实现类)CGLIB代理true实现了接口CGLIB代理true未实现接口 (只有实现类)CGLIB代理

proxyTargetClass 可以通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 来设置。(Spring Framework)

注意!!!

在Spring Boot 2.X开始,已经默认使用CGLIB代理,我们可以通过设置配置项 spring.aop.proxy-target-class=false 来修改默认代理方式为JDK代理。

此外,在Spring Boot中使用 @EnableAspectJAutoProxy 注解进行AOP代理的配置将无效,因为Spring Boot默认使用AopAutoConfiguration进行AOP代理的装配。

也就是说,在Spring Boot 2.x版本之后,如果我们手动添加了@EnableAspectJAutoProxy注解,可能会失效,因为Spring Boot默认会使用AopAutoConfiguration进行AOP的装配。你可以通过查看Spring Boot的文档或源码,以及进行实际测试来验证这一点。

 

可参考:Spring框架(Spring Framework)文档原文:

JDK- and CGLIB-based proxies  

This section serves as the definitive documentation on how the ProxyFactoryBean chooses to create either a JDK-based proxy or a CGLIB-based proxy for a particular target object (which is to be proxied).

The behavior of the ProxyFactoryBean with regard to creating JDK- or CGLIB-based proxies changed between versions 1.2.x and 2.0 of Spring. The ProxyFactoryBean now exhibits similar semantics with regard to auto-detecting interfaces as those of the TransactionProxyFactoryBean class.

If the class of a target object that is to be proxied (hereafter simply referred to as the target class) does not implement any interfaces, a CGLIB-based proxy is created. This is the easiest scenario, because JDK proxies are interface-based, and no interfaces means JDK proxying is not even possible. You can plug in the target bean and specify the list of interceptors by setting the interceptorNames property. Note that a CGLIB-based proxy is created even if the proxyTargetClass property of the ProxyFactoryBean has been set to false. (Doing so makes no sense and is best removed from the bean definition, because it is, at best, redundant, and, at worst confusing.)

If the target class implements one (or more) interfaces, the type of proxy that is created depends on the configuration of the ProxyFactoryBean.

If the proxyTargetClass property of the ProxyFactoryBean has been set to true, a CGLIB-based proxy is created. This makes sense and is in keeping with the principle of least surprise. Even if the proxyInterfaces property of the ProxyFactoryBean has been set to one or more fully qualified interface names, the fact that the proxyTargetClass property is set to true causes CGLIB-based proxying to be in effect.

If the proxyInterfaces property of the ProxyFactoryBean has been set to one or more fully qualified interface names, a JDK-based proxy is created. The created proxy implements all of the interfaces that were specified in the proxyInterfaces property. If the target class happens to implement a whole lot more interfaces than those specified in the proxyInterfaces property, that is all well and good, but those additional interfaces are not implemented by the returned proxy.

If the proxyInterfaces property of the ProxyFactoryBean has not been set, but the target class does implement one (or more) interfaces, the ProxyFactoryBean auto-detects the fact that the target class does actually implement at least one interface, and a JDK-based proxy is created. The interfaces that are actually proxied are all of the interfaces that the target class implements. In effect, this is the same as supplying a list of each and every interface that the target class implements to the proxyInterfaces property. However, it is significantly less work and less prone to typographical errors.

Spring Boot关于AOP的文档好像暂时还没有。 

AopAutoConfiguration是Spring Boot提供的自动配置类,用于自动配置AOP相关的功能。它会根据Spring Boot的默认行为,自动选择合适的AOP代理方式,而无需手动配置@EnableAspectJAutoProxy。

我们先来用程序验证一下:

package com.example.aop.component;

public interface Iface {

void say();

}

package com.example.aop.component;

import org.springframework.stereotype.Component;

@Component

public class UserComponent implements Iface{

public void say() {

System.out.println("say...");

}

}

package com.example.aop.component;

import org.springframework.stereotype.Component;

@Component

public class UserComponent2{

public void say() {

System.out.println("say...");

}

}

package com.example.aop;

import com.example.aop.component.Iface;

import com.example.aop.component.UserComponent;

import com.example.aop.component.UserComponent2;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.ApplicationContext;

@SpringBootApplication

public class AopApplication {

public static void main(String[] args) {

ApplicationContext context = SpringApplication.run(AopApplication.class, args);

//SpringBoot 默认情况下, 接口类和普通类都可以被正确代理

UserComponent bean = context.getBean(UserComponent.class);

System.out.println(bean.getClass().toString());

UserComponent2 bean2 = (UserComponent2) context.getBean("userComponent2");

System.out.println(bean2.getClass().toString());

}

}

那么怎么配置代理呢?我们通过程序具体来看:

package com.example.aop;

import com.example.aop.component.Iface;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.ApplicationContext;

@SpringBootApplication

public class AopApplication {

public static void main(String[] args) {

ApplicationContext context = SpringApplication.run(AopApplication.class, args);

//SpringBoot 默认情况下, 接口类和普通类都可以被正确代理

// UserComponent bean = context.getBean(UserComponent.class);

// System.out.println(bean.getClass().toString());

//

// UserComponent2 bean2 = (UserComponent2) context.getBean("userComponent2");

// System.out.println(bean2.getClass().toString());

//设置JDK代理

// UserComponent bean = context.getBean(UserComponent.class);

// System.out.println(bean.getClass().toString());// 不是接口,JDK不能正常代理

Iface bean = (Iface) context.getBean("userComponent");

System.out.println(bean.getClass().toString()); //是接口,JDK可以正常代理

UserComponent2 bean2 = (UserComponent2) context.getBean("userComponent2");

System.out.println(bean2.getClass().toString()); // CGLib代理

}

}

所以“什么时候使用JDK动态代理?什么时候使用CGLIB动态代理?”

标准答案是:基于程序员的配置。

在没有程序员明确配置的情况下,Spring会根据默认配置来决定使用JDK动态代理还是CGLIB动态代理。在Spring 2.X 之前,默认配置是false,即使用JDK代理。如果设置为true,则使用CGLIB代理。在Spring 2.X 之后,默认配置是true,即使用CGLIB代理。当使用JDK代理时,如果目标对象实现了接口,则使用JDK代理;否则,使用CGLIB代理。当配置使用CGLIB代理时,不论目标对象是否实现了接口,都将使用CGLIB代理。

至于“实现了接口”的判定: 如果beanClass实现了接口,且接口至少有一个自定义方法,则使用JDK代理,否则GCLib代理。

Spring AOP中使用了JDK动态代理和CGLIB动态代理两种方式,但它们的底层都是基于JDK提供的反射机制实现的。

 代理工厂代码:

createAopProxy的实现在 DefaultAopProxyFactory中:

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

private static final long serialVersionUID = 7930414337282325166L;

public DefaultAopProxyFactory() {

}

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {

// 判断是否在原生镜像中,或者配置不优化、不代理目标类、没有用户提供的代理接口

if (NativeDetector.inNativeImage() || !config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {

// 如果满足条件,则创建并返回基于JDK动态代理的AopProxy对象

return new JdkDynamicAopProxy(config);

} else {

// 否则,获取目标类

Class targetClass = config.getTargetClass();

if (targetClass == null) {

// 如果目标类为空,抛出异常

throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");

} else {

// 判断是否适合使用CGLIB动态代理,条件是目标类不是接口、不是代理类、不是Lambda类

return (!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) && !ClassUtils.isLambdaClass(targetClass)) ?

// 如果满足条件,则创建并返回基于CGLIB动态代理的AopProxy对象

new ObjenesisCglibAopProxy(config) :

// 否则,创建并返回基于JDK动态代理的AopProxy对象

new JdkDynamicAopProxy(config);

}

}

}

//…………

}

代码思路和流程:

方法接受一个AdvisedSupport类型的参数config,用于配置AOP代理。首先,通过NativeDetector.inNativeImage()检测是否在原生镜像中,或者检查配置是否不需要优化、不需要代理目标类、没有用户提供的代理接口。如果满足条件,则创建并返回一个基于JDK动态代理的AopProxy对象。如果上述条件不满足,即需要进一步检查是否适合使用CGLIB动态代理。

首先,获取目标类targetClass。如果目标类为空,则抛出异常。否则,判断目标类是否适合使用CGLIB动态代理,条件是目标类不是接口、不是代理类、不是Lambda类。如果满足条件,则创建并返回一个基于CGLIB动态代理的AopProxy对象。如果不满足上述条件,则创建并返回一个基于JDK动态代理的AopProxy对象。

接下来就是创建代理了:

JDK动态代理:

public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {

//…………

public Object getProxy() {

return this.getProxy(ClassUtils.getDefaultClassLoader());

}

public Object getProxy(@Nullable ClassLoader classLoader) {

if (logger.isTraceEnabled()) {

logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());

}

return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this);

}

//…………

}

CGLIB动态代理:

//

// Source code recreated from a .class file by IntelliJ IDEA

// (powered by FernFlower decompiler)

//

class CglibAopProxy implements AopProxy, Serializable {

//…………

public Object getProxy() {

return this.getProxy((ClassLoader)null);

}

public Object getProxy(@Nullable ClassLoader classLoader) {

if (logger.isTraceEnabled()) {

logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource());

}

try {

Class rootClass = this.advised.getTargetClass();

Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

Class proxySuperClass = rootClass;

int x;

if (rootClass.getName().contains("$$")) {

proxySuperClass = rootClass.getSuperclass();

Class[] additionalInterfaces = rootClass.getInterfaces();

Class[] var5 = additionalInterfaces;

int var6 = additionalInterfaces.length;

for(x = 0; x < var6; ++x) {

Class additionalInterface = var5[x];

this.advised.addInterface(additionalInterface);

}

}

this.validateClassIfNecessary(proxySuperClass, classLoader);

Enhancer enhancer = this.createEnhancer();

if (classLoader != null) {

enhancer.setClassLoader(classLoader);

if (classLoader instanceof SmartClassLoader && ((SmartClassLoader)classLoader).isClassReloadable(proxySuperClass)) {

enhancer.setUseCache(false);

}

}

enhancer.setSuperclass(proxySuperClass);

enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));

enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);

enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

Callback[] callbacks = this.getCallbacks(rootClass);

Class[] types = new Class[callbacks.length];

for(x = 0; x < types.length; ++x) {

types[x] = callbacks[x].getClass();

}

enhancer.setCallbackFilter(new ProxyCallbackFilter(this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));

enhancer.setCallbackTypes(types);

return this.createProxyClassAndInstance(enhancer, callbacks);

} catch (IllegalArgumentException | CodeGenerationException var9) {

throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() + ": Common causes of this problem include using a final class or a non-visible class", var9);

} catch (Throwable var10) {

throw new AopConfigException("Unexpected AOP exception", var10);

}

}

//…………

}

综上,我们最后做一个总结:

Spring AOP 使用的是那种代理? 

关于Spring Framework 和 Spring Boot分开讨论:

共同点:底层实现都是JDK和CGLib

不同点:

Spring Framework:

如果目标对象实现了接口,则Spring AOP会选择使用JDK动态代理。如果目标对象没有实现接口,则Spring AOP会选择使用CGLIB动态代理。 Spring Boot:

在Spring Boot中,默认情况下,无论目标对象是否实现了接口,都会使用CGLIB动态代理。如果需要强制使用JDK动态代理,则需要进行相应的配置。Spring Boot 2.x版本之前,默认使用JDK代理,与Spring Framework保持一致。而在Spring Boot 2.x版本之后,默认使用CGLIB代理。

参考链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: