REST with Spring and Jackson full data binding

2019-01-07 05:38发布

问题:

I'm using Spring MVC to handle JSON POST requests. Underneath the covers I'm using the MappingJacksonHttpMessageConverter built on the Jackson JSON processor and enabled when you use the mvc:annotation-driven.

One of my services receives a list of actions:

@RequestMapping(value="/executeActions", method=RequestMethod.POST)
    public @ResponseBody String executeActions(@RequestBody List<ActionImpl> actions) {
        logger.info("executeActions");
        return "ACK";
    }

I have found that Jackson maps the requestBody to a List of java.util.LinkedHashMap items (simple data binding). Instead, I would like the request to be bound to a List of typed objects (in this case "ActionImpl").

I know this is easy to do if you use Jackson's ObjectMapper directly:

List<ActionImpl> result = mapper.readValue(src, new TypeReference<List<ActionImpl>>() { }); 

but I was wondering what's the best way to achieve this when using Spring MVC and MappingJacksonHttpMessageConverter. Any hints?

Thanks

回答1:

I suspect problem is due to type erasure, i.e. instead of passing generic parameter type, maybe only actions.getClass() is passed; and this would give type equivalent of List< ?>.

If this is true, one possibility would be to use an intermediate sub-class, like:

public class ActionImplList extends ArrayList<ActionImpl> { }

because this will the retain type information even if only class is passed. So then:

public @ResponseBody String executeActions(@RequestBody ActionImplList actions)

would do the trick. Not optimal but should work.

I hope someone with more Spring MVC knowledge can shed light on why parameter type is not being passed (perhaps it's a bug?), but at least there is a work around.



回答2:

I have found that you can also work around the type erasure issue by using an array as the @RequestBody instead of a collection. For example, the following would work:

public @ResponseBody String executeActions(@RequestBody ActionImpl[] actions) { //... }


回答3:

For your information, the feature will be available in Spring 3.2 (see https://jira.springsource.org/browse/SPR-9570)

I just tested it on current M2 and it works like a charm out of the box (no need to provide additionnal annotation to provide the parameterized type, it will be automatically resolved by new MessageConverter)



回答4:

This question is already old, but I think I can contribute a bit anyway.

Like StaxMan pointed out, this is due to type erasure. It definitely should be possible, because you can get the generic arguments via reflection from the method definition. However, the problem is the API of the HttpMessageConverter:

T read(Class<? extends T> clazz, HttpInputMessage inputMessage);

Here, only List.class will be passed to the method. So, as you can see, it is impossible to implement a HttpMessageConverter that calculates the real type by looking at the method parameter type, as that is not available.

Nevertheless, it is possible to code your own workaround - you just won't be using HttpMessageConverter. Spring MVC allows you to write your own WebArgumentResolver that kicks in before the standard resolution methods. You can for example use your own custom annotation (@JsonRequestBody?) that directly uses an ObjectMapper to parse your value. You will be able to provide the parameter type from the method:

final Type parameterType= method.getParameterTypes()[index];
List<ActionImpl> result = mapper.readValue(src, new TypeReference<Object>>() {
    @Override
    public Type getType() {
        return parameterType;
    }
});

Not really the way TypeReference was intended to be used I presume, but ObjectMapper doesn't provide a more suitable method.



回答5:

Have you tried declaring the method as:

executeActions(@RequestBody TypeReference<List<ActionImpl>> actions)

I haven't tried it, but based on your question it's the first thing I would try.