初始化

This commit is contained in:
2026-02-22 18:56:10 +08:00
commit 26677972a6
3112 changed files with 255972 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-modules</artifactId>
<version>${revision}</version>
</parent>
<name>easyflow-module-log</name>
<artifactId>easyflow-module-log</artifactId>
<dependencies>
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-web</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-common-satoken</artifactId>
</dependency>
<!-- Javassist for line number -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,9 @@
package tech.easyflow.log;
public class ActionTypes {
public static final String INSERT = "INSERT";
public static final String DELETE = "DELETE";
public static final String UPDATE = "UPDATE";
public static final String QUERY = "QUERY";
}

View File

@@ -0,0 +1,127 @@
package tech.easyflow.log;
import jakarta.servlet.http.HttpServletRequest;
import tech.easyflow.common.util.RequestUtil;
import tech.easyflow.common.util.StringUtil;
import tech.easyflow.log.annotation.LogRecord;
import tech.easyflow.log.entity.WriteLog;
import tech.easyflow.log.mapper.WriteLogMapper;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import cn.dev33.satoken.stp.StpUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
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.math.BigInteger;
import java.util.Date;
import java.util.Enumeration;
@Aspect
@Component
public class LogAspect {
private static final int maxLengthOfParaValue = 512;
private final WriteLogMapper logService;
private final LogRecordProperties config;
public LogAspect(WriteLogMapper logService, LogRecordProperties config) {
this.logService = logService;
this.config = config;
}
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *) " +
"|| execution(* tech.easyflow.common.web.controller.BaseCurdController.*(..))")
public void pointcut() {
}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String servletPath = request.getServletPath();
//匹配前缀
if (StringUtil.hasText(config.getRecordActionPrefix()) && !servletPath.startsWith(config.getRecordActionPrefix())) {
return proceedingJoinPoint.proceed();
}
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Class<?> controllerClass = signature.getDeclaringType();
Method method = signature.getMethod();
String params = getRequestParamsString(request);
try {
return proceedingJoinPoint.proceed();
} finally {
WriteLog sysLog = new WriteLog();
LogRecord logRecord = method.getAnnotation(LogRecord.class);
if (StpUtil.isLogin()) {
BigInteger accountId = SaTokenUtil.getLoginAccount().getId();
sysLog.setAccountId(accountId);
}
sysLog.setActionName(buildActionName(logRecord, method));
sysLog.setActionType(logRecord != null ? logRecord.actionType() : null);
sysLog.setActionClass(controllerClass.getName());
sysLog.setActionMethod(method.getName());
sysLog.setActionUrl(request.getRequestURL().toString());
sysLog.setActionIp(RequestUtil.getIpAddress(request));
sysLog.setActionParams(params);
sysLog.setStatus(1);
sysLog.setCreated(new Date());
logService.insert(sysLog);
}
}
private String buildActionName(LogRecord logRecord, Method method) {
if (logRecord != null && StringUtil.hasText(logRecord.value())) {
return logRecord.value();
} else {
//todo 这里可以通过方法名,去获取 Controller 的实体类,在获取其表备注信息,进一步进行判断
return method.getName();
}
}
private String getRequestParamsString(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
Enumeration<String> e = request.getParameterNames();
if (e.hasMoreElements()) {
while (e.hasMoreElements()) {
String name = e.nextElement();
String[] values = request.getParameterValues(name);
if (values.length == 1) {
sb.append(name).append("=");
if (values[0] != null && values[0].length() > maxLengthOfParaValue) {
sb.append(values[0], 0, maxLengthOfParaValue).append("...");
} else {
sb.append(values[0]);
}
} else {
sb.append(name).append("[]={");
for (int i = 0; i < values.length; i++) {
if (i > 0) {
sb.append(",");
}
sb.append(values[i]);
}
sb.append("}");
}
sb.append(" ");
}
}
return sb.toString();
}
}

View File

@@ -0,0 +1,19 @@
package tech.easyflow.log;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "easyflow.log-record")
public class LogRecordProperties {
private String recordActionPrefix;
public String getRecordActionPrefix() {
return recordActionPrefix;
}
public void setRecordActionPrefix(String recordActionPrefix) {
this.recordActionPrefix = recordActionPrefix;
}
}

View File

@@ -0,0 +1,18 @@
package tech.easyflow.log.annotation;
import java.lang.annotation.*;
/**
* @author michael yang (fuhai999@gmail.com)
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LogRecord {
String value();
String actionType() default "";
}

View File

@@ -0,0 +1,13 @@
package tech.easyflow.log.annotation;
import java.lang.annotation.*;
/**
* 标记后LogReporterDisabled 将跳过此方法或类的日志输出
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogReporterDisabled {
}

View File

@@ -0,0 +1,40 @@
package tech.easyflow.log.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import tech.easyflow.log.reporter.ActionLogReporterProperties;
import tech.easyflow.log.reporter.ActionReportInterceptor;
@MapperScan("tech.easyflow.log.mapper")
@Configuration
public class LogModuleConfig implements WebMvcConfigurer {
private final ActionLogReporterProperties logProperties;
private final ActionReportInterceptor actionReportInterceptor;
public LogModuleConfig(ActionLogReporterProperties logProperties,
ActionReportInterceptor actionReportInterceptor) {
this.logProperties = logProperties;
this.actionReportInterceptor = actionReportInterceptor;
System.out.println("启用模块 >>>>>>>>>> module-log");
}
/**
* 注册日志拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
if (!logProperties.isEnabled()) {
return;
}
registry.addInterceptor(actionReportInterceptor)
.addPathPatterns(logProperties.getIncludePatterns().toArray(new String[0]))
.excludePathPatterns(logProperties.getExcludePatterns().toArray(new String[0]));
}
}

View File

@@ -0,0 +1,181 @@
package tech.easyflow.log.entity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.annotation.Table;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
@Table(value = "tb_sys_log", comment = "操作日志表")
public class WriteLog implements Serializable {
private static final long serialVersionUID = 1L;
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
private BigInteger id;
/**
* 操作人
*/
@Column(comment = "操作人")
private BigInteger accountId;
/**
* 操作名称
*/
@Column(comment = "操作名称")
private String actionName;
/**
* 操作的类型
*/
@Column(comment = "操作的类型")
private String actionType;
/**
* 操作涉及的类
*/
@Column(comment = "操作涉及的类")
private String actionClass;
/**
* 操作涉及的方法
*/
@Column(comment = "操作涉及的方法")
private String actionMethod;
/**
* 操作涉及的 URL 地址
*/
@Column(comment = "操作涉及的 URL 地址")
private String actionUrl;
/**
* 操作涉及的用户 IP 地址
*/
@Column(comment = "操作涉及的用户 IP 地址")
private String actionIp;
/**
* 操作请求参数
*/
@Column(comment = "操作请求参数")
private String actionParams;
/**
* 操作请求body
*/
@Column(comment = "操作请求body")
private String actionBody;
/**
* 操作状态 1 成功 9 失败
*/
@Column(comment = "操作状态 1 成功 9 失败")
private Integer status;
/**
* 操作时间
*/
@Column(comment = "操作时间")
private Date created;
public BigInteger getId() {
return id;
}
public void setId(BigInteger id) {
this.id = id;
}
public BigInteger getAccountId() {
return accountId;
}
public void setAccountId(BigInteger accountId) {
this.accountId = accountId;
}
public String getActionName() {
return actionName;
}
public void setActionName(String actionName) {
this.actionName = actionName;
}
public String getActionType() {
return actionType;
}
public void setActionType(String actionType) {
this.actionType = actionType;
}
public String getActionClass() {
return actionClass;
}
public void setActionClass(String actionClass) {
this.actionClass = actionClass;
}
public String getActionMethod() {
return actionMethod;
}
public void setActionMethod(String actionMethod) {
this.actionMethod = actionMethod;
}
public String getActionUrl() {
return actionUrl;
}
public void setActionUrl(String actionUrl) {
this.actionUrl = actionUrl;
}
public String getActionIp() {
return actionIp;
}
public void setActionIp(String actionIp) {
this.actionIp = actionIp;
}
public String getActionParams() {
return actionParams;
}
public void setActionParams(String actionParams) {
this.actionParams = actionParams;
}
public String getActionBody() {
return actionBody;
}
public void setActionBody(String actionBody) {
this.actionBody = actionBody;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
}

View File

@@ -0,0 +1,14 @@
package tech.easyflow.log.mapper;
import tech.easyflow.log.entity.WriteLog;
import com.mybatisflex.core.BaseMapper;
/**
* 映射层。
*
* @author michael
* @since 2024-01-28
*/
public interface WriteLogMapper extends BaseMapper<WriteLog> {
}

View File

@@ -0,0 +1,80 @@
package tech.easyflow.log.reporter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "easyflow.log.reporter")
public class ActionLogReporterProperties {
/**
* 是否启用 Action 报告
*/
private boolean enabled = true;
/**
* 采样率0.0 ~ 1.01.0 = 100%
*/
private double sampleRate = 1.0;
/**
* 包含的路径模式
*/
private List<String> includePatterns = Arrays.asList("/**");
/**
* 排除的路径模式
*/
private List<String> excludePatterns = Arrays.asList(
"/static/**",
"/assets/**",
"/js/**",
"/css/**",
"/images/**",
"/favicon.ico",
"/actuator/**",
"*.js",
"*.css",
"*.png",
"*.jpg",
"*.gif",
"*.ico"
);
// getter and setter
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public double getSampleRate() {
return sampleRate;
}
public void setSampleRate(double sampleRate) {
this.sampleRate = sampleRate;
}
public List<String> getIncludePatterns() {
return includePatterns;
}
public void setIncludePatterns(List<String> includePatterns) {
this.includePatterns = includePatterns;
}
public List<String> getExcludePatterns() {
return excludePatterns;
}
public void setExcludePatterns(List<String> excludePatterns) {
this.excludePatterns = excludePatterns;
}
}

View File

@@ -0,0 +1,282 @@
package tech.easyflow.log.reporter;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.ContentCachingResponseWrapper;
import tech.easyflow.common.util.RequestUtil;
import tech.easyflow.log.annotation.LogReporterDisabled;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
@Component
public class ActionReportInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger("ACTION_REPORT");
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
static {
SDF.setTimeZone(TimeZone.getTimeZone("GMT+8")); // 北京时间
}
private static final String DIVIDER = "─────────────────────────────────────────────────────────────────────────────";
private static final int MAX_JSON_LENGTH = 512;
private static final List<String> SENSITIVE_KEYS = Arrays.asList("password", "passwd", "secret", "token", "key");
private final ActionLogReporterProperties logProperties;
public ActionReportInterceptor(ActionLogReporterProperties logProperties) {
this.logProperties = logProperties;
}
// 存储是否需要记录日志的标志
private static final ThreadLocal<Boolean> SHOULD_LOG = ThreadLocal.withInitial(() -> false);
// 存储开始时间
private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();
// 存储 HandlerMethod供 afterCompletion 使用
private static final ThreadLocal<HandlerMethod> HANDLER_METHOD = new ThreadLocal<>();
// 存储 ModelAndView供 afterCompletion 使用
private static final ThreadLocal<ModelAndView> MODEL_AND_VIEW = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 默认不记录
SHOULD_LOG.set(false);
if (!logProperties.isEnabled() || !(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod hm = (HandlerMethod) handler;
Method method = hm.getMethod();
// 采样
if (logProperties.getSampleRate() < 1.0 && Math.random() > logProperties.getSampleRate()) {
return true;
}
// 排除 LogReporterDisabled 注解
if (method.isAnnotationPresent(LogReporterDisabled.class) ||
method.getDeclaringClass().isAnnotationPresent(LogReporterDisabled.class)) {
return true;
}
// 设置标志:需要记录日志
SHOULD_LOG.set(true);
START_TIME.set(System.currentTimeMillis());
HANDLER_METHOD.set(hm);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
// 仅暂存 modelAndView供 afterCompletion 使用
if (SHOULD_LOG.get()) {
MODEL_AND_VIEW.set(modelAndView);
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (!Boolean.TRUE.equals(SHOULD_LOG.get())) {
return;
}
HandlerMethod hm = HANDLER_METHOD.get();
if (hm == null) return;
Method method = hm.getMethod();
ModelAndView modelAndView = MODEL_AND_VIEW.get();
StringBuilder sb = new StringBuilder(2048);
sb.append("\n").append(DIVIDER).append('\n');
String timestamp = SDF.format(new Date());
sb.append("EasyFlow action report -------- ").append(timestamp).append(" -------------------------\n");
sb.append("Request : ").append(request.getMethod())
.append(" ").append(request.getRequestURI()).append("\n");
// 打印参数GET / POST 表单参数),脱敏
Map<String, String[]> params = request.getParameterMap();
if (!params.isEmpty()) {
Map<String, Object> maskedParams = new LinkedHashMap<>();
for (Map.Entry<String, String[]> entry : params.entrySet()) {
String key = entry.getKey();
String[] values = entry.getValue();
if (values != null && values.length == 1) {
maskedParams.put(key, isSensitive(key) ? "***" : values[0]);
} else {
maskedParams.put(key, isSensitive(key) ? "***" : values);
}
}
sb.append("Params : ").append(JSON.toJSONString(maskedParams)).append("\n");
}
// ====== 读取 POST Body ======
String methodStr = request.getMethod();
if ("POST".equalsIgnoreCase(methodStr) || "PUT".equalsIgnoreCase(methodStr) || "PATCH".equalsIgnoreCase(methodStr)) {
String body = RequestUtil.readBodyString(request);
if (body != null && !body.trim().isEmpty()) {
try {
Object obj = JSON.parse(body);
if (obj instanceof Map) {
obj = maskSensitiveValues((Map<String, Object>) obj);
}
String maskedBody = JSON.toJSONString(obj);
if (maskedBody.length() > MAX_JSON_LENGTH) {
maskedBody = maskedBody.substring(0, MAX_JSON_LENGTH) + " ... (truncated)";
}
sb.append("Body : ").append(maskedBody.replace("\n", " ")).append("\n");
} catch (Exception e) {
String truncated = body.length() > 200 ? body.substring(0, 200) + "..." : body;
sb.append("Body : ").append(truncated).append("\n");
}
}
}
// Controller & Method 位置(含行号)
Class<?> clazz = method.getDeclaringClass();
String fileName = clazz.getSimpleName() + ".java";
int lineNumber = JavassistLineNumUtils.getLineNumber(method);
String lineStr = lineNumber > 0 ? String.valueOf(lineNumber) : "?";
String controllerLocation = clazz.getName() + ".(" + fileName + ":" + lineStr + ")";
sb.append("Controller : ").append(controllerLocation).append("\n");
// 构建方法签名
sb.append("Method : ").append(buildMethodSignature(method));
// ====== 处理 ModelAndView ======
if (modelAndView != null && modelAndView.getViewName() != null) {
sb.append('\n').append("Render : ").append(modelAndView.getViewName());
Map<String, Object> model = modelAndView.getModel();
if (!model.isEmpty()) {
sb.append("\nModel : ");
try {
String json = JSON.toJSONString(maskSensitiveValues(model), JSONWriter.Feature.WriteMapNullValue);
if (json.length() > MAX_JSON_LENGTH) {
json = json.substring(0, MAX_JSON_LENGTH) + " ... (truncated)";
}
sb.append(json.replace("\n", " "));
} catch (Exception e) {
sb.append("(failed to serialize)");
}
}
} else {
sb.append('\n').append("Render : (none)");
}
// ====== 尝试获取最终响应体 ======
if (response instanceof ContentCachingResponseWrapper) {
ContentCachingResponseWrapper wrapper = (ContentCachingResponseWrapper) response;
byte[] body = wrapper.getContentAsByteArray();
if (body.length > 0) {
String content = new String(body, 0, Math.min(body.length, 1024), StandardCharsets.UTF_8);
try {
Object obj = JSON.parse(content);
if (obj instanceof Map) {
obj = maskSensitiveValues((Map<String, Object>) obj);
}
String masked = JSON.toJSONString(obj);
if (masked.length() > MAX_JSON_LENGTH) {
masked = masked.substring(0, MAX_JSON_LENGTH) + " ... (truncated)";
}
sb.append('\n').append("Response : ").append(masked.replace("\n", " "));
} catch (Exception e) {
String truncated = content.length() > 200 ? content.substring(0, 200) + "..." : content;
sb.append('\n').append("Response : ").append(truncated);
}
}
}
// ====== 异常信息 ======
if (ex != null) {
sb.append('\n')
.append("Status : FAILED\n")
.append("Exception : ").append(ex.getClass().getSimpleName())
.append(": ").append(ex.getMessage() != null ? ex.getMessage().split("\n")[0] : "Unknown");
}
// ====== 耗时 ======
Long start = START_TIME.get();
long took = start != null ? System.currentTimeMillis() - start : -1;
if (took >= 0) {
sb.append('\n')
.append("----------------------------------- took ").append(took).append(" ms --------------------------------");
} else {
sb.append('\n').append("----------------------------------- took ? ms --------------------------------");
}
sb.append('\n').append(DIVIDER);
logger.info(sb.toString());
} finally {
// 清理 ThreadLocal
SHOULD_LOG.remove();
START_TIME.remove();
HANDLER_METHOD.remove();
MODEL_AND_VIEW.remove();
}
}
// ====================== 工具方法 ======================
private boolean isSensitive(String key) {
return SENSITIVE_KEYS.stream().anyMatch(s -> key.toLowerCase().contains(s));
}
private Map<String, Object> maskSensitiveValues(Map<String, Object> model) {
Map<String, Object> result = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : model.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (isSensitive(key)) {
result.put(key, "***");
} else if (value instanceof Map) {
result.put(key, maskSensitiveValues((Map<String, Object>) value));
} else {
result.put(key, value);
}
}
return result;
}
/**
* 构建方法签名methodName(paramType paramName, ...)
*/
private String buildMethodSignature(Method method) {
StringBuilder sig = new StringBuilder();
sig.append(method.getName()).append("(");
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter param = parameters[i];
sig.append(param.getType().getSimpleName())
.append(" ")
.append(param.getName());
if (i < parameters.length - 1) {
sig.append(", ");
}
}
sig.append(")");
return sig.toString();
}
}

View File

@@ -0,0 +1,44 @@
package tech.easyflow.log.reporter;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.bytecode.MethodInfo;
import java.util.concurrent.ConcurrentHashMap;
public class JavassistLineNumUtils {
private static final ClassPool CLASS_POOL = ClassPool.getDefault();
private static final ConcurrentHashMap<String, Integer> LINE_CACHE = new ConcurrentHashMap<>();
/**
* 获取方法在源码中的起始行号
*/
public static int getLineNumber(java.lang.reflect.Method method) {
String key = method.getDeclaringClass().getName() + "." + method.getName();
return LINE_CACHE.computeIfAbsent(key, k -> {
try {
CtClass ctClass = CLASS_POOL.get(method.getDeclaringClass().getName());
CtClass[] params = toCtClasses(method.getParameterTypes());
CtMethod ctMethod = ctClass.getDeclaredMethod(method.getName(), params);
MethodInfo methodInfo = ctMethod.getMethodInfo();
return methodInfo.getLineNumber(0);
} catch (Exception e) {
return 0;
}
});
}
private static CtClass[] toCtClasses(Class<?>[] parameterTypes) throws NotFoundException {
CtClass[] result = new CtClass[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
result[i] = CLASS_POOL.get(parameterTypes[i].getName());
}
return result;
}
}

View File

@@ -0,0 +1,122 @@
package tech.easyflow.log.reporter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import java.io.IOException;
import static org.springframework.core.Ordered.HIGHEST_PRECEDENCE;
/**
* 响应缓存 Filter支持基于路径的排除规则
*/
@Component
@Order(HIGHEST_PRECEDENCE)
@ConditionalOnProperty(
prefix = "easyflow.log.reporter",
name = "enabled",
havingValue = "true",
matchIfMissing = true // 默认开启
)
public class ResponseCachingFilter implements Filter {
@Autowired
private ActionLogReporterProperties logProperties;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String uri = httpRequest.getRequestURI();
String method = httpRequest.getMethod();
// 1如果是 OPTIONS 请求,跳过(通常为预检)
if ("OPTIONS".equalsIgnoreCase(method)) {
chain.doFilter(request, response);
return;
}
// // 检查是否为 SSE 请求
// if (isSseRequest(httpRequest)) {
// chain.doFilter(request, response);
// return;
// }
// 检查是否匹配排除路径
if (isExcluded(uri)) {
chain.doFilter(request, response);
return;
}
// 检查是否匹配包含路径(一般为 /**,可省略)
if (!isIncluded(uri)) {
chain.doFilter(request, response);
return;
}
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpRequest);
if (isSseRequest(httpRequest)) {
// SSE 请求不缓存
chain.doFilter(requestWrapper, response);
return;
}
HttpServletResponse httpResponse = (HttpServletResponse) response;
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpResponse);
try {
chain.doFilter(requestWrapper, responseWrapper);
} finally {
responseWrapper.copyBodyToResponse(); // 必须调用
}
}
private boolean isExcluded(String uri) {
return logProperties.getExcludePatterns().stream().anyMatch(p -> match(uri, p));
}
private boolean isIncluded(String uri) {
return logProperties.getIncludePatterns().stream().anyMatch(p -> match(uri, p));
}
/**
* 判断是否为 SSE 请求(基于标准 Accept 头)
*/
private boolean isSseRequest(HttpServletRequest request) {
String accept = request.getHeader("Accept");
return accept != null && accept.contains("text/event-stream");
}
/**
* 简单的路径匹配(支持 * 和 **
* 注意:这里简化实现,生产可替换为 AntPathMatcher
*/
private boolean match(String path, String pattern) {
if (pattern.equals("/**")) {
return true;
}
if (pattern.endsWith("/**")) {
String prefix = pattern.substring(0, pattern.length() - 3);
return path.startsWith(prefix);
}
if (pattern.endsWith("*")) {
String prefix = pattern.substring(0, pattern.length() - 1);
return path.startsWith(prefix);
}
if (pattern.contains("*") && !pattern.contains("/**")) {
// 支持 *.js, *.css
String p = pattern.replace("*", "").replace(".", "\\.");
return path.matches(".*" + p + ".*");
}
return path.equals(pattern);
}
}