第7篇:Web集成模块 - HTTP请求的全链路追踪

前言

前面我们实现了链路追踪的核心机制,本章将其与Web环境深度集成,实现HTTP请求级别的全链路追踪。这是将框架应用到实际Web应用的关键一步。

Web集成的核心挑战

HTTP请求的生命周期追踪

graph LR A[HTTP请求] --> B[TraceIdFilter] B --> C[RequestLogFilter] C --> D[LogHandlerInterceptor] D --> E[Controller方法] E --> F[Service方法] F --> G[响应返回]

集成要点:

  • 🎯 请求入口:在请求开始时初始化追踪上下文
  • 🔄 上下文传递:确保整个请求链路中上下文的一致性
  • 📊 请求统计:记录HTTP请求的完整信息
  • 🧹 资源清理:在请求结束时清理ThreadLocal

TraceIdFilter - 追踪ID过滤器

java 复制代码
package com.simpleflow.log.web.filter;

import com.simpleflow.log.context.ThreadLocalTraceHolder;
import com.simpleflow.log.context.TraceContext;
import com.simpleflow.log.util.RequestIdGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 追踪ID过滤器
 * 
 * 为每个HTTP请求初始化或传递追踪上下文
 * 支持从HTTP头中读取上游传递的追踪信息
 */
@Component
@Order(1) // 最高优先级
public class TraceIdFilter implements Filter {
    
    private static final Logger logger = LoggerFactory.getLogger(TraceIdFilter.class);
    
    // HTTP头常量
    private static final String HEADER_REQUEST_ID = "X-Request-ID";
    private static final String HEADER_TRACE_ID = "X-Trace-ID";
    private static final String HEADER_USER_ID = "X-User-ID";
    private static final String HEADER_SESSION_ID = "X-Session-ID";
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        try {
            // 初始化追踪上下文
            TraceContext traceContext = initializeTraceContext(httpRequest);
            
            // 将追踪ID写入响应头
            writeTraceHeaders(httpResponse, traceContext);
            
            logger.debug("请求开始 - URI: {}, 追踪信息: {}", 
                httpRequest.getRequestURI(), traceContext);
            
            // 继续处理请求
            chain.doFilter(request, response);
            
        } finally {
            // 清理ThreadLocal,防止内存泄漏
            ThreadLocalTraceHolder.clearCurrentTrace();
            logger.debug("请求结束 - URI: {}, 上下文已清理", httpRequest.getRequestURI());
        }
    }
    
    /**
     * 初始化追踪上下文
     */
    private TraceContext initializeTraceContext(HttpServletRequest request) {
        // 尝试从HTTP头中获取上游传递的追踪信息
        String requestId = getHeaderValue(request, HEADER_REQUEST_ID);
        String traceId = getHeaderValue(request, HEADER_TRACE_ID);
        String userId = getHeaderValue(request, HEADER_USER_ID);
        String sessionId = getHeaderValue(request, HEADER_SESSION_ID);
        
        TraceContext traceContext;
        
        if (traceId != null) {
            // 如果有上游追踪ID,继续使用
            traceContext = new TraceContext(
                requestId != null ? requestId : RequestIdGenerator.generate(),
                traceId
            );
            logger.debug("继续上游追踪链路 - TraceID: {}", traceId);
        } else {
            // 否则创建新的追踪链路
            traceContext = ThreadLocalTraceHolder.initTrace();
            logger.debug("开始新的追踪链路 - TraceID: {}", traceContext.getTraceId());
        }
        
        // 设置用户和会话信息
        if (userId != null) {
            traceContext.setUserId(userId);
        }
        if (sessionId != null) {
            traceContext.setSessionId(sessionId);
        }
        
        // 添加HTTP请求信息
        traceContext.addExtra("method", request.getMethod());
        traceContext.addExtra("uri", request.getRequestURI());
        traceContext.addExtra("remoteAddr", getClientIpAddress(request));
        traceContext.addExtra("userAgent", request.getHeader("User-Agent"));
        
        // 设置到ThreadLocal
        ThreadLocalTraceHolder.setCurrentTrace(traceContext);
        
        return traceContext;
    }
    
    /**
     * 将追踪信息写入响应头
     */
    private void writeTraceHeaders(HttpServletResponse response, TraceContext traceContext) {
        if (traceContext != null) {
            response.setHeader(HEADER_REQUEST_ID, traceContext.getRequestId());
            response.setHeader(HEADER_TRACE_ID, traceContext.getTraceId());
            
            if (traceContext.getUserId() != null) {
                response.setHeader(HEADER_USER_ID, traceContext.getUserId());
            }
        }
    }
    
    /**
     * 获取HTTP头值
     */
    private String getHeaderValue(HttpServletRequest request, String headerName) {
        String value = request.getHeader(headerName);
        return (value != null && !value.trim().isEmpty()) ? value.trim() : null;
    }
    
    /**
     * 获取客户端真实IP地址
     */
    private String getClientIpAddress(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        
        String xRealIp = request.getHeader("X-Real-IP");
        if (xRealIp != null && !xRealIp.isEmpty()) {
            return xRealIp;
        }
        
        return request.getRemoteAddr();
    }
}

RequestLogFilter - 请求日志过滤器

java 复制代码
package com.simpleflow.log.web.filter;

import com.simpleflow.log.context.ThreadLocalTraceHolder;
import com.simpleflow.log.web.utils.RequestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 请求日志过滤器
 * 
 * 记录HTTP请求的详细信息,包括请求参数、响应状态、执行时间等
 */
@Component
@Order(2) // 在TraceIdFilter之后执行
public class RequestLogFilter implements Filter {
    
    private static final Logger logger = LoggerFactory.getLogger(RequestLogFilter.class);
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        long startTime = System.currentTimeMillis();
        String requestId = ThreadLocalTraceHolder.getCurrentRequestId();
        
        try {
            // 记录请求开始日志
            logRequestStart(httpRequest, requestId);
            
            // 继续处理请求
            chain.doFilter(request, response);
            
            // 记录请求完成日志
            long duration = System.currentTimeMillis() - startTime;
            logRequestEnd(httpRequest, httpResponse, requestId, duration);
            
        } catch (Exception e) {
            // 记录请求异常日志
            long duration = System.currentTimeMillis() - startTime;
            logRequestError(httpRequest, requestId, duration, e);
            throw e;
        }
    }
    
    /**
     * 记录请求开始日志
     */
    private void logRequestStart(HttpServletRequest request, String requestId) {
        if (!logger.isInfoEnabled()) {
            return;
        }
        
        String method = request.getMethod();
        String uri = request.getRequestURI();
        String queryString = request.getQueryString();
        String clientIp = RequestUtils.getClientIpAddress(request);
        
        StringBuilder logMsg = new StringBuilder();
        logMsg.append("HTTP请求开始 - ");
        logMsg.append("RequestID: ").append(requestId).append(", ");
        logMsg.append("Method: ").append(method).append(", ");
        logMsg.append("URI: ").append(uri);
        
        if (queryString != null) {
            logMsg.append("?").append(queryString);
        }
        
        logMsg.append(", ClientIP: ").append(clientIp);
        
        // 记录重要的HTTP头
        String userAgent = request.getHeader("User-Agent");
        if (userAgent != null) {
            logMsg.append(", UserAgent: ").append(userAgent);
        }
        
        String contentType = request.getContentType();
        if (contentType != null) {
            logMsg.append(", ContentType: ").append(contentType);
        }
        
        logger.info(logMsg.toString());
    }
    
    /**
     * 记录请求完成日志
     */
    private void logRequestEnd(HttpServletRequest request, HttpServletResponse response, 
                              String requestId, long duration) {
        if (!logger.isInfoEnabled()) {
            return;
        }
        
        String method = request.getMethod();
        String uri = request.getRequestURI();
        int status = response.getStatus();
        
        StringBuilder logMsg = new StringBuilder();
        logMsg.append("HTTP请求完成 - ");
        logMsg.append("RequestID: ").append(requestId).append(", ");
        logMsg.append("Method: ").append(method).append(", ");
        logMsg.append("URI: ").append(uri).append(", ");
        logMsg.append("Status: ").append(status).append(", ");
        logMsg.append("Duration: ").append(duration).append("ms");
        
        // 根据响应状态选择日志级别
        if (status >= 500) {
            logger.error(logMsg.toString());
        } else if (status >= 400) {
            logger.warn(logMsg.toString());
        } else {
            logger.info(logMsg.toString());
        }
    }
    
    /**
     * 记录请求异常日志
     */
    private void logRequestError(HttpServletRequest request, String requestId, 
                               long duration, Exception e) {
        String method = request.getMethod();
        String uri = request.getRequestURI();
        
        StringBuilder logMsg = new StringBuilder();
        logMsg.append("HTTP请求异常 - ");
        logMsg.append("RequestID: ").append(requestId).append(", ");
        logMsg.append("Method: ").append(method).append(", ");
        logMsg.append("URI: ").append(uri).append(", ");
        logMsg.append("Duration: ").append(duration).append("ms, ");
        logMsg.append("Error: ").append(e.getMessage());
        
        logger.error(logMsg.toString(), e);
    }
}

LogHandlerInterceptor - 拦截器实现

java 复制代码
package com.simpleflow.log.web.interceptor;

import com.simpleflow.log.context.ThreadLocalTraceHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 日志处理拦截器
 * 
 * 在Controller方法执行前后记录详细的处理信息
 */
@Component
public class LogHandlerInterceptor implements HandlerInterceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(LogHandlerInterceptor.class);
    
    private static final String START_TIME_ATTRIBUTE = "startTime";
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                           Object handler) throws Exception {
        
        if (!logger.isDebugEnabled()) {
            return true;
        }
        
        // 记录开始时间
        request.setAttribute(START_TIME_ATTRIBUTE, System.currentTimeMillis());
        
        String requestId = ThreadLocalTraceHolder.getCurrentRequestId();
        String handlerName = handler.getClass().getSimpleName();
        
        logger.debug("Controller处理开始 - RequestID: {}, Handler: {}, URI: {}", 
            requestId, handlerName, request.getRequestURI());
        
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, 
                          Object handler, ModelAndView modelAndView) throws Exception {
        
        if (!logger.isDebugEnabled()) {
            return;
        }
        
        Long startTime = (Long) request.getAttribute(START_TIME_ATTRIBUTE);
        if (startTime != null) {
            long duration = System.currentTimeMillis() - startTime;
            String requestId = ThreadLocalTraceHolder.getCurrentRequestId();
            
            logger.debug("Controller处理完成 - RequestID: {}, Duration: {}ms, Status: {}", 
                requestId, duration, response.getStatus());
        }
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                              Object handler, Exception ex) throws Exception {
        
        if (ex != null) {
            String requestId = ThreadLocalTraceHolder.getCurrentRequestId();
            logger.error("Controller处理异常 - RequestID: {}, Error: {}", 
                requestId, ex.getMessage(), ex);
        }
        
        // 清理请求属性
        request.removeAttribute(START_TIME_ATTRIBUTE);
    }
}

RequestUtils - 请求工具类

java 复制代码
package com.simpleflow.log.web.utils;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * 请求工具类
 * 
 * 提供HTTP请求信息提取的工具方法
 */
public class RequestUtils {
    
    /**
     * 获取客户端真实IP地址
     */
    public static String getClientIpAddress(HttpServletRequest request) {
        // 检查是否通过代理
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
            // 多级代理的情况,第一个IP为客户端真实IP
            return xForwardedFor.split(",")[0].trim();
        }
        
        String xRealIp = request.getHeader("X-Real-IP");
        if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
            return xRealIp;
        }
        
        String proxyClientIp = request.getHeader("Proxy-Client-IP");
        if (proxyClientIp != null && !proxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(proxyClientIp)) {
            return proxyClientIp;
        }
        
        String wlProxyClientIp = request.getHeader("WL-Proxy-Client-IP");
        if (wlProxyClientIp != null && !wlProxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(wlProxyClientIp)) {
            return wlProxyClientIp;
        }
        
        return request.getRemoteAddr();
    }
    
    /**
     * 获取所有请求头
     */
    public static Map<String, String> getAllHeaders(HttpServletRequest request) {
        Map<String, String> headers = new HashMap<>();
        
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String headerValue = request.getHeader(headerName);
            headers.put(headerName, headerValue);
        }
        
        return headers;
    }
    
    /**
     * 获取所有请求参数
     */
    public static Map<String, String[]> getAllParameters(HttpServletRequest request) {
        return new HashMap<>(request.getParameterMap());
    }
    
    /**
     * 构建完整的请求URL
     */
    public static String getFullRequestUrl(HttpServletRequest request) {
        StringBuilder url = new StringBuilder();
        url.append(request.getScheme()).append("://");
        url.append(request.getServerName());
        
        if (request.getServerPort() != 80 && request.getServerPort() != 443) {
            url.append(":").append(request.getServerPort());
        }
        
        url.append(request.getRequestURI());
        
        if (request.getQueryString() != null) {
            url.append("?").append(request.getQueryString());
        }
        
        return url.toString();
    }
    
    /**
     * 检查是否为Ajax请求
     */
    public static boolean isAjaxRequest(HttpServletRequest request) {
        String requestedWith = request.getHeader("X-Requested-With");
        return "XMLHttpRequest".equals(requestedWith);
    }
    
    /**
     * 获取用户Agent信息
     */
    public static String getUserAgent(HttpServletRequest request) {
        return request.getHeader("User-Agent");
    }
}

Web配置类

java 复制代码
package com.simpleflow.log.web.config;

import com.simpleflow.log.web.interceptor.LogHandlerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web日志配置
 */
@Configuration
public class WebLogConfiguration implements WebMvcConfigurer {
    
    @Autowired
    private LogHandlerInterceptor logHandlerInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logHandlerInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/health", "/actuator/**");
    }
}

实战测试

java 复制代码
@RestController
@RequestMapping("/api/users")
@LogClass(prefix = "用户API")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    @LogMethod(logArgs = true, logResult = true)
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    @LogMethod(sensitiveFields = {"password"})
    public ResponseEntity<User> createUser(@RequestBody UserCreateRequest request) {
        User user = userService.createUser(request);
        return ResponseEntity.ok(user);
    }
}

日志输出示例:

yaml 复制代码
2024-08-23 10:30:15.123 INFO  - HTTP请求开始 - RequestID: REQ_20240823103015123_host_001, Method: GET, URI: /api/users/1, ClientIP: 192.168.1.100
2024-08-23 10:30:15.124 DEBUG - Controller处理开始 - RequestID: REQ_20240823103015123_host_001, Handler: UserController, URI: /api/users/1
2024-08-23 10:30:15.125 INFO  - 用户API - UserService.findById开始执行,参数:[1]
2024-08-23 10:30:15.150 INFO  - 用户API - UserService.findById执行成功,耗时:25ms,返回值:{"id":1,"name":"张三"}
2024-08-23 10:30:15.151 DEBUG - Controller处理完成 - RequestID: REQ_20240823103015123_host_001, Duration: 27ms, Status: 200
2024-08-23 10:30:15.152 INFO  - HTTP请求完成 - RequestID: REQ_20240823103015123_host_001, Method: GET, URI: /api/users/1, Status: 200, Duration: 29ms

本章小结

✅ 完成的任务

  1. TraceIdFilter:实现请求级别的追踪上下文初始化
  2. RequestLogFilter:记录HTTP请求的完整生命周期
  3. LogHandlerInterceptor:在Controller层面记录处理信息
  4. RequestUtils:提供HTTP请求信息提取工具
  5. 集成配置:完成Web环境的自动配置

🎯 学习要点

  • 过滤器链的执行顺序和职责分工
  • HTTP头传递实现分布式追踪
  • ThreadLocal管理在Web环境中的重要性
  • 资源清理防止内存泄漏的机制

💡 思考题

  1. 如何处理文件上传请求的日志记录?
  2. 微服务间如何传递追踪信息?
  3. 如何优化高并发下的日志性能?

🚀 下章预告

下一章我们将开发Spring Boot Starter,学习如何创建自己的自动配置模块,让框架可以像官方组件一样开箱即用。


💡 设计原则 : Web集成的关键在于无侵入、全覆盖、高性能。通过Filter和Interceptor的配合,我们实现了HTTP请求的全链路追踪。

相关推荐
IT_陈寒18 分钟前
Python性能优化必知必会:7个让代码快3倍的底层技巧与实战案例
前端·人工智能·后端
小猪咪piggy25 分钟前
【JavaEE】(18) MyBatis 进阶
java·java-ee·mybatis
多读书19326 分钟前
JavaEE进阶-文件操作与IO流核心指南
java·java-ee
叫我阿柒啊28 分钟前
Java全栈工程师的实战面试:从基础到微服务的全面解析
java·数据库·vue.js·spring boot·微服务·前端开发·全栈开发
练习时长两年半的Java练习生(升级中)32 分钟前
从0开始学习Java+AI知识点总结-27.web实战(Maven高级)
java·学习·maven
拾忆,想起1 小时前
Redis发布订阅:实时消息系统的极简解决方案
java·开发语言·数据库·redis·后端·缓存·性能优化
艾莉丝努力练剑1 小时前
【C语言16天强化训练】从基础入门到进阶:Day 14
java·c语言·学习·算法
BioRunYiXue1 小时前
FRET、PLA、Co-IP和GST pull-down有何区别? 应该如何选择?
java·服务器·网络·人工智能·网络协议·tcp/ip·eclipse
SimonKing1 小时前
想搭建知识库?Dify、MaxKB、Pandawiki 到底哪家强?
java·后端·程序员
程序员清风1 小时前
为什么Tomcat可以把线程数设置为200,而不是2N?
java·后端·面试