1. Overview

In Spring applications, most of the beans are singletons. And we generally achieve the collaboration by declaring beans as dependencies. However, using non-singleton beans as a dependency generally requires additional effort when implementing our classes. Throughout this tutorial, we'll examine how we can access a prototype-scoped bean from a singleton bean.

2. Sample Application

Let's start with our sample application.

We have the Timer class:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Timer {

    private final String id = UUID.randomUUID().toString();
    private long start;

    public void start() {
        start = System.nanoTime();
    }

    public void stop() {
        long elapsed = System.nanoTime() - start;
        System.out.println(id + ": " + elapsed);
    }
}

Here, the Timer class has the prototype scope - SCOPE_PROTOTYPE. We're also declaring an id field to help us identify the Timer instances.

Then the ClientService class declares Timer as a dependency:

@Component
public class ClientService {

    private final Timer timer;
    
    @Autowired
    public ClientService(Timer timer) {
        this.timer = timer;
    }

    public void doWork() {
        timer.start();
        timer.stop();
    }
}

Note that Timer and ClientService have different bean scopes. ClientService is a singleton whereas Timer is a prototype-scoped bean.

3. Default Behavior

Let's talk about the default behavior.

When we declare a prototype-scoped bean as a dependency in multiple beans, Spring creates a new instance for each injection point. For example, Spring injects a new instance of Timer, when it initializes the ClientService bean. If we had another class - let's say MonitorService -, it would also get a new Timer instance. However, the injected instance doesn't change after the beans are initialized. This means that ClientService will hold the same Timer instance as long as it lives.

This behavior can be desired in some cases. But if we want to get a new instance for each method invocation - not just for initialization - we must guide Spring in that direction.

In the following sections, we'll examine different ways to acquire a new Timer instance whenever we invoke ClientService.doWork.

4. Get with ApplicationContext

Firstly, we'll use ApplicationContext to get a bean honoring its scope. If the bean is a singleton, ApplicationContext returns the same instance. However, if the bean is a prototype, ApplicationContext returns a new instance.

@Component
public class ClientService {

    private final ApplicationContext context;

    @Autowired
    public ClientService(ApplicationContext context) {
        this.context = context;
    }

    public void doWork() {
        final Timer timer = context.getBean(Timer.class);
        timer.start();
        timer.stop();
    }
}

Here, we're declaring ApplicationContext as a dependency. Then in the doWork method, we're calling ApplicationContext.getBean to acquire a new instance for each invocation.

When we call doWork multiple times:

public static void main(String[] args) {
    ConfigurableApplicationContext applicationContext =
      new AnnotationConfigApplicationContext(Application.class);
    final ClientService clientService = applicationContext.getBean(ClientService.class);
    
    clientService.doWork();
    clientService.doWork();
    clientService.doWork();
    
    applicationContext.close();
}

The output shows the different Timer.id values as expected:

265c15fd-dce6-4ee8-b8e2-e896f1d804bc: 2162
a304740c-a134-4615-aed9-d4b728c8e3dc: 259
3a3e58b5-710e-487f-93c3-24de5d35b0c6: 251

5. Get with ObjectFactory, ObjectProvider, and Provider

Next, we'll examine some factory classes in order to acquire prototype-scoped beans.

Firstly, we'll use the ObjectFactory class. It returns a shared or independent instance according to the bean's scope:

@Component
public class ClientService {

    private final ObjectFactory<Timer> timerObjectFactory;

    @Autowired
    public ClientService(ObjectFactory<Timer> timerObjectFactory) {
        this.timerObjectFactory = timerObjectFactory;
    }

    public void doWork() {
        final Timer timer = timerObjectFactory.getObject();
        timer.start();
        timer.stop();
    }
}

In this example, we're declaring ObjectFactory<Timer> as a dependency. And in the doWork method, we're calling ObjectFactory.getObject to get a new Timer instance. When we run the application, Spring injects the timerObjectFactory variable.

Spring also provides ObjectProvider which extends the ObjectFactory interface. Its usage is similar to ObjectFactory:

@Component
public class ClientService {

    private final ObjectProvider<Timer> timerObjectProvider;

    @Autowired
    public ClientService(ObjectProvider<Timer> timerObjectProvider) {
        this.timerObjectProvider = timerObjectProvider;
    }

    public void doWork() {
        final Timer timer = timerObjectProvider.getObject();
        timer.start();
        timer.stop();
    }
}

Another factory class supported by Spring is javax.inject.Provider. It returns a new instance given that the bean is prototype-scoped:

@Component
public class ClientService {

    private final Provider<Timer> timerProvider;

    @Autowired
    public ClientService(Provider<Timer> timerProvider) {
        this.timerProvider = timerProvider;
    }

    public void doWork() {
        final Timer timer = timerProvider.get();
        timer.start();
        timer.stop();
    }
}

6. Get with @Lookup Method

Another approach to acquire a prototype bean is via the @Lookup annotation. For this purpose, we must create a method with the desired return type and annotate it with @Lookup:

@Component
public abstract class ClientService {

    public void doWork() {
        Timer timer = getTimer();
        timer.start();
        timer.stop();
    }

    @Lookup
    protected abstract Timer getTimer();
}

Here, we have the abstract getTimer method - annotated with @Lookup. Also, note that its return type is TimerSo Spring creates a subclass extending ClientService at runtime - using CGLIB - and then overrides the getTimer method. The overridden method returns a shared or new instance according to the bean's scope. In this example, we declared the method and the class as abstract, but it isn't mandatory. Having a concrete class with a dummy method implementation is also valid.

Alternatively, we can also use @Lookup to get the target bean with its name:

@Lookup(value = "timer")
protected abstract Timer getTimerWithName();

Here, Spring looks up a Timer whose bean name is timer.

7. Get with Lookup in Java-based Configuration

In the previous approach, we relied on the @Lookup annotation and CGLIB-powered subclasses. Now, we'll implement a similar solution without using annotations in a Java-based configuration:

public abstract class ClientService {

    public void doWork() {
        Timer timer = getTimer();
        timer.start();
        timer.stop();
    }

    protected abstract Timer getTimer();
}

Here, we have the abstract ClientService class with the abstract getTimer method. We also don't use any stereotype annotation - @Component, @Repository, etc.

Unlike the other examples, we'll now create a @Configuration class:

@Configuration
public class ApplicationConfiguration {

    @Bean
    public ClientService clientService() {
        return new ClientService() {
            @Override
            protected Timer getTimer() {
                return timer();
            }
        };
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Timer timer() {
        return new Timer();
    }
}

In this configuration, we're defining two beans: Timer and Clientservice. In the timer method, we're defining the scope as prototype. Then in the clientService method, we're providing an implementation for ClientService calling the timer method. Note that since Spring subclasses @Configuration classes using CGLIB at runtime, ClientService invokes the timer method on a CGLIB-generated subclass.

8. Get with ServiceLocatorFactoryBean

Lastly, we'll examine the ServiceLocatorFactoryBean class to access a prototype-scoped bean from a singleton.

The first step is creating a factory interface:

public interface TimerFactory {

    Timer getTimer();

    Timer getTimer(String name);
}

In this factory class, we have two methods. The getTimer(String name) method is defined to get a bean with its name.

Then in a configuration class, we must define a bean of type ServiceLocatorFactoryBean:

@Bean
public ServiceLocatorFactoryBean timerLocator() {
    final ServiceLocatorFactoryBean locator = new ServiceLocatorFactoryBean();
    locator.setServiceLocatorInterface(TimerFactory.class);
    return locator;
}

The important part is that we're setting TimerFactory as the locator interface - setServiceLocatorInterface(TimerFactory.class).

After this bean definition, Spring creates a dynamic proxy at runtime implementing our factory interface. In the end, we'll inject it as a dependency:

@Component
public class ClientService {

    private final TimerFactory timerFactory;

    @Autowired
    public ClientService(TimerFactory timerFactory) {
        this.timerFactory = timerFactory;
    }

    public void doWork() {
        final Timer timer = timerFactory.getTimer();
        timer.start();
        timer.stop();
    }
}

9. Summary

In this tutorial, we examined several ways to inject a prototype-scoped bean into a singleton bean using Spring.

As always, the source code for all examples in this tutorial is available on Github.