Enterprise Java

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. The name attribute specifies that this bean will be named MessageService.
  • The @Scope(BeanDefinition.SCOPE_PROTOTYPE) annotation specifies that a new instance of MessageService 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 of ApplicationContext.
  • applicationContext.getBean(beanName, "Hello, World!") retrieves a new instance of the MessageService bean with the message – Hello, World!
  • applicationContext.getBean(beanName, "Goodbye, World!") retrieves another new instance of the MessageService bean with the message – Goodbye, World!.

The output is:

Fig 1: Output from running spring prototype bean example with runtime arguments
Fig 1: Output from running spring prototype bean example with runtime arguments

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 a String parameter (the message) and return an instance of MessageService.
  • createMessageService method is responsible for creating instances of the MessageService bean using the injected Function. It accepts a String parameter message, which represents the message to be passed to the MessageService 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.

Download
You can download the full source code of this example here: Spring Prototype Bean with Runtime Arguments

Omozegie Aziegbe

Omos holds a Master degree in Information Engineering with Network Management from the Robert Gordon University, Aberdeen. Omos is currently a freelance web/application developer who is currently focused on developing Java enterprise applications with the Jakarta EE framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button