Как создать экземпляр подтипа c для параметра @RequestBody на основе запрошенного URI для метода контроллера Spring MVC? - PullRequest
0 голосов
/ 08 февраля 2020

Учитывая следующую базовую c модель домена:

abstract class BaseData { ... }

class DataA extends BaseData { ... }

class DataB extends BaseData { ... }

Я хочу написать конечную точку контроллера Spring MVC таким образом ...

@PostMapping(path="/{typeOfData}", ...)
ResponseEntity<Void> postData(@RequestBody BaseData baseData) { ... }

Требуемый конкретный тип baseData может быть выведен из typeOfData в пути.

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

Проблема, с которой я сталкиваюсь, заключается в как «сообщить» процессу десериализации, чтобы был создан конкретный конкретный тип.

Я могу придумать два способа сделать это.

Сначала используйте пользовательский HttpMessageConverter ...


  @Bean
  HttpMessageConverter httpMessageConverter() {

    return new MappingJackson2HttpMessageConverter() {
      @Override
      public Object read(final Type type, final Class<?> contextClass, final HttpInputMessage inputMessage)
          throws IOException, HttpMessageNotReadableException {

        // TODO How can I set this dynamically ?
        final Type subType = DataA.class;

        return super.read(subType, contextClass, inputMessage);
      }
    };
  }

... что дает мне задачу определить подтип на основе HttpInputMessage. Возможно, я мог бы использовать Filter, чтобы установить пользовательский заголовок раньше, когда URL доступен для меня, или я мог бы использовать ThreadLocal, также установленный через Filter. Ни один из них не звучит идеально для меня.

Мой второй подход заключается в том, чтобы снова использовать Filter и на этот раз обернуть входящую полезную нагрузку во внешний объект, который затем предоставит тип таким образом, который позволит Джексону выполнить работать через @JsonTypeInfo. На данный момент это, вероятно, мой предпочтительный подход.

Я исследовал HandlerMethodArgumentResolver, но если я пытаюсь зарегистрировать пользовательский, он регистрируется ПОСЛЕ RequestResponseBodyMethodProcessor, и этот класс имеет приоритет.

1 Ответ

0 голосов
/ 08 февраля 2020

Хм, поэтому, набрав все это, я быстро проверил что-то в RequestResponseBodyMethodProcessor, прежде чем опубликовать вопрос, и нашел другой путь для исследования, который работал аккуратно.

Извините @Configuration / @RestController / WebMvcConfigurer ma sh -up и publi c поля, все для краткости. Вот что сработало для меня и дало именно то, что я хотел:

@Configuration
@RestController
@RequestMapping("/dummy")
public class DummyController implements WebMvcConfigurer {

  @Target(ElementType.PARAMETER)
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  @interface BaseData {}

  public static class AbstractBaseData {}

  public static class DataA extends AbstractBaseData {
    public String a;
  }

  public static class DataB extends AbstractBaseData {
    public String b;
  }

  private final MappingJackson2HttpMessageConverter converter;

  DummyController(final MappingJackson2HttpMessageConverter converter) {
    this.converter = converter;
  }

  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {

    resolvers.add(
        new RequestResponseBodyMethodProcessor(Collections.singletonList(converter)) {

          @Override
          public boolean supportsParameter(MethodParameter parameter) {
            return parameter.hasParameterAnnotation(BaseData.class)
                && parameter.getParameterType() == AbstractBaseData.class;
          }

          @Override
          protected <T> Object readWithMessageConverters(
              NativeWebRequest webRequest, MethodParameter parameter, Type paramType)
              throws IOException, HttpMediaTypeNotSupportedException,
                  HttpMessageNotReadableException {

            final String uri =
                webRequest.getNativeRequest(HttpServletRequest.class).getRequestURI();

            return super.readWithMessageConverters(
                webRequest, parameter, determineActualType(webRequest, uri));
          }

          private Type determineActualType(NativeWebRequest webRequest, String uri) {
            if (uri.endsWith("data-a")) {
              return DataA.class;
            } else if (uri.endsWith("data-b")) {
              return DataB.class;
            }

            throw new HttpMessageNotReadableException(
                "Unable to determine actual type for request URI",
                new ServletServerHttpRequest(
                    webRequest.getNativeRequest(HttpServletRequest.class)));
          }
        });
  }

  @PostMapping(
      path = "/{type}",
      consumes = MediaType.APPLICATION_JSON_VALUE,
      produces = MediaType.APPLICATION_JSON_VALUE)
  ResponseEntity<? extends AbstractBaseData> post(@BaseData AbstractBaseData baseData) {
    return ResponseEntity.ok(baseData);
  }
}

Ключ к этому в том, что я перестал использовать @RequestBody, потому что именно это мешало мне переопределить встроенное поведение. Используя @BaseData вместо этого, я получаю HandlerMethodArgumentResolver, который уникально поддерживает параметр.

Кроме этого, это был случай объединения двух объектов, которые уже сделали то, что мне было нужно, так что autowire a MappingJackson2HttpMessageConverter и создать экземпляр RequestResponseBodyMethodProcessor с этим одним преобразователем. Затем выберите правильный метод для переопределения, чтобы я мог контролировать, какой тип параметра использовался в момент, когда у меня был доступ к URI.

Быстрый тест. Учитывая следующую полезную нагрузку для обоих запросов ...

{"a": "A", "b": "B"}

POST http://localhost: 8081 / dummy / data-a

... дает ответ ...

{"a": "A"}

POST http://localhost: 8081 / dummy / data-b

... дает ответ ...

{"b": "B"}

In В нашем реальном примере это означает, что мы сможем написать по одному методу, который поддерживает POST / PUT. Нам нужно построить объекты и настроить проверку, возможно - или в качестве альтернативы, если мы используем OpenAPI 3.0, который мы исследуем, мы можем сгенерировать модель и проверить без написания какого-либо дополнительного кода ... но это отдельная задача;)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...