首页 > 解决方案 > 如何根据 Spring MVC 控制器方法的请求 URI 为 @RequestBody 参数实例化特定子类型?

问题描述

给定以下基本域模型:

abstract class BaseData { ... }

class DataA extends BaseData { ... }

class DataB extends BaseData { ... }

因此,我想编写一个 Spring MVC 控制器端点...

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

可以从路径中的 typeOfData 推断出所需的 baseData 的具体类型。

这使我可以使用一种方法来处理具有不同正文有效负载的多个 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 对我可用时,我可以使用 a来设置自定义标头,或者我可以ThreadLocal通过 a 使用 a 也设置Filter。对我来说听起来都不理想。

我的第二种方法是再次使用 a Filter,这次将传入的有效负载包装在一个外部对象中,然后以一种使杰克逊能够通过@JsonTypeInfo. 目前,这可能是我的首选方法。

我已经调查过HandlerMethodArgumentResolver了,但是如果我尝试注册一个自定义的,它会在之后注册,RequestResponseBodyMethodProcessor并且该类具有优先权。

标签: javaspring-bootspring-mvcjackson

解决方案


嗯,所以在输入所有这些内容之后,我在发布问题之前快速检查了一些内容,RequestResponseBodyMethodProcessor并找到了另一个探索途径,这很有效。

为简洁起见,请原谅@Configuration// mash @RestController- WebMvcConfigurerup 和 public 字段。这对我有用,并完全实现了我想要的:

@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唯一支持参数的。

除此之外,这是一个组装已经完成我需要的两个对象的情况,因此使用该转换器自动装配 aMappingJackson2HttpMessageConverter并实例化 a 。RequestResponseBodyMethodProcessor然后选择正确的方法来覆盖,以便我可以控制在我可以访问 URI 的点上使用的参数类型。

快速测试。给定两个请求的以下有效负载...

{“a”:“A”,“b”:“B”}

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

......给出......的回应

{“一”:“一”}

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

......给出......的回应

{“乙”:“乙”}

在我们的实际示例中,这意味着我们将能够编写一个支持 POST / PUT 的方法。我们可能需要构建对象并配置验证 - 或者,如果我们使用我们正在研究的 OpenAPI 3.0,我们可以生成模型并验证,而无需编写任何进一步的代码......但这是一项单独的任务;)


推荐阅读