How do I declare another Jackson ObjectMapper without affecting the "clients" of the original bean?
I have a spring-boot application that provides json REST API. For mapping objects to json, the built-in jackson ObjectMapper is used, configured with spring-boot.
Now I need to read some data from the yaml file and I found that an easy way to do this is to use Jackson - for that I need to declare another ObjectMapper to convert the yaml to objects. I have declared this new mapper bean with a specific name to be able to inject it into my service related to reading from a yaml file:
@Bean(YAML_OBJECT_MAPPER_BEAN_ID)
public ObjectMapper yamlObjectMapper() {
return new ObjectMapper(new YAMLFactory());
}
But I need a way to tell all other "clients" of the original json ObjectMapper to keep using this bean. So I basically need the @Primary annotation on the original bean. Is there a way to achieve this without having to override the original ObjectMapper in my own config (I'll have to dig through the spring-boot code to find and copy its config)?
One solution I found is to declare a FactoryBean for the ObjectMapper and return it to the already declared bean as suggested in this answer . I found out while debugging that my original bean is called "_halObjectMapper", so my factoryBean will look for that bean and return it:
public class ObjectMapperFactory implements FactoryBean<ObjectMapper> {
ListableBeanFactory beanFactory;
public ObjectMapper getObject() {
return beanFactory.getBean("_halObjectMapper", ObjectMapper.class);
}
...
}
Then in my config class I declare it as @Primary bean to make sure it is the first choice for auto-provisioning:
@Primary
@Bean
public ObjectMapperFactory objectMapperFactory(ListableBeanFactory beanFactory) {
return new ObjectMapperFactory(beanFactory);
}
However, I am not 100% happy with this solution because it relies on a bean name that is out of my control and it also looks like a hack. Is there a cleaner solution?
Thank!
source to share
You can define two ObjectMapper
beans and declare them primary, for example:
@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 annotation @Qualifier
like:
@Autowired
@Qualifier("Your_id")
private ObjectMapper yamlMapper;
Update
You can dynamically add ObjectMapper
to Spring bean factory at runtime like:
@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 to context
without modifying the existing bean ( sysout
prints two beans at the end of the method init
). Then you can use "yamlMapper" as a qualifier for its auto-mapper anywhere.
Update 2 (from the author of the question):
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());
}
source to share
Another option is to wrap the custom mapper in a 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 a call getMapper
after you type YamlObjectMapper
.
source to share
I believe that defining an explicit primitive object transformer for the MVC layer should work like this:
@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 auto-connect via YAML_OBJECT_MAPPER_BEAN_ID
.
source to share
I just realized that I don't need to use FactoryBean, I could also declare a normal bean as @Primary and return it to the original bean, like so:
@Bean
@Primary
public ObjectMapper objectMapper(@Qualifier("_halObjectMapper") ObjectMapper objectMapper) {
return objectMapper;
}
This makes the configuration a little cleaner, but still requires the exact name of the original ObjectMapper. I guess I'll stay with this solution.
source to share