Spring 中的多个请求共享一个 ID

One ID shared between many requests in Spring

我想问一些类似 conversationId 的问题,它可以在一次 API 通话中在多个 requests/responses 之间共享。例如,我们有 /test 个端点,这个端点调用另一个服务,然后另一个 return 响应基于获得的结果。它看起来像:

  Conversation:
  1) Sending test request.
  2) API calls /info endpoint.
  3) Returns /info response.
  4) API calls /example endpoint.
  5) Returns /example response.
  6) Returns /test response.

当我们同时调用 /test API 4-5 次时,日志可能会很乱,我们不知道如何组合各个调用。

我想用任何随机请求范围 ID 标记它,例如高值数字。它可能看起来像:

  1) Sending test request of ID 12345.
  2) Sending test request of ID 98765.
  3) API calls /info endpoint of ID 12345.
  4) Returns /info response of ID 12345.
  5) API calls /info endpoint of ID 98765.
  6) API calls /example endpoint of ID 12345.
  7) Returns /info response of ID 98765.
  and so on

是否有任何 Spring 机制以请求方式在 ClientHttpRequestInterceptorGenericFilterBean 之间共享数据而不重新创建那些 bean?就像一个生命周期从 doFilter 开始并在那里结束的自定义作用域 bean。

1.使用侦探 (限制 - Spring Cloud Sleuth 是 Spring 基于引导的)

Spring sleuth 可以提供跟踪 ID 和跨度 ID。使用跟踪 ID,您可以组合单个调用。

下面的例子可能对某些人有用。

添加spring侦探

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>

你的/test

        @GetMapping("/test")
        public String test() {

            LOG.info("Sending test request.");
            LOG.info("API calls /info endpoint.");
            // Calling /info service or endpoint
            LOG.info("Returns /info response.");
            LOG.info("API calls /example endpoint.");
            // Calling /example service or endpoint
            LOG.info("Returns /example response.");
            LOG.info("Returns /test response");

            return "test";
        }

同时调用/test API 5次,服务器调用日志如下所示

2022-04-28 00:40:11.108  INFO [test,be0b352e1f8664d1,be0b352e1f8664d1] 10766 --- [nio-9091-exec-5] c.k.d.d.a.AccountServiceApplication      : Sending test request.
2022-04-28 00:40:11.109  INFO [test,29c2e863354fca33,29c2e863354fca33] 10766 --- [nio-9091-exec-1] c.k.d.d.a.AccountServiceApplication      : Sending test request.
2022-04-28 00:40:11.115  INFO [test,29c2e863354fca33,29c2e863354fca33] 10766 --- [nio-9091-exec-1] c.k.d.d.a.AccountServiceApplication      : API calls /info endpoint.
2022-04-28 00:40:11.115  INFO [test,29c2e863354fca33,29c2e863354fca33] 10766 --- [nio-9091-exec-1] c.k.d.d.a.AccountServiceApplication      : Returns /info response.
2022-04-28 00:40:11.115  INFO [test,29c2e863354fca33,29c2e863354fca33] 10766 --- [nio-9091-exec-1] c.k.d.d.a.AccountServiceApplication      : API calls /example endpoint.
2022-04-28 00:40:11.115  INFO [test,29c2e863354fca33,29c2e863354fca33] 10766 --- [nio-9091-exec-1] c.k.d.d.a.AccountServiceApplication      : Returns /example response.
2022-04-28 00:40:11.115  INFO [test,29c2e863354fca33,29c2e863354fca33] 10766 --- [nio-9091-exec-1] c.k.d.d.a.AccountServiceApplication      : Returns /test response
2022-04-28 00:40:11.115  INFO [test,be0b352e1f8664d1,be0b352e1f8664d1] 10766 --- [nio-9091-exec-5] c.k.d.d.a.AccountServiceApplication      : API calls /info endpoint.
2022-04-28 00:40:11.116  INFO [test,be0b352e1f8664d1,be0b352e1f8664d1] 10766 --- [nio-9091-exec-5] c.k.d.d.a.AccountServiceApplication      : Returns /info response.
2022-04-28 00:40:11.116  INFO [test,be0b352e1f8664d1,be0b352e1f8664d1] 10766 --- [nio-9091-exec-5] c.k.d.d.a.AccountServiceApplication      : API calls /example endpoint.
2022-04-28 00:40:11.116  INFO [test,be0b352e1f8664d1,be0b352e1f8664d1] 10766 --- [nio-9091-exec-5] c.k.d.d.a.AccountServiceApplication      : Returns /example response.
2022-04-28 00:40:11.116  INFO [test,be0b352e1f8664d1,be0b352e1f8664d1] 10766 --- [nio-9091-exec-5] c.k.d.d.a.AccountServiceApplication      : Returns /test response
2022-04-28 00:40:11.109  INFO [test,b26d689f73b974b7,b26d689f73b974b7] 10766 --- [nio-9091-exec-4] c.k.d.d.a.AccountServiceApplication      : Sending test request.
2022-04-28 00:40:11.116  INFO [test,b26d689f73b974b7,b26d689f73b974b7] 10766 --- [nio-9091-exec-4] c.k.d.d.a.AccountServiceApplication      : API calls /info endpoint.
2022-04-28 00:40:11.116  INFO [test,b26d689f73b974b7,b26d689f73b974b7] 10766 --- [nio-9091-exec-4] c.k.d.d.a.AccountServiceApplication      : Returns /info response.
2022-04-28 00:40:11.116  INFO [test,b26d689f73b974b7,b26d689f73b974b7] 10766 --- [nio-9091-exec-4] c.k.d.d.a.AccountServiceApplication      : API calls /example endpoint.
2022-04-28 00:40:11.116  INFO [test,b26d689f73b974b7,b26d689f73b974b7] 10766 --- [nio-9091-exec-4] c.k.d.d.a.AccountServiceApplication      : Returns /example response.
2022-04-28 00:40:11.116  INFO [test,b26d689f73b974b7,b26d689f73b974b7] 10766 --- [nio-9091-exec-4] c.k.d.d.a.AccountServiceApplication      : Returns /test response
2022-04-28 00:40:11.109  INFO [test,4f28a79ecc2ce6da,4f28a79ecc2ce6da] 10766 --- [nio-9091-exec-2] c.k.d.d.a.AccountServiceApplication      : Sending test request.
2022-04-28 00:40:11.109  INFO [test,ee7bfa89b05b69ad,ee7bfa89b05b69ad] 10766 --- [nio-9091-exec-3] c.k.d.d.a.AccountServiceApplication      : Sending test request.
etc....

请注意,应用程序名称位于开头的括号内。这些括号由 Sleuth 添加。它们代表应用程序名称、跟踪 ID 和跨度 ID。

此外,您可以通过编程方式获取当前轨迹。

        @Autowired
        private Tracer tracer;

        @GetMapping("/test")
        public String account() {

            LOG.info("Sending test request, " + "trace ID=" + tracer.currentSpan().context().traceId().toString());
            LOG.info("API calls /info endpoint, " + "trace ID=" + tracer.currentSpan().context().traceId().toString());
            // Calling /info service or endpoint
            LOG.info("Returns /info response, " + "trace ID=" + tracer.currentSpan().context().traceId().toString());
            LOG.info("API calls /example endpoint, " + "trace ID="
                    + tracer.currentSpan().context().traceId().toString());
            // Calling /example service or endpoint
            LOG.info("Returns /example response, " + "trace ID=" + tracer.currentSpan().context().traceId().toString());
            LOG.info("Returns /test response, " + "trace ID=" + tracer.currentSpan().context().traceId().toString());

            return "test";
        }

日志

....Sending test request, trace ID=51322ee614962951
.... API calls /info endpoint, trace ID=51322ee614962951
.... Sending test request, trace ID=4bc5017e2beceaf8
.... Sending test request, trace ID=75b638bda78a7bb5
.... Returns /info response, trace ID=51322ee614962951
.... API calls /info endpoint, trace ID=4bc5017e2beceaf8
.... API calls /example endpoint, trace ID=51322ee614962951
.... Sending test request, trace ID=f20cde67b4376777
  etc....


2。您可以使用线程上下文映射

        @GetMapping("/test")
        public String test() {
            ThreadContext.put("id", UUID.randomUUID().toString());
            LOG.info("Sending test request, " + ThreadContext.get("id"));
            LOG.info("API calls /info endpoint, " + ThreadContext.get("id"));
            // Calling /info service or endpoint
            LOG.info("Returns /info response, " + ThreadContext.get("id"));
            LOG.info("API calls /example endpoint, " + ThreadContext.get("id"));
            // Calling /example service or endpoint
            LOG.info("Returns /example response, " + ThreadContext.get("id"));
            LOG.info("Returns /test response, " + ThreadContext.get("id"));

            return "test";
        }

你可以使用MDC along with OncePerRequestFilter这样的东西来满足这种要求。:

    @Component
    public class MDCFilter implements OncePerRequestFilter{
    @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            MDC.put("request_ID", someNumberGeneratorUtility());
            try {
                filterChain.doFilter(request, response);
            } finally {
                MDC.remove("request_ID");
            }
        }
}

我已经创建了 RequestContext bean 作为我的请求数据的持有者,例如 conversationId:

@Component
@RequestScope
public class RequestContext {
    private final String conversationId;

    public RequestContext() {
        conversationId = UUID.randomUUID().toString();
    }

    public String getConversationId() {
        return conversationId;
    }
}

然后,将其注入我的过滤器:

@Component
public class LoggingFilter extends GenericFilterBean {

    private final RequestContext requestContext;

    public RestServerLoggingFilter(RequestContext requestContext) {
        this.requestContext = requestContext;
    }


...
}

但最重要的是配置和顺序:

@Configuration
public class RestServerConfiguration {

    private static final int LOGGING_FILTER_ORDER = -104;

    @Bean
    public FilterRegistrationBean<RestServerLoggingFilter> initFilter(LoggingFilter filter) {
        FilterRegistrationBean<RestServerLoggingFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(filter);
        registrationBean.setDispatcherTypes(
                EnumSet.complementOf(EnumSet.of(DispatcherType.ERROR))
        );
        registrationBean.setOrder(LOGGING_FILTER_ORDER);
        return registrationBean;
    }

}

过滤器堆叠到 ApplicationFilterChain class 下的列表中。当我们浏览这个堆栈时,我们有一个 OrderedRequestContextFilter bean,其顺序 -105 初始化 RequestScopeinitContextHolders 定义它的属性。了解这一点非常重要。然后您可以将请求的范围 bean 注入任何过滤器,即链中 OrderedRequestContextFilter 之后。