Spring Prototype Beans with Runtime Arguments
Spring offers various bean scopes, with the default being singleton, which creates a single instance throughout the application. Prototype scope, on the other hand, creates a new instance every time the bean is requested. But what if we need a prototype bean that can be customized with runtime arguments? This article explores several methods to achieve this in Spring.
1. Define the Prototype Bean
Spring provides several bean scopes, among which the two most commonly used are Singleton (A single instance per Spring container) and Prototype (A new instance every time the bean is requested). In this article, we will use the prototype scope.
To demonstrate this, we will create a simple Spring Boot application with the following dependencies spring-boot-starter-web
and spring-boot-starter-test
.
First, define a simple prototype-scoped bean that accepts runtime arguments. Below is a simple MessageService
bean.
public class MessageService { private String message; public MessageService() { } public MessageService(String message) { this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public void printMessage() { System.out.println("Message: " + message); } }
1.1 Create Configuration Class for Prototype Scoped Bean
@Configuration @ComponentScan(basePackages = { "com.jcg.prototypescopebean" }) public class AppConfig { @Bean(name="MessageService") @Scope(BeanDefinition.SCOPE_PROTOTYPE) public MessageService createMessageService(String message) { return new MessageService(message); } }
In the code above:
- The
createMessageService
method is annotated with@Bean
, indicating that it will produce a bean managed by the Spring container. Thename
attribute specifies that this bean will be named MessageService. - The
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
annotation specifies that a new instance ofMessageService
should be created each time it is requested, instead of using a single shared instance.
2. Using Application Context
We can retrieve the bean from the application context multiple times within our main class to verify that it returns different instances. We access and print messages from our prototype bean from two different instances.
@SpringBootApplication public class PrototypescopebeanApplication implements CommandLineRunner { @Autowired private ApplicationContext applicationContext; public static void main(String[] args) { SpringApplication.run(PrototypescopebeanApplication.class, args); } @Override public void run(String... args) throws Exception { String beanName = "MessageService"; // Retrieve and use the prototype bean MessageService messageService1 = (MessageService) applicationContext.getBean(beanName, "Hello, World!"); messageService1.printMessage(); MessageService messageService2 = (MessageService) applicationContext.getBean(beanName, "Goodbye, World!"); messageService2.printMessage(); } }
From the code above, each call to applicationContext.getBean
with the same bean name but different parameters creates a new instance of the MessageService
bean.
@Autowired
is used to inject an instance ofApplicationContext
.applicationContext.getBean(beanName, "Hello, World!")
retrieves a new instance of theMessageService
bean with the message – Hello, World!applicationContext.getBean(beanName, "Goodbye, World!")
retrieves another new instance of theMessageService
bean with the message – Goodbye, World!.
The output is:
3. Using the @Lookup
Annotation
The @Lookup
annotation allows injecting a method that retrieves a new prototype bean instance whenever called. This approach is ideal for situations where the bean is used within another bean.
To demonstrate the use of the @Lookup
annotation for retrieving prototype-scoped beans, let’s create a separate component class that contains the @Lookup
method. We’ll then inject this component into the main class and use it to retrieve and print messages from our prototype beans.
3.1 Creating the Lookup Component
Next, Create a separate component class with the @Lookup
method:
@Component public class MessageServiceFactory { @Lookup public MessageService createMessageService(String message) { return new MessageService(message); } }
3.2 Main Application Class
Now, inject the MessageServiceFactory
into the main application class and use it to retrieve and print messages from the prototype beans:
@SpringBootApplication public class PrototypescopebeanApplication implements CommandLineRunner { @Autowired private MessageServiceFactory messageServiceFactory; public static void main(String[] args) { SpringApplication.run(PrototypescopebeanApplication.class, args); } @Override public void run(String... args) throws Exception { MessageService messageService1 = messageServiceFactory.createMessageService("Hello, World!"); messageService1.printMessage(); MessageService messageService2 = messageServiceFactory.createMessageService("Goodbye, World!"); messageService2.printMessage(); } }
4. Using ObjectProvider
for Managing Prototype-Scoped Beans
To enhance the code by using ObjectProvider
, we can leverage its ability to retrieve beans lazily and manage prototype-scoped beans more elegantly. Here’s how to modify the PrototypescopebeanApplication
class to use ObjectProvider
for the MessageService
bean.
4.1 Create the ObjectProvider Component
Create a separate component class that uses ObjectProvider
to retrieve the MessageService
bean:
@Component public class MessageServiceProvider { @Autowired private ObjectProvider<MessageService> messageServiceProvider; public MessageService getMessageService(String message) { return messageServiceProvider.getObject(message); } }
The ObjectProvider<MessageService>
is injected into the class using the @Autowired
annotation. ObjectProvider
is a Spring utility that allows for lazy retrieval of beans and is particularly useful for prototype-scoped beans.
getMessageService
method takes a String
parameter message
. It calls messageServiceProvider.getObject(message)
to retrieve a new instance of MessageService
with the provided message. Since MessageService
is defined with prototype scope, each call to getObject
returns a new instance of MessageService
.
4.2 Update Main Application Class
Inject the MessageServiceProvider
into the main application class and use it to retrieve and print messages from the prototype beans:
@SpringBootApplication public class PrototypescopebeanApplication implements CommandLineRunner { @Autowired private MessageServiceProvider messageServiceProvider; public static void main(String[] args) { SpringApplication.run(PrototypescopebeanApplication.class, args); } @Override public void run(String... args) throws Exception { // Retrieve and use the prototype bean MessageService messageService1 = messageServiceProvider.getMessageService("Hello, World!"); messageService1.printMessage(); MessageService messageService2 = messageServiceProvider.getMessageService("Goodbye, World!"); messageService2.printMessage(); } }
5. Using the Functional Interface
We can also use a Function
interface to define the creation logic of our prototype-scoped beans. Here is the updated AppConfig
class that includes a Function<String, MessageService>
bean definition:
5.1 Create a MessageServiceFunction Class
Create a separate class that uses the Function
interface to create instances of the MessageService
bean:
@Component public class MessageServiceFunction { @Autowired private Function messageServiceFunction; public MessageService createMessageService(String message) { return messageServiceFunction.apply(message); } }
In the above class,
private Function<String, MessageService> messageServiceFunction
field represents a function that will take aString
parameter (the message) and return an instance ofMessageService
.createMessageService
method is responsible for creating instances of theMessageService
bean using the injectedFunction
. It accepts aString
parametermessage
, which represents the message to be passed to theMessageService
bean.
5.2 Update Configuration Class
@Configuration @ComponentScan(basePackages = { "com.jcg.prototypescopebean" }) public class AppConfig { @Bean(name="MessageService") @Scope(BeanDefinition.SCOPE_PROTOTYPE) public MessageService createMessageService(String message) { return new MessageService(message); } @Bean public Function<String, MessageService> serviceFactory() { return message -> new MessageService(message); } }
The above class defines a Function<String, MessageService>
bean named serviceFactory
, which uses a lambda expression to create instances of the MessageService
bean.
5.3 Update the Main Application Class
Inject the MessageServiceFunction
into the main application class and use it to create instances of the MessageService
bean:
@SpringBootApplication public class PrototypescopebeanApplication implements CommandLineRunner { @Autowired private MessageServiceFunction messageServiceFunction; public static void main(String[] args) { SpringApplication.run(PrototypescopebeanApplication.class, args); } @Override public void run(String... args) throws Exception { // Retrieve and use the prototype bean MessageService messageService1 = messageServiceFunction.createMessageService("Hello, World!"); messageService1.printMessage(); MessageService messageService2 = messageServiceFunction.createMessageService("Goodbye, World!"); messageService2.printMessage(); } }
6. Conclusion
In this article, we explored different strategies for managing prototype-scoped beans in a Spring Boot application. We examined the use of ObjectProvider
to lazily retrieve prototype beans, ensuring that each request results in a new instance. Subsequently, we introduced the Function
interface, demonstrating how it can be utilized to encapsulate bean creation logic within a separate class. Additionally, we explored the @Lookup
annotation, which offers a concise way to retrieve prototype beans directly from the Spring container.
7. Download the Source Code
This was an article on how to create a Spring Prototype Bean with Runtime Arguments.
You can download the full source code of this example here: Spring Prototype Bean with Runtime Arguments