简介

在做博客项目的时候需要记录用户操作日志,这时候需要自定义注解与切面来实现。

步骤

1、依赖

需要引入相应依赖,这里肯定时能和数据库关联了(简单的来说就是能够在浏览器上进行增删改查)。

1
2
3
4
5
<!-- aop的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2、自定义日志注解

定义一个方法级别的@Log注解,用于标注需要监控的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 定义一个Log的日志注解,用在方法上
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}

配置 AOP 切面

在配置 AOP 切面之前,我们需要了解下 aspectj 相关注解的作用:

  • @Aspect:声明该类为一个注解类;
  • @Pointcut:定义一个切点,后面跟随一个表达式,表达式可以定义为切某个注解,也可以切某个 package 下的方法;

切点定义好后,就是围绕这个切点做文章了:

  • @Before: 在切点之前,织入相关代码;
  • @After: 在切点之后,织入相关代码;
  • @AfterReturning: 在切点返回内容后,织入相关代码,一般用于对返回值做些加工处理的场景;
  • @AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理;
  • @Around: 环绕,可以在切入点前后织入代码,并且可以自由的控制何时执行切点;

image.png

3、创建库表和实体

在数据库中创建一张sys_operation_log表,用于保存用户的操作日志

a. 数据库

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
/*
Navicat Premium Data Transfer

Source Server : test
Source Server Type : MySQL
Source Server Version : 80020
Source Host : 127.0.0.1:3306
Source Schema : blog

Target Server Type : MySQL
Target Server Version : 80020
File Encoding : 65001

Date: 09/11/2021 14:09:47
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_operation_log
-- ----------------------------
DROP TABLE IF EXISTS `tb_operation_log`;
CREATE TABLE `tb_operation_log` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键id',
`opt_module` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作模块',
`opt_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作类型',
`opt_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作url',
`opt_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作方法',
`opt_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作描述',
`request_param` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '请求参数',
`request_method` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '请求方式',
`response_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '返回数据',
`user_id` int NOT NULL COMMENT '用户id',
`nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户昵称',
`ip_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作ip',
`ip_source` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作地址',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 690 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of tb_operation_log
-- ----------------------------
INSERT INTO `tb_operation_log` VALUES (688, '角色模块', '新增或修改', '/admin/role', 'com.minzheng.blog.controller.RoleController.saveOrUpdateRole', '保存或更新角色', '[{\"id\":1,\"resourceIdList\":[165,192,193,194,195,166,183,184,246,247,167,199,200,201,168,185,186,187,188,189,190,191,254,169,208,209,170,234,235,236,237,171,213,214,215,216,217,224,172,240,241,244,245,267,269,270,173,239,242,276,174,205,206,207,175,218,219,220,221,222,223,176,202,203,204,230,238,177,229,232,233,243,178,196,197,198,257,258,179,225,226,227,228,231,180,210,211,212],\"roleLabel\":\"admin\",\"roleName\":\"管理员\"}]', 'POST', '{\"code\":20000,\"flag\":true,\"message\":\"操作成功\"}', 1, '管理员', '127.0.0.1', '', '2021-08-24 11:25:33', NULL);
INSERT INTO `tb_operation_log` VALUES (689, '角色模块', '新增或修改', '/admin/role', 'com.minzheng.blog.controller.RoleController.saveOrUpdateRole', '保存或更新角色', '[{\"id\":3,\"resourceIdList\":[192,195,183,246,199,185,191,254,208,234,237,213,241,239,276,205,218,223,202,230,238,232,243,196,257,258,225,231,210],\"roleLabel\":\"test\",\"roleName\":\"测试\"}]', 'POST', '{\"code\":20000,\"flag\":true,\"message\":\"操作成功\"}', 1, '管理员', '127.0.0.1', '', '2021-08-24 11:25:40', NULL);
INSERT INTO `tb_operation_log` VALUES (690, '文章模块', '新增或修改', '/admin/articles', 'com.minzheng.blog.controller.ArticleController.saveOrUpdateArticle', '添加或修改文章', '[{\"articleContent\":\"## 目录\\n\\n恭喜你已成功运行博客,开启你的文章之旅吧\",\"articleCover\":\"https://www.static.talkxj.com/articles/bd74062266c1fb04f3084968231c0580.jpg\",\"articleTitle\":\"测试文章\",\"categoryName\":\"测试分类\",\"id\":47,\"isTop\":0,\"originalUrl\":\"\",\"status\":1,\"tagNameList\":[],\"type\":1}]', 'POST', '{\"code\":20000,\"flag\":true,\"message\":\"操作成功\"}', 1, '管理员', '127.0.0.1', '', '2021-10-04 16:08:02', NULL);

SET FOREIGN_KEY_CHECKS = 1;

b. 实体类

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
52
53
54
55
56
57
58
59
60
61
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@TableName("tb_operation_log")
@ApiModel(value = "OperationLog对象", description = "")
public class OperationLog implements Serializable {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "主键id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;

@ApiModelProperty(value = "操作模块")
private String optModule;

@ApiModelProperty(value = "操作类型")
private String optType;

@ApiModelProperty(value = "操作url")
private String optUrl;

@ApiModelProperty(value = "操作方法")
private String optMethod;

@ApiModelProperty(value = "操作描述")
private String optDesc;

@ApiModelProperty(value = "请求参数")
private String requestParam;

@ApiModelProperty(value = "请求方式")
private String requestMethod;

@ApiModelProperty(value = "返回数据")
private String responseData;

@ApiModelProperty(value = "用户id")
private Integer userId;

@ApiModelProperty(value = "用户昵称")
private String nickname;

@ApiModelProperty(value = "操作ip")
private String ipAddress;

@ApiModelProperty(value = "操作地址")
private String ipSource;

@ApiModelProperty(value = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@ApiModelProperty(value = "更新时间")
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;


}

4、切面和切点

定义一个 LogAspect 类,使用@Aspect标注让其成为一个切面,切点为使用@OptLog注解标注的方法,使用@AfterReturning后置增强:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package com.dzgu.myblog.aspect;

import com.alibaba.fastjson.JSON;
import com.dzgu.myblog.annotation.OptLog;
import com.dzgu.myblog.entity.OperationLog;
import com.dzgu.myblog.mapper.OperationLogMapper;
import com.dzgu.myblog.utils.IpUtils;
import com.dzgu.myblog.utils.UserUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;

/**
* @description: 操作日志切面处理
* @Author: dzgu
* @Date: 2021/11/8 23:22
*/
@Component
@Aspect
public class OptLogAspect {
@Autowired
private OperationLogMapper operationLogMapper;

/**
* 设置操作日志切入点 记录操作日志 在注解的位置切入代码
*/
@Pointcut("@annotation(com.dzgu.myblog.annotation.OptLog)")
public void optLogPoincut() {
}

/**
* 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
* AfterReturning value可以是切点表达式execution 也可以是定义好的切入点。。。
*
* @param joinPoint 切入点,
* @param keys 返回结果
*/
@AfterReturning(value = "optLogPoincut()", returning = "keys")
public void saveOptLog(JoinPoint joinPoint, Object keys) {
// 获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 从获取RequestAttributes中获取HttpServletRequest的信息
HttpServletRequest request = (HttpServletRequest) Objects.requireNonNull(requestAttributes).resolveReference(RequestAttributes.REFERENCE_REQUEST);
OperationLog operationLog = new OperationLog();
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取操作
Api api = (Api) signature.getDeclaringType().getAnnotation(Api.class);
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
OptLog optLog = method.getAnnotation(OptLog.class);
// 操作模块
operationLog.setOptModule(api.tags()[0]);
// 操作类型
operationLog.setOptType(optLog.optType());
// 操作描述
operationLog.setOptDesc(apiOperation.value());
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName = method.getName();
methodName = className + "." + methodName;
// 请求方式
operationLog.setRequestMethod(Objects.requireNonNull(request).getMethod());
// 请求方法
operationLog.setOptMethod(methodName);
// 请求参数
operationLog.setRequestParam(JSON.toJSONString(joinPoint.getArgs()));
// 返回结果
operationLog.setResponseData(JSON.toJSONString(keys));
// 请求用户ID
operationLog.setUserId(UserUtils.getLoginUser().getId());
// 请求用户
operationLog.setNickname(UserUtils.getLoginUser().getNickname());
// 请求IP
String ipAddress = IpUtils.getIpAddress(request);
operationLog.setIpAddress(ipAddress);
operationLog.setIpSource(IpUtils.getIpSource(ipAddress));
// 请求URL
operationLog.setOptUrl(request.getRequestURI());
operationLogMapper.insert(operationLog);
}


}

5、测试

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
package com.dzgu.myblog.controller;

import com.dzgu.myblog.annotation.OptLog;
import com.dzgu.myblog.service.ArticleService;
import com.dzgu.myblog.vo.ApiResponse;
import com.dzgu.myblog.vo.ArticleVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

import static com.dzgu.myblog.constant.OptTypeConst.SAVE_OR_UPDATE;

/**
* @description: 文章
* @Author: dzgu
* @Date: 2021/11/8 23:09
*/
@Api(tags = "文章模块")
@RestController
public class ArticleController {
@Autowired
private ArticleService articleService;

/**
* 添加或修改文章
*
* @param articleVO 文章信息
* @return {@link ApiResponse<>}
*/
@OptLog(optType = SAVE_OR_UPDATE)
@ApiOperation(value = "添加或修改文章")
@PostMapping("/admin/articles")
public ApiResponse<?> saveOrUpdataArticle(@Valid @RequestBody ArticleVO articleVO){
articleService.saveOrUpdateArticle(articleVO);
return ApiResponse.ok();
}
}

可以存到数据库
image.png