SpringBoot最佳实践之 - 使用AOP记录操作日志
1. 前言
本篇博客是个人在工作中遇到的需求。针对此需求,开发了具体的实现代码。并不是普适的记录操作日志的方式。以阅读本篇博客的朋友,可以参考此篇博客中记录日志的方式,可能会对你有些许帮助和启发。
2. 需求描述
有一个后台管理系统,此系统具有不同角色的用户,比如管理员、操作员、审计员等。当这些角色的用户登录到系统中,以及其在系统中所触发的 <增删改> 操作。我都想记录操作日志。然后存储到数据库中。比如记录如下:
数据库中有了数据,就可以在查询出来显示到页面上。对于一个业务敏感的后台管理系统来说,就可以通过这里查看哪些用户操作了什么功能。操作的结果是成功还是失败,如果操作失败,失败的原因是什么。如下:
3. 需求实现
3.1 准备工作
3.1.1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
3.1.2 数据库脚本
用户表 t_user
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`user_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
`phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
`email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`status` tinyint(4) NULL DEFAULT 0 COMMENT '用户状态(0:可用;1:禁用)',
`delete_flag` tinyint(4) NULL DEFAULT NULL COMMENT '删除标记(0:未删除;1:已删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, '张三', '123456', '18178526349', '123@qq.com', '2024-10-29 08:42:34', '2024-10-29 08:42:37', 0, 0);
SET FOREIGN_KEY_CHECKS = 1;
操作日志表 t_system_log
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_system_log
-- ----------------------------
DROP TABLE IF EXISTS `t_system_log`;
CREATE TABLE `t_system_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`operate_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '触发的动作',
`operate_user_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作用户名',
`operate_time` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作时间',
`operate_result` tinyint(4) NULL DEFAULT NULL COMMENT '0成功/1失败',
`operate_fail_reason` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '操作失败原因',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 800 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_system_log
-- ----------------------------
INSERT INTO `t_system_log` VALUES (792, '登录', '张三', '2024-10-29 10:06:09', 0, NULL);
INSERT INTO `t_system_log` VALUES (793, '登录', '张三', '2024-10-29 10:07:13', 1, '用户名或密码错误');
INSERT INTO `t_system_log` VALUES (794, '登录', '张三', '2024-10-29 10:09:22', 1, '用户名或密码错误');
INSERT INTO `t_system_log` VALUES (795, '登录', '张三', '2024-10-29 10:11:31', 1, '用户名或密码错误');
INSERT INTO `t_system_log` VALUES (796, '添加商品', '张三', '2024-10-29 10:19:11', 0, NULL);
INSERT INTO `t_system_log` VALUES (797, '添加商品', '张三', '2024-10-29 10:19:32', 1, '商品已存在');
INSERT INTO `t_system_log` VALUES (798, '下架商品', '张三', '2024-10-29 10:41:58', 0, NULL);
INSERT INTO `t_system_log` VALUES (799, '下架商品', '张三', '2024-10-29 10:42:22', 1, '商品正在发货中,无法下架');
SET FOREIGN_KEY_CHECKS = 1;
3.2 需要的组件说明
1)自定义注解 @Operation:把自定义注解标注在Controller方法上,后续通过切面识别Controller方法上标注的注解,以及注解的value值,从而实现记录操作日志功能;
2)切面类 LogAspect: 识别标注有@Operation注解的Controller方法,在方法执行过程中进行切面操作;
3)日志实体类 SystemLog:记录日志,对应的实体类,需要把记录的信息保存到数据库中;
4) 用户实体类 User: 用户实体类;
5)业务异常类:自定义的异常类;
6)统一错误码枚举类:自定义的错误码枚举类,把项目中出现的错误码统一存放在此处,便于管理;
3.3 组件代码
3.3.1 自定义注解 @Operation
package com.shg.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Operation {
String value();
}
3.3.2 切面类 LogAspect
package com.shg.aspect;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.shg.annotation.Operation;
import com.shg.model.pojo.SystemLog;
import com.shg.model.pojo.User;
import com.shg.service.RecordLogService;
import com.shg.service.UserService;
import org.aspectj.lang.ProceedingJoinPoint;
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.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Objects;
@Component
@Aspect
public class LogAspect {
private final UserService userService;
private final RecordLogService recordLogService;
public LogAspect(UserService userService, RecordLogService recordLogService) {
this.userService = userService;
this.recordLogService = recordLogService;
}
@Pointcut(value = "@annotation(com.shg.annotation.Operation)")
private void pointCut() {
}
@Around(value = "pointCut()")
public Object recordLog(ProceedingJoinPoint pjp) throws Throwable {
// 拿到请求对象Request
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
// 通过request获取请求头中的登录用户[此处是模拟直接在请求头中携带一个用户id,真实开发是在请求头中携带一个token,然后通过token去redis中查询用户信息,包括用户权限信息等]
String userId = request.getHeader("userId");
// 通过userId 去数据库中查询用户信息
User userFromDB = userService.getById(userId);
// 拿到方法上标注的自定义注解的value值,这样就可以知道当前这个用户是在做什么操作了
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
Operation annotation = method.getAnnotation(Operation.class);
String value;
Object result = null;
if (!Objects.isNull(annotation)) {
value = annotation.value();
// 当你在某个方法上标注了 @Operation自定义注解,并且给这个注解的value进行合法赋值后,才记录日志(比如增删改操作),而对于查询方法,一般不需要在Controller方法上标注@Operation注解
if (StrUtil.isNotBlank(value)) {
SystemLog systemLog = new SystemLog();
systemLog.setOperateName(value);
systemLog.setOperateUserName(userFromDB.getUserName());
systemLog.setOperateTime(DateUtil.formatDateTime(new Date()));
try {
result = pjp.proceed();
systemLog.setOperateResult(0);
recordLogService.save(systemLog);
} catch (Exception e) {
systemLog.setOperateResult(1);
systemLog.setOperateFailReason(e.getMessage());
recordLogService.save(systemLog);
throw e;
} finally {
System.out.println("finally...");
}
}
}
return result;
}
}
3.3.3 日志实体类
package com.shg.model.pojo;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@TableName("t_system_log")
public class SystemLog implements Serializable {
private Integer id;
private String operateName;
private String operateUserName;
private String operateTime;
private String operateFailReason;
/**
* 0成功/1失败
*/
@ApiModelProperty("0成功/1失败")
private Integer operateResult;
}
3.3.4 用户实体类
package com.shg.model.pojo;
import java.time.LocalDateTime;
import java.util.Date;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_user")
public class User implements Serializable {
private static final long serialVersionUID = -45223488720491550L;
/**
* 自增主键
*/
@TableId
private Integer id;
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
private String password;
/**
* 手机号
*/
private String phone;
/**
* 邮箱
*/
private String email;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 修改时间
*/
private LocalDateTime updateTime;
/**
* 用户状态(0:可用;1:禁用)
*/
private Integer status;
/**
* 删除标记(0:未删除;1:已删除)
*/
private Integer deleteFlag;
}
3.3.5 业务异常类
package com.shg.exception;
import com.shg.common.ResponseCodeEnum;
import lombok.Data;
@Data
public class BizException extends RuntimeException{
private Integer code;
private String message;
public BizException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
public BizException(ResponseCodeEnum responseCodeEnum) {
super(responseCodeEnum.getMessage());
this.code = responseCodeEnum.getCode();
this.message = responseCodeEnum.getMessage();
}
}
3.3.6 统一错误码枚举类
package com.shg.common;
public enum ResponseCodeEnum {
SUCCESS(0, "success"),
SYSTEM_EXCEPTION(500, "System internal exception"),
USERNAME_OR_PASSWORD_FAIL(1001, "用户名或密码错误"),
USER_NOT_EXISTS(1002,"用户不存在"),
GOODS_ID_EXISTS(2001, "商品已存在"),
DELETE_GOODS_FAIL(2001, "商品正在发货中,无法下架");
private final int code;
private final String message;
ResponseCodeEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
3.2.7 Controller类
package com.shg.controller;
import com.shg.annotation.Operation;
import com.shg.common.ResponseCodeEnum;
import com.shg.common.ResultMessage;
import com.shg.exception.BizException;
import com.shg.model.pojo.User;
import com.shg.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private UserService userService;
@GetMapping("/test1")
public ResultMessage<String> test1() {
return ResultMessage.success("这是测试接口...");
}
@Operation(value = "登录")
@GetMapping(value = "/login")
public ResultMessage login(Integer id) {
User user = userService.getById(1);
if (user.getId() == 1) {
throw new BizException(ResponseCodeEnum.USERNAME_OR_PASSWORD_FAIL);
}
return ResultMessage.success("登录成功", user);
}
@Operation(value = "添加商品")
@PostMapping(value = "/addGoods")
public ResultMessage addGoods(@RequestParam Integer goodsId) {
if (goodsId == 2) {
throw new BizException(ResponseCodeEnum.GOODS_ID_EXISTS);
}
return ResultMessage.success("商品添加成功", "模拟添加商品成功");
}
@Operation(value = "下架商品")
@PostMapping(value = "/deleteGoods")
public ResultMessage deleteGoods(@RequestParam Integer goodsId) {
if (goodsId == 4) {
throw new BizException(ResponseCodeEnum.DELETE_GOODS_FAIL);
}
return ResultMessage.success("商品下架成功", "模拟商品下架成功");
}
}
5. 其他
具体代码示例参考:springboot-best-practice: 初次提交
如果此篇文章对你有帮助,感谢点个赞~~