文章目录
- 1. 什么是AOP
- 2. AOP的几个核心概念
- 3. AOP的常用场景
- 4. 使用AOP记录操作日志
- 4.1 准备工作
- 4.1.1 引入Maven依赖
- 4.1.2 UserController.java
- 4.1.3 User.java
- 4.1.4 UserService.java
- 4.2 具体实现(以根据id查询用户信息为例)
- 4.2.1 定义切面类(切入点和环绕增强)
- 4.2.2 自定义注解
- 4.2.3 为方法添加自定义注解
- 4.3 测试
- 5. Spring中的事务是如何实现的
1. 什么是AOP
AOP(Aspect Oriented Programming),面向切面编程
AOP 主要的功能是将 与业务无关,但却对多个对象产生影响的公共行为或逻辑
抽取并封装为一个可重用的模块,这个可重用的模块称为切面(Aspect)
AOP 能够减少系统中的重复代码,降低模块间的耦合度,同时提高系统的可维护性
如果想了解 Spring 事务失效的情况,可以参考我的另一篇博文:Spring中事务失效的常见场景及解决方法
2. AOP的几个核心概念
AOP 的核心概念主要包括以下几个:
- 切面(Aspect):
- 切面是AOP中的一个核心概念,它代表了一个横切关注点(cross-cutting concern),即将多个模块中共有的行为抽象出来形成的一个独立模块。在Spring AOP中,切面通常是通过使用
@Aspect
注解的类来实现的
- 切面是AOP中的一个核心概念,它代表了一个横切关注点(cross-cutting concern),即将多个模块中共有的行为抽象出来形成的一个独立模块。在Spring AOP中,切面通常是通过使用
- 连接点(Join Point):
- 连接点是在程序执行过程中的一个特定点,例如方法的调用、异常的抛出等。在Spring AOP中,只支持方法的连接点
- 切入点(Pointcut):
- 切入点是一组连接点的定义,它定义了哪些连接点会被切面所拦截。通常使用正则表达式或者特定的表达式语言来指定哪些方法会被拦截
- 通知(Advice):
- 通知定义了切面在特定的连接点上要执行的动作。通知有多种类型:
- 前置通知(Before):在连接点之前执行
- 后置通知(After):在连接点之后执行,无论方法是否正常结束
- 返回通知(After Returning):在连接点正常返回后执行
- 异常通知(After Throwing):在连接点抛出异常后执行
- 环绕通知(Around):包围一个连接点的通知,可以在方法调用前后执行自定义的行为
- 通知定义了切面在特定的连接点上要执行的动作。通知有多种类型:
- 目标对象(Target Object):
- 目标对象是指被一个或多个切面所通知的对象。在Spring AOP中,目标对象通常是Spring容器中的Bean
- 代理(Proxy):
- AOP通过代理模式来实现对目标对象的增强。代理对象会在运行时创建,并用来代替目标对象。当调用代理对象的方法时,代理会根据切面的配置来执行相应的通知
- 织入(Weaving):
- 织入是将切面的通知应用到目标对象并创建新的代理对象的过程。这个过程可以在编译时、类加载时或运行时进行
3. AOP的常用场景
- 记录操作日志:记录日志属于公共的行为,每一个 Service 都需要记录操作日志,直接在每一个 Service 里面编写记录日志的代码不太合适,使用 AOP 就可以很方便地完成记录日志操作
- 处理缓存:如果某些业务要添加缓存,直接写在 Service 层会造成代码耦合的情况,我们可以利用 AOP 的切面,拦截需要添加缓存的业务方法,为业务方法添加缓存
- Spring 中内置的事务处理
4. 使用AOP记录操作日志
4.1 准备工作
4.1.1 引入Maven依赖
在 SpringBoot 项目的 pom.xml 文件中引入 aspectjweaver 的 Maven 依赖
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>
4.1.2 UserController.java
import cn.edu.scau.aop.pojo.User;
import cn.edu.scau.aop.service.UserService;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/user")
public class UserController {private final UserService userService;public UserController(UserService userService) {this.userService = userService;}@GetMapping("/getById/{id}")public User getById(@PathVariable("id") Integer id) {return userService.getById(id);}@PostMapping("/save")public void save(@RequestBody User user) {userService.save(user);}@PutMapping("/update")public void update(User user) {userService.update(user);}@DeleteMapping("/delete/{id}")public void delete(@PathVariable("id") Integer id) {userService.delete(id);}}
4.1.3 User.java
public class User {private Integer id;private String name;private Integer age;private Short sex;private Short status;private String image;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}public Short getSex() {return sex;}public void setSex(Short sex) {this.sex = sex;}public Short getStatus() {return status;}public void setStatus(Short status) {this.status = status;}public String getImage() {return image;}public void setImage(String image) {this.image = image;}@Overridepublic String toString() {return "User{" +"id=" + id +", name='" + name + '\'' +", age=" + age +", sex=" + sex +", status=" + status +", image='" + image + '\'' +'}';}}
4.1.4 UserService.java
import cn.edu.scau.aop.pojo.User;
import org.springframework.stereotype.Service;@Service
public class UserService {public User getById(Integer id) {return new User();}public void save(User user) {System.out.println("保存用户信息");}public void update(User user) {System.out.println("更新用户信息");}public void delete(Integer id) {System.out.println("删除用户信息");}}
4.2 具体实现(以根据id查询用户信息为例)
在我们的开发过程中,大多都有记录操作日志的需求
当用户访问某一个接口时,我们需要记录发起请求的用户是谁,请求的方式是什么,访问地址是什么,访问了哪一个模块,登录的 IP 地址,操作时间等
接下来我们分析一下利用 AOP 记录操作日志的具体实现方式
假如后台有四个请求的接口:登录、新增用户、更新用户、删除用户
我们以查询用户为例,利用 AOP 提供的环绕通知做一个切面
4.2.1 定义切面类(切入点和环绕增强)
用 @Aspect 注解表名当前类是一个切面类,而且切面类需要交由 Spring 管理
import cn.edu.scau.aop.annotation.Log;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import java.lang.reflect.Method;
import java.util.Date;/*** 切面类*/
@Component
@Aspect
public class SystemAspect {@Pointcut("@annotation(cn.edu.scau.aop.annotation.Log)")private void pointcut() {}@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {// 通过解析 session 或 token 获取用户名// 获取被增强类和方法的信息Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;// 获取被增强的方法对象Method method = methodSignature.getMethod();// 从方法中解析注解if (method != null) {Log logAnnotation = method.getAnnotation(Log.class);System.out.println(logAnnotation.name());}// 方法名字String name = null;if (method != null) {name = method.getName();}System.out.println("方法名:" + name);// 通过工具类获取Request对象RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;HttpServletRequest request = null;if (servletRequestAttributes != null) {request = servletRequestAttributes.getRequest();}// 访问的URLString url = null;if (request != null) {url = request.getRequestURI();}System.out.println("访问的URL:" + url);// 请求方式String methodName = null;if (request != null) {methodName = request.getMethod();}System.out.println("请求方式:" + methodName);// 登录IPString ipAddress = null;if (request != null) {ipAddress = getIpAddress(request);}System.out.println("登录IP:" + ipAddress);// 操作时间System.out.println("操作时间:" + new Date());// 将操作日志保存到数据库return joinPoint.proceed();}/*** 获取 IP 地址** @param request HttpServletRequest* @return String*/public String getIpAddress(HttpServletRequest request) {String ip = request.getHeader("x-forwarded-for");if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;}}
切面类中有一个切点表达式
这个切点表达式找的是一个注解,也就是说,如果某个方法上添加了 Log 注解,进化就会进入到环绕通知中进行增强
环绕通知增强如下
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {// 通过解析 session 或 token 获取用户名// 获取被增强类和方法的信息Signature signature = joinPoint.getSignature();MethodSignature methodSignature = (MethodSignature) signature;// 获取被增强的方法对象Method method = methodSignature.getMethod();// 从方法中解析注解if (method != null) {Log logAnnotation = method.getAnnotation(Log.class);System.out.println("模块名称:" + logAnnotation.name());}// 方法名字String name = null;if (method != null) {name = method.getName();}System.out.println("方法名:" + name);// 通过工具类获取Request对象RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;HttpServletRequest request = null;if (servletRequestAttributes != null) {request = servletRequestAttributes.getRequest();}// 访问的URLString url = null;if (request != null) {url = request.getRequestURI();}System.out.println("访问的URL:" + url);// 请求方式String methodName = null;if (request != null) {methodName = request.getMethod();}System.out.println("请求方式:" + methodName);// 登录IPString ipAddress = null;if (request != null) {ipAddress = getIpAddress(request);}System.out.println("登录IP:" + ipAddress);// 操作时间System.out.println("操作时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));// 将操作日志保存到数据库return joinPoint.proceed();
}/*** 获取 IP 地址** @param request HttpServletRequest* @return String*/
public String getIpAddress(HttpServletRequest request) {String ip = request.getHeader("x-forwarded-for");if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}
4.2.2 自定义注解
那 Log 注解是从哪里来的呢,其实是我们自定义的,注解中的 name 属性是模块的名称
import java.lang.annotation.*;@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {/*** 模块名称*/String name() default "";}
4.2.3 为方法添加自定义注解
我们在需要记录操作日志的方法上添加自定义注解
4.3 测试
启动项目后,我们在浏览器输入以下网址访问接口
http://localhost:8080/user/getById/1
查看控制台,发现操作日志已成功打印操作日志
5. Spring中的事务是如何实现的
Spring支持编程式事务管理和声明式事务管理两种方式
- 编程式事务控制:需使用 TransactionTemplate 来进行实现,对业务代码有侵入性,项目中很少使用
- 声明式事务管理:声明式事务管理建立在 AOP 之上,本质是通过 AOP 对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务
声明式事务的示意图
joinPoint.proceed
是真正要执行的目标对象的方法,在方法执行前开启事务,方法成功执行之后提交事务
如果方法在执行的过程中出错了,需要回滚事务,在 catch 代码块中会有一个回滚事务的操作