Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Jackson ObjectMapper in Jersey 2 with Spring

I'm having some issues migrating Jersey from 1.x to 2.x. My application uses Jersey to provide REST web services, with data served in JSON via Jackson and Spring 4 to handle the dependency injection.

In Jersey 1.x i used to write JsonDeserializer as components managed by Spring, so i could access my services to load from the persistance layer my domain object during the deserialization, but in 2.x i'm having issues getting the injection of services in the deserializers to work. The approach i follewd was inspired by this blog post: http://www.runningasroot.com/blog/2012/05/02/autowiring-jackson-deserializers-in-spring/

This is dependency section of my pom.xml:

<dependencies>
    <!-- Spring -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!-- Jersey -->
    <dependency>
        <groupId>org.glassfish.jersey.containers</groupId>
        <artifactId>jersey-container-servlet</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.ext</groupId>
        <artifactId>jersey-spring3</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-multipart</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-json-jackson</artifactId>
        <version>${jersey.version}</version>
    </dependency>
    <!-- Commons Codec -->
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>${commons-codec.version}</version>
    </dependency>

    <!-- cut -->

<dependencies>

Jersey version is 2.7, Spring 4.0.2.RELEASE.

This is my web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <module-name>myApp/module-name>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>it.mgt.myApp.config.ApplicationConfig</param-value>
    </context-param>

    <servlet>
        <servlet-name>jersey-serlvet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>it.mgt.myApp.config.JerseyConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>jersey-serlvet</servlet-name>
        <url-pattern>/api/*</url-pattern>
    </servlet-mapping>

</web-app>

This is my Spring configuration class:

@Configuration
@ComponentScan({"it.mgt.myApp"})
@PropertySource("classpath:myApp.properties")
public class ApplicationConfig {

    // Cut

}

This is my Jersey Resource config class:

public class JerseyConfig extends ResourceConfig {

    public JerseyConfig() {
        packages("it.mgt.myApp");

        register(MultiPartFeature.class);

        register(RequestContextFilter.class);

        register(ObjectMapperContextResolver.class);
        register(JacksonFeature.class);

        register(CorsRequestFilter.class);
        register(SignatureProcessingFilter.class);
        register(AuthorizationFeature.class);
        register(CorsResponseFilter.class);
        register(new UserBinder());
    }
}

This is my ObjectMapperContextResolver class:

@Component
@Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {

    @Autowired
    private SpringObjectMapper objectMapper;

    public ObjectMapperContextResolver() {
        super();
    }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return objectMapper;
    }

}

I think @Provider annotation is redundant with the registrationg in the resource config class.

This is my SpringObjectMapper class:

@Component
public class SpringObjectMapper extends ObjectMapper {

    private static final long serialVersionUID = 1413033425692174337L;

    @Autowired
    ApplicationContext applicationContext;

    public SpringObjectMapper() {
        this.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
        this.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, true);
    }

    @Override
    @Autowired
    public void setHandlerInstantiator(HandlerInstantiator hi) {
        super.setHandlerInstantiator(hi);
    }

}

This is my SpringBeanHandlerInstantiator class:

@Component
public class SpringBeanHandlerInstantiator extends HandlerInstantiator {

    private ApplicationContext applicationContext;

    @Autowired
    public SpringBeanHandlerInstantiator(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public JsonDeserializer<?> deserializerInstance(DeserializationConfig dc, Annotated antd, Class<? extends JsonDeserializer<?>> type) {
        try {
            return (JsonDeserializer<?>) applicationContext.getBean(type);
        } catch (Exception e) {
        }

        return null;
    }

    @Override
    public KeyDeserializer keyDeserializerInstance(DeserializationConfig dc, Annotated antd, Class<? extends KeyDeserializer> type) {
        try {
            return (KeyDeserializer) applicationContext.getBean(type);
        } catch (Exception e) {
        }

        return null;
    }

    @Override
    public JsonSerializer<?> serializerInstance(SerializationConfig sc, Annotated antd, Class<? extends JsonSerializer<?>> type) {
        try {
            return (JsonSerializer<?>) applicationContext.getBean(type);
        } catch (Exception e) {
        }

        return null;
    }

    @Override
    public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> mc, Annotated antd, Class<? extends TypeResolverBuilder<?>> type) {
        try {
            return (TypeResolverBuilder<?>) applicationContext.getBean(type);
        } catch (Exception e) {
        }

        return null;
    }

    @Override
    public TypeIdResolver typeIdResolverInstance(MapperConfig<?> mc, Annotated antd, Class<? extends TypeIdResolver> type) {
        try {
            return (TypeIdResolver) applicationContext.getBean(type);
        } catch (Exception e) {
        }

        return null;
    }

}

This is my domain entity class, serializers and deserializers are static inner classes:

@JsonSerialize(using = User.Serializer.class)
@JsonDeserialize(using = User.Deserializer.class)
public class User {

    @Component
    public static class Serializer extends JsonSerializer<User> {

        @Override
        public void serialize(User obj, JsonGenerator jg, SerializerProvider sp) throws IOException, JsonProcessingException {
            // Cut
        }

    }

    @Component
    public static class Deserializer extends JsonDeserializer<User> {

        @Autowired
        SomeService someService;

        @Override
        public User deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
            User user = new User();

            // Cut
            // Use someService here
        }

    }

    // Cut

}

I tried to put a brakpoint in ObjectMapperContextResolver.getContext(Class type) but it never get hit, i suspect that's the root of the problem, but after two days of attempts and studying jersey docs i'm running out of ideas.

Anybody can point me out on how to achive this properly?

like image 983
Michele Tibaldi Avatar asked Mar 15 '14 10:03

Michele Tibaldi


People also ask

Is it possible to customize the underlying Jackson objectmapper?

Since I launched this project, one of the most wanted feature was to add support for customizing the underlying Jackson ObjectMapper, and since version 2.1.1, this can be done either declaratively or programmatically. In this article, you are going to see how do customize the ObjectMapper when using the hibernate-types project.

Does jackson2objectmapperbuilder register all modules on the classpath?

According to the Jackson2ObjectMapperBuilder documentation, it will also register some modules if they're present on the classpath: The advantage of this approach is that the Jackson2ObjectMapperBuilder offers a simple and intuitive way to build an ObjectMapper.

How to generate JSON from a Java object in objectmapper?

The methods writeValueAsString and writeValueAsBytes of ObjectMapper class generate a JSON from a Java object and return the generated JSON as a string or as a byte array: String carAsString = objectMapper.writeValueAsString(car);

Is it possible to programmatically map JSON with Hibernate-types?

As already explained, the hibernate-types open-source project allows you to map JSON, ARRAY when using JPA and Hibernate. Since I launched this project, one of the most wanted feature was to add support for customizing the underlying Jackson ObjectMapper, and since version 2.1.1, this can be done either declaratively or programmatically.


1 Answers

After further attempts, it turned out the @Component on the ObjectMapperContextResolver was causing Jersey 2.x to not use the provider even if it was explicitly registered in Jersey configuration class. This is the opposite of Jersey 1.x behaviour, where the @Component was needed.

Removing it did the trick, as odd it may seems. The @Autowired SpringObjectMapper in ObjectMapperContextResolver was still injected by Jersey.

From jersey docs i wasn't able to tell if this is by design or if this is a bug.

like image 52
Michele Tibaldi Avatar answered Oct 20 '22 19:10

Michele Tibaldi