簡體   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