SpringBootAOP记录用户操作日志
github地址:springboot-learn
Spring框架中,使用AOP的场景是非常多的,本文就是自定义注解搭配AOP来实现用户操作监控。
环境搭建
SpringBoot
自己搭建SpringBoot
POM依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- aop依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
自定义注解
定义一个方法级别的@Log
注解,用于标注需要监控的方法:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
@Target
@Target注解,是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的。它使用一个枚举类型定义如下:
public enum ElementType {
/** 类,接口(包括注解类型)或枚举的声明 */
TYPE,
/** 属性的声明 */
FIELD,
/** 方法的声明 */
METHOD,
/** 方法形式参数声明 */
PARAMETER,
/** 构造方法的声明 */
CONSTRUCTOR,
/** 局部变量声明 */
LOCAL_VARIABLE,
/** 注解类型声明 */
ANNOTATION_TYPE,
/** 包的声明 */
PACKAGE
}
@Retention
@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。
注解的生命周期有三个阶段:1、Java源文件阶段;2、编译到class文件阶段;3、运行期阶段。同样使用了RetentionPolicy枚举类型定义了三个阶段:
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
* (注解将被编译器忽略掉)
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
* (注解将被编译器记录在class文件中,但在运行时不会被虚拟机保留,这是一个默认的行为)
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
* (注解将被编译器记录在class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到)
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
详细介绍:
- 如果一个注解被定义为RetentionPolicy.SOURCE,则它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和一个注释是一样的效果,只能被阅读Java文件的人看到;
- 如果一个注解被定义为RetentionPolicy.CLASS,则它将被编译到Class文件中,那么编译器可以在编译时根据注解做一些处理动作,但是运行时JVM(Java虚拟机)会忽略它,我们在运行期也不能读取到;
- 如果一个注解被定义为RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到Class对象中。那么在程序运行阶段,我们可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段。我们实际开发中的自定义注解几乎都是使用的RetentionPolicy.RUNTIME;
- 在默认的情况下,自定义注解是使用的RetentionPolicy.CLASS。
创建库表和实体
创建表
库中创建一张sys_log表,用于保存用户的操作日志,数据库采用mysql 8:
CREATE TABLE sys_log(
id INT(20) AUTO_INCREMENT PRIMARY KEY COMMENT "id",
username VARCHAR(50) NOT NULL COMMENT "用户名",
operation VARCHAR(50) NOT NULL COMMENT "用户操作",
`time` INT (11) NOT NULL COMMENT "响应时间",
method VARCHAR(200) NOT NULL COMMENT "请求方法",
params VARCHAR(200) NOT NULL COMMENT "请求参数",
ip VARCHAR(64) NOT NULL COMMENT "ip地址",
create_time DATE NOT NULL COMMENT "创建时间"
);
实体
库表对应的实体:
@ToString
@Data
public class SysLog implements Serializable {
private Integer id;
private String username;
private String operation;
private Integer time;
private String method;
private String params;
private String ip;
private Date createTime;
}
保存日志的方法
使用mybatis来操作数据库,定义一个SysLogDao接口,包含一个保存操作日志的抽象方法
日志保存接口
@Component
@Mapper
public interface SysLogDao {
void savaSysLog(SysLog sysLog);
}
日志保存接口对应的mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spbt.dao.SysLogDao">
<insert id="savaSysLog" parameterType="com.spbt.pojo.SysLog">
insert into sys_log(username,operation,time,method,params,ip,create_time) values
(#{username},#{operation},#{time},#{method},#{params},#{ip},#{createTime})
</insert>
</mapper>
切面和切点
定义一个LogAspect类,使用@Aspect
标注让其成为一个切面,切点为使用@Log
注解标注的方法,使用@Around
环绕通知
@Slf4j
@Aspect
@Component
public class LogAspect {
@Autowired
private SysLogDao sysLogDao;
@Pointcut("@annotation(com.spbt.annotation.Log)")
public void pointcut() {
}
@Around("pointcut()")
public void around(ProceedingJoinPoint point) {
long beginTime = System.currentTimeMillis();
try {
point.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
long time = System.currentTimeMillis() - beginTime;
saveLog(point, time);
}
private void saveLog(ProceedingJoinPoint point, long time) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
SysLog sysLog = new SysLog();
Log annotation = method.getAnnotation(Log.class);
//注解上的描述 用户操作
if (annotation != null) {
sysLog.setOperation(annotation.value());
}
//请求方法名
String className = point.getTarget().getClass().getName();
String methodName = signature.getName();
sysLog.setMethod(className + "." + methodName + "()");
//请求参数
Object[] args = point.getArgs();
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
if (args != null && parameterNames != null) {
String params = "";
for (int i = 0; i < args.length; i++) {
params += " " + parameterNames[i] + ":" + args[i];
}
sysLog.setParams(params);
}
//获取request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
//设置ip地址
sysLog.setIp(IPUtils.getIpAddr(request));
sysLog.setUsername("zcb");
sysLog.setTime((int) time);
sysLog.setCreateTime(new Date());
// 保存系统日志
log.info("用户操作信息:{}", sysLog);
sysLogDao.savaSysLog(sysLog);
}
}
AOP详情请查看:spring中AOP详解
测试
Controller建立
@RestController
public class IndexController {
@Log("执行方法一")
@GetMapping("/one/{name}")
public void methodOne(@PathVariable("name") String name) {
}
@Log("执行方法二")
@GetMapping("/two")
public void methodTwo() throws InterruptedException {
Thread.sleep(2000);
}
@Log("执行方法三")
@GetMapping("/three/{name}/{age}")
public void methodThree(String name, String age) {
}
}
启动项目,分别访问:
- http://localhost:8080/web/one?name=KangKang
- http://localhost:8080/web/two
- http://localhost:8080/web/three?name=zcb&age=25
见贤思齐焉见不贤而内自省也