ever alian
ever alian

Reputation: 1060

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

Upvotes: 1

Views: 1208

Answers (1)

Denis Zavedeev
Denis Zavedeev

Reputation: 8297

Intro

Consider the following part of your code:

public class SimpleBean {
   @Autowired
   SimplePrototypeBean prototypeBean;
}

what do you expect the prototypeBean field to refer to?

  • Should it always be the same instance of a PrototypeBean?
  • Or should it enclose the prototype logic somehow?

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

  • When default configuration used (without specifying the proxyMode), the field will appear to us as the same prototype instance

  • But when you specify TARGET_CLASS or INTERFACES then not the PrototypeBean instance will be injected, but its proxy, (see Scoped beans as dependencies):

    That is, you need to inject a proxy object that exposes the same public interface as the scoped object but that can also retrieve the real target object from the relevant scope (such as an HTTP request) and delegate method calls onto the real object.

    and when the scope is prototype, then:

    every method call on the shared proxy leads to the creation of a new target instance to which the call is then being forwarded.

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());
    }
}
  • It is a Spring Boot application
  • RandomHolder is a prototype bean in the IoC container (identical to the way you declared the getPrototypeBean bean)
  • The RandomHolder has one field which we expect to be the same.

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:

  • the output is the same
    random = 2628323
    random = 2628323
    
  • Spring won't create a proxy, but will create a new instance every time it requested

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.

Upvotes: 3

Related Questions