10
votes

I have a spring-boot application that exposes a json REST API. For mapping objects to json it uses the built-in jackson ObjectMapper configured by spring-boot.

Now I need to read some data from a yaml file and I found that an easy way to do it is using Jackson - for this I need to declare a different ObjectMapper to convert yaml to objects. I declared this new mapper bean with a specific name to be able to inject it in my service dealing with reading from the yaml file:

@Bean(YAML_OBJECT_MAPPER_BEAN_ID)
public ObjectMapper yamlObjectMapper() {
    return new ObjectMapper(new YAMLFactory());
}

But I need a way to tell all the other "clients" of the original json ObjectMapper to keep using that bean. So basically I would need a @Primary annotation on the original bean. Is there a way to achieve this without having to redeclare the original ObjectMapper in my own configuration (I'd have to dig through spring-boot code to find and copy its configuration)?

One solution I found is to declare a FactoryBean for ObjectMapper and make it return the already declared bean, as suggested in this answer. I found by debugging that my original bean is called "_halObjectMapper", so my factoryBean will search for this bean and return it:

public class ObjectMapperFactory implements FactoryBean<ObjectMapper> {

    ListableBeanFactory beanFactory;

    public ObjectMapper getObject() {
        return beanFactory.getBean("_halObjectMapper", ObjectMapper.class);
    }
    ...
}

Then in my Configuration class I declare it as a @Primary bean to make sure it's the first choice for autowiring:

@Primary
@Bean
public ObjectMapperFactory objectMapperFactory(ListableBeanFactory beanFactory) {
    return new ObjectMapperFactory(beanFactory);
}

Still, I'm not 100% happy with this solution because it relies on the name of the bean which is not under my control, and it also seems like a hack. Is there a cleaner solution?

Thanks!

4
I also tried to use YAMLMapper (which extends ObjectMapper) as my custom bean type, but spring would still complain that it finds two beans of type ObjectMapper. I didn't expect this, although in a way it's plausible. So again I learned something new today. It means that if I autowire a field of type List<Object> I'll obtain all the available beans in the context... - Timi
check out the update to my answer.. - Darshan Mehta

4 Answers

5
votes

You can define two ObjectMapper beans and declare one as primary, e.g.:

@Bean("Your_id")
public ObjectMapper yamlObjectMapper() {
    return new ObjectMapper(new YAMLFactory());
}

@Bean
@Primary
public ObjectMapper objectMapper() {
    return new ObjectMapper();
}

Once done, you can use your objectmapper bean with @Qualifier annotation, e.g.:

@Autowired
@Qualifier("Your_id")
private ObjectMapper yamlMapper;

Update

You can dynamically add your ObjectMapper to Spring's bean factory at runtime, e.g.:

@Configuration
public class ObjectMapperConfig {

    @Autowired
    private ConfigurableApplicationContext  context;

    @PostConstruct
    private void init(){
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ObjectMapper.class);
        builder.addConstructorArgValue(new JsonFactory());
        DefaultListableBeanFactory factory = (DefaultListableBeanFactory) context.getBeanFactory();
        factory.registerBeanDefinition("yamlMapper", builder.getBeanDefinition());
        Map<String, ObjectMapper> beans = context.getBeansOfType(ObjectMapper.class);
        beans.entrySet().forEach(System.out::println);
    }
}

The above code adds a new bean into context without changing the existing bean (sysout prints two beans in the end of init method). You can then use "yamlMapper" as qualifier to autowire it anywhere.

Update 2 (from question author):

The solution suggested in 'Update' works and here's a simplified version:

@Autowired
private DefaultListableBeanFactory beanFactory;

@PostConstruct
private void init(){
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(YAMLMapper.class);
    beanFactory.registerBeanDefinition("yamlMapper", builder.getBeanDefinition());
}
4
votes

Other option is to wrap custom mapper into custom object:

@Component
public class YamlObjectMapper {
    private final ObjectMapper objectMapper;

    public YamlObjectMapper() {
        objectMapper = new ObjectMapper(new YAMLFactory());
    }

    public ObjectMapper getMapper() {
        return objectMapper;
    }
}

Unfortunately this approach requires calling getMapper after you inject YamlObjectMapper.

2
votes

I believe defining explicit primary object mapper for MVC layer should work this way:

 @Primary
 @Bean
 public ObjectMapper objectMapper() {
     return Jackson2ObjectMapperBuilder.json().build();
 }

All beans that autowire object mapper via type will use above bean. Your Yaml logic can autowire via YAML_OBJECT_MAPPER_BEAN_ID.

0
votes

I just realized that I don't need to use a FactoryBean, I could just as well declare a regular bean as @Primary and make it return the original bean, like this:

@Bean
@Primary
public ObjectMapper objectMapper(@Qualifier("_halObjectMapper") ObjectMapper objectMapper) {
    return objectMapper;
}

This makes the configuration slightly cleaner, but still requires the exact name of the original ObjectMapper. I guess I'll stay with this solution, though.