Spring MVC configuration + Jackson + Guava multimap

4.1k Views Asked by At

I'm struggling with this:

We have a Table class with a Guava multimap (simplified code, basically 1 member, 2 constructors, getter and setter for the multimap):

public class Table {

    private LinkedHashMultimap<String,Field> fields;

    public Table(){
        this.fields = LinkedHashMultimap.create();
    };

    public Table (LinkedHashMultimap<String, Field> fields){
        this.fields= fields;
    }

    public LinkedHashMultimap<String, Field> getFields() {
        return fields;
    }

    public void setFields(LinkedHashMultimap<String, Field> fields) {
        this.fields = fields;
    }
}

And I want to serialise this using Spring MVC 3.2.11 using jackson 2.4.3.

POM relevant dependencies are:

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-core</artifactId>
  <version>2.4.3</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.4.3</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-guava</artifactId>
  <version>2.4.3</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>3.2.11.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>3.2.11.RELEASE</version>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>servlet-api</artifactId>
  <version>2.5</version>
  <scope>provided</scope>
</dependency>

My spring.xml NOW looks like this (following this example)

<bean id="abstractJacksonObjectMapper"
      class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"
      p:targetMethod="registerModule">
    <property name="targetObject">
        <bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
              p:indentOutput="true">
            <!--<property name="featuresToDisable">-->
                <!--<util:constant static-field="com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES" />-->
            <!--</property>-->
        </bean>
    </property>
    <property name="arguments">
        <list>
            <bean class="com.fasterxml.jackson.datatype.guava.GuavaModule" />
        </list>
    </property>
</bean>

<bean id="abstractMappingJacksonHttpMessageConverter"
      class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"
      abstract="true"/>

<bean id="abstractMappingJacksonJsonView"
      class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"
      abstract="true"
      p:extractValueFromSingleKeyModel="true"/>

<bean id="jacksonObjectMapper" parent="abstractJacksonObjectMapper" />

<bean id="mappingJacksonHttpMessageConverter"
      parent="abstractMappingJacksonHttpMessageConverter"
      p:objectMapper-ref="jacksonObjectMapper"
      p:supportedMediaTypes="application/json" />

<bean id="mappingJacksonJsonView"
      parent="abstractMappingJacksonJsonView"
      p:objectMapper-ref="jacksonObjectMapper"
      p:contentType="application/json" />

I also tried this other approach using an extended Jackson2ObjectMapperFactoryBean:

<!-- Json rendering configuration-->
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="order" value="1" />
    <property name="mediaTypes">
        <map>
            <entry key="json" value="application/json" />
            <entry key="xml" value="application/xml" />
        </map>
    </property>
    <property name="defaultViews">
        <list>
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView">
                <property name="objectMapper">
                    <bean class="my.package.Jackson2ObjectMapperFactoryBean"/>
                </property>
            </bean>
        </list>
    </property>
    <property name="ignoreAcceptHeader" value="true" />
</bean>

And then the FactoryBean looks like this:

package my.package;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.guava.GuavaModule;

public class Jackson2ObjectMapperFactoryBean extends org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean{

public ObjectMapper getObject(){

    ObjectMapper objectMapper =super.getObject();
    objectMapper.registerModule(new GuavaModule());

    return objectMapper;
}

}

I have a Test class that works fine and is not using Spring at all (just testing Table.class + Jackson + guava) Simplified:

Table study = getTable();

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new GuavaModule());

String tableString = mapper.writeValueAsString(table);

It serialises it properly:

{
  "fields":{
    "Field1":[
      {
        "index":0,
        "header":"Field1",
        "fieldType":"fieldtype",
        "description":null,
        "cleanHeader":null
      }
    ],
    "Field2":[
      {
        "index":1,
        "header":"Field2",
        "fieldType":"fieldtype",
        "description":null,
        "cleanHeader":null
      }
    ]
  }
}

Using spring (any of the 2 approaches) I'm getting:

{
  "fields":{
    "empty": false
  }
}

My controller has a @ResponseBody annotation and it's returning a Table.

EDITED: I'm debugging deep into spring classes (firs time, ;-)) and org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor is handling the request. Is this related to my problem...Is my spring xml, somehow in contradiction with the @ResponseBody annotation?

Any ideas what I'm doing wrong?

NOTE: I need the Multimap, can't be a standard Java collection.

3

There are 3 best solutions below

0
On BEST ANSWER

After all I found out that the @ResponseBody annotation was forcing to use a different "viewResolver", that was using Jackson, but without the Guava module.

So, to fix this I removed the @ResponseBody annotation in my Controller method:

<!-- language: java -->
@ResponseBody
@RequestMapping("table")
public Table getTable() {

Unfortunately, this was returning: {Table:{...}} and was an artefact introduced by the ValueHandlder (ModelAttributeMethodProcessor).

At the end, what is working now is:

1.- Restore the @ResponseBody 2.- Register the Guava Module within the MappingJackson2HttpMessageConverter that come by default with the @ResponseBody handler.

This is how the spring xml looks like: Much clean and simpler:

<!-- JSON parser configuration-->
<bean id="guavaObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper"/>

<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject"><ref local="guavaObjectMapper" /></property>
    <property name="targetMethod"><value>registerModule</value></property>
    <property name="arguments">
        <list>
            <bean id="guavaModule" class="com.fasterxml.jackson.datatype.guava.GuavaModule"/>
        </list>
    </property>
</bean>


<mvc:annotation-driven>
    <mvc:message-converters register-defaults="true">
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper">
                <ref  local="guavaObjectMapper"/>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>
2
On

Dunno if the solution below fits your problem exactly, but just in case you want to configure your SpringBootApplication to also serialize guava, you could enhance your @Configuration class using a @Bean like this:

@Configuration
public class Application extends SpringBootServletInitializer {

    @Bean
    ObjectMapper customizeJacksonConfiguration() {
        ObjectMapper om = new ObjectMapper();
        om.registerModule(new GuavaModule());
        return om;
    }
}
1
On

I think you need to add an mvc:messsage-converters section to your spring.xml. For instance

 <mvc:annotation-driven ignoreDefaultModelOnRedirect="true" >
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.ResourceHttpMessageConverter"/>
        <bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter"/>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>