原文链接:https://www.longkui.site/program/java/spring-boot-annotation/6375/
前面一篇文章简单介绍了常见的自定义注解:spring boot自定义注解(0)—常见类型
这篇文章介绍一下spring boot如何通过自定义注解实现记录操作日志过程。
0.准备工作
首先创建一个srping boot项目,如果不会可以参考这篇文章:Spring Boot(1)—创建并运行项目
需要引入AOP依赖和fastjson依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.7.7
site.longkui
app
0.0.1-SNAPSHOT
app
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
mysql
mysql-connector-java
5.1.34
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.4
org.aspectj
aspectjtools
1.8.9
com.alibaba
fastjson
1.2.79
org.springframework.boot
spring-boot-maven-plugin
编写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 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
查看数据库的情况:
可以看到已经成功插入到数据库中了