Affects: 5.3.22

Issue Details

According to the below attached code, My expectation is that spring framework creates one single instance of ObjectMapper class, But actual result is that it creates 2 instances of it.

Run details

  • By running the below attached class as it is, the following result appears:
Setting applied: class SettingOne
Setting applied: class SettingTwo
Created the [com.fasterxml.jackson.databind.ObjectMapper@592e843a] instance of object mapper with [2] total applied settings.
Removed the [settingOne] -> [SettingOne] setting bean.
Removed the [settingTwo] -> [SettingTwo] setting bean.
Created the [com.fasterxml.jackson.databind.ObjectMapper@64bc21ac] instance of object mapper with [0] total applied settings.
objectMapperReference1: com.fasterxml.jackson.databind.ObjectMapper@592e843a
objectMapperReference2: com.fasterxml.jackson.databind.ObjectMapper@592e843a
objectMapperReference3: com.fasterxml.jackson.databind.ObjectMapper@64bc21ac
objectMapperReference4: com.fasterxml.jackson.databind.ObjectMapper@64bc21ac
  • By running it with the commented <-- focus line line, the following result appears:
Setting applied: class SettingOne
Setting applied: class SettingTwo
Created the [com.fasterxml.jackson.databind.ObjectMapper@592e843a] instance of object mapper with [2] total applied settings.
objectMapperReference1: com.fasterxml.jackson.databind.ObjectMapper@592e843a
objectMapperReference2: com.fasterxml.jackson.databind.ObjectMapper@592e843a
objectMapperReference3: com.fasterxml.jackson.databind.ObjectMapper@592e843a
objectMapperReference4: com.fasterxml.jackson.databind.ObjectMapper@592e843a

Demo code (ready for copy paste, apart of maven details)

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

public class Demo {
    public static void main(final String... args) {
        final AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.register(ApplicationConfiguration.class);
        applicationContext.refresh();

        final ObjectMapper objectMapperReference1 = applicationContext.getBean(ObjectMapper.class);
        final ObjectMapper objectMapperReference2 = applicationContext.getBean(ObjectMapper.class);

        removeConsumedSettings(applicationContext); // <-- focus line

        final ObjectMapper objectMapperReference3 = applicationContext.getBean(ObjectMapper.class);
        final ObjectMapper objectMapperReference4 = applicationContext.getBean(ObjectMapper.class);

        System.out.println("objectMapperReference1: " + objectMapperReference1);
        System.out.println("objectMapperReference2: " + objectMapperReference2);
        System.out.println("objectMapperReference3: " + objectMapperReference3);
        System.out.println("objectMapperReference4: " + objectMapperReference4);
    }

    private static void removeConsumedSettings(final ApplicationContext applicationContext) {
        final BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory();
        applicationContext.getBeansOfType(Setting.class).forEach((name, bean) -> {
            beanDefinitionRegistry.removeBeanDefinition(applicationContext.getBeanNamesForType(bean.getClass())[0]);
            System.out.printf("Removed the [%s] -> [%s] setting bean.%n", name, bean.getClass().getName());
        });
    }
}

interface Setting {
    void apply(ObjectMapper objectMapper);
}

class SettingOne implements Setting {
    @Override
    public void apply(final ObjectMapper objectMapper) {
        System.out.println("Setting applied: " + this.getClass());
    }
}

class SettingTwo implements Setting {
    @Override
    public void apply(final ObjectMapper objectMapper) {
        System.out.println("Setting applied: " + this.getClass());
    }
}

@Configuration
class ApplicationConfiguration {
    @Bean
    public Setting settingOne() {
        return new SettingOne();
    }

    @Bean
    public Setting settingTwo() {
        return new SettingTwo();
    }

    @Bean
    public ObjectMapper objectMapper(final List<Setting> settings) {
        final ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();
        settings.forEach(setting -> setting.apply(objectMapper));
        System.out.printf("Created the [%s] instance of object mapper with [%d] total applied settings.%n", objectMapper, settings.size());
        return objectMapper;
    }
}

Comment From: snicoll

    final BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory();

You can't cast a BeanFactory to a BeanRegistry like this and/or removing bean definitions manually as you do when the context is running.

Comment From: me0x847206

Well, then a question appears: is there any way to correctly remove a bean definition from spring context after a successful bootstrapping process?

Comment From: bclozel

I'm afraid there is not. What would be the expected behavior in that case? Should all the injected singletons be removed as well? How can we ensure that the global state of the system is not unstable or plain invalid?

As Stephane explained, casting the bean factory like this is not supported. We might even make some methods throw exceptions once the context has been refreshed.

Comment From: me0x847206

I see. Thanks for answer.

The cast part may look strange but, as Internet is full with such incorrect solutions then, most probably it is something needed or asked.

What would be the expected behavior in that case? Should all the injected singletons be removed as well? How can we ensure that the global state of the system is not unstable or plain invalid?

Is there a chance then, for spring framework, to contain one more layer of abstraction - somewhere around configuration - with a kind of consumable-scoped instances of beans which are instantiated and used during the application bootstrapping process and immediately removed once used? A kind of short-living instances similar to request scoped but at configuration level only.

The reason I'm asking is that currently some kind of spring based applications become over-populated with beans which are not needed for entire application life but at configuration phase only.