记录api调用日志
Created: December 11, 2024 2:57 PM
1.定义实体类
基类:
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
public class BaseEntity implements Serializable { /**
* 主键ID
*/
@DbComment("主键ID")
@Id
@Column(length = 50)
@NotBlank(message = "id不能为空")
private String id;
/**
* 记录创建时间
*/
@DbComment("记录创建时间")
@NullDefault(NullDefault.DEFAULT_DATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@ApiModelProperty(hidden = true)
private Timestamp itime;
/**
* 记录最新修改时间
*/
@DbComment("记录最新修改时间")
@NullDefault(NullDefault.DEFAULT_DATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@ApiModelProperty(hidden = true)
private Timestamp utime;
/**
* 创建人
*/
@DbComment("创建人")
@Column(length = 50)
@ApiModelProperty(hidden = true)
private String iuserid;
/**
* 创建人名称
*/
@DbComment("创建人名称")
@Column(length = 100)
@ApiModelProperty(hidden = true)
private String iusername;
/**
* 修改人id
*/
@DbComment("修改人id")
@Column(length = 50)
@ApiModelProperty(hidden = true)
private String uuserid;
/**
* 修改人名称
*/
@DbComment("修改人名称")
@Column(length = 100)
@ApiModelProperty(hidden = true)
private String uusername;
}
import javax.persistence.Column;
import javax.persistence.Lob;
import javax.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* @author: Mr.Guo
* @since: 2025/9/9 17:41
* <p>@description:<br>
*
* <P>@point:<br>
* <P>@update:<br>
*/
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "zx_api_invoke_log")
public class ApiRecordEntity extends BaseEntity {
/**
* 企业ID
*/
@Column(name = "company_id", length = 50)
@DbComment("企业/组织唯一标识")
private String qyid;
/**
* API路径
*/
@Column(name = "api_path", length = 500, nullable = false)
@DbComment("API调用路径")
private String apiPath;
/**
* 请求方法
*/
@Column(name = "http_method", length = 10, nullable = false)
@DbComment("HTTP请求方法(GET/POST等)")
private String httpMethod;
/**
* 请求头(JSON格式)
*/
@Lob
@DbComment("HTTP请求头(JSON格式)")
private String requestHeaders;
/**
* 请求参数(JSON格式)
*/
@Lob
@DbComment("请求参数(JSON格式)")
private String requestParams;
/**
* 请求体内容
*/
@Lob
@DbComment("请求体内容")
private String requestBody;
/**
* 客户端IP
*/
@Column(name = "client_ip", length = 50)
@DbComment("客户端IP地址")
private String clientIp;
// 响应信息
/**
* 响应状态码
*/
@Column(name = "response_status", nullable = false)
@DbComment("HTTP响应状态码")
private Integer responseStatus;
/**
* 响应头(JSON格式)
*/
@Lob
@DbComment("HTTP响应头(JSON格式)")
private String responseHeaders;
/**
* 响应体内容
*/
@Lob
@DbComment("响应体内容")
private String responseBody;
/**
* 调用耗时(毫秒)
*/
@Column(name = "cost_time")
@DbComment("API调用耗时(毫秒)")
private Long costTime;
/**
* 业务状态码
*/
@Column(name = "biz_code", length = 50)
@DbComment("业务状态码")
private String bizCode;
/**
* 调用结果
*/
@Column(name = "success", nullable = false)
@DbComment("是否调用成功")
private Boolean success;
/**
* 错误信息
*/
@Column(name = "error_msg", length = 1000)
@DbComment("错误信息")
private String errorMsg;
/**
* 追踪ID
*/
@Column(name = "trace_id", length = 64)
@DbComment("分布式追踪ID")
private String traceId;
/**
* 用户代理
*/
@Column(name = "user_agent", length = 500)
@DbComment("客户端User-Agent")
private String userAgent;
}
2.重复读写流包装类实现:
import java.io.*;
import java.nio.charset.StandardCharsets;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* @author: Mr.Guo
* @since: 2025/9/10 16:19
* <p>@description:<br>
*
* <P>@point:<br>
* <P>@update:<br>
*/
public class FixedResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream contentBuffer = new ByteArrayOutputStream();
private ServletOutputStream outputStream;
private PrintWriter writer;
public FixedResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (outputStream == null) {
outputStream = new TeeOutputStream(super.getOutputStream(), contentBuffer);
}
return outputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
if (writer == null) {
writer = new PrintWriter(new OutputStreamWriter(getOutputStream(), StandardCharsets.UTF_8));
}
return writer;
}
public byte[] getContentAsBytes() {
if (writer != null) {
writer.flush();
}
return contentBuffer.toByteArray();
}
public void copyToResponse() throws IOException {
// 不进行任何缓冲操作,直接写入原始响应
if (!isCommitted() && contentBuffer.size() > 0) {
getResponse().getOutputStream().write(contentBuffer.toByteArray());
}
}
private static class TeeOutputStream extends ServletOutputStream {
private final OutputStream branch;
private final OutputStream original;
public TeeOutputStream(OutputStream original, OutputStream branch) {
this.original = original;
this.branch = branch;
}
@Override public void write(int b) throws IOException {
original.write(b);
branch.write(b);
}
@Override public void write(byte[] b) throws IOException {
original.write(b);
branch.write(b);
}
@Override public void write(byte[] b, int off, int len) throws IOException {
original.write(b, off, len);
branch.write(b, off, len);
}
@Override public void flush() throws IOException {
original.flush();
branch.flush();
}
@Override public void close() throws IOException {
original.close();
branch.close();
}
@Override public boolean isReady() { return true; }
@Override public void setWriteListener(WriteListener writeListener) {}
}
}
3.记录日志业务类实现:
package com.zx.base.common.filter.log;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zx.base.common.domain.ApiRecordEntity;
import com.zx.base.common.mapper.ApiRecordMapper;
import com.zx.framework.security.pojo.UserInfo;
import com.zx.framework.util.tools.CodeUtil;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import cn.hutool.json.JSONUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
/**
* @author: Mr.Guo
* @since: 2025/9/9 18:20
* <p>@description:<br>
*
* <P>@point:<br>
* <P>@update:<br>
*/
@Slf4j
@Component
public class ApiRecordService {
@Resource
private ApiRecordMapper apiRecordMapper;
/**
* 添加API调用记录
*
* @param userInfo 用户信息
* @param request HTTP请求对象
* @param response HTTP响应对象
*/
@SneakyThrows void addApiRecord(UserInfo userInfo, HttpServletRequest request, HttpServletResponse response,
FixedResponseWrapper wrapper) {
log.debug("addApiRecord() called with: userInfo = [" + userInfo + "], request = [" + request + "], response = [" + response + "]");
ApiRecordEntity apiRecordEntity = ApiRecordEntity.builder()
.id(CodeUtil.getGUID())
.iuserid(userInfo.getUserid())
.iusername(userInfo.getNickname())
.qyid(userInfo.getEnterid())
.itime(new Timestamp(System.currentTimeMillis()))
.build();
long startTime = (Long) request.getAttribute("apiStartTime");
try {
// 获取包装后的响应
if (wrapper != null) {
// 获取响应内容
byte[] content = wrapper.getContentAsBytes();
String contentStr = new String(content, StandardCharsets.UTF_8);
apiRecordEntity.setResponseBody(contentStr);
// 解析业务状态码
parseBusinessStatus(apiRecordEntity, contentStr);
try {
// 请求信息
apiRecordEntity.setApiPath(request.getRequestURI());
apiRecordEntity.setHttpMethod(request.getMethod());
apiRecordEntity.setRequestHeaders(JSONUtil.toJsonStr(getRequestHeaders(request)));
apiRecordEntity.setRequestParams(JSONUtil.toJsonStr(request.getParameterMap()));
apiRecordEntity.setRequestBody(getRequestBody(request));
apiRecordEntity.setClientIp(getClientIp(request));
// 用户代理信息
apiRecordEntity.setUserAgent(request.getHeader("User-Agent"));
// 响应信息
apiRecordEntity.setResponseStatus(response.getStatus());
apiRecordEntity.setResponseHeaders(JSONUtil.toJsonStr(getResponseHeaders(response)));
// 设置调用结果
boolean isSuccess = response.getStatus() < 400 &&
(apiRecordEntity.getBizCode() == null ||
"SUCCESS".equals(apiRecordEntity.getBizCode()) ||
"200".equals(apiRecordEntity.getBizCode()));
apiRecordEntity.setSuccess(isSuccess);
// 追踪ID(可以从MDC或请求头获取)
String traceId = MDC.get("traceId");
if (traceId == null) {
traceId = request.getHeader("X-Trace-Id");
}
apiRecordEntity.setTraceId(traceId != null ? traceId : CodeUtil.getGUID());
request.setAttribute("logId",apiRecordEntity.getId());
} catch (Exception e) {
log.error("记录API调用日志异常", e);
apiRecordEntity.setSuccess(false);
apiRecordEntity.setErrorMsg("记录日志时发生异常");
} finally {
// 计算耗时
apiRecordEntity.setCostTime(System.currentTimeMillis() - startTime);
// 异步保存日志(推荐)
asyncSaveApiRecord(apiRecordEntity);
}
}
} catch (Exception e) {
log.error("API记录失败", e);
}
}
// 获取Spring自动配置的ObjectMapper
@Autowired
private ObjectMapper objectMapper;
private void parseBusinessStatus(ApiRecordEntity record, String responseBody) {
try {
JsonNode json = objectMapper.readTree(responseBody);
if (json != null) {
record.setBizCode(json.path("code").asText("UNKNOWN"));
if (json.has("msg")) {
record.setErrorMsg(json.get("msg").asText());
}
if (json.has("message")) {
record.setErrorMsg(json.get("message").asText());
}
}
} catch (Exception e) {
log.debug("解析响应JSON失败", e);
}
}
// 辅助方法:获取请求头Map
private Map<String, String> getRequestHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, request.getHeader(name));
}
return headers;
}
// 辅助方法:获取响应头Map
private Map<String, String> getResponseHeaders(HttpServletResponse response) {
Map<String, String> headers = new HashMap<>();
Collection<String> headerNames = response.getHeaderNames();
for (String name : headerNames) {
headers.put(name, response.getHeader(name));
}
return headers;
}
// 辅助方法:获取客户端真实IP
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
// 辅助方法:获取请求体内容
private String getRequestBody(HttpServletRequest request) {
try {
if (request instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
return new String(wrapper.getContentAsByteArray(), request.getCharacterEncoding());
}
return "NOT_CACHED_REQUEST_BODY";
} catch (Exception e) {
log.warn("获取请求体失败", e);
return "ERROR_READING_BODY";
}
}
// 异步保存API记录
@Async
public void asyncSaveApiRecord(ApiRecordEntity record) {
try {
apiRecordMapper.insert(record);
} catch (Exception e) {
log.error("保存API调用记录失败", e);
}
}
}
4.过滤器里拦截
package com.zx.base.common.filter.log;
import com.zx.framework.security.pojo.UserInfo;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@RequiredArgsConstructor
@Slf4j
public class ContentCachingFilter implements Filter {
private final ApiRecordService apiRecordService;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化逻辑
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String path = httpRequest.getRequestURI();
// 关键点1:必须每次创建新的包装器实例
// 包装响应(关键改进:使用新的FixedResponseWrapper)
FixedResponseWrapper wrapper = new FixedResponseWrapper((HttpServletResponse) response);
response.reset();
response.resetBuffer();
try {
chain.doFilter(request, wrapper);
if (isSensitivePath(path)) {
UserInfo userInfo = (UserInfo) request.getAttribute("userInfo");
apiRecordService.addApiRecord(userInfo,httpRequest,httpServletResponse,wrapper);
}
} catch (Exception e) {
log.error("doFilter: ",e );
// 异常处理
}finally {
// 关键改进:确保响应被写回
if (!wrapper.isCommitted() && wrapper.getContentAsBytes().length > 0) {
try {
wrapper.copyToResponse();
} catch (IllegalStateException e) {
log.error("响应已提交,无法写入:", e);
}
}
}
}
@Override
public void destroy() {
// 清理资源
}
/**
* 是否是要校验的地址.
*
* @param path
* @return
*/
private boolean isSensitivePath(String path) {
return path.contains("sensitive")
|| path.contains("ocr")
|| path.contains("fadada")
|| path.contains("baidu-map")
|| path.contains("wps")
|| path.contains("amap");
}
}

1268

被折叠的 条评论
为什么被折叠?



