19

I have written a custom JsonDeserializer which contains an autowired service, as follows:

public class PersonDeserializer extends JsonDeserializer<Person> {

    @Autowired
    PersonService personService;

    @Override
    public Person deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {

        // deserialization occurs here which makes use of personService

        return person;
    }
}

When I first made use of this deserializer I was getting NPEs as personService was not being autowired. From looking at other SO answers (in particular, this one) it appears there is two ways of getting the autowiring to work.

Option 1 is to use SpringBeanAutowiringSupport within the constructor of the custom deserializer:

public PersonDeserializer() { 

    SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this); 
}

Option 2 is to use a HandlerInstantiator and register it with my ObjectMapper bean:

@Component
public class SpringBeanHandlerInstantiator extends HandlerInstantiator {

    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<? extends JsonDeserializer<?>> deserClass) {

        try {

            return (JsonDeserializer<?>) applicationContext.getBean(deserClass);

        } catch (Exception e) {

            // Return null and let the default behavior happen
            return null;
        }
    }
}

@Configuration  
public class JacksonConfiguration {

    @Autowired
    SpringBeanHandlerInstantiator springBeanHandlerInstantiator;

    @Bean
    public ObjectMapper objectMapper() {

        Jackson2ObjectMapperFactoryBean jackson2ObjectMapperFactoryBean = new Jackson2ObjectMapperFactoryBean();
        jackson2ObjectMapperFactoryBean.afterPropertiesSet();

        ObjectMapper objectMapper = jackson2ObjectMapperFactoryBean.getObject();

        // add the custom handler instantiator
        objectMapper.setHandlerInstantiator(springBeanHandlerInstantiator);

        return objectMapper;
    }
}

I have tried both options and they work equally well. Clearly option 1 is much easier as it's only three lines of code, but my question is: are there any disadvantages to using SpringBeanAutowiringSupport compared to the HandlerInstantiator approach? My application is going to be deserializing hundreds of objects per minute, if that makes any difference.

Any advice/feedback is appreciated.

Community
  • 1
  • 1
smilin_stan
  • 1,693
  • 2
  • 28
  • 40
  • Did you managed to find any drawback against SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this); ? – Gonçalo Cardoso Feb 03 '16 at 10:38
  • Good question. Wondering if anyone has any _actual_ answers. All of the existing answers don't actually talk about any of the pros/cons, but just reiterate what's already in the question. – Christopher Schneider Sep 28 '21 at 19:44

3 Answers3

6

Adding to the Amir Jamak's answer, you don't have to create custom HandlerInstantiator as Spring has it already which is SpringHandlerInstantiator.

What you need to do is to hook it up to Jackson2ObjectMapperBuilder in Spring configuration.

@Bean
public HandlerInstantiator handlerInstantiator(ApplicationContext applicationContext) {
    return new SpringHandlerInstantiator(applicationContext.getAutowireCapableBeanFactory());
}

@Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder(HandlerInstantiator handlerInstantiator) {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    builder.handlerInstantiator(handlerInstantiator);
    return builder;
}
maxhuang
  • 2,681
  • 1
  • 21
  • 26
1

As suggested in this comment and found on this link you need to create custom HandlerInstantiator:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;

@Component
public class SpringBeanHandlerInstantiator extends HandlerInstantiator {

    private ApplicationContext applicationContext;

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

    @Override
    public JsonDeserializer<?> deserializerInstance(DeserializationConfig config,
            Annotated annotated,
            Class<?> deserClass) {
        try {
            return (JsonDeserializer<?>) applicationContext.getBean(deserClass);
        } catch (Exception e) {
            // Return null and let the default behavior happen
        }
        return null;
    }

    @Override
    public KeyDeserializer keyDeserializerInstance(DeserializationConfig config,
            Annotated annotated,
            Class<?> keyDeserClass) {
        try {
            return (KeyDeserializer) applicationContext.getBean(keyDeserClass);
        } catch (Exception e) {
            // Return null and let the default behavior happen
        }
        return null;
    }

    @Override
    public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
        try {
            return (JsonSerializer<?>) applicationContext.getBean(serClass);
        } catch (Exception e) {
            // Return null and let the default behavior happen
        }
        return null;
    }

    @Override
    public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated,
            Class<?> builderClass) {
        try {
            return (TypeResolverBuilder<?>) applicationContext.getBean(builderClass);
        } catch (Exception e) {
            // Return null and let the default behavior happen
        }
        return null;
    }

    @Override
    public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
        try {
            return (TypeIdResolver) applicationContext.getBean(resolverClass);
        } catch (Exception e) {
            // Return null and let the default behavior happen
        }
        return null;
    }
}

Custom ObjectMapper:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;


public class CustomObjectMapper extends ObjectMapper {
    private static final long serialVersionUID = -8865944893878900100L;

    @Autowired
    ApplicationContext applicationContext;

    public JamaxObjectMapper() {
        // Problems serializing Hibernate lazily initialized collections?  Fix here.
//        HibernateModule hm = new HibernateModule();
//        hm.configure(com.fasterxml.jackson.module.hibernate.HibernateModule.Feature.FORCE_LAZY_LOADING, true);
//        this.registerModule(hm);

        // Jackson confused by what to set or by extra properties?  Fix it.
        this.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        this.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    }

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

and register your custom ObjectMapper:

<bean id="jacksonObjectMapper" class="com.acme.CustomObjectMapper" />
<bean id="jacksonMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
    <property name="prefixJson" value="false" />
    <property name="supportedMediaTypes" value="application/json" />
    <property name="objectMapper" ref="jacksonObjectMapper" />
</bean>

At this moment you can use:

@JsonDeserialize(contentUsing=PersonDeserializer.class)
public void setPerson(Person person) {
    ...
}

... and personService will not be null.

Community
  • 1
  • 1
Amir Jamak
  • 351
  • 1
  • 2
  • 15
1

cleanup to the above answer using spring boot,

@Bean
public HandlerInstantiator handlerInstantiator(ApplicationContext context) {
    return new SpringHandlerInstantiator(context.getAutowireCapableBeanFactory());
}

@Bean
public ObjectMapper objectMapper(HandlerInstantiator handlerInstantiator) {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    builder.handlerInstantiator(handlerInstantiator);
    return builder.build();
}
Bharath
  • 1,787
  • 1
  • 20
  • 37
  • Nice one, thanks! I recently figured out that the `Jackson2ObjectMapperBuilder` has a fluent API as well, making things even more convenient: `return Jackson2ObjectMapperBuilder.json().handlerInstantiator(springHandlerInstantiator).build();` – pixelstuermer Nov 26 '19 at 08:34
  • 1
    In Spring Boot (at least in 2.0+) JacksonAutoConfiguration should create automatically Jackson2ObjectMapperBuilder with handlerInstantiator properly configured. So no special configuration is needed. – martinsefcik May 20 '20 at 21:01