Prototype beans in singletons
- Spring bean scope singleton vs prototype
- Inject prototype bean into singleton in Spring
- Spring @Lookup annotation example
- ObjectFactory and Provider in Spring
- How to get new prototype bean instance in Spring
- Spring ApplicationContext getBean for prototype scope
- Spring Scoped Proxy
Spring bean scopes
First things first – let’s remember crucial scope types for Spring beans: singleton, prototype, request, session.
Singleton beans (default scope) have only one shared instance of the bean per Spring IoC container.
Prototype beans are created each time they’re requested from the contianer.
Injection problem
Suppose we have this prototype bean:
@Service
@Scope(scopeName = "prototype")
public class MyPrototypeBean {
private final UUID uuid = UUID.randomUUID();
public String getId() {
return uuid.toString();
}
}
And this REST controller, that injects and uses MyPrototypeBean:
@RestController
public class MySingletonBean {
@Autowired
private MyPrototypeBean prototypeBean;
@GetMapping("/")
public String id() {
return prototypeBean.getId();
}
}
When we run this code, we’ll find that id will always be the same. Why?
Because the dependencies of a singleton bean are resolved once, even if they’re prototypes.
First solution: manual use of ApplicationContext
We can inject ApplicationContext (or BeanFactory) and get prototype bean from the container:
@RestController
public class MySingletonBean {
@Autowired
private ApplicationContext appContext;
@GetMapping("/")
public String id() {
return appContext.getBean(MyPrototypeBean.class).getId();
}
}
Second solution: @Lookup
In essence, this solution is an enhancement for the first one.
The @Lookup annotation in Spring tells the framework to dynamically replace a method call with logic that returns a new bean instance from the Spring context – even if the bean is prototype-scoped, and we’re calling it from a singleton-scoped class.
If we define abstract method using @Lookup, Spring will generate the implementation at runtime:
@Lookup
abstract MyPrototypeBean getMyPrototypeBean();
We should use it as follows:
MyPrototypeBean myPrototypeBean = getMyPrototypeBean();
String id = myPrototypeBean.getId();
Notice, that getMyPrototypeBean() is abstract and can’t be private. Also, keep in mind that the service class should be abstract as well.
Third solution: ObjectFactory<T>
When we may want to use ObjectFactory? Well, it’s especially convenient for the tests: ObjectFactory has one getObject() method that can easily be mocked.
But why not use setter-injection then? The answer is that if we use setter-injection – then prototype characteristics of MyPrototypeBean disappear.
@RestController
public class MySingletonBean {
@Autowired
private ObjectFactory<MyPrototypeBean> objectFactory;
@GetMapping("/")
public String id() {
return objectFactory.getObject().getId();
}
}
Fourth option: Provider<T>
The reasons to use Provider<T> are the same as for ObjectFactory<T>.
It requires jakarta.inject dependency:
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<version>2.0.1</version>
</dependency>
And looks like this:
@RestController
public class MySingletonBean {
@Autowired
private Provider<MyPrototypeBean> provider;
@GetMapping("/")
public String id() {
return provider.get().getId();
}
}
Fifth option: Scoped Proxy
When we set proxyMode within @Scope annotation for a bean, Spring replaces actual bean with proxy in IoC container.
Then, each method call is delegated to a real instance, resolved from the scope (e.g., request scope).
Therefore, Scoped proxies work correctly in environments where the scope context stays active across multiple method calls, like:
- HTTP requests (with
@RequestScope) - HTTP sessions (with
@SessionScope)
In theory, it’s still possible to use with prototype.
However, one should remember that If we have a prototype scope + scoped proxy, then the new instance is created each time we do a method call.
@Service
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyPrototypeBean {
private final String id = UUID.randomUUID().toString();
public String getId() {
return id;
}
}
We can inject MyPrototypeBean into MySingletonBean as usual:
@RestController
public class MySingletonBean {
@Autowired
private MyPrototypeBean myPrototypeBean;
@GetMapping("/")
public String id() {
return myPrototypeBean.get().getId();
}
}
New bean will be created for each myPrototypeBean() method call.