SpringBoot: Interceptor для чтения определенного поля из запроса и установки его в ответ - PullRequest
5 голосов
/ 02 марта 2020

Все запросы и ответы, обрабатываемые нашим Spring Rest Controller, имеют раздел Common, который имеет определенные значения:

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

В настоящее время все функции моего контроллера читают common и копируют его в ответ вручную. Я хотел бы переместить его в какой-то перехватчик.

Я пытался сделать это, используя ControllerAdvice и 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;
  }
}

Это работает, когда я тестирую отправку одного запроса за раз. Но гарантируется ли, что afterBodyRead и beforeBodyWrite вызывается в одном потоке, когда поступает несколько запросов?

Если нет, или даже иначе, каков наилучший способ сделать это?

Ответы [ 4 ]

3 голосов
/ 10 марта 2020

Я думаю, что вам не нужны ваши собственные 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) стандартное веб-приложение Spring на основе сервлетов у нас есть поток-запрос сценарий. Запрос обрабатывается одним из рабочих потоков через все фильтры и процедуры. Цепочка обработки будет выполняться одним и тем же потоком от начала до конца. Так что afterBodyRead и beforeBodyWrite гарантированно будут выполняться одним и тем же потоком для данного запроса.

2) Ваш RequestResponseAdvice сам по себе не имеет состояния. Мы использовали RequestContextHolder.getRequestAttributes(), который является ThreadLocal и объявлен как

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

И ThreadLocal javado c сообщает:

его класс предоставляет локальные переменные потока. Эти переменные отличаются от своих обычных аналогов тем, что каждый поток, который обращается к одному (через метод get или set), имеет свою собственную, независимо инициализированную копию переменной.

Так что я не вижу ни одного потока вопросы безопасности в этом sulotion.

1 голос
/ 10 марта 2020

Быстрый ответ: RequestBodyAdvice и ResponseBodyAdvice вызываются в одном потоке для одного запроса.

Вы можете отладить реализацию по адресу: ServletInvocableHandlerMethod#invokeAndHandle

То, как вы это делаете, небезопасно:

  • ThreadLocal следует определять как static final, в противном случае оно похоже на любое другое свойство класса
  • Исключение, выдаваемое в тело пропустит вызов ResponseBodyAdvice (следовательно, данные локального потока не удаляются)

«Более безопасный способ»: сделать так, чтобы тело запроса поддерживало любой класс (не только MyGenericPojo), в afterBodyRead метод:

  • Первый вызов ThreadLocal#remove
  • Проверьте, указан ли тип MyGenericPojo, затем установите для общих данных значение threadlocal
0 голосов
/ 10 марта 2020

Также я уже ответил на эту тему, но я предпочитаю другой способ решения таких проблем

Я бы использовал 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, которая отмечает конечные точки, где должно произойти обогащение.

0 голосов
/ 09 марта 2020

Если вы копируете только метаданные из запроса в ответ, вы можете выполнить одно из следующих действий:

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);

}
...