In Spring 3, it's very easy to get a view to return JSON. However, there is no built-in mechanism to return JSONP. I was able to find a pretty good tutorial That uses Spring's DelegatingFilterProxy. I implemented this solution and I got it to work, but I didn't like the fact that I had to create a separate filter and a bunch of other classes just for that. I also wanted to use a specific extension (.jsonp) for JSONP just to make it more explicit. Spring uses MappingJacksonJsonView to return JSON. I figured that I could extend this view and have it return JSONP instead of JSON.
Although I pulled my hair out for an hour or two (I was getting a 404 when hitting my controller with with a .jsonp extension; I'll explain why, later), the resulting solution is actually pretty simple. First, you need to extend MappingJacksonJsonView to create your own view. Since MappingJacksonJsonView already handles the creation of JSON, all we need to do is pad it with our callback. The way to do this is to override the MappingJacksonJsonView#render(Map
package net.vivin.mvc.spring.view.jsonp; import org.springframework.web.servlet.view.json.MappingJacksonJsonView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; public class MappingJacksonJsonpView extends MappingJacksonJsonView { /** * Default content type. Overridable as bean property. */ public static final String DEFAULT_CONTENT_TYPE = "application/javascript"; @Override public String getContentType() { return DEFAULT_CONTENT_TYPE; } /** * Prepares the view given the specified model, merging it with static * attributes and a RequestContext attribute, if necessary. * Delegates to renderMergedOutputModel for the actual rendering. * @see #renderMergedOutputModel */ @Override public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if("GET".equals(request.getMethod().toUpperCase())) { @SuppressWarnings("unchecked") Map<String, String[]> params = request.getParameterMap(); if(params.containsKey("callback")) { response.getOutputStream().write(new String(params.get("callback")[0] + "(").getBytes()); super.render(model, request, response); response.getOutputStream().write(new String(");").getBytes()); response.setContentType("application/javascript"); } else { super.render(model, request, response); } } else { super.render(model, request, response); } } }
The DEFAULT_CONTENT_TYPE public static property and the getContentType() method are important. Spring uses this method (among other things) to find the best candidate view. I spent at least an hour trying to figure out why I was getting 404's. It was because I hadn't overridden the property and the accessor to return the correct content-type. Going back to the render method itself, you can see that it's pretty simple. First I make sure that the request method is GET. Then I get the request parameter-map and check for the callback parameter. If this parameter exists, I write out the name of the callback with an open parenthesis. Then I call the parent class's render method. This method belongs to MappingJacksonJsonView and will write out JSON to the response. Finally, I write out a closing parenthesis and a semicolon to the response and set the content type to application/javascript. So as you can see, I've converted JSON into JSONP.
The next thing you need to do is include an entry for your new view in the springmvc-servlet.xml file. This part also took a little work. I wasn't sure how to include my new view within the ContentNegotiatingViewResolver bean. But this post proved to be remarkably helpful. The post actually talks about returning XML or JSON (based on the extension), but I was able to adapt it to return JSON or JSON. Here's what it looks like:
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver"> <property name="favorPathExtension" value="true"/> <property name="mediaTypes"> <map> <entry key="json" value="application/json"/> <entry key="jsonp" value="application/javascript"/> </map> </property> <property name="defaultViews"> <list> <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView"/> <bean class="net.vivin.mvc.spring.view.jsonp.MappingJacksonJsonpView"/> </list> </property> </bean>
If you notice, the value property for jsonp has been set to application/javascript. Spring uses this value and checks it against the view class to figure out the best candidate view. If you didn't override the DEFAULT_CONTENT_TYPE property and the associated getter, you would get a 404.
Finally, you need a url-pattern entry under servlet-mapping in your web.xml file so that the new .jsonp extension is recognized:
<servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>*.json</url-pattern> <url-pattern>*.jsonp</url-pattern> ... </servlet-mapping>
That's pretty much it. With this code in place, you can hit your controller with http://my.domain.com/controllerName/actionName.jsonp?callback=myCallback and you'll get your data back as JSONP! I thought this solution was pretty simple and a little less invasive than creating a filter. Comments/suggestions/criticisms for improvement are welcome!
This is AAAAAAAAAAAWWWWWWWWWWWWWWEEEEEEEEESSSSSSSSSSOOOOOOOOOOMMMMMMMMMEEEEEEE!!!!!!!!!!!!
Great solution, thanks for posting this 🙂 Worked the first time I ran it.
Thanks. This was very helpful!
@Cosmo Scrivanich
@JayW
Glad you found it helpful!
Great Article. I too liked the fact that it all is done in one JAVA class instead of being in a set of different files.
One thing I didn’t understand is why do all this, when the desired result could be acheived by doing
ObjectMapper mapper = new ObjectMapper();
String str = callBackMethod + “(“+mapper.writeValueAsString(myJavaObject)+”)”;
Thanks,
Sanjay.
@Sanjay Manchiganti: Yes, that’s definitely a possibility, but the reason I didn’t implement it like that is because I didn’t want to duplicate functionality that already exists in MappingJacksonJsonView. Furthermore, MappingJacksonJsonView does a bunch of other things like filtering out certain attributes from the model; I didn’t want to duplicate that functionality either. Hence, I decided to use what MappingJacksonJsonView already does, and simply add the padding around that.
This is AWESOME, THANK YOU.!
Thanks so much for this post. Just a typo suggestion: when you say ‘I was able to adapt it to return JSON or JSON’, I think you want to say ‘JSON or JSONP’.
I actually really like Sanjay’s approach, its super simple, and still works for people using @ResponseBody
@Chris Marx: The reason I didn’t go with that approach is because I would be duplicating functionality that is already found inside the JSON mapper. Additionally you would also have to filter out a few attributes that cannot be serialized, so it’s not as simple as calling the object mapper.
What do you recommend for people who aren’t using models anymore, and instead are using @ResponseBody to automatically serialize responses?
@Chris Marx: I can see that as a valid use-case. In that situation I would probably abstract that logic out into a service.
yeah, no reason to have those lines of code littering the controllers-
Hi this is great extension of MappingJacksonJsonView.
I wonder if is possible to handle the same way controllers with @ResponseBody annotation where you return collection or object.
Spring then search for MessageConverter that can produce accepted media type. So you could extend MappingJacksonHttpMessageConverter providing converter for mediaType application/javascript. I tried this path for a short time with no luck, as it seems that MessageConverters rely only on accepted media type from request.
@Dalibor:
I haven’t tried this with @ResponseBody but Chris Marx mentioned earlier that you can still do this, but you would have to duplicate some code. However that can be pushed into a service.
great post! thank you very much
I tried to solve this problem by trying to not using the view but by @ResquestMapping and message converter. My solution also supports custom json callback (padding?). It is similar to what Dalibor (#14) was asking.
My have documented my soluton here: http://www.iceycake.com/2012/06/xml-json-jsonp-web-service-endpoints-spring-3-1/
this link is dead, can you resurrect it for those of us patching ancient applications….
@Iceycake: Thanks for the link, Iceycake! That looks like a very good approach.
Hi,
Your solution has only one problem: when the client doesn’t provide the “callback” parameter it returns JSON output and that doesn’t match the content type “application/javascript”.
Anyway, it’s a good alternative.
Cheers!
This is AWESOME!
so elegant solution.
Thanks