繁体   English   中英

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

[英]SpringBoot: Interceptor to read particular field from request and set it in the response

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

{
    "common": {
        "requestId": "foo-bar-123",
        "otherKey1": "value1",
        "otherKey2": "value2",
        "otherKey3": "value3"
    },
    ...
}

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

我尝试使用ControllerAdviceThreadLocal来做到这一点:

@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;
  }
}

这在我测试一次发送一个请求时有效。 但是,当多个请求到来时,是否可以保证在同一个线程中调用afterBodyReadbeforeBodyWrite

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

我认为不需要你自己的ThreadLocal你可以使用请求属性。

@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;
}

编辑

Optional s 可以替换为

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

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

编辑 2

关于线程安全

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

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

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
        new NamedThreadLocal<>("Request attributes");

和 ThreadLocal javadoc 指出:

他的类提供线程局部变量。 这些变量不同于它们的普通对应变量,因为每个访问一个(通过其 get 或 set 方法)的线程都有自己的、独立初始化的变量副本。

所以我没有看到这个 sulotion 有任何线程安全问题。

快速回答: RequestBodyAdviceResponseBodyAdvice在同一线程内针对一个请求调用。

您可以在以下ServletInvocableHandlerMethod#invokeAndHandle调试实现: ServletInvocableHandlerMethod#invokeAndHandle

但是,您这样做的方式并不安全:

  • ThreadLocal应该定义为static final ,否则它类似于任何其他类属性
  • body 中抛出的异常将跳过ResponseBodyAdvice调用(因此不会删除线程本地数据)

“更安全的方式”:在afterBodyRead方法MyGenericPojo请求正文支持任何类(不仅仅是MyGenericPojo ):

  • 首先调用ThreadLocal#remove
  • 检查类型是否为MyGenericPojo然后将公共数据设置为 threadlocal

我也已经回答了这个线程,但我更喜欢另一种方法来解决此类问题

我会在这种情况下使用 Aspect-s。

我已经将它包含在一个文件中,但您应该创建适当的单独类。

@Aspect
@Component
public class CommonEnricher {

    // annotation to mark methods that should be intercepted
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface EnrichWithCommon {
    }


    @Configuration
    @EnableAspectJAutoProxy
    public static class CommonEnricherConfig {}

    // Around query to select methods annotiated with @EnrichWithCommon
    @Around("@annotation(com.example.CommonEnricher.EnrichWithCommon)")
    public Object enrich(ProceedingJoinPoint joinPoint) throws Throwable {
        MyGenericPojo myGenericPojo = (MyGenericPojo) joinPoint.getArgs()[0];

        var common = myGenericPojo.getCommon();
        if (common.getRequestId() == null) {
            common.setRequestId(UUID.randomUUID().toString());
        }

        //actual rest controller  method invocation
        MyGenericPojo res = (MyGenericPojo) joinPoint.proceed();

        //adding common to body
        res.setCommon(common);
        return res;
    }

    //example controller
    @RestController
    @RequestMapping("/")
    public static class MyRestController {

        @PostMapping("/test" )
        @EnrichWithCommon // mark method to intercept
        public MyGenericPojo test(@RequestBody  MyGenericPojo myGenericPojo) {
            return myGenericPojo;
        }
    }
}

我们在这里有一个注释@EnrichWithCommon ,它标记了应该发生富集的端点。

如果它只是从请求复制到响应的元数据,则可以执行以下操作之一:

1-将元存储在请求/响应标头中,只需使用过滤器进行复制:

@WebFilter(filterName="MetaDatatFilter", urlPatterns ={"/*"})
public class MyFilter implements Filter{
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {

    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;        
    httpServletResponse.setHeader("metaData", httpServletRequest.getHeader("metaData"));        
}

}

2- 将工作移至服务层,您可以在其中通过可重用的通用方法进行处理,或通过 AOP 运行

public void copyMetaData(whatEverType request,whatEverType response) {
    response.setMeta(request.getMeta);

}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM