Reputation: 1060
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
Reputation: 8297
Consider the following part of your code:
public class SimpleBean {
@Autowired
SimplePrototypeBean prototypeBean;
}
what do you expect the prototypeBean
field to refer to?
PrototypeBean
?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.
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());
}
}
RandomHolder
is a prototype bean in the IoC container (identical to the way you declared the getPrototypeBean
bean)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 RandomHolder
s and invokes methods on new instances of them.
proxyMode = ScopedProxyMode.TARGET_CLASS
when we drop the proxyMode
argument:
random = 2628323
random = 2628323
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