javaspringspring-bootspring5

Prototype bean returns multiple instances within singleton object


I'm new to spring and doing some study about proxyMode=ScopedProxyMode.TARGET_CLASS. I wrote a simple project to test this with singleton and prototype bean. But it prints a new prototype bean instance when I print the object.

public class SimplePrototypeBean {
    private String name;

    //getter setters.       
}

Singleton bean

public class SimpleBean {

   @Autowired
   SimplePrototypeBean prototypeBean;

   public void setTextToPrototypeBean(String name) {
       System.out.println("before set > " + prototypeBean);
       prototypeBean.setText(name);
       System.out.println("after set > " + prototypeBean);
   }

   public String getTextFromPrototypeBean() {
       return prototypeBean.getText();
   }    
}

Config class.

@Configuration
public class AppConfig {
    
  @Bean 
  SimpleBean getTheBean() {
    return new SimpleBean();
  }

  @Bean
  @Scope(value = "prototype", proxyMode=ScopedProxyMode.TARGET_CLASS)
  public SimplePrototypeBean getPrototypeBean(){
    return new  SimplePrototypeBean();
  }
} 

Unit test

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class SimpleConfigTest {

@Test
public void simpleTestAppConfig() {
    
    ApplicationContext ctx =
             new AnnotationConfigApplicationContext(AppConfig.class);
    
    for (String beanName : ctx.getBeanDefinitionNames()) {
         System.out.println("Bean " + beanName);
    }

    SimpleBean simpleBean1 = (SimpleBean) ctx.getBean("getTheBean"); 
    SimpleBean simpleBean2 = (SimpleBean) ctx.getBean("getTheBean"); 
              
              
    simpleBean1.setTextToPrototypeBean("XXXX");
    simpleBean2.setTextToPrototypeBean("YYYY");
          
    System.out.println(simpleBean1.getTextFromPrototypeBean());
    System.out.println(simpleBean2.getTextFromPrototypeBean());
    System.out.println(simpleBean2.getPrototypeBean());     
  } 
}

Output

Bean org.springframework.context.annotation.internalAutowiredAnnotationProcessor
Bean org.springframework.context.annotation.internalCommonAnnotationProcessor
Bean org.springframework.context.event.internalEventListenerProcessor
Bean org.springframework.context.event.internalEventListenerFactory
Bean appConfig
Bean getTheBean
Bean scopedTarget.getPrototypeBean
Bean getPrototypeBean
springCertification.com.DTO.SimpleBean@762ef0ea
springCertification.com.DTO.SimpleBean@762ef0ea
before set > springCertification.com.DTO.SimplePrototypeBean@2f465398
after set > springCertification.com.DTO.SimplePrototypeBean@610f7aa
before set > springCertification.com.DTO.SimplePrototypeBean@6a03bcb1
after set > springCertification.com.DTO.SimplePrototypeBean@21b2e768
null
null
springCertification.com.DTO.SimplePrototypeBean@17a7f733

See above output always showing new instance and the value in the text field is null. I'm running only once this app. So I'm expecting only 2 prototype instances will be created as I call simpleBean1 and simpleBean2. Can someone explain to me why this is happening and how to fix it to have only 2 prototype objects where simpleBean1 holds one prototypeBean and simpleBean2 holds another prototypeBean


Solution

  • Intro

    Consider the following part of your code:

    public class SimpleBean {
       @Autowired
       SimplePrototypeBean prototypeBean;
    }
    

    what do you expect the prototypeBean field to refer to?

    Prototype means, every time we ask an IoC container for a bean it will return a new instance

    That is when you call any method, incuding the toString method, on the SimplePrototypeBean bean, Spring creates a new target instance of SimplePrototypeBean underneath to invoke the method on.


    Another mcve

    You can try the following MCVE to gain the understanding:

    @Component
    @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public class RandomHolder {
        private final int random = ThreadLocalRandom.current().nextInt();
    
        public int getRandom() {
            return random;
        }
    }
    

    And the class with main:

    @SpringBootApplication
    @AllArgsConstructor
    public class SoApplication implements ApplicationRunner {
        private final RandomHolder randomHolder;
    
        public static void main(String[] args) {
            SpringApplication.run(SoApplication.class, args);
        }
    
        @Override
        public void run(ApplicationArguments args) {
            System.out.println("random = " + randomHolder.getRandom());
            System.out.println("random = " + randomHolder.getRandom());
        }
    }
    

    When we run the application returned values from the getRandom method can be different, here is a sample output:

    random = 183673952
    random = 1192775015
    

    as we now know, the randomHolder refers to a proxy, and when a method is invoked on it, the new target instance of RandomHolder is created and the method is invoked on it.

    You can imagine that the proxy looks like this:

    public class RandomHolderProxy extends RandomHolder {
        private final Supplier<RandomHolder> supplier = RandomHolder::new;
    
        @Override
        public int getRandom() {
            return supplier.get().getRandom();
        }
    }
    

    that is, it has an ability to create RandomHolders and invokes methods on new instances of them.

    Without proxyMode = ScopedProxyMode.TARGET_CLASS

    when we drop the proxyMode argument:

    If we add another component:

    @AllArgsConstructor
    @Component
    public class ApplicationRunner2 implements ApplicationRunner {
        private final RandomHolder randomHolder;
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            System.out.println("ApplicationRunner2: " + randomHolder.getRandom());
            System.out.println("ApplicationRunner2: " + randomHolder.getRandom());
        }
    }
    

    then the output could be:

    random = -1884463062
    random = -1884463062
    ApplicationRunner2: 1972043512
    ApplicationRunner2: 1972043512
    

    So I'm expecting only 2 prototype instances will be created as I call simpleBean1 and simpleBean2.

    Your expectation is a little bit inexact there, you have as many instances of prototype bean created as many times you have any methods invoked on.

    Can someone explain to me why this is happening

    I hope, my explanation was clear enough

    and how to fix it to have only 2 prototype objects where simpleBean1 holds one prototypeBean and simpleBean2 holds another prototypeBean

    The problem here is not in the prototype scope, but in the scope of SimpleBean: it is a singleton, so you have the same instance of SimpleBean when you do:

    SimpleBean simpleBean1 = (SimpleBean) ctx.getBean("getTheBean"); 
    

    just add an assertion to your test method:

    SimpleBean simpleBean1 = (SimpleBean) ctx.getBean("getTheBean");
    SimpleBean simpleBean2 = (SimpleBean) ctx.getBean("getTheBean");
    
    Assertions.assertSame(simpleBean2, simpleBean1);
    

    it won't fail.

    Once again, hope this helps.