简体   繁体   中英

How can I instantiate a specific sub-type for a @RequestBody parameter based on the requested URI for a Spring MVC controller method?

Given the following basic domain model:

abstract class BaseData { ... }

class DataA extends BaseData { ... }

class DataB extends BaseData { ... }

I want to write a Spring MVC controller endpoint thus ...

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

The required concrete type of baseData can be inferred from the typeOfData in the path.

This allows me to have a single method that can handle multiple URLs with different body payloads. I would have a concrete type for each payload but I don't want to have to create multiple controller methods that all do the same thing (albeit each would do very little).

The challenge that I am facing is how to "inform" the deserialization process so that the correct concrete type is instantiated.

I can think of two ways to do this.

First use a custom 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);
      }
    };
  }

... which gives me the challenge to determine the subType based on the HttpInputMessage . Possibly I could use a Filter to set a custom header earlier when the URL is available to me, or I could use a ThreadLocal also set via a Filter . Neither sounds ideal to me.

My second approach would be to again use a Filter and this time wrap the incoming payload in an outer object which would then provide the type in a way that enables Jackson to do the work via @JsonTypeInfo . At the moment this is probably my preferred approach.

I have investigated HandlerMethodArgumentResolver but if I try to register a custom one it is registered AFTER the RequestResponseBodyMethodProcessor and that class takes priority.

Hmm, so after typing all of that out I had a quick check of something in the RequestResponseBodyMethodProcessor before posting the question and found another avenue to explore, which worked neatly.

Excuse the @Configuration / @RestController / WebMvcConfigurer mash-up and public fields, all for brevity. Here's what worked for me and achieved exactly what I wanted:

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

The key to this is that I stopped using @RequestBody because that is what was preventing me overriding the built-in behaviour. By using @BaseData instead I get a HandlerMethodArgumentResolver that uniquely supports the parameter.

Other than that it was a case of assembling the two objects that already did what I needed, so autowire a MappingJackson2HttpMessageConverter and instantiate a RequestResponseBodyMethodProcessor with that one converter. Then pick the right method to override so that I could control what parameter type was used at a point that I had access to the URI.

Quick test. Given the following payload for both requests ...

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

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

... gives a response of ...

{ "a": "A" }

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

... gives a response of ...

{ "b": "B" }

In our real-world example this means that we will be able to write one method each that supports the POST / PUT. We need to build the objects and configure the validation possibly - or alternatively if we use OpenAPI 3.0 which we are investigating we could generate the model and validate without writing any further code ... but that's a separate task ;)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM