A
A
Anton Mikhailov2019-07-13 14:37:19
Java
Anton Mikhailov, 2019-07-13 14:37:19

Spring Boot: how to create custom bean without affecting autoconfiguration behavior?

Question for Spring Boot experts.
I need to inject a custom ObjectMapper into my component, but at the same time I don’t want to break the default mapper created automatically in JacksonAutoConfiguration , because in all other places in the application the default version is used, supplemented with settings from application.yml / bootstrap.yml : all kinds of custom serialization/deserialization settings for dates, enums, etc. That is, I do not want to touch the default option, because it is already correctly configured to work with the standard settings options.
I create my config :

@Configuration
public class CustomMapperConfig {

    @Bean(name = "myCustomMapper")
    public ObjectMapper myCustomMapper() {
    	ObjectMapper mapper = new ObjectMapper();
        //do some customizing
        return mapper;
    }
}

And I want to use different mapper implementations:
@Component
public class MyComponent {
  
  @Autowired @Qualifier("myCustomMapper")
  ObjectMapper myCustomMapper; // хочу использовать тут свой бин 

  @Autowired
  ObjectMapper objectMapper;
    // хочу тут использовать стандартный бин из автоконфига,  
    // но инжектится все равно моя кастомная реализация
}

But as a result, all places where ObjectMapper is used start using my custom bean.
There is no difference - inject by name, specify Qualifier explicitly or specify nothing at all - my own implementation will still be used.
The fact is that JacksonAutoConfiguration uses this setting:
@Bean
@Primary
@ConditionalOnMissingBean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
  return builder.createXmlMapper(false).build();
}

Despite the fact that @Primary is specified here, @ConditionalOnMissingBean works in priority : the framework sees that I have my own implementation and does not create from the autoconfig. To get around this, I tried the following options: 1) Inject JacksonAutoConfiguration -> into CustomMapperConfig to explicitly set the initialization order + set the lowest priority of your implementation

@Configuration
@AllArgsConstructor
public class CustomMapperConfig {
    private final JacksonAutoConfiguration autoConfiguration;

    @Bean(name = "myCustomMapper")
    @Order(Ordered.LOWEST_PRECEDENCE)
    public ObjectMapper myCustomMapper() {
        //...
    }
}

It doesn't work: yes, the classes are initialized in the right order, but my create method still executes before the method in JacksonAutoConfiguration . Accordingly, then it again works out the behavior of @ConditionalOnMissingBean
2) Inject the default mapper into your config and configure it (this option would also suit me - a custom setting would not interfere with the default behavior in other places):
option 1:
@Configuration
@AllArgsConstructor
public class CustomMapperConfig {

    private final ObjectMapper defaultMapper;

    @Bean(name = "myCustomMapper")
    public ObjectMapper myCustomMapper() {
        //customize
        return defaultMapper;
    }
}

option 2:
@Configuration
public class CustomMapperConfig {

    @Bean(name = "myCustomMapper")
    public ObjectMapper myCustomMapper(ObjectMapper defaultMapper) {
        //customize
        return defaultMapper;
    }
}

As a result, it crashes with the following error:
The dependencies of some of the beans in the application context form a cycle: customMapperConfig defined in file...

Of course, you can create a bean in your own configuration in exactly the same way as it is done in autoconfiguration, in fact duplicating it so that it consumes all the same settings as the default one. But I really would not want to do this: this is a copy-paste and library hack.
Is it possible to somehow save the creation of the default bean from the autoconfig? Or what is specified there @ConditionalOnMissingBean rigidly prevails over the rest of the settings?

Answer the question

In order to leave comments, you need to log in

3 answer(s)
J
JeRRy_froyo, 2019-07-13
@JeRRy_froyo

I can suggest this option:

@Configuration
public class CustomMapperConfig {

    public CustomMapperConfig(ApplicationContext context) {
        ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
        //customize
    }
}

UPD:
here you can register your bean (if you still need separate objects) through context.getAutowireCapableBeanFactory()
if you use only the default one, then you can probably make it even easier:
public CustomMapperConfig(ObjectMapper objectMapper) {
    //customize
}

B
BorLaze, 2019-07-14
@BorLaze

The other day I was also digging with a similar question.
So, autoconfiguration always works after those configs that are defined in the application.
As for the question, I would try

@Bean(name = "myCustomMapper")
    @DependsOn({"objectMapper"})
    public ObjectMapper myCustomMapper(@Lazy @Qualifier("objectMapper") ObjectMapper defaultMapper) {
        //customize
        return defaultMapper;
    }

In general, to play around with such directives - perhaps it will be possible to explain to the spring what exactly you want to receive at the input.

L
lexas, 2019-07-15
@lexas

It seems to me that it is better to take the path of least resistance and create two MPs. One to replace the main, and the second custom with autowire-candidate=false.
Yes, an extra bean declaration, but more readable and without a workaround.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question