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.