Custom HttpMessageConverter with @ResponseBody to do Json things
Asked Answered
R

8

31

I don't like Jackson.

I want to use ajax but with Google Gson.

So I'm trying to figure out how to implement my own HttpMessageConverter to use it with @ResponseBody annotation. Can someone take a time to show me the way I should go? What configurations should I turn on? Also I'm wondering if I can do this and still use <mvc:annotation-driven />?

Thanks in advance.

I've already asked it in Spring Community Foruns about 3 days ago with no answer so I'm asking here to see if I get a better chance. Spring Community Forums link to my question

I've also made an exhaustive search on the web and found something interesting on this subject but it seems they're thinking to put it in Spring 3.1 and I'm still using spring 3.0.5: Jira's Spring Improvement ask

Well... now I'm trying to debug Spring code to find out myself how to do this, but I'm having some problems like I've said here: Spring Framework Build Error

If there is another way to do this and I'm missing it, please let me know.

Raychel answered 16/2, 2011 at 16:31 Comment(0)
R
39

Well... it was so hard to find the answer and I had to follow so many clues to incomplete information that I think it will be good to post the complete answer here. So it will be easier for the next one searching for this.

First I had to implement the custom HttpMessageConverter:


package net.iogui.web.spring.converter;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.Charset;

import org.springframework.http.HttpInputMessage;
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;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    private Gson gson = new Gson();

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    public GsonHttpMessageConverter(){
        super(new MediaType("application", "json", DEFAULT_CHARSET));
    }

    @Override
    protected Object readInternal(Class<? extends Object> clazz,
                                  HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {

        try{
            return gson.fromJson(convertStreamToString(inputMessage.getBody()), clazz);
        }catch(JsonSyntaxException e){
            throw new HttpMessageNotReadableException("Could not read JSON: " + e.getMessage(), e);
        }

    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    protected void writeInternal(Object t, 
                                 HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

        //TODO: adapt this to be able to receive a list of json objects too

        String json = gson.toJson(t);

        outputMessage.getBody().write(json.getBytes());
    }

    //TODO: move this to a more appropriated utils class
    public String convertStreamToString(InputStream is) throws IOException {
        /*
         * To convert the InputStream to String we use the Reader.read(char[]
         * buffer) method. We iterate until the Reader return -1 which means
         * there's no more data to read. We use the StringWriter class to
         * produce the string.
         */
        if (is != null) {
            Writer writer = new StringWriter();

            char[] buffer = new char[1024];
            try {
                Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                int n;
                while ((n = reader.read(buffer)) != -1) {
                    writer.write(buffer, 0, n);
                }
            } finally {
                is.close();
            }
            return writer.toString();
        } else {
            return "";
        }
    }

}

Then I had to strip off the annnotaion-driven tag and configure all by my own hands on the spring-mvc configuration file:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <!-- Configures the @Controller programming model -->

    <!-- To use just with a JSR-303 provider in the classpath 
    <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
    -->

    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean" />

    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="webBindingInitializer">
            <bean class="net.iogui.web.spring.util.CommonWebBindingInitializer" />
        </property>
        <property name="messageConverters">
            <list>
                <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
                <bean class="org.springframework.http.converter.StringHttpMessageConverter" />
                <bean class="org.springframework.http.converter.ResourceHttpMessageConverter" />
                <bean class="net.iogui.web.spring.converter.GsonHttpMessageConverter" />
                <bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter" />
                <bean class="org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter" />
                <!-- bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter" /-->
            </list>
        </property>
    </bean>
    <bean id="handlerMapping" class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />


    <context:component-scan base-package="net.iogui.teste.web.controller"/>

    <!-- Forwards requests to the "/" resource to the "login" view -->
    <mvc:view-controller path="/" view-name="home"/>

    <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources/ directory -->
    <mvc:resources mapping="/resources/**" location="/resources/" />

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/view/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

See that, to make the Formater and Validator to work, we have to build a custom webBindingInitializer too:


package net.iogui.web.spring.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.context.request.WebRequest;

public class CommonWebBindingInitializer implements WebBindingInitializer {

    @Autowired(required=false)
    private Validator validator;

    @Autowired
    private ConversionService conversionService;

    @Override
    public void initBinder(WebDataBinder binder, WebRequest request) {
        binder.setValidator(validator);
        binder.setConversionService(conversionService);
    }

}

An Interesting thing to see is that In order to make the configuration work without the annotaion-driven tag, we have to manually configure a AnnotationMethodHandlerAdapter and a DefaultAnnotationHandlerMapping. And in order to make the AnnotationMethodHandlerAdapter capable of handling formatting and validation, we had to configure a validator, a conversionService and to build a custom webBindingInitializer.

I hope all this helps someone else besides me.

On my desperate search, this @Bozho post was extremely util. I am also grateful to @GaryF couse his answer took me to the @Bozho post. To you that are trying to do this in Spring 3.1, see @Robby Pond answer.. A lot easier, isn't it?

Raychel answered 16/2, 2011 at 16:31 Comment(2)
Thanks, I'm ok with jackson, though I was thinking how I'd do the same with Gson. Btw, you could have used Apache Commons IOUtils to get the string out of the InputStream. I usually prefer using 3rd party libraries where possible as I don't need to improve the code when a faster/better option comes up, I just change the version in maven :).Latecomer
Given that AnnotationMethodHandlerAdapter is now deprecated, does this same config work with RequestMappingHandlerAdapter ?Kironde
H
16

You need to create a GsonMessageConverter that extends AbstractHttpMessageConverter and use the mvc-message-converters tag to register your message converter. That tag will let your converter take precedence over the Jackson one.

Hammers answered 16/2, 2011 at 21:21 Comment(2)
Note that I've specified: "I've also made an exaustive search on the web and found something interesting on this subject but it seems they're thinking to put it in Spring 3.1 and I'm still using spring 3.0.5." See the link on Jira's Improvement RequestRaychel
Same here... an answer with a example would be nice.Divest
Y
8

If you want to add a message converter without messing with xml here is a simple example

@Autowired
private RequestMappingHandlerAdapter adapter;

@PostConstruct
public void initStuff() {
    List<HttpMessageConverter<?>> messageConverters = adapter.getMessageConverters();
    BufferedImageHttpMessageConverter imageConverter = new BufferedImageHttpMessageConverter();;
    messageConverters.add(0,imageConverter);
}
Yoohoo answered 4/10, 2014 at 22:35 Comment(0)
M
6

I had situation where usage of Jackson would require me to alter other group's (in the same company) code. Didn't like that. So I chose to use Gson and register TypeAdapters as needed.

Hooked up a converter and wrote a few integration tests using spring-test (used to be spring-mvc-test). No matter what variation I tried (using mvc:annotation-driven OR manual definition of the bean). None of them worked. Any combination of these always used the Jackson Converter which kept on failing.

Answer> Turns out that MockMvcBuilders' standaloneSetup method "hard" coded the message converters to default versions and ignored all my changes above. Here is what worked:

@Autowired
private RequestMappingHandlerAdapter adapter;

public void someOperation() {
  StandaloneMockMvcBuilder smmb = MockMvcBuilders.standaloneSetup(controllerToTest);
  List<HttpMessageConverter<?>> converters = adapter.getMessageConverters();
  HttpMessageConverter<?> ary[] = new HttpMessageConverter[converters.size()];
  smmb.setMessageConverters(conveters.toArray(ary));
  mockMvc = smmb.build();
   .
   .
}

Hope this helps someone, in the end I used annotation-driven and re-purposing android's converter

Melbourne answered 3/12, 2013 at 23:54 Comment(0)
D
4

Notice that GsonHttpMessageConverter was added recently to Spring (4.1)

Deandreadeane answered 23/10, 2014 at 17:0 Comment(0)
C
3

Robby Pond is basically correct, but note that his suggestion to use the mvc:message-converters tag requires that you use 3.1. Since 3.1 is currently only a milestone release (M1), I'd suggest registering your converter this way after creating it:

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="messageConverters">
      <util:list id="beanList">
        <ref bean="someMessageConverter"/>
        <ref bean="someOtherMessageConverter"/>
      </util:list>
    </property>
</bean>
Cosmogony answered 16/2, 2011 at 21:28 Comment(3)
Ok, and I can still use the <mvc:annotation-driven /> tag? All the adapters automaticaly registered by these tag will be registered? Or I will have to strip off the annotation-driven tag and do all the configuration with my own hands?Raychel
You are right, the solution is to manually setup AnnotationMethodHandlerAdapter and it's messageConverters but it means that I can't use annotaion-driven and I have to do all setup manually. It was so dificult and took me so many time to get in my objetive that I will post it all here so if someone else came searching for this, it will be a really usefull answer.Raychel
@logui Glad you figured out the rest. It looks like this will be less painful in Spring 3.1: blog.springsource.com/2011/02/21/…Cosmogony
K
3

Or as mentioned in Jira's Spring Improvement ask, write a BeanPostProcessor that adds your HttpMessageConvertor to the AnnotationMethodHandlerAdapter

Keystroke answered 2/5, 2011 at 4:53 Comment(0)
S
0

You can do this by writing the WebConfig file as a Java File. Extend your config file with WebMvcConfigurerAdapter and override extendMessageConverters method to add your intented Message Convertor. This method will retain the default converters added by Spring and will add your convertor at the end. Apparently you have full control with the list and you can add where ever you want in the list.

@Configuration
@EnableWebMvc
@ComponentScan(basePackageClasses={WebConfig.class})
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
      converters.add(new GsonHttpMessageConverter());
   }
}

package net.iogui.web.spring.converter;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.nio.charset.Charset;

import org.springframework.http.HttpInputMessage;
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;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

private Gson gson = new Gson();

public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

public GsonHttpMessageConverter(){
    super(new MediaType("application", "json", DEFAULT_CHARSET));
}

@Override
protected Object readInternal(Class<? extends Object> clazz,
                              HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {

    try{
        return gson.fromJson(convertStreamToString(inputMessage.getBody()), clazz);
    }catch(JsonSyntaxException e){
        throw new HttpMessageNotReadableException("Could not read JSON: " + e.getMessage(), e);
    }

}

@Override
protected boolean supports(Class<?> clazz) {
    return true;
}

@Override
protected void writeInternal(Object t, 
                             HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

    //TODO: adapt this to be able to receive a list of json objects too

    String json = gson.toJson(t);

    outputMessage.getBody().write(json.getBytes());
}

//TODO: move this to a more appropriated utils class
public String convertStreamToString(InputStream is) throws IOException {
    /*
     * To convert the InputStream to String we use the Reader.read(char[]
     * buffer) method. We iterate until the Reader return -1 which means
     * there's no more data to read. We use the StringWriter class to
     * produce the string.
     */
    if (is != null) {
        Writer writer = new StringWriter();

        char[] buffer = new char[1024];
        try {
            Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
            int n;
            while ((n = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, n);
            }
        } finally {
            is.close();
        }
        return writer.toString();
    } else {
        return "";
    }
}
Spoilsman answered 20/4, 2015 at 21:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.