需要引入的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
表结构设计 两张表,一张记录操作日志,一张记录错误日志
DROP TABLE IF EXISTS "public"."sys_opera_log";
CREATE TABLE "public"."sys_opera_log" (
"id" int8 NOT NULL,
"module" varchar(255) COLLATE "pg_catalog"."default",
"type" varchar(255) COLLATE "pg_catalog"."default",
"desc" varchar(255) COLLATE "pg_catalog"."default",
"method" varchar(255) COLLATE "pg_catalog"."default",
"req_type" varchar(255) COLLATE "pg_catalog"."default",
"req_param" text COLLATE "pg_catalog"."default",
"res_param" text COLLATE "pg_catalog"."default",
"user_id" varchar(64) COLLATE "pg_catalog"."default",
"user_name" varchar(255) COLLATE "pg_catalog"."default",
"ip" varchar(255) COLLATE "pg_catalog"."default",
"create_time" timestamp(6),
"uri" varchar(255) COLLATE "pg_catalog"."default",
"take_up_time" int8
)
;
COMMENT ON COLUMN "public"."sys_opera_log"."module" IS '功能模块';
COMMENT ON COLUMN "public"."sys_opera_log"."type" IS '操作类型';
COMMENT ON COLUMN "public"."sys_opera_log"."desc" IS '操作描述';
COMMENT ON COLUMN "public"."sys_opera_log"."method" IS '请求方法';
COMMENT ON COLUMN "public"."sys_opera_log"."req_type" IS '请求类型';
COMMENT ON COLUMN "public"."sys_opera_log"."req_param" IS '请求参数';
COMMENT ON COLUMN "public"."sys_opera_log"."res_param" IS '响应参数';
COMMENT ON COLUMN "public"."sys_opera_log"."ip" IS 'ip地址';
COMMENT ON COLUMN "public"."sys_opera_log"."create_time" IS '时间';
COMMENT ON COLUMN "public"."sys_opera_log"."take_up_time" IS '耗时';
COMMENT ON TABLE "public"."sys_opera_log" IS '系统操作日志';
-- ----------------------------
-- Primary Key structure for table sys_opera_log
-- ----------------------------
ALTER TABLE "public"."sys_opera_log" ADD CONSTRAINT "sys_opear_log_pkey" PRIMARY KEY ("id");
DROP TABLE IF EXISTS "public"."sys_error_log";
CREATE TABLE "public"."sys_error_log" (
"id" int8 NOT NULL,
"req_param" text COLLATE "pg_catalog"."default",
"name" varchar(255) COLLATE "pg_catalog"."default",
"message" text COLLATE "pg_catalog"."default",
"user_id" varchar(64) COLLATE "pg_catalog"."default",
"user_name" varchar(255) COLLATE "pg_catalog"."default",
"method" varchar(255) COLLATE "pg_catalog"."default",
"uri" varchar(255) COLLATE "pg_catalog"."default",
"ip" varchar(255) COLLATE "pg_catalog"."default",
"create_time" timestamp(6),
"type" varchar(255) COLLATE "pg_catalog"."default"
)
;
COMMENT ON COLUMN "public"."sys_error_log"."req_param" IS '请求参数';
COMMENT ON COLUMN "public"."sys_error_log"."name" IS '异常名称';
COMMENT ON COLUMN "public"."sys_error_log"."message" IS '异常信息';
COMMENT ON COLUMN "public"."sys_error_log"."user_id" IS '操作用户id';
COMMENT ON COLUMN "public"."sys_error_log"."user_name" IS '操作用户名字';
COMMENT ON COLUMN "public"."sys_error_log"."method" IS '操作方法';
COMMENT ON COLUMN "public"."sys_error_log"."ip" IS 'ip';
COMMENT ON COLUMN "public"."sys_error_log"."create_time" IS '时间';
COMMENT ON COLUMN "public"."sys_error_log"."type" IS '请求方式';
COMMENT ON TABLE "public"."sys_error_log" IS '系统异常日志';
-- ----------------------------
-- Primary Key structure for table sys_error_log
-- ----------------------------
ALTER TABLE "public"."sys_error_log" ADD CONSTRAINT "sys_error_log_pkey" PRIMARY KEY ("id");
1.创建一个自定义注解接口
package com.common.aop;
import java.lang.annotation.*;
/**
* @author qy
* @date 2024-10-22 09:51:09
*/
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented
public @interface OperaLog {
/**
* 操作模块
*/
String operaModule() default "";
/**
* 操作类型
*/
String operaType() default "";
/**
* 操作说明
*/
String operaDesc() default "";
}
2.创建切面处理类,用于操作日志记录
package com.serviceImpl.aop;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.alibaba.fastjson2.JSON;
import com.bjcj.kpi.common.aop.OperaLog;
import com.bjcj.kpi.mapper.sys.SysUserMapper;
import com.bjcj.kpi.model.pojo.sys.SysErrorLog;
import com.bjcj.kpi.model.pojo.sys.SysOperaLog;
import com.bjcj.kpi.serviceImpl.sys.SysErrorLogService;
import com.bjcj.kpi.serviceImpl.sys.SysOperaLogService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* 切面处理类,操作日志记录处理
*
* @author qy
* @date 2024-10-22 09:56:02
*/
@Aspect
@Component
@Slf4j
public class OperaLogAspect {
@Resource
SysOperaLogService sysOperaLogService;
@Resource
SysUserMapper sysUserMapper;
@Resource
SysErrorLogService sysErrorLogService;
/**
* 统计请求的处理时间
*/
ThreadLocal<Long> startTime = new ThreadLocal<>();
/**
* 设置操作日志切入点 记录操作日志 在注解的位置切入代码
*/
@Pointcut("@annotation(com.bjcj.kpi.common.aop.OperaLog)")
public void operaLogPointCut() {
}
@Before("operaLogPointCut()")
public void doBefore() {
// 接收到请求,记录请求开始时间
startTime.set(System.currentTimeMillis());
}
/**
* 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
*
* @param joinPoint 切入点
* @param keys 返回结果
*/
@AfterReturning(value = "operaLogPointCut()", returning = "keys")
public void saveOperaLog(JoinPoint joinPoint, Object keys) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
try {
SysOperaLog operaLog = new SysOperaLog();
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取操作
OperaLog opLog = method.getAnnotation(OperaLog.class);
if (opLog != null) {
String operaModule = opLog.operaModule();
String operaType = opLog.operaType();
String operaDesc = opLog.operaDesc();
operaLog.setModule(operaModule);
operaLog.setType(operaType);
operaLog.setDesc(operaDesc);
}
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 获取请求的方法名
String methodName = method.getName();
methodName = className + "." + methodName;
// 请求方法
operaLog.setMethod(methodName);
operaLog.setReqType(request.getMethod());
// 请求的参数
String params = "";
if (StrUtil.equals(request.getMethod(), "GET")
|| StrUtil.equals(request.getMethod(), "DELETE")) {
params = JSON.toJSONString(getRequestParam(request));
}
if (StrUtil.equals(request.getMethod(), "POST")) {
params = JSON.toJSONString(joinPoint.getArgs()[0]);
}
operaLog.setReqParam(params);
operaLog.setUserId(String.valueOf(StpUtil.getLoginId()));
operaLog.setUserName(this.sysUserMapper.selectById(String.valueOf(StpUtil.getLoginId())).getUsername());
operaLog.setIp(JakartaServletUtil.getClientIP(request, "Cdn-Src-Ip"));
operaLog.setUri(request.getRequestURI());
operaLog.setResParam(JSON.toJSONString(keys));
operaLog.setTakeUpTime(System.currentTimeMillis() - startTime.get());
sysOperaLogService.save(operaLog);
} catch (Exception e) {
log.error("操作日志生成错误:", e);
}
}
/**
* <h2>异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行</h2>
*
* @param joinPoint:
* @param e:
* @return void
* @author Guow
* @date 2023/12/8 11:00
*/
@AfterThrowing(pointcut = "operaLogPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
try {
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取切入点所在的方法
Method method = signature.getMethod();
// 获取请求的类名
String className = joinPoint.getTarget().getClass().getName();
// 请求的参数
String params = "";
if (StrUtil.equals(request.getMethod(), "GET")
|| StrUtil.equals(request.getMethod(), "DELETE")) {
params = JSON.toJSONString(getRequestParam(request));
}
if (StrUtil.equals(request.getMethod(), "POST")) {
params = JSON.toJSONString(joinPoint.getArgs()[0]);
}
sysErrorLogService.save(
SysErrorLog.builder()
.reqParam(params)
.method(className + "." + method.getName())
.name(e.getClass().getName())
.message(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()))
.userId(String.valueOf(StpUtil.getLoginId()))
.userName(this.sysUserMapper.selectById(String.valueOf(StpUtil.getLoginId())).getUsername())
.uri(request.getRequestURI())
.type(request.getMethod())
.ip(JakartaServletUtil.getClientIP(request, "Cdn-Src-Ip"))
.build()
);
} catch (Exception e2) {
log.error("异常日志生成错误:", e2);
}
}
/**
* <h2>获取get请求参数</h2>
*
* @return java.util.Map<java.lang.String, java.lang.String>
* @author Guow
* @date 2023/12/1 9:18
*/
private Map<String, String> getRequestParam(HttpServletRequest request) {
// 返回体
HashMap<String, String> res = new HashMap<>();
// 获取 GET 请求参数
Map<String, String[]> parameterMap = request.getParameterMap();
// 遍历参数Map
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String paramName = entry.getKey();
String[] paramValues = entry.getValue();
if (paramValues != null && paramValues.length > 0) {
res.put(paramName, paramValues[0]);
}
}
return res;
}
/**
* <h2>转换异常信息为字符串</h2>
*
* @param exceptionName:
* @param exceptionMessage:
* @param elements:
* @return java.lang.String
* @author Guow
* @date 2023/12/8 10:56
*/
public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
StringBuffer strbuff = new StringBuffer();
for (StackTraceElement stet : elements) {
strbuff.append(stet + "<br/>");
}
String message = exceptionName + ":" + exceptionMessage + "<br/>" + strbuff.toString();
return message;
}
}
3.常量工具类
package com.bjcj.kpi.common.constant;
/**
* @author qy
* @date 2024-10-22 09:45
*/
public class OperaLogConstant {
/**
* 新增/编辑
*/
public static final String CREATE_OR_UPDATE = "新增/编辑";
/**
* 新增
*/
public static final String CREATE = "新增";
/**
* 更新
*/
public static final String UPDATE = "更新";
/**
* 浏览
*/
public static final String LOOK = "浏览";
/**
* 删除
*/
public static final String DELETE = "删除";
/**
* 下载
*/
public static final String DOWNLOAD = "下载";
/**
* 上传
*/
public static final String UPLOAD = "上传";
/**
* 通知
*/
public static final String NOTICE = "通知";
}
4.使用示例,在接口上增加注解即可
@DeleteMapping("/data/{id}")
@Operation(summary = "删除字典值", description = "删除字典值")
@ApiOperationSupport(order = 5)
@Transactional(rollbackFor = Exception.class)
@OperaLog(operaModule = "删除字典值",operaType = OperaLogConstant.DELETE)
public JsonResult deleteDictValue(@PathVariable("id") String id) {
return sysDictService.del(id);
}