文章目录
- Spring AOP 是什么
- 什么是 AOP
- AOP 组成
- 切面(Aspect)
- 连接点(Join Point)
- 切点(Pointcut)
- 通知(Advice)
- 实现 Spring AOP
- 添加 Spring AOP 框架支持
- execution表达式
- 定义切面、切点、通知
- Spring AOP 的原理
- SpringBoot 统一功能处理
- 拦截器
- 统一异常处理
- 统一数据返回格式
Spring AOP 是什么
Spring AOP(面向切面编程)是 Spring 框架的一个模块,用于支持切面编程。
什么是 AOP
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它旨在通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,以提高代码的模块性和可维护性。横切关注点包括那些在应用程序中散布在多个地方、不属于特定模块的功能,例如日志记录、事务管理、安全性检查等。
想象一下你在写一个软件应用程序,而这个应用程序中有一些功能是所有模块都需要用到的,比如日志记录、性能监测或者权限控制。Spring AOP就像是一个魔法工具,它允许你在不改变你原有代码的情况下,把这些共同的功能从各个地方剥离出来,形成一个独立的“切面”(Aspect)。
这个切面就像是一个横切的面,横切到你整个应用程序中的某些点。比如,你可以告诉 Spring AOP:“每次有人调用这个方法前,先执行一下这个日志记录的功能。” Spring AOP 就会在运行时,把这个日志记录的功能“插”到那个方法调用的前面,而不需要你去手动修改方法里的代码。
所以,Spring AOP 就是一个让你在不乱改原有代码的情况下,往程序中添加一些共同功能的工具。
AOP 组成
切面(Aspect)
切面(Aspect)包含了切点(Pointcut)和通知(Advice),它既包含了横切逻辑的定义,也包括了连接点的定义。
切面是包含了:通知、切点和切面的类,相当于 AOP 实现的某个功能的集合
连接点(Join Point)
应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
连接点相当于需要被增强的某个 AOP 功能的所有方法
切点(Pointcut)
Pointcut 是匹配 Join Point 的谓词。
Pointcut 的作用是提供一组规则(使用 AspectJ pointcut expression language 来描述)来匹配 Join Point,给满足规则的 Join Point 添加 Advice
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)
通知(Advice)
切面也是有目标的——它必须完成的工作。在 AOP 术语中,切面的工作被称为通知。
通知定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的问题。Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
- 前置通知使用
@Before
:通知方法会在目标方法调用之前执行。 - 后置通知使用
@After
:通知方法会在目标方法返回或者抛出异常后调用 - 返回之后通知使用
@AfterReturning
:通知方法会在目标方法返回后调用 - 抛异常后通知使用
@AfterThrowing
:通知方法会在目标方法抛出异常后调用 - 环绕通知使用
@Around
:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
整个过程如图:
总结:
- 切面(Aspect):定义 AOP 业务类型的(当前 AOP 是做什么的)
- 连接点(Join Point):有可能调用 AOP 的地方
- 切点(Pointcut):定义 AOP 拦截规则
- 通知(Advice)[增强方法]:定义什么时候,干什么事。
- 前置通知:拦截的目标方法之前执行的通知(事件)
- 后置通知:拦截的目标方法之后执行的通知(事件)
- 返回之后通知:拦截的目标方法返回数据之后的通知
- 抛出异常之后的通知:拦截的目标方法抛出异常之后执行的通知
- 环绕通知:在拦截方法执行前后都执行的通知
实现 Spring AOP
添加 Spring AOP 框架支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
execution表达式
execution
是一种用于定义切点表达式的关键字。切点表达式决定了哪些方法将会被拦截并应用通知。
execution
表达式的基本语法如下:
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
modifiers-pattern
: 修饰符模式,表示方法的修饰符,如public
、private
、protected
等。return-type-pattern
: 返回类型模式,表示方法的返回类型。declaring-type-pattern
: 声明类型模式,表示方法所属的类。method-name-pattern
: 方法名模式,表示方法的名称。param-pattern
: 参数模式,表示方法的参数。throws-pattern
: 异常模式,表示方法可能抛出的异常。
通配符 *
可以用于匹配任意字符。
通配符 ..
(两个点),表示零个或多个元素的匹配。它可以用于表示任意数量的子包、任意数量的参数或任意数量的字符。
其中,带 ?
的非必需参数,不带 ?
的是必需参数,如 param-pattern
可以填 (..)
表示匹配任意参数列表的方法,不填,也就是 ()
,表示匹配无参的方法。不管你填不填,它都会发挥作用。而像 return-type-pattern
和 method-name-pattern
就都是必填的了。非必需参数被省略,就表示所有,相当于 *
execution
表达式示例:
execution(* com.example.service.*.*(..))
: 匹配com.example.service
包中所有类的所有方法。execution(* save*(..))
: 匹配所有以 “save” 开头的方法。execution(public * *(..))
: 匹配所有public
修饰的方法。execution(* com.example..*Service.*(..))
: 匹配com.example
包及其子包中所有类的以 “Service” 结尾的方法。
定义切面、切点、通知
@Component
@Aspect // 标识当前类为一个切面
public class LoginAOP {
// 定义切点(拦截的规则)
@Pointcut("execution(* org.example.mybatisdemo.controller.UserController.*(..))")
public void pointcut() {
}
// 前置通知
@Before("pointcut()")
public void before() {
System.out.println("执行了前置通知");
}
}
package org.example.mybatisdemo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getUsers")
public void getUsers() {
System.out.println("执行了 getUsers 方法");
}
}
使用浏览器访问:http://localhost:8080/user/getUsers
控制台输出的结果:
执行了前置通知
执行了 getUsers 方法
前置通知成功在 getUsers 方法前执行了
其他通知就是用的注解不一样,不多赘述
环绕通知的方法体比较特殊:
- 环绕通知使用
ProceedingJoinPoint
参数来执行目标方法,并且可以决定是否继续执行目标方法或者在执行前后进行其他操作。
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 在目标方法执行之前的逻辑
System.out.println("执行环绕通知的前置方法");
// 执行目标方法
Object result = joinPoint.proceed();
// 在目标方法执行之后的逻辑
System.out.println("执行环绕通知的后置方法");
return result;
}
结果:
执行环绕通知的前置方法
执行了前置通知
执行了 getUsers 方法
执行了后置通知
执行环绕通知的后置方法
- 环绕通知可以和前置通知后置通知同时存在,环绕通知的前后置方法会环绕在前后置通知的外侧。
Spring AOP 的原理
Spring AOP 是基于动态代理的实现。在运行时,Spring AOP 使用 JDK 动态代理或者 CGLIB(Code Generation Library)动态代理为目标对象生成代理对象,然后将切面织入代理对象中。这样,在调用目标对象的方法时,代理对象可以在适当的连接点上执行相关的通知逻辑。这种动态代理的方式实现了切面的透明织入,而不需要修改目标对象的源代码。
默认情况下,实现了接口的类,使用 AOP 会基于 JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类
织入(Weaving):代理的生成时机
织入指的是将切面的代码插入到应用程序的目标对象中的过程。
在AOP中,织入可以发生在以下三个阶段:
- 编译时织入(Compile-Time Weaving): 织入发生在源代码被编译成字节码的阶段。这需要特殊的编译器支持,例如使用 AspectJ编译器。在编译时织入时,切面的代码被直接编译到目标类的字节码中。
- 加载时织入(Load-Time Weaving): 织入发生在类加载的过程中。通过使用 Java 代理机制或者字节码增强技术,切面的代码被动态地织入到目标类中。在加载时织入时,目标类的字节码被增强,添加了切面的逻辑。
- 运行时织入(Runtime Weaving): 织入发生在应用程序运行的过程中。通过使用动态代理,切面的代码被动态地织入到目标对象中。Spring AOP通常采用运行时织入,使用JDK动态代理或CGLIB动态代理。
JDK 和 CGLIB 实现的区别
-
基于接口 vs. 基于类:
- JDK 动态代理: JDK 动态代理是基于接口的。它要求目标对象必须实现一个或多个接口,然后使用
java.lang.reflect.Proxy
类生成代理对象。代理对象实现了目标接口,并将方法调用委托给InvocationHandler
接口的实现类处理。 - CGLIB: CGLIB(Code Generation Library)是基于类的。它通过继承目标类,并生成目标类的子类,来创建代理对象。因此,对于没有实现接口的类,CGLIB 也能生成代理。
- JDK 动态代理: JDK 动态代理是基于接口的。它要求目标对象必须实现一个或多个接口,然后使用
-
代理对象生成方式:
- JDK 动态代理: 使用
java.lang.reflect.Proxy
类和InvocationHandler
接口。代理对象是在运行时动态生成的,基于接口的代理。 - CGLIB: 使用字节码生成库,通过修改字节码生成目标类的子类。代理对象是在运行时生成的,基于类的代理。
- JDK 动态代理: 使用
-
性能:
- JDK 动态代理: 由于基于接口,所以在代理的性能上通常比较高效。但仅支持对接口的代理,无法对类进行代理。
- CGLIB: 由于生成子类,性能可能相对较低。但支持对类的代理,包括没有实现接口的类。
-
使用场景:
- JDK 动态代理: 适用于要代理的类实现了接口的情况,更符合面向对象的设计原则。常用于 Spring AOP 的默认代理方式。
- CGLIB: 适用于要代理的类没有实现接口的情况,或者想要对类进行代理。常用于一些第三方库,如 Hibernate。
SpringBoot 统一功能处理
拦截器
拦截器(Interceptor)是一种用于在请求处理的不同阶段执行额外逻辑的组件。拦截器可以用于拦截和处理 Spring MVC 中的 Web 请求,也可以用于拦截方法的执行,实现 AOP(Aspect-Oriented Programming)的一部分。
创建拦截器:
接口定义: Spring 的拦截器接口是 HandlerInterceptor
,其中包含了三个方法,分别表示在请求处理的不同阶段执行的逻辑:
preHandle(request, response, handler)
: 在处理请求之前被调用,返回值决定是否继续执行后续的处理器(Controller)。postHandle(request, response, handler, modelAndView)
: 在处理请求后、视图渲染之前被调用,可以对ModelAndView进行修改。afterCompletion(request, response, handler, ex)
: 在整个请求处理完成后被调用,通常用于资源清理工作。
package org.example.springbootaop.config;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
@Configuration
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登录判断业务
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
return true;
}
response.setStatus(401);
return false;
}
}
配置拦截器:要使用拦截器,需要通过 InterceptorRegistry
配置类配置拦截器。指定在什么样的url下执行拦截器的方法
例:
package org.example.springbootaop.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Autowired
LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 拦截所有请求
.excludePathPatterns("/user/login")// 排除不拦截的 url
.excludePathPatterns("/user/reg"); // 排除不拦截的 url
}
}
访问 http://localhost:8080/user/getInfo,成功拦截并返回 401 状态码
统一异常处理
统一异常处理是一种通过一个中心化的机制来处理应用程序中发生的异常的方法。
使用 @ControllerAdvice
+ @ExceptionHandler
来实现
@ControllerAdvice:
@ControllerAdvice
是一个类级别的注解,用于定义一个全局控制器通知。- 通过
@ControllerAdvice
注解的类可以包含用于处理全局异常、全局数据绑定、全局数据预处理等的方法。
@ExceptionHandler:
@ExceptionHandler
是一个方法级别的注解,用于标识一个方法用于处理特定类型的异常。- 这个方法通常在
@ControllerAdvice
注解的类中定义,用于处理全局性的异常。
package org.example.springbootaop.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler(Exception.class)
@ResponseBody
public HashMap<String, Object> exAdvice(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", "-1");
result.put("msg", e.getMessage());
return result;
}
}
使用@ControllerAdvice
注解的类可以处理整个应用程序中所有@RequestMapping
方法抛出的异常。
在上述代码中,exAdvice
方法用于处理异常,并返回一个包含的错误信息。这个信息会显示到浏览器,以便用户能够得知发生了异常。
@ExceptionHandler(Exception.class)
表示处理所有异常。
统一数据返回格式
实现 ResponseBodyAdvice
接口来对 Controller 方法的返回值进行处理。
package org.example.springbootaop.config;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true; // 在所有 Controller 的方法返回前进行拦截
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "");
result.put("data", body);
return result;
}
}
这样,无论 Controller 的方法返回什么类型的数据,都会在最终返回给客户端之前经过 beforeBodyWrite
方法的处理,将其封装成统一的格式。