SpringBoot 的两大核心
概念
AOP(Aspect Oriented Programming,面向切面编程、面向方面编程),其实就是面向特定方法编程。AOP 是 OOP(面向对象编程)的一种延续,二者互补,并不对立。AOP 是一种编程思想,动态代理是这种思想的主流实现方式。
AOP 之所以叫面向切面编程,是因为它的核心思想就是将横切关注点从核心业务逻辑中分离出来,形成一个个的切面(Aspect)。
应用场景:
- 日志记录:自定义日志记录注解,利用 AOP,一行代码即可实现日志记录。
- 性能统计:利用 AOP 在目标方法的执行前后统计方法的执行时间,方便优化和分析。
- 事务管理:
@Transactional
注解可以让 Spring 为我们进行事务管理比如回滚异常操作,免去了重复的事务管理逻辑。@Transactional
注解就是基于 AOP 实现的。
- 权限控制:利用 AOP 在目标方法执行前判断用户是否具备所需要的权限,如果具备,就执行目标方法,否则就不执行。例如,SpringSecurity 利用
@PreAuthorize
注解一行代码即可自定义权限校验。
- 接口限流:利用 AOP 在目标方法执行前通过具体的限流算法和实现对请求进行限流处理。
- 缓存管理:利用 AOP 在目标方法执行前后进行缓存的读取和更新。
优势:
- 代码无侵入
- 减少重复代码
- 提高开发效率
- 维护方便
AOP 的常见实现方式有动态代理、字节码操作等方式。
Spring AOP 是 Spring 框架的高级技术,旨在管理 bean 对象的过程中,丰要通过底层的动态代理机制,对特定的方法进行编程。
AOP是一种思想,Spring AOP是这个思想的一种实现。AOP和Spring AOP的关系相当于IoC和DI之间的关系一样。
引入 AOP 依赖:
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
|
一些概念:
- 横切关注点 Cross-cutting Concerns :多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)。
- 连接点 JoinPoint:可以被 AOP 控制的方法(暗含方法执行时的相关信息)
- 切面 Aspect:对横切关注点进行封装的类,一个切面是一个类。切面可以定义多个通知,用来实现具体的功能。描述通知与切入点的对应关系(通知 + 切入点)
- 通知 Advice:指那些重复的逻辑,也就是共性功能 (最终体现为一个方法)
- 切入点 PointCut:匹配连接点的条件,通知仅会在切入点方法执行时被应用。一个切点是一个表达式,它用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义。
- 目标对象 Target:通知所应用的对象。
- 织入 Weaving:织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。常见的织入时机有两种,分别是编译期织入(Compile-Time Weaving 如:AspectJ)和运行期织入(Runtime Weaving 如:AspectJ、Spring AOP)。
入门示例
一个 AOP 程序的示例:为 DeptServletImpl 类的所有方法增加一层统计其运行时间逻辑。
解释:
TimeAspect
是一个 AOP 程序。
@Aspect
注解说明当前类是 AOP 类(切面类)。
- 这个类中的模板方法执行以下操作:
- 记录开始时间
- 调用原始方法运行
- 记录结束时间并输出日志
@Around
注解填写切入点表达式,针对某些方法进行编程
执行流程:
SpringAOP 的底层是基于动态代理实现的。程序最后注入的就不是目标对象,而是代理对象。代理对象完成了我们所编写的增强的功能。
通知类型
通知类型 |
名称 |
在目标方法执行位置 |
备注 |
IDEA 中的图标 |
@Around |
环绕通知 |
前、后 |
如果原始方法抛异常,那么原始方法后的代码就不会执行 |
|
@Before |
前置通知 |
前 |
|
|
@After |
后置通知 |
后 |
无论是否有异常都执行,因此又叫做最终通知 |
|
@AfterReturning |
返回后通知 |
后 |
发生异常不会执行 |
|
@AfterThrowing |
抛出异常后通知 |
- |
发生异常后执行 |
|
关于环绕通知:
@Around
环绕通知需要自己调用 ProceedingJoinPoint.proceed()
来让原始方法执行,其他通知不需要考虑目标方法执行
@Around
环绕通知方法的返回值,必须指定为 Object
,来接收原始方法的返回值。
@PointCut
:该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。被注解的方法访问级别:
private
:仅能在当前切面类中引用该表达式
public
:在其他外部的切面类中也可以引用该表达式
切面类示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| @Slf4j @Component @Aspect public class MyAspect1 {
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") public void pt(){}
@Before("pt()") public void before(){ log.info("before ..."); }
@Around("pt()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { log.info("around before ...");
Object result = proceedingJoinPoint.proceed();
log.info("around after ..."); return result; }
@After("pt()") public void after(){ log.info("after ..."); }
@AfterReturning("pt()") public void afterReturning(){ log.info("afterReturning ..."); }
@AfterThrowing("pt()") public void afterThrowing(){ log.info("afterThrowing ..."); } }
|
通知顺序
通知顺序:当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
- 不同切面类中,默认按照切面类的类名字母排序
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
- 用
@Order(number)
加在切面类上来数字控制顺序
- 目标方法前的通知方法:数字小的先执行
- 目标方法后的通知方法:数字小的后执行
切入点表达式
切入点表达式:描述切入点方法的一种表达式。主要用来决定项目中的哪些方法需要加入通知。
常见形式:
execution(...)
:根据方法的签名来匹配
@annotation(...)
:根据注解匹配
execution()
execution
主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
1 2 3 4 5 6
| execution([访问修饰符] 返回值 [packageName.className]methodName(methodParams) [throws 异常])
|
可以使用的通配符:
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
根据业务需要,可以使用 &&
、||
、!
来组合比较复杂的切入点表达式。
一些示例:
1 2 3
| execution(* com.*.service.*.update*(*)) execution(* com.*.uuanqin..DeptService.*(..)) execution(* *(..))
|
1 2 3 4 5 6
| @Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
@Pointcut( "execution(* com.itheima.service.impl.DeptServiceImpl.list(..)) || "+ "execution(* com.itheima.service.impl.DeptServiceImpl.delete(..))" )
|
书写建议:
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是
find
开头,更新类方法都是 update
开头。
- 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用
..
,可以使用 *
匹配单个包。
@annotation()
@annotation
切入点表达式,用于匹配标识有特定注解的方法。
自定义注解:
1 2 3
| @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyLog {}
|
切入点表达式示例,它将作用于标注有 @MyLog
的方法上:
1
| @Pointcut("@annotation(your.package.MyLog)")
|
连接点
在 Spring 中用 JoinPoint 抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
两种对象获取信息的方式是一致的:
- 对于
@Around
通知,获取连接点信息只能使用 ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能使用
JoinPoint
,它是 ProceedingJoinPoint
的父类型
获取目标对象的相关信息示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| @Slf4j @Aspect @Component public class MyAspect8 {
@Pointcut("execution(* com.uuanqin.service.DeptService.*(..))") private void pt(){}
@Before("pt()") public void before(JoinPoint joinPoint){ log.info("MyAspect8 ... before ..."); String className = joinPoint.getTarget().getClass().getName(); log.info("目标对象的类名:{}", className);
String methodName = joinPoint.getSignature().getName(); log.info("目标方法的方法名:{}",methodName);
Object[] args = joinPoint.getArgs(); log.info("目标方法运行时传入的参数:{}", Arrays.toString(args)); }
@Around("pt()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { log.info("MyAspect8 around before …");
String className = joinPoint.getTarget().getClass().getName(); log.info("目标对象的类名:{}", className);
String methodName = joinPoint.getSignature().getName(); log.info("目标方法的方法名:{}",methodName);
Object[] args = joinPoint.getArgs(); log.info("目标方法运行时传入的参数:{}", Arrays.toString(args));
Object result = joinPoint.proceed();
log.info("目标方法运行的返回值:{}",result);
log.info("MyAspect8 around after …"); return result; } }
|
应用实例
记录操作日志
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长。
思路:需要对所有业务类中的增、删、改 方法添加统一功能,使用 AOP 技术最为方便。
步骤:
- 引入 AOP 起步依赖
- 准备好日志相关的表结构,并引入实体类
- 准备好一个自定义注解,实现切入点表达式的
@annotation()
方法。
- 定义切面类,完成记录操作日志的逻辑。
权限控制
背景:在 Controller 中,一些方法需要获取管理员权限才能调用。我们可以通过加上注解的方式实现权限控制。
定义一些权限常量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public interface UserConstant {
String USER_LOGIN_STATE = "user_login";
String DEFAULT_ROLE = "user";
String ADMIN_ROLE = "admin";
String BAN_ROLE = "ban";
}
|
定义注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuthCheck {
String mustRole() default "";
}
|
AOP 切面实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
@Aspect @Component public class AuthInterceptor {
@Around("@annotation(authCheck)") public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable { String mustRole = authCheck.mustRole(); RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
return joinPoint.proceed(); } }
|
在 Controller 中可以通过加上一条注解即可实现权限校验:
1 2 3 4 5 6 7 8 9 10 11 12
|
@PostMapping("/update") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse<Boolean> updateInterfaceInfo(@RequestBody InfoUpdateRequest InfoUpdateRequest) { return ResultUtils.success(result); }
|
关于 Request 不同的获取方式详看这篇文章:RequestContextHolder详解(获取request对象的四种方法) - 周文豪 - 博客园 (cnblogs.com)
全局异常处理器
@RestControllerAdvice
是 Spring 框架提供的一个切面注解,用于定义全局异常处理器和全局数据绑定设置。它结合了 @ControllerAdvice
和 @ResponseBody
两个注解的功能:
@ControllerAdvice
是一个用于定义全局控制器增强(即全局异常处理和全局数据绑定)的注解。通过使用 @ControllerAdvice
,我们可以将异常处理和数据绑定逻辑集中到一个类中,避免在每个控制器中重复编写相同的异常处理代码。
@ResponseBody
是用于指示控制器方法返回的对象将被直接写入响应体中的注解。它告诉 Spring 将方法的返回值序列化为 JSON 或其他适当的响应格式,并将其作为 HTTP 响应的主体返回给客户端。
作用:
- 捕获代码中所有的异常,内部消化,让前端得到更详细的业务报错 / 信息
- 同时屏蔽掉项目框架本身的异常(不暴露服务器内部状态)
- 集中处理,比如记录日志
全局异常处理器切面类示例写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class) public BaseResponse<?> businessExceptionHandler(BusinessException e) { log.error("BusinessException", e); return ResultUtils.error(e.getCode(), e.getMessage()); }
@ExceptionHandler(RuntimeException.class) public BaseResponse<?> runtimeExceptionHandler(RuntimeException e) { log.error("RuntimeException", e); return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误"); } }
|
注意事项:
@RestControllerAdvice
并不能捕获所有异常,例如 Error 类的子类(如 OutOfMemoryError
)通常无法被捕获。
- 某些异常可能会被其他全局异常处理器或框架层面的异常处理机制捕获,而不会被
@RestControllerAdvice
处理。
Error
及其子类:Error
是 Throwable
的子类,表示严重的错误,通常由虚拟机抛出,如 OutOfMemoryError
、StackOverflowError
等。这些异常通常意味着应用程序处于不可恢复的状态
,因此无法被 @RestControllerAdvice
捕获。
ThreadDeath
:ThreadDeath
是 Error
的子类,它表示线程意外终止的异常。与其他 Error
一样,ThreadDeath
异常也无法被 @RestControllerAdvice
捕获。
VirtualMachineError
及其子类:VirtualMachineError
是 Error
的子类,表示与 Java 虚拟机相关的错误,如 InternalError
、UnknownError
等。这些错误通常与虚拟机的内部状态或配置有关,无法被 @RestControllerAdvice
捕获。
下面罗列一些上面全局异常处理器配套的类。
自定义异常类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) { super(message); this.code = code; }
public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage()); this.code = errorCode.getCode(); }
public BusinessException(ErrorCode errorCode, String message) { super(message); this.code = errorCode.getCode(); }
public int getCode() { return code; } }
|
自定义错误码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
public enum ErrorCode {
SUCCESS(0, "ok"), PARAMS_ERROR(40000, "请求参数错误"), NOT_LOGIN_ERROR(40100, "未登录"), NO_AUTH_ERROR(40101, "无权限"), NOT_FOUND_ERROR(40400, "请求数据不存在"), FORBIDDEN_ERROR(40300, "禁止访问"), SYSTEM_ERROR(50000, "系统内部异常"), OPERATION_ERROR(50001, "操作失败");
private final int code;
private final String message;
ErrorCode(int code, String message) { this.code = code; this.message = message; }
public int getCode() { return code; }
public String getMessage() { return message; }
}
|
返回工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
public class ResultUtils {
public static <T> BaseResponse<T> success(T data) { return new BaseResponse<>(0, data, "ok"); }
public static BaseResponse error(ErrorCode errorCode) { return new BaseResponse<>(errorCode); }
public static BaseResponse error(int code, String message) { return new BaseResponse(code, null, message); }
public static BaseResponse error(ErrorCode errorCode, String message) { return new BaseResponse(errorCode.getCode(), null, message); } }
|
抛异常工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
public class ThrowUtils {
public static void throwIf(boolean condition, RuntimeException runtimeException) { if (condition) { throw runtimeException; } }
public static void throwIf(boolean condition, ErrorCode errorCode) { throwIf(condition, new BusinessException(errorCode)); }
public static void throwIf(boolean condition, ErrorCode errorCode, String message) { throwIf(condition, new BusinessException(errorCode, message)); } }
|
本文参考