24

I have a controller method that handles ajax calls and returns JSON. I am using the JSON library from json.org to create the JSON.

I could do the following:

@RequestMapping(method = RequestMethod.POST)
@ResponseBody
public String getJson()
{
    JSONObject rootJson = new JSONObject();

    // Populate JSON

    return rootJson.toString();
}

But it is inefficient to put together the JSON string, only to have Spring write it to the response's output stream.

Instead, I can write it directly to the response output stream like this:

@RequestMapping(method = RequestMethod.POST)
public void getJson(HttpServletResponse response)
{
    JSONObject rootJson = new JSONObject();

    // Populate JSON

    rootJson.write(response.getWriter());
}

But it seems like there would be a better way to do this than having to resort to passing the HttpServletResponse into the handler method.

Is there another class or interface that can be returned from the handler method that I can use, along with the @ResponseBody annotation?

tereško
  • 58,060
  • 25
  • 98
  • 150
John S
  • 21,212
  • 8
  • 46
  • 56
  • What is wrong with "passing the HttpServletResponse into the handler method"? – arahant Mar 08 '13 at 05:04
  • 2
    @arahant - That's a fair question. I guess I'm under the impression that, although it's possible, it's not the correct way of doing things. There's an answer by @Ralph in [this question](http://stackoverflow.com/questions/5673260/downloading-a-file-from-spring-controllers) that's says it makes testing harder. Before I write a lot of handler methods that do this, I wanted to find out if there's another way. So far it looks like I would have to write a custom `HttpMessageConverter`. – John S Mar 08 '13 at 05:14

3 Answers3

32

You can have the Output Stream or the Writer as an parameter of your controller method.

@RequestMapping(method = RequestMethod.POST)
public void getJson(Writer responseWriter) {
    JSONObject rootJson = new JSONObject();
    rootJson.write(responseWriter);
}

@see Spring Reference Documentation 3.1 Chapter 16.3.3.1 Supported method argument types

p.s. I feel that using OutputStream or Writer as an parameter is still much more easier to use in tests than a HttpServletResponse - and thanks for paying attention to what I have written ;-)

Community
  • 1
  • 1
Ralph
  • 118,862
  • 56
  • 287
  • 383
  • Thanks for the answer. Passing an `OutputStream` or a `Writer` is preferable to passing the `HttpServletResponse`. – John S Mar 10 '13 at 22:06
4

In the end, I wrote an HttpMessageConverter for this. With it, I can do the following:

@RequestMapping(method = RequestMethod.POST)
@ResponseBody
public JSONObject getJson()
    throws JSONException
{
    JSONObject rootJson = new JSONObject();

    // Populate JSON

    return rootJson;
}

Here is my HttpMessageConverter class:

package com.example;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;

public class JsonObjectHttpMessageConverter
    extends AbstractHttpMessageConverter<JSONObject>
{
    private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    public JsonObjectHttpMessageConverter()
    {
        super(new MediaType("application", "json"), new MediaType("text", "javascript"));
    }

    @Override
    protected boolean supports(Class<?> clazz)
    {
        return JSONObject.class.equals(clazz);
    }

    @Override
    protected JSONObject readInternal(Class<? extends JSONObject> clazz,
                                      HttpInputMessage            inputMessage)
        throws IOException,
               HttpMessageNotReadableException
    {
        throw new UnsupportedOperationException();
    }

    @Override
    protected void writeInternal(JSONObject        jsonObject,
                                 HttpOutputMessage outputMessage)
        throws IOException,
               HttpMessageNotWritableException
    {
        PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputMessage.getBody(),
                                                                    getContentTypeCharset(outputMessage)));

        try
        {
            jsonObject.write(writer);
            writer.flush();
        }
        catch (JSONException e)
        {
            throw new HttpMessageNotWritableException(e.getMessage(), e);
        }
    }

    private Charset getContentTypeCharset(HttpMessage message)
    {
        MediaType contentType = message.getHeaders().getContentType();

        Charset charset = (contentType != null) ? contentType.getCharSet() : null;

        return (charset != null) ? charset : DEFAULT_CHARSET;
    }
}

The HttpMessageConverter must be registered with Spring. This can be done in the dispatcher-servlet.xml file like this:

<beans ...>

    ...    

    <mvc:annotation-driven conversion-service="conversionService" validator="validator">
        <mvc:argument-resolvers>
            ...
        </mvc:argument-resolvers>
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/plain;charset=UTF-8</value>
                        <value>application/json;charset=UTF-8</value>
                        <value>*/*</value>
                    </list>
                </property>
                <property name="writeAcceptCharset" value="false" />
            </bean>
            <bean class="com.example.JsonObjectHttpMessageConverter" />
            <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
                <property name="objectMapper" ref="jacksonObjectMapper" />
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    ...

</beans>

As you can see, I have other HttpMessageConverter objects registered too. The order does matter.

John S
  • 21,212
  • 8
  • 46
  • 56
  • Thank you for taking the time to reply your own post with an updated answer, which is IMHO the best one. – Steve Marion Aug 08 '16 at 07:56
  • @SteveMARION - You're welcome. It was looking like I was the only one who would bother doing this. I figured this answer would not get as many votes as the accepted answer, but I thought it would be closer. – John S Aug 09 '16 at 14:37
1

Note that if you use the OutputStream or Writer it requires you to write the headers yourself.

One workaround is to use InputStreamResource/ResourceHttpMessageConverter

Hadrien
  • 49
  • 3