记录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");
    }

}
Logo

加入社区!打开量化的大门,首批课程上线啦!

更多推荐