前面一篇文章简单介绍了常见的自定义注解:spring boot自定义注解(0)—常见类型
这篇文章介绍一下spring boot如何通过自定义注解实现记录操作日志过程。
0.准备工作
首先创建一个srping boot项目,如果不会可以参考这篇文章:Spring Boot(1)—创建并运行项目
需要引入AOP依赖和fastjson依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>site.longkui</groupId>
<artifactId>app</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>app</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>
<scope>test</scope>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
<!--mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.8.9</version>
</dependency>
<!-- 引入 fastjson 依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
编写sql,参考如下
DROP TABLE IF EXISTS `log_record`; CREATE TABLE `log_record` ( `id` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'id主键', `module` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '所属模块', `describe` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '操作描述内容', `path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求路径', `method` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求方法', `IP` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'IP地址', `qualifiedName` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求名', `inputParam` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '请求参数', `outputParam` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '请出参数', `errorMsg` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '异常信息', `requestTime` datetime NULL DEFAULT NULL COMMENT '请求开始时间', `responseTime` datetime NULL DEFAULT NULL COMMENT '请求响应时间', `costTime` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '接口耗时', `status` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求是否成功', `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
1.创建实体类
package site.longkui.app.entity.logrecord; import java.io.Serializable; import java.util.Date; public class LogRecordEntity implements Serializable { //主键id private String id; //所属模块 private String module; //操作内容描述 private String describe; //请求路径 private String path; //请求方法 private String method; //IP地址 private String IP; //请求名 private String qualifiedName; //请求参数 private String inputParam; //请出参数 private String outputParam; //异常信息 private String errorMsg; //请求开始时间 private Date requestTime; // 请求响应时间 private Date responseTime; // 接口耗时,单位:ms private String costTime; // 请求是否成功 private String status; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getModule() { return module; } public void setModule(String module) { this.module = module; } public String getDescribe() { return describe; } public void setDescribe(String describe) { this.describe = describe; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getIP() { return IP; } public void setIP(String IP) { this.IP = IP; } public String getQualifiedName() { return qualifiedName; } public void setQualifiedName(String qualifiedName) { this.qualifiedName = qualifiedName; } public String getInputParam() { return inputParam; } public void setInputParam(String inputParam) { this.inputParam = inputParam; } public String getOutputParam() { return outputParam; } public void setOutputParam(String outputParam) { this.outputParam = outputParam; } public String getErrorMsg() { return errorMsg; } public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; } public Date getRequestTime() { return requestTime; } public void setRequestTime(Date requestTime) { this.requestTime = requestTime; } public Date getResponseTime() { return responseTime; } public void setResponseTime(String String) { this.responseTime = responseTime; } public String getCostTime() { return costTime; } public void setCostTime(String costTime) { this.costTime = costTime; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } @Override public String toString() { return "LogRecordEntity{" + "id='" + id + '\'' + ", module='" + module + '\'' + ", describe='" + describe + '\'' + ", path='" + path + '\'' + ", method='" + method + '\'' + ", IP='" + IP + '\'' + ", qualifiedName='" + qualifiedName + '\'' + ", inputParam='" + inputParam + '\'' + ", outputParam='" + outputParam + '\'' + ", errorMsg='" + errorMsg + '\'' + ", requestTime='" + requestTime + '\'' + ", responseTime='" + responseTime + '\'' + ", costTime='" + costTime + '\'' + ", status='" + status + '\'' + '}'; } }
2.定义自定义注解
package site.longkui.app.annotate; import java.lang.annotation.*; @Target( {ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface OperateLog { //操作描述 String describe() default ""; //操作模块 String module() default ""; }
这个地方我们自定义了一个注解,这个注解使用在方法上面,用于标记AOP切面会拦截这个方法,并且记录请求日志信息。
3.定义AOP切面
package site.longkui.app.aop; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import site.longkui.app.annotate.OperateLog; import site.longkui.app.entity.logrecord.LogRecordEntity; import site.longkui.app.mapper.LogRecordMapper; import javax.servlet.http.HttpServletRequest; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Date; // 标记当前类是一个切面类 @Aspect // 将当前类放入IOC容器 @Component public class LogRecordAspect { @Autowired LogRecordMapper logRecordMapper; /** * 创建线程局部变量 */ private ThreadLocal<LogRecordEntity> threadLocal = new ThreadLocal<>(); /** * 定义切入点,这里我们使用AOP切入自定义【@OperateLog】注解的方法 */ @Pointcut("@annotation(site.longkui.app.annotate.OperateLog)") public void methodPointCut() { } /** * 前置通知,【执行Controller方法之前】执行该通知方法 */ @Before("methodPointCut()") public void beforeAdvice() { System.out.println("前置通知......"); } /** * 后置通知,【Controller方法执行完成,返回方法的返回值之前】执行该通知方法 */ @After("methodPointCut()") public void afterAdvice() { System.out.println("后置通知......"); } /** * 环绕通知,执行Controller方法的前后执行 * * @param point 连接点 */ @Around("methodPointCut()") public Object Around(ProceedingJoinPoint point) throws Throwable { System.out.println("环绕通知之前....."); // 获取当前请求对象 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder .getRequestAttributes()).getRequest(); if (request == null) { return null; } //获取当前请求相关信息 LogRecordEntity logRecordEntity = new LogRecordEntity(); //实体类 logRecordEntity.setPath(request.getRequestURI()); //获取请求地址 logRecordEntity.setMethod(request.getMethod()); //获取请求方式 // logRecordEntity.setId(request.getRemoteHost()); //弃用,改用自定义方法 logRecordEntity.setIP(getIRealIPAddr(request)); //获取ip logRecordEntity.setRequestTime(new Date(System.currentTimeMillis())); //获取系统时间作为请求时间 // 反射获取调用方法 MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); if (method.isAnnotationPresent(OperateLog.class)) { // 获取注解信息 OperateLog annotation = method.getAnnotation(OperateLog.class); logRecordEntity.setModule(annotation.module()); //注解信息中的 module的内容 logRecordEntity.setDescribe(annotation.describe()); //注解信息中的 describe 的内容 } //获取全限定类名称 String name = method.getName(); logRecordEntity.setQualifiedName(name); // 获取请求参数 String inputParam = JSONObject.toJSONString(point.getArgs()); logRecordEntity.setInputParam(inputParam); // 设置局部变量 threadLocal.set(logRecordEntity); //此处打印获取的具体内容 System.out.println(logRecordEntity.toString()); //也可以调用既定方法往数据库里写入数据 //logRecordMapper.insertLogRecord(logRecordEntity); Object ret = point.proceed(); return ret; } /** * 返回值通知,Controller执行完成之后,返回方法的返回值时候执行 * * @param ret 返回值的名称 */ @AfterReturning(pointcut = "methodPointCut()", returning = "ret") public Object afterReturning(Object ret) { System.out.println("返回值通知......ret=" + ret); // 获取日志实体对象 LogRecordEntity entity = this.getEntity(); String outputParam = JSON.toJSONString(ret); entity.setOutputParam(outputParam); // 保存响应参数 entity.setStatus("成功"); // 设置成功标识 //保存到数据库中,在这里调用可以把返回值一起调用 threadLocal.remove(); System.out.println(entity); try { logRecordMapper.insertLogRecord(entity); } catch (Exception e) { e.toString(); } return ret; } /** * 异常通知,当Controller方法执行过程中出现异常时候,执行该通知 * * @param ex 异常名称 */ @AfterThrowing(pointcut = "methodPointCut()", throwing = "ex") public void throwingAdvice(Throwable ex) { System.out.println("异常通知......"); // 获取日志实体对象 LogRecordEntity entity = this.getEntity(); StringWriter errorMsg = new StringWriter(); ex.printStackTrace(new PrintWriter(errorMsg, true)); entity.setErrorMsg(errorMsg.toString()); // 保存响应参数 entity.setStatus("error"); // 设置成功标识 //可以插入到数据库中 threadLocal.remove(); System.out.println(entity); } //获取真实IP private String getIRealIPAddr(HttpServletRequest request) { String ipAddress; ipAddress = request.getHeader("x-forwarded-for"); if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) { // 根据网卡取本机配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { e.printStackTrace(); } ipAddress = inet.getHostAddress(); } } // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length() // = 15 if (ipAddress.indexOf(",") > 0) { ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); } } return ipAddress; } private LogRecordEntity getEntity() { // 获取局部变量 LogRecordEntity entity = threadLocal.get(); long start = entity.getRequestTime().getTime(); long end = System.currentTimeMillis(); // 获取响应时间、耗时 entity.setCostTime((end - start) + "ms"); entity.setResponseTime(String.valueOf(end)); return entity; } }
这里我们写了切面,并且通过自定义方法logRecordMapper.insertLogRecord往数据库里写入数据。
4.编写测试类并进行测试
//根据id查询一个学生 @GetMapping("/getStudentById/{id}") @OperateLog(describe = "根据id查询一个学生",module = "学生模块") public JSONObject getStudentById(@PathVariable("id") String id){ try { JSONObject jsonObject=studentsService.getStudentById(id); return jsonObject; }catch (Exception e){ e.toString(); logger.error(e.toString()); return null; } }
访问接口:localhost:8082/api/students/getList/1003
查看数据库的情况:
可以看到已经成功插入到数据库中了