一、添加依赖

在Spring框架中,使用AOP配合自定义注解可以方便的实现用户操作的监控。首先搭建一个基本的Spring Boot Web环境开启Spring Boot,然后引入必要依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

二、自定义注解

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

1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}

三、创建库表和实体

3.1 建表

在数据库中创建一张sys_log表,用于保存用户的操作日志,数据库采用mysql5.7.23

1
2
3
4
5
6
7
8
9
10
11
12
drop table if exists `sys_log`;
create table `sys_log` (
`id` int(20) not null auto_increment comment 'id',
`username` varchar(50) character set utf8 collate utf8_general_ci null comment '用户名',
`operation` varchar(50) character set utf8 collate utf8_general_ci null comment '用户操作',
`time` int(11) null comment '响应时间',
`method` varchar(200) character set utf8 collate utf8_general_ci null comment '请求方法',
`params` varchar(500) character set utf8 collate utf8_general_ci null comment '请求参数',
`ip` varchar(64) character set utf8 collate utf8_general_ci null comment 'ip地址',
`create_time` DATETIME null comment '创建时间',
primary key (`id`) using btree
) engine = innodb auto_increment = 1 character set = utf8 collate = utf8_general_ci row_format = dynamic;

3.2 创建实体

库表对应的实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Getter
@Setter
public class SysLog implements Serializable {
private static final long serialVersionUID = -6309732882044872298L;

private Integer id;
private String username;
private String operation;
private Integer time;
private String method;
private String params;
private String ip;

@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}

四、保存日志的方法

为了方便,这里直接使用Spring JdbcTemplate来操作数据库。定义一个SysLogDao接口,包含一个保存操作日志的抽象方法:

1
2
3
public interface SysLogDao {
void saveSysLog(SysLog syslog);
}

其实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Repository
public class SysLogDaoImpl implements SysLogDao {
@Autowired
private JdbcTemplate jdbcTemplate;

@Override
public void saveSysLog(SysLog syslog) {
StringBuffer sql = new StringBuffer("insert into sys_log ");
sql.append("(username,operation,time,method,params,ip,create_time) ");
sql.append("values(:username,:operation,:time,:method,");
sql.append(":params,:ip,:createTime)");

NamedParameterJdbcTemplate npjt = new NamedParameterJdbcTemplate(this.jdbcTemplate.getDataSource());
npjt.update(sql.toString(), new BeanPropertySqlParameterSource(syslog));
}
}

五、切面和切点

定义一个LogAspect类,使用@Aspect标注让其成为一个切面,切点为使用@Log注解标注的方法,使用@Around环绕通知:

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
@Aspect
@Component
public class LogAspect {
@Autowired
private SysLogDao sysLogDao;

@Pointcut("@annotation(com.wno704.boot.aspect.Log)")
public void pointcut() { }

@Around("pointcut()")
public Object around(ProceedingJoinPoint point) {
Object result = null;
long beginTime = System.currentTimeMillis();
try {
// 执行方法
result = point.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
// 保存日志
saveLog(point, time);
return result;
}

private void saveLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLog sysLog = new SysLog();
Log logAnnotation = method.getAnnotation(Log.class);
if (logAnnotation != null) {
// 注解上的描述
sysLog.setOperation(logAnnotation.value());
}
// 请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLog.setMethod(className + "." + methodName + "()");
// 请求的方法参数值
Object[] args = joinPoint.getArgs();
// 请求的方法参数名称
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paramNames = u.getParameterNames(method);
if (args != null && paramNames != null) {
String params = "";
for (int i = 0; i < args.length; i++) {
params += " " + paramNames[i] + ": " + args[i];
}
sysLog.setParams(params);
}
// 获取request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 设置IP地址
sysLog.setIp(IPUtils.getIpAddr(request));
// 模拟一个用户名
sysLog.setUsername("mrbird");
sysLog.setTime((int) time);
sysLog.setCreateTime(new Date());
// 保存系统日志
sysLogDao.saveSysLog(sysLog);
}
}

六、测试

TestController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class TestController {
@Log("执行方法一")
@GetMapping("/one")
public void methodOne(String name) { }

@Log("执行方法二")
@GetMapping("/two")
public void methodTwo() throws InterruptedException {
Thread.sleep(2000);
}

@Log("执行方法三")
@GetMapping("/three")
public void methodThree(String name, String age) { }
}

最终项目目录如下图所示:

启动项目,分别访问:
http://localhost:8080/web/one?name=wno704
http://localhost:8080/web/two
http://localhost:8080/web/three?name=wno704&age=28

查询数据库: