这里写自定义目录标题
1、操作日志SQL表
表名:t_operation_log
数据库:MySQL
版本: 5.6.40
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 50640
Source Host : localhost:3306
Source Schema : Test
Target Server Type : MySQL
Target Server Version : 50640
File Encoding : 65001
Date: 20/12/2022 14:47:30
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_operation_log
-- ----------------------------
DROP TABLE IF EXISTS `t_operation_log`;
CREATE TABLE `t_operation_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`operation_user_id` int(11) NULL DEFAULT NULL COMMENT '操作人ID',
`operation_username` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作人名称',
`operation_module` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '操作模块',
`operation_events` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '具体操作事件',
`operation_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '操作url',
`operation_data` varchar(3048) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作附带数据',
`operation_status` tinyint(1) NOT NULL COMMENT '操作是否正常,1正常操作, 0 操作异常',
`operation_ip` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作所在IP',
`operation_result` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作结果',
`add_time` datetime(0) NOT NULL COMMENT '操作时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '1 删除,0 未删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '操作日志表' ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
2、表映射实体类
增删改查用的是 mybatis-plus, 如果不是用的 mybatis-plus 需要增加一个 insert 的方法
实体类名称:OperationLog.java
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.time.LocalDateTime;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import org.apache.commons.lang3.builder.ToStringBuilder;
/**
* <p>
* 操作日志表
* </p>
*
*/
@TableName("t_operation_log")
@ApiModel(value = "OperationLog对象", description = "操作日志表")
public class OperationLog implements Serializable,Cloneable {
private static final long serialVersionUID = 1L;
/**
* 实现 Cloneable 克隆拷贝
* 创建一个 默认 对象,用于作为克隆的源数据
*/
private static final OperationLog log = new OperationLog();
/**
* 获取克隆对象, 避免new的方式创建
* @return {@link OperationLog}
*/
public static OperationLog getInstance(){
try {
return log.clone();
} catch (CloneNotSupportedException e) {
return new OperationLog();
}
}
/**
* 重写克隆方法
* @return {@link OperationLog}
*/
public OperationLog clone() throws CloneNotSupportedException {
return (OperationLog) super.clone();
}
/**
* 私有化构造函数,不允许 new
*/
private OperationLog(){
this.deleted = false;
}
@TableId(type = IdType.AUTO)
private Integer id;
@ApiModelProperty("操作人ID")
private Integer operationUserId;
@ApiModelProperty("操作人名称")
private String operationUsername;
@ApiModelProperty("操作模块")
private String operationModule;
@ApiModelProperty("具体操作事件")
private String operationEvents;
@ApiModelProperty("操作Url")
private String operationUrl;
@ApiModelProperty("操作附带数据")
private String operationData;
@ApiModelProperty("操作是否正常,1正常操作, 0 操作异常")
private Boolean operationStatus;
@ApiModelProperty("操作结果")
private String operationResult;
@ApiModelProperty("操作所在IP")
private String operationIp;
@ApiModelProperty("操作时间")
private LocalDateTime addTime;
@ApiModelProperty("1 删除,0 未删除")
private Boolean deleted;
public String getOperationUsername() {
return operationUsername;
}
public void setOperationUsername(String operationUsername) {
this.operationUsername = operationUsername;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getOperationUserId() {
return operationUserId;
}
public void setOperationUserId(Integer operationUserId) {
this.operationUserId = operationUserId;
}
public String getOperationModule() {
return operationModule;
}
public void setOperationModule(String operationModule) {
this.operationModule = operationModule;
}
public String getOperationEvents() {
return operationEvents;
}
public void setOperationEvents(String operationEvents) {
this.operationEvents = operationEvents;
}
public String getOperationUrl() {
return operationUrl;
}
public void setOperationUrl(String operationUrl) {
this.operationUrl = operationUrl;
}
public String getOperationData() {
return operationData;
}
public void setOperationData(String operationData) {
this.operationData = operationData;
}
public Boolean getOperationStatus() {
return operationStatus;
}
public void setOperationStatus(Boolean operationStatus) {
this.operationStatus = operationStatus;
}
public String getOperationResult() {
return operationResult;
}
public void setOperationResult(String operationResult) {
this.operationResult = operationResult;
}
public String getOperationIp() {
return operationIp;
}
public void setOperationIp(String operationIp) {
this.operationIp = operationIp;
}
public LocalDateTime getAddTime() {
return addTime;
}
public void setAddTime(LocalDateTime addTime) {
this.addTime = addTime;
}
public Boolean getDeleted() {
return deleted;
}
public void setDeleted(Boolean deleted) {
this.deleted = deleted;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("id", id)
.append("operationUserId", operationUserId)
.append("operationUsername", operationUsername)
.append("operationModule", operationModule)
.append("operationEvents", operationEvents)
.append("operationUrl", operationUrl)
.append("operationData", operationData)
.append("operationStatus", operationStatus)
.append("operationResult", operationResult)
.append("operationIp", operationIp)
.append("addTime", addTime)
.append("deleted", deleted)
.toString();
}
}
3、Service层
新增 操作日志,查询 我并没有写 SQL语句,都是用的 mybatis-plus 默认增删改查接口
4、操作日志注注解
4.1 创建 日志注解文件
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <h2>操作日志注解</h2>
* <p>
*
* </p>
*/
@Target({
ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLogDesc {
/**
* 操作模块
*/
String module();
/**
* 操作事件
*/
String events();
}
4.2 在请求方法上标注注解
/**
* 分页查询
*/
@GetMapping("/pageQuery")
@OperationLogDesc(module = "测试管理>测试记录", events = "删除记录")
public String deleted(QueryDTO dto) {
return "请求成功";
}
5、AOP 操作日志记录
使用 AOP 切入请求层 记录操作日志并保存
AOP maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
``
AOP操作日志配置文件
import com.mh.jishi.annotation.OperationLogDesc;
import com.mh.jishi.util.IpUtil;
import io.netty.util.concurrent.FastThreadLocal;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
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 javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Objects;
/**
*
**/
@Aspect
@Component
@Slf4j
public class LoggerAspect{
/**
* FastThreadLocal 依赖于 netty,如果不想用netty,可以使用jdk自带的 ThreadLocal
*/
final FastThreadLocal<OperationLog> logFastThreadLocal = new FastThreadLocal<>();
final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
/**
* 切入连接点,使用固定 controller层下的所有文件
*/
@Pointcut(value = "execution(* com.test.api.controller..*(..)))")
public void logPointcut() {
}
/**
* 请求前置通知
*/
@Before("logPointcut()")
public void beforLogger(JoinPoint joinPoint) {
// 获取请求参数
String params = Arrays.toString(joinPoint.getArgs());
// 鉴权会话获取 当前登录的用户信息,我用的是 shiro,根据情况改变
Subject currentUser = SecurityUtils.getSubject();
User user = (User)currentUser.getPrincipal();
Integer userId = null;
String userName = null;
if(User!= null){
userId = User.getId();
userName = User.getUsername();
}else{
/*
因为登录接口没有登录会话状态,无法获取到用户信息,从 登录请求参数中获取 登录用户名
@see 例如:登录请求肯定是 post请求,上方 "params" 参数已经获取到 请求参数信息,只要判断里面是否有用户名信息 验证是否为登录接口,然后字符串截取获取用户名。。这个方法是我能想到最快捷的
示例登录接口参数:我的登录 请求json [LoginDTO(username=1001010, password=132456,code='A5C5')]
*/
if(params.contains("username=")){
userName = params.substring(params.indexOf("username=") + 9, params.indexOf(", password="));
// 登录参数密码 简单脱密一下
params = params.replace("password=", "changshayueluqu_");
}
}
LocalDateTime now = LocalDateTime.now();
log.info("--------请求前置日志输出开始--------");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
log.info("请求访问时间: {}", dateTimeFormatter.format(now));
// 获取请求url
String requestUrl = request.getRequestURL().toString();
log.info("请求url: {}", requestUrl);
// 获取method
log.info("请求方式: {}", request.getMethod());
log.info("请求参数列表: {}", params);
log.info("操作人ID: {}", userId);
// 验证请求方法是否带有操作日志注解
Method signature = ((MethodSignature) joinPoint.getSignature()).getMethod();
OperationLogDesc operationLogDesc = signature.getAnnotation(OperationLogDesc.class);
if (operationLogDesc != null) {
// 操作日志记录
OperationLog operationLog = OperationLog.getInstance();
operationLog.setAddTime(now);
operationLog.setOperationModule(operationLogDesc.module());
operationLog.setOperationEvents(operationLogDesc.events());
operationLog.setOperationData(params);
operationLog.setOperationUrl(requestUrl);
// 操作人ID
operationLog.setOperationUserId(userId);
operationLog.setOperationUsername(userName);
// IP地址
operationLog.setOperationIp(IpUtil.getIpAddr(request));
logFastThreadLocal.set(operationLog);
}
}
/**
* 请求后置通知,请求完成会进入到这个方法
*
* @param result 响应结果json
*/
@AfterReturning(value = "logPointcut()", returning = "result")
public void afterReturningLogger(Object result) {
// 程序运时间(毫秒)
log.info("请求结束时间: {}", dateTimeFormatter.format(LocalDateTime.now()));
log.info("--------后台管理请求后置日志输出完成--------");
// 保存操作日志
OperationLog operationLog = logFastThreadLocal.get();
if (operationLog != null) {
operationLog.setOperationStatus(true);
// 用的 是 阿里巴巴的 fastjson
operationLog.setOperationResult(JSONObject.toJSONString(result));
// 调用具体的 service 保存到数据库中
operationLogService.save(operationLog);
// 移除本地线程数据
logFastThreadLocal.remove();
}
}
/**
* 异常通知,请求异常会进入到这个方法
*/
@AfterThrowing(value = "logPointcut()", throwing = "throwable")
public void throwingLogger(Throwable throwable) {
log.error("ErrorMessage:请根据异常产生时间前往异常日志查看相关信息");
log.error("--------后台管理请求异常日志输出完成--------");
// 保存操作日志
OperationLog operationLog = logFastThreadLocal.get();
if (operationLog != null) {
operationLog.setOperationStatus(false);
String throwableStr = throwable.toString();
if(throwableStr.contains(":")){
throwableStr = throwableStr.substring(throwableStr.indexOf(":") + 1);
}
operationLog.setOperationResult(throwableStr);
// 调用具体的 service 保存到数据库中
operationLogService.save(operationLog);
// 移除本地线程数据
logFastThreadLocal.remove();
}
}
}
IP 工具类
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IpUtil {
private static final Logger log = LoggerFactory.getLogger(IpUtil.class);
private IpUtil() {
}
public static String getIpAddr(HttpServletRequest request) {
String ipAddress;
try {
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 ("127.0.0.1".equals(ipAddress)) {
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException var4) {
log.error(var4.getMessage(), var4);
}
ipAddress = ((InetAddress)Objects.requireNonNull(inet)).getHostAddress();
}
}
if (ipAddress != null && ipAddress.length() > 15 && ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
} catch (Exception var5) {
ipAddress = "";
}
return ipAddress;
}
public static String getIpAddr() {
return getV4OrV6IP();
}
public static String getV4OrV6IP() {
String ip = null;
String test = "http://test.ipw.cn";
StringBuilder inputLine = new StringBuilder();
BufferedReader in = null;
try {
URL url = new URL(test);
HttpURLConnection urlConnection = (HttpURLConnection)url.openConnection();
in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), StandardCharsets.UTF_8));
String read;
while((read = in.readLine()) != null) {
inputLine.append(read);
}
ip = inputLine.toString();
} catch (Exception var16) {
log.error("获取网络IP地址异常,这是具体原因: ", var16);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException var15) {
var15.printStackTrace();
}
}
}
if (ip == null) {
ip = "127.0.0.1";
log.info("获取网络IP地址异常, 赋值默认ip: 【{}】", ip);
}
return ip;
}
}