有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

JavaSpringBoot:从请求中读取特定字段并在响应中设置它的拦截器

我们的Spring Rest控制器处理的所有请求和响应都有一个公共部分,该部分具有某些值:

{
    "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

如果没有,或者甚至没有,那么最好的方法是什么


共 (4) 个答案

  1. # 1 楼答案

    快速回答:RequestBodyAdviceResponseBodyAdvice在同一个线程中为一个请求调用

    可以在以下位置调试实现:^{}

    但你这样做并不安全:

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

    “更安全的方式”:在afterBodyRead方法中,使请求主体支持任何类(而不仅仅是MyGenericPojo):

    • 第一次呼叫ThreadLocal#remove
    • 检查类型是否为MyGenericPojo,然后将公共数据设置为threadlocal
  2. # 2 楼答案

    我认为不需要你自己的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可以替换为

    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)RequestResponseDevice本身是无状态的。我们使用了RequestContextHolder.getRequestAttributes(),它是ThreadLocal,并声明为

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

    ThreadLocal javadoc声明:

    his class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.

    所以我没有看到任何线程安全问题

  3. # 3 楼答案

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

    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-将工作转移到服务层,在那里你可以通过一个可重用的通用方法来完成cope,或者让它通过AOP运行

    public void copyMetaData(whatEverType request,whatEverType response) {
        response.setMeta(request.getMeta);
    
    }
    
  4. # 4 楼答案

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

    在这个场景中,我会使用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,它标记了应该进行充实的端点