Spring Boot中如何同时追踪自研API与第三方API调用
解决方案:在RestTemplateInterceptor中获取自研API请求信息
以下是几种适配Spring Boot 3.1.0的实用方案,可实现自研API与第三方API调用信息的关联追踪:
方法一:利用Spring内置RequestContextHolder直接获取
Spring的RequestContextHolder会将当前请求的HttpServletRequest绑定到线程上下文(同步调用场景默认生效),可直接在RestTemplateInterceptor中提取自研API的请求信息:
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import java.io.IOException; public class RestTemplateTrackingInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { // 获取自研API的请求上下文 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { HttpServletRequest selfRequest = attributes.getRequest(); // 提取自研API核心请求信息 String selfApiUrl = selfRequest.getRequestURL().toString(); String selfHttpMethod = selfRequest.getMethod(); String clientIp = selfRequest.getRemoteAddr(); // 执行第三方API调用并计算耗时 long startTime = System.currentTimeMillis(); ClientHttpResponse response = execution.execute(request, body); long duration = System.currentTimeMillis() - startTime; // 关联自研与第三方API信息,存入数据库 saveTrackingRecord(selfApiUrl, selfHttpMethod, clientIp, request.getURI().toString(), request.getMethod().name(), response.getStatusCode().value(), duration); return response; } // 非请求触发的RestTemplate调用(如定时任务),直接执行不记录关联信息 return execution.execute(request, body); } // 自行实现写入API_CALLS_TRACKING表的逻辑 private void saveTrackingRecord(String selfApiUrl, String selfHttpMethod, String clientIp, String thirdPartyUrl, String thirdPartyMethod, int statusCode, long duration) { // Oracle数据库插入操作 } }
注意事项
- 同步调用场景下直接生效;若使用
@Async异步调用第三方API,需手动传递请求上下文:// 在控制器方法中开启异步时传递上下文 RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); CompletableFuture.runAsync(() -> { RequestContextHolder.setRequestAttributes(attributes); // RestTemplate调用逻辑 }); - 必须处理
RequestAttributes为null的情况,避免空指针异常。
方法二:自定义ThreadLocal存储请求信息
如果需要自定义存储字段或场景特殊,可通过ThreadLocal在自研API入口传递请求信息:
1. 定义ThreadLocal工具类
public class RequestInfoHolder { private static final ThreadLocal<RequestInfo> THREAD_LOCAL = new ThreadLocal<>(); public static void setRequestInfo(RequestInfo info) { THREAD_LOCAL.set(info); } public static RequestInfo getRequestInfo() { return THREAD_LOCAL.get(); } public static void clear() { THREAD_LOCAL.remove(); } // 自定义请求信息实体类 public static class RequestInfo { private String apiUrl; private String httpMethod; private String clientIp; public RequestInfo(String apiUrl, String httpMethod, String clientIp) { this.apiUrl = apiUrl; this.httpMethod = httpMethod; this.clientIp = clientIp; } // getter方法省略 } }
2. 在自研API控制器中存入信息
import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class CrawlController { private final RestTemplate restTemplate; private final TrackingService trackingService; // 构造注入依赖 public CrawlController(RestTemplate restTemplate, TrackingService trackingService) { this.restTemplate = restTemplate; this.trackingService = trackingService; } @GetMapping("/crawl") public String crawl(HttpServletRequest request) { RequestInfoHolder.RequestInfo requestInfo = new RequestInfoHolder.RequestInfo( request.getRequestURL().toString(), request.getMethod(), request.getRemoteAddr() ); RequestInfoHolder.setRequestInfo(requestInfo); try { // 调用第三方API String thirdPartyResponse = restTemplate.getForObject("https://third-party.com/data", String.class); // 记录自研API调用结果 trackingService.saveSelfApiRecord(requestInfo, "SUCCESS"); return thirdPartyResponse; } catch (Exception e) { trackingService.saveSelfApiRecord(requestInfo, "FAILURE"); throw e; } finally { // 必须清理ThreadLocal,防止内存泄漏 RequestInfoHolder.clear(); } } }
3. 在RestTemplateInterceptor中提取信息
import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import java.io.IOException; public class RestTemplateTrackingInterceptor implements ClientHttpRequestInterceptor { private final TrackingService trackingService; public RestTemplateTrackingInterceptor(TrackingService trackingService) { this.trackingService = trackingService; } @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { long startTime = System.currentTimeMillis(); ClientHttpResponse response = null; RequestInfoHolder.RequestInfo selfRequestInfo = RequestInfoHolder.getRequestInfo(); try { response = execution.execute(request, body); return response; } finally { if (selfRequestInfo != null) { long duration = System.currentTimeMillis() - startTime; int statusCode = response != null ? response.getStatusCode().value() : -1; String thirdPartyStatus = statusCode >= 200 && statusCode < 300 ? "SUCCESS" : "FAILURE"; // 记录第三方API追踪信息 trackingService.saveThirdPartyRecord(selfRequestInfo, request.getURI().toString(), request.getMethod().name(), thirdPartyStatus, duration); } } } }
注意事项
- 务必在
finally块中清理ThreadLocal,避免线程池场景下的内存泄漏。 - 可根据需求灵活扩展
RequestInfo的字段。
方法三:结合过滤器统一收集请求信息
若多个API需要追踪,可通过Filter统一拦截自研API请求,减少控制器重复代码:
import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; public class RequestTrackingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest httpRequest) { RequestInfoHolder.RequestInfo requestInfo = new RequestInfoHolder.RequestInfo( httpRequest.getRequestURL().toString(), httpRequest.getMethod(), httpRequest.getRemoteAddr() ); RequestInfoHolder.setRequestInfo(requestInfo); try { chain.doFilter(request, response); } finally { RequestInfoHolder.clear(); } } else { chain.doFilter(request, response); } } }
注册过滤器到Spring Boot:
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FilterConfig { @Bean public FilterRegistrationBean<RequestTrackingFilter> requestTrackingFilter() { FilterRegistrationBean<RequestTrackingFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new RequestTrackingFilter()); // 指定需要拦截的API路径 registrationBean.addUrlPatterns("/crawl"); return registrationBean; } }
内容的提问来源于stack exchange,提问作者ginbarca
相关产品推荐
相关产品推荐

