如何将HttpServletRequest与HttpServletResponse对象转发至其他服务
实现方案
分为业务服务侧过滤器改造、通用日志存储服务开发两部分,核心注意点是HttpServletRequest/HttpServletResponse为Servlet容器绑定的本地对象,无法直接跨服务传输,需先提取可序列化原始字段后再发送。
1. 业务服务侧Servlet过滤器实现
1.1 核心改造逻辑
- 用可重复读取的Wrapper包装请求响应对象,避免读取请求/响应体后业务逻辑无法获取数据,Spring Web环境可直接使用自带的
ContentCachingRequestWrapper、ContentCachingResponseWrapper,无Spring依赖可自行实现缓存请求/响应体的Wrapper类 - 过滤器优先级设为最高,确保所有请求都能被拦截
- 业务逻辑执行完成后再提取响应相关数据,异步发送到通用服务,不要阻塞业务主线程
- 增加降级逻辑:通用服务不可用时直接丢弃日志/本地暂存,不影响正常业务请求
1.2 参考代码
// 日志传输DTO,可序列化 @Data @Builder public class AccessLogRawDTO implements Serializable { // 业务服务标识,用于通用服务区分日志来源 private String serviceCode; // 请求原始数据 private Map<String, String> requestHeaders; private String requestUri; private String requestMethod; private byte[] requestBody; private String remoteAddr; // 响应原始数据 private Integer responseStatus; private Map<String, String> responseHeaders; private byte[] responseBody; // 请求时间戳 private Long timestamp; }
// 过滤器实现 @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class AccessLogFilter implements Filter { private final AsyncLogSender asyncLogSender; // 最大存储的请求/响应体长度,超过则截断 private static final int MAX_BODY_LENGTH = 1024 * 1024; public AccessLogFilter(AsyncLogSender asyncLogSender) { this.asyncLogSender = asyncLogSender; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ContentCachingRequestWrapper reqWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request); ContentCachingResponseWrapper respWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response); try { // 正常执行业务逻辑 chain.doFilter(reqWrapper, respWrapper); } finally { // 组装原始日志DTO AccessLogRawDTO.AccessLogRawDTOBuilder dtoBuilder = AccessLogRawDTO.builder() .serviceCode("your-service-code") .requestHeaders(Collections.list(reqWrapper.getHeaderNames()) .stream() .collect(Collectors.toMap(k -> k, reqWrapper::getHeader))) .requestUri(reqWrapper.getRequestURI()) .requestMethod(reqWrapper.getMethod()) .remoteAddr(reqWrapper.getRemoteAddr()) .responseStatus(respWrapper.getStatusCode()) .responseHeaders(respWrapper.getHeaderNames() .stream() .collect(Collectors.toMap(k -> k, respWrapper::getHeader))) .timestamp(System.currentTimeMillis()); // 截断过长的请求/响应体 byte[] reqBody = reqWrapper.getContentAsByteArray(); dtoBuilder.requestBody(reqBody.length > MAX_BODY_LENGTH ? Arrays.copyOf(reqBody, MAX_BODY_LENGTH) : reqBody); byte[] respBody = respWrapper.getContentAsByteArray(); dtoBuilder.responseBody(respBody.length > MAX_BODY_LENGTH ? Arrays.copyOf(respBody, MAX_BODY_LENGTH) : respBody); // 异步发送日志到通用服务 asyncLogSender.sendAsync(dtoBuilder.build()); // 把缓存的响应内容写回给客户端,必须执行 respWrapper.copyBodyToResponse(); } } }
2. 通用日志存储服务实现
2.1 API设计
对外提供HTTP POST接口,路径可定义为/api/v1/access-log/collect,接收参数为上述AccessLogRawDTO,接口返回成功/失败标识即可。
如果日志量级较大,可替换为Kafka等消息队列接收日志,实现削峰填谷,避免突发流量打垮存储服务。
2.2 字段提取与存储逻辑
所有字段清洗、提取逻辑全部放在通用服务内完成,业务侧无需感知:
- 提取请求头中的traceId、用户标识、设备信息等自定义字段
- 对URI做标准化格式化,提取路径参数、业务标识
- 对状态码做分类打标,比如2xx为成功、4xx为客户端错误、5xx为服务端错误
- 敏感字段脱敏:对请求/响应体中的密码、手机号、身份证号等敏感信息做替换处理
- 处理完成后将结构化数据存入MongoDB,可按服务标识、日期做集合拆分,提升查询性能
2.3 兼容扩展
预留自定义字段扩展位,不同业务服务如果有特殊日志字段需要存储,可放在DTO的扩展字段中,通用服务无需修改代码即可兼容存储。
内容的提问来源于stack exchange,提问作者Aditya Joshi
相关产品推荐
相关产品推荐

