It has been a while since I wrote my previous post which outlined a sample application using ExtJS and Spring. Since then, we’ve seen a new major version of both frameworks released, with ExtJS up to v3.3 and Spring sitting at 3.0.5.
ExtJS added a lot of cool features on the path to 3.3, but the most significant amount of cool stuff happened with the release of Spring 3.0. This release of Spring made it significantly easier to use JSON and REST, which eliminated a lot of the hackery that I had to do in my template with Spring 2.5.
I’ve been working on an updated version of my template, which makes use of all these new features, and decided to present it as a work-in-progress. The first part I want to talk about is how to get an Ext.data.JsonStore talking to a Spring MVC controller for CRUD operations. Loiane Groner wrote a pretty nice post on getting this wired in with an EditGrid, but it missed some of the real coolness with Spring 3.0.
I’ve pushed my sample application to GitHub to make it easier to get the big picture. The sample is a very simple webapp which uses a single domain object called Contact. The full project includes the Maven POM file along with a whole project which uses an HSQLDB with a full MVC stack in Spring. I’m going to ignore everything below the controller for this tutorial, but you can check out the rest in the full project.
Let’s start simple by demonstrating the GET operation, which will retrieve a list of all the contacts we have in our repository. Here is the application snippet from ContactController. Note, I’m using images so I can easily highlight sections. You can grab the raw source from the GitHub link above.
So the easy stuff first. I’ve annotated the ContactsController class with the @Controller stereotype from Spring to enable auto discovery. The @RequestMapping attribute says that this controller with catch all requests for ‘/contacts’. The
getContacts() method is setup to catch any HTTP GET request, thanks the @RequestMapping annotation on the method itself. Finally, the real magic happens with the @ResponseBody annotation on the method return value.
@ResponseBody tells Spring it should not try to find a view and should just encode whatever object you are returning and send it back to the browser. Since we’re going to make an XMLHttpRequest from the browser, Spring will automagically encode the response in JSON format. The only caveat is that the Jackson JSON processer must be on the classpath, which my Maven POM file takes care of.
On the client side, the App.js file defines the Ext.data.Record, Ext.data.Reader, Ext.data.Proxy and Ext.data.Store. For simple applications, it is usually overkill to declare them like this, but I’m explicitly defining them to show how they will integrate with the Writer for updates.
For the Proxy, I’m setting restful to true, which means it will use the HTTP verbs GET, POST, PUT and DELETE for the CRUD operations. The url is set to send all those to the our controller above.
To make things interesting, I include a date field in the definition of the Record. This is used to show off one of the cool features of Jackson JSON and how it handles serialization.
Finally, I create the Store with the Proxy and Reader. I set autoSave to false, so I can manually force saves and loads from the console in Chrome.
In the current state, I haven’t wired in any widgets, so I’ll use the console to exercise the store. If you run the WAR file after generating it with Maven (
After executing the
load() method of the store, you can see it loaded 10 items. If you enabled resource tracking, you can see the actual request looked like this:
The response is a JSON string that looks like
This is the typical response an Ext.data.Reader is looking for. Notice that the dates are correctly formatted as YYYY-MM-DD. This is handled via a custom attribute on the Contact domain object:
The method itself returns a Date. Without the @JsonSerialize attribute, this property would be serialized in the JSON response by calling its
toString() method, which is not a desirable outcome. The ExtDateSerializer class does custom serialization of the Date object to convert it to a String using a DateFormat. The @JsonProperty annotation configures what name the property should have when serialized to a JSON object. Without this annotation, the property would have the name
birthDate. I’m overriding that, specifying it should be called
dob when serialized.
Now lets try to go the other direction and write (POST) a new Record from our Ext.data.Store to the ContactsController. Here is the (incorrect) Writer. We’ll get to that “incorrect” part in a minute, but first the easy stuff:
You must set encode to false, or else the Writer will try to send the payload as HTTP form data, not JSON. The error you get looks like this if you don’t set encode to false:
Note the HTTP error code is 415. Every other error you get will be a 500. It is only if you don’t set encode to false that you’ll get a 415.
Next we set writeAllFields to true. If you don’t do this, only the properties that have changed for a Record will be transmitted. We want everything server side for deserialization, so this ensures the whole object shows up at the server.
Finally, I set listful to true. This is hugely important for getting JSON deserialization to work correctly in the controller. This option says to wrap all updates inside a JSON array, even if there is only a single Record being sent. Since you can’t overload controller methods in Spring MVC controllers, we need to be able to catch both single and multi-record updates with a single method. Setting the writer to listful ensures that happens.
The controller method catching our new Record looks likes this:
The @RequestMapping annotation says that this method will catch all POST requests to the
/contacts URL. The @RequestBody is new in Spring 3 and is the incoming analog to @ResponseBody. It basically tells Spring to attempt to deserialize the incoming payload into a Java object, in this case an array of Contacts. The deserialization is determined by the request headers. In this case, Spring will see it is JSON and will automagically use Jackson JSON to try and deserialize the payload to the correct object type.
Note that I return a list of Contact objects. ExtJS is expecting the return value to be a list of the same objects, post-save, with the id property set. It assumes these are in the same order that was sent, so do not shuffle the order around.
Setting listful to true in the Writer is what allows us to configure this method to accept an array of Contact objects. Even if there is only one Record coming from the Ext.data.Writer, it will be wrapped in a JSON array and deserialized into an array of Contact objects.
Now what about the birthdate property? Again, I use a Jackson JSON annotation to configure what field gets read (“dob” in this case), and how that field gets turned back in to a java.util.Date instance.
My ExtDateDeserializer class does the opposite of the ExtDateSerializer class. It uses a DateFormat to parse the string in the JSON property for
dob back into a java.util.Date.
So lets try to create a new Contact and save it. Here’s the console output:
This is where we get to that “incorrect” thingy I mentioned above. The default Writer will attempt to serialize this
save() request like this:
So the Writer is sending a JSON object, which contains a property called “data” which contains the array of new App.Contact records we recreated. The problem is that Spring and Jackson are looking for a simple JSON array that contains the new App.Contact records. So we need to customize the Writer to make this work right. Here is the custom Writer implementation:
I override the render method of the default Writer to directly output the JSON array of Records, and not put the wrapper object around it. Using this SpringWriter instead of the default Writer results in a payload like this:
Notice there is no longer the wrapper. When we run the console code again, we get a successful save:
So that covers reading the list of Contacts and adding a new Contact. Again, the code is available on GitHub so you can dig in to it. I’ll follow-up with another post on the PUT (update) and DELETE methods, along with wiring this in to some widgets.