简体   繁体   中英

Spring 3 AJAX POST request with @RequestBody and @ModelAttribute and @SessionAttribute used together?

Have a Java spring MVC web app, and am making a jquery ajax post request. My Controller is setup to receive and send json data. Everything works, the JSON string is well formatted, and the Controller can create and populate a Command object and populate it with the contents of the JSON request data. However, I am updating data for a Contact object, and my JSP form element only contains a subset of all the data required for the DB update. In my initial GET request for the JSP page with the form I retrieve all the necessary data from the DB, populate a Contact Command object, and then bind that command object to the Model.

If I were doing a normal POST submit form submission, I believe that just declaring my command object as @SessionAttribute, and referencing that Command object using @ModelAttribute in my onSubmit() POST method would be sufficient. Spring would retrieve the already populated command object from my session and then bind (overwrite) those values that have changed as a result of the POST request. This updated command object could then be used as the parameter for a DB update.

However, I am using Spring 3 and leveraging @RequestBody paramater type. I cannot get Spring to both give me the session object and automatically bind the new values from the request. It either gives me just the old session command object (without applying the changes) or a new Command Object with only the values from the POST request.

Here is a little code - doesn't work:

@SessionAttributes("contactCommand")
@Controller
public class ContactController {


  @RequestMapping(value = "/editContact", method=RequestMethod.GET)
public String init(ModelMap model, Locale locale, HttpServletRequest request, HttpServletResponse response) throws GeneralException {
    final ContactCommand cmd = new ContactCommand();
    // populate with data from DB etc
    model.addAttribute("contactCommand", cmd);
    // etc
}

@RequestMapping(value="/editContact",method=RequestMethod.POST, consumes = "application/json", produces = "application/json")
public @ResponseBody Map<String, ? extends Object> editContactInfo(@RequestBody @ModelAttribute("contactCommand") ContactCommand cmd, HttpServletRequest request, HttpServletResponse response) throws GeneralException {

// do business logic with command object here

}

Can anyone please tell me what is the "standard" or "easiest" way to use @RequestBody with JSON request data and make that bind to an existing / @ModelAttribute populated Command object so that the Command object fully constituted with both old and new data (in the same way it is easily achieved using a full POST http submit).

A related question is what is wrong with the code above? Can @SessionAttribute and @RequestBody with JSON content all be used together? If so, please explain how! Thank you so much for any input.

My work around is to let Spring create the new Command object and auto-populate with form data. Then make a separate call / retrieve manually from session the old command object, finally manually copy all those attributes that were not present in the form submission into the new command object. Now I have all the necessary data together in one command object to apply my SQL update with. There must be an easier way.... ;)

UPDATE:

Found this SOF post today while further researching this problem:

Spring Partial Update Object Data Binding

It appears there is no known SPRING solution out of the box but a lot of demand to know the best way to handle it. In my case, yes, I am using nested domain objects so the workaround offered in the post is no good. Does anyone have any other ideas? To be clear, I wish to POST JSON format data to the Controller (not simply http form post data).

Ok, I've opened a Spring Source JIRA request for this one, perhaps it is a much needed improvement:

https://jira.springsource.org/browse/SPR-10552

Or else, it is a case of leveraging the Jackson conversion capabilities in clever ways which sounds like a lot of plumbing.

This isn't a complete answer, but I hope it will point you in the right direction.

Following is a class that we use to do deep binding from JSON to an existing object using Jackson. This is adapted from a bug report for Jackson here: https://jira.springsource.org/browse/SPR-10552

public class JsonBinder
{
    private ObjectMapper objectMapper;

    public JsonBinder( ObjectMapper objectMapper )
    {
        super();
        this.objectMapper = checkNotNull( objectMapper );
    }

    public void bind( Object objToBindInto, InputStream jsonStream ) throws JsonProcessingException, IOException
    {
        JsonNode root = objectMapper.readTree( checkNotNull( jsonStream ) );
        applyRecursively( checkNotNull( objToBindInto ), root );
    }

    private void applyRecursively( Object objToBindInto, JsonNode node ) throws JsonProcessingException, IOException
    {
        PropertyAccessor propAccessor = null;

        for( Iterator<Entry<String, JsonNode>> i = node.fields(); i.hasNext(); )
        {
            Entry<String, JsonNode> fieldEntry = i.next();
            JsonNode child = fieldEntry.getValue();
            if( child.isArray() )
            {
                // We ignore arrays so they get instantiated fresh every time
                // root.remove(fieldEntry.getKey());
            }
            else
            {
                if( child.isObject() )
                {
                    if( propAccessor == null )
                    {
                        propAccessor = PropertyAccessorFactory.forDirectFieldAccess( objToBindInto );
                    }
                    Object o2 = propAccessor.getPropertyValue( fieldEntry.getKey() );
                    if( o2 != null )
                    {

                        // Only remove the JsonNode if the object already exists
                        // Otherwise it will be instantiated when the parent gets
                        // deserialized
                        i.remove();
                        applyRecursively( o2, child );
                    }
                }
            }
        }
        ObjectReader jsonReader = objectMapper.readerForUpdating( objToBindInto );
        jsonReader.readValue( node );
    }
}

We use this along with a an implementation of Spring's HandlerMethodArgumentResolver.

We don't use a lot of Spring's MVC framework. We are just building a JSON API backend using a lot of different parts of Spring. It is a pretty good amount of plumbing to get it all working, but now our controllers are very simple.

Unfortunately I can't show all of our code, it's pretty long anyways. I hope this solves at least part of the problem.

Why do you want to annotate ModelAttribute with @RequestBody, Just having @SessionAttribute and referencing that Command object using @ModelAttribute is sufficient in case of JSON too.

What is your motive behind using @RequestBody

See http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/web/bind/annotation/ModelAttribute.html and

http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/web/bind/annotation/RequestBody.html

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