SpringBoot:拦截器从请求中读取特定字段并将其设置在响应中

dal*_*y42 5 java rest spring interceptor spring-boot

我们的 Spring Rest Controller 处理的所有请求和响应都有一个 Common 部分,该部分具有某些值:

{
    "common": {
        "requestId": "foo-bar-123",
        "otherKey1": "value1",
        "otherKey2": "value2",
        "otherKey3": "value3"
    },
    ...
}
Run Code Online (Sandbox Code Playgroud)

目前我所有的控制器功能都common在手动读取并将其复制到响应中。我想将它移动到某种拦截器中。

我尝试使用ControllerAdviceand来做到这一点ThreadLocal

@ControllerAdvice
public class RequestResponseAdvice extends RequestBodyAdviceAdapter
    implements ResponseBodyAdvice<MyGenericPojo> {

  private ThreadLocal<Common> commonThreadLocal = new ThreadLocal<>();

  /* Request */

  @Override
  public boolean supports(
      MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
    return MyGenericPojo.class.isAssignableFrom(methodParameter.getParameterType());
  }

  @Override
  public Object afterBodyRead(
      Object body,
      HttpInputMessage inputMessage,
      MethodParameter parameter,
      Type targetType,
      Class<? extends HttpMessageConverter<?>> converterType) {
    var common = (MyGenericPojo)body.getCommon();
    if (common.getRequestId() == null) {
       common.setRequestId(generateNewRequestId()); 
    }
    commonThreadLocal(common);
    return body;
  }

  /* Response */

  @Override
  public boolean supports(
      MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
    return MyGenericPojo.class.isAssignableFrom(returnType.getParameterType());
  }

  @Override
  public MyGenericPojo beforeBodyWrite(
      MyGenericPojo body,
      MethodParameter returnType,
      MediaType selectedContentType,
      Class<? extends HttpMessageConverter<?>> selectedConverterType,
      ServerHttpRequest request,
      ServerHttpResponse response) {
    body.setCommon(commonThreadLocal.get());
    commonThreadLocal.remove();
    return body;
  }
}
Run Code Online (Sandbox Code Playgroud)

这在我测试一次发送一个请求时有效。但是,它是保证afterBodyReadbeforeBodyWrite被称为在同一个线程,当多个请求来了?

如果没有,或者甚至没有,这样做的最佳方法是什么?

Non*_*ika 4

我认为没有必要ThreadLocal你自己使用request属性即可。

@Override
public Object afterBodyRead(
        Object body,
        HttpInputMessage inputMessage,
        MethodParameter parameter,
        Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType) {

    var common = ((MyGenericPojo) body).getCommon();
    if (common.getRequestId() == null) {
        common.setRequestId(generateNewRequestId());
    }

    Optional.ofNullable((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
            .map(ServletRequestAttributes::getRequest)
            .ifPresent(request -> {request.setAttribute(Common.class.getName(), common);});

    return body;
}


@Override
public MyGenericPojo beforeBodyWrite(
        MyGenericPojo body,
        MethodParameter returnType,
        MediaType selectedContentType,
        Class<? extends HttpMessageConverter<?>> selectedConverterType,
        ServerHttpRequest request,
        ServerHttpResponse response) {

    Optional.ofNullable(RequestContextHolder.getRequestAttributes())
            .map(rc -> rc.getAttribute(Common.class.getName(), RequestAttributes.SCOPE_REQUEST))
            .ifPresent(o -> {
                Common common = (Common) o;
                body.setCommon(common);
            });

    return body;
}
Run Code Online (Sandbox Code Playgroud)

编辑

Optionals 可以替换为

RequestContextHolder.getRequestAttributes().setAttribute(Common.class.getName(),common,RequestAttributes.SCOPE_REQUEST);

RequestContextHolder.getRequestAttributes().getAttribute(Common.class.getName(),RequestAttributes.SCOPE_REQUEST);
Run Code Online (Sandbox Code Playgroud)

编辑2

关于线程安全

1) 标准的基于 servlet 的 Spring Web 应用程序我们有每个请求一个线程的场景。请求由工作线程之一通过所有过滤器和例程进行处理。处理链从头到尾将由同一个线程执行。因此afterBodyReadbeforeBodyWrite保证对于给定的请求由同一线程执行。

2) 您的 RequestResponseAdvice 本身是无状态的。我们使用RequestContextHolder.getRequestAttributes()的是 ThreadLocal 并声明为

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
        new NamedThreadLocal<>("Request attributes");
Run Code Online (Sandbox Code Playgroud)

ThreadLocal javadoc 指出:

他的类提供了线程局部变量。这些变量与其正常对应变量的不同之处在于,访问一个变量(通过其 get 或 set 方法)的每个线程都有其自己的、独立初始化的变量副本。

所以我在这个解决方案中没有看到任何线程安全问题。