Mixing Bean Scopes – Proxy and @Lookup

Is possible that in your design you will encounter situations where beans have the Singleton Scope but dependencies on beans with the Prototype scope.

Let’s consider the following scenario: we have the MochaCoffee bean with the Singleton scope. This class has a dependency on the Drink class, which has a Prototype scope. Additionally, we have implemented the prepareCoffee() method, where we add specific data to build the coffee drink. Also we have a method called getInstance() that return the number of MochaCoffee instances created so far.

@Component
@Primary
@Qualifier("Mocha")
public class MochaCoffee implements Coffee {

    private static int instances = 0;

    Drink mocha;

    @Override
    public void prepareCoffee() {
        mocha.setBase("Expresso");
        mocha.setModifiers(new String[] {"Chocolate Syrup", "Steamed milk", "Whiped Cream"});
        mocha.setGarnish("Whipped Cream");

        System.out.println("Making a Mocha...");
        System.out.println(mocha.drinkDetails());
    }

    @Autowired
    public void setMocha(Drink mocha) {
        this.mocha = mocha;
    }

    public Drink getMocha(){
        return this.mocha;
    }

    public static int getInstances(){
        return MochaCoffee.instances;
    }
}

The Drink bean has an Prototype scope. Each time a Drink is created in the constructor, we increment the counter instances by 1. Additionally, we have implemented two methods: one called getInstance() that returns the number of Drink instances created so far and the other one called drinkDetails() that displays all the Drink data.

@Component("coffe")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Drink {

    //This field is for track the number of instances created.
    private static int instances = 0;

    private int id;
    private String base;
    private String[] modifiers;
    private String garnish;

    public Drink(){
        instances++;
        System.out.println("Drink constructor called");
    }

    public static int getInstances() {
        return Drink.instances;
    }

    // Getters and setters
    
    public String drinkDetails() {
        return "Drink{" +
                "id=" + id +
                ", base='" + base + '\'' +
                ", modifiers=" + Arrays.toString(modifiers) +
                ", garnish='" + garnish + '\'' +
                '}';
    }
}

And finally, we have to modify The CoffeeshopApplication class in order to show the instances of the MochaCoffee and Drink beans.

@SpringBootApplication
public class CoffeeshopApplication {

	public static void main(String[] args) {

		//Here we create the ApplicationContext that manages the beans and dependencies
		ApplicationContext appContext = SpringApplication.run(CoffeeshopApplication.class, args);

		System.out.println("\nMochaCoffee bean with singleton scope");
		//Retrieve the singleton bean from application Context
		MochaCoffee coffe1 = appContext.getBean(MochaCoffee.class);
		System.out.println(coffe1);
		coffe1.prepareCoffee();

		//Retrieve the Prototype bean from application Context
		Drink drink1 = coffe1.getMocha();
		Drink drink2 = coffe1.getMocha();
		Drink drink3 = coffe1.getMocha();

		System.out.println("\nDrink bean with prototype scope");
		System.out.println(drink1);
		System.out.println(drink2);
		System.out.println(drink3);

		//Print the number of MochaCoffee instances created
		System.out.println("Number of Mocha Coffee instances: " + MochaCoffee.getInstances());

		//Print the number of Drink instances created
		System.out.println("Number of Drink instances: " + Drink.getInstances());

	}

}

After executing the CoffeeShop Application, we observe the following output in the console. In this output, we can verify that the MochaCoffee bean has a specific memory address. However, an issue arises when dealing with the Drink bean, which has a Prototype scope. Despite being a Prototype bean, Spring returns the same memory address for multiple instances of the Drink bean. This occurs because when a prototype bean is injected into a singleton bean, it loses its prototype behavior and behaves as a singleton.

2023-07-11 15:44:51.075  INFO 12872 --- [           main] c.p.c.proxy.CoffeeshopApplication        : No active profile set, falling back to 1 default profile: "default"
Drink constructor called
Make Coffee Setter Implementation invoked
Make Coffee Implementation Constructor invoked
2023-07-11 15:44:51.460  INFO 12872 --- [           main] c.p.c.proxy.CoffeeshopApplication        : Started CoffeeshopApplication in 0.715 seconds (JVM running for 1.024)

MochaCoffee bean with singleton scope
com.programmingsquirrel.coffeeshop.proxy.impl.MochaCoffee@721eb7df
Making a Mocha...
Drink{id=0, base='Expresso', modifiers=[Chocolate Syrup, Steamed milk, Whiped Cream], garnish='Whipped Cream'}

Drink bean with prototype scope
com.programmingsquirrel.coffeeshop.proxy.impl.Drink@df4b72
com.programmingsquirrel.coffeeshop.proxy.impl.Drink@df4b72
com.programmingsquirrel.coffeeshop.proxy.impl.Drink@df4b72
Number of Mocha Coffee instances: 0
Number of Drink instances: 1

How we can solve this problem, well Spring has several ways to handle this problem, we will focus on one of them the Proxy.

Proxy

Spring cannot inject the prototype bean into the singleton bean after it has created. To sort this we use the proxy. To use the proxy we need to declare a bean with a prototype scope, but also we need to use the proxyMode property with the value of ScopedProxyMode.TARGET_CLASS.

How does it work? The Prototype bean is not directly autowired into the Singleton bean at the time of its creation. Instead, a proxy or “placeholder” object is autowired. This proxy adds a level of indirection. When the developer requests the prototype bean from Spring, a proxy is created and is returned by the application context.

With the proxy mode in place, the Spring container can inject a new object into the Singleton bean when a method on the proxy object is called. This mechanism ensures that each time a method is invoked on the proxy, a fresh instance of the Prototype bean is provided, maintaining the expected behavior of the Prototype scope.

Let’s add to the Drink bean the proxy.

@Component("coffe")
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Drink {

    //THis field is for track the number of instances created.
    private static int instances = 0;

    private int id;
    private String base;
    private String[] modifiers;
    private String garnish;

    public Drink(){
        instances++;
        System.out.println("Drink constructor called");
    }

    public static int getInstances() {
        return Drink.instances;
    }

    //Getters and Setters

    public String drinkDetails() {
        return "Drink{" +
                "id=" + id +
                ", base='" + base + '\'' +
                ", modifiers=" + Arrays.toString(modifiers) +
                ", garnish='" + garnish + '\'' +
                '}';
    }
}

Now, after run the CoffeeShop Application, we will get the below console output, where now we can verify that the Drink constructor is called three times because the Drink objects has different memory address.

2023-07-11 17:54:23.554  INFO 13756 --- [           main] c.p.c.proxy.CoffeeshopApplication        : No active profile set, falling back to 1 default profile: "default"
Make Coffee Setter Implementation invoked
Make Coffee Implementation Constructor invoked
2023-07-11 17:54:23.989  INFO 13756 --- [           main] c.p.c.proxy.CoffeeshopApplication        : Started CoffeeshopApplication in 0.755 seconds (JVM running for 1.092)

MochaCoffee bean with singleton scope
com.programmingsquirrel.coffeeshop.proxy.impl.MochaCoffee@15f193b8

Drink bean with prototype scope
Drink constructor called
com.programmingsquirrel.coffeeshop.proxy.impl.Drink@3dedb4a6
Drink constructor called
com.programmingsquirrel.coffeeshop.proxy.impl.Drink@57f64f5e
Drink constructor called
com.programmingsquirrel.coffeeshop.proxy.impl.Drink@415e0bcb
Number of Mocha Coffee instances: 0
Number of Drink instances: 3

@Lookup

Another method to address the issue of mixed scopes is by using the @Lookup annotation on the getMocha() method. This annotation instructs Spring to return an instance of the Drink type, similar to calling beanFactory.getBean(Drink.class).

It’s important to consider that the Singleton scope is designed to minimize the number of objects created, so the scope should only be changed when necessary. Increasing the number of objects can have an impact on memory usage and garbage collection.

The code of this entry, can be find in the  CoffeShop project on GitHub under the package com.programmingsquirrel.coffeeshop.proxy

Thanks for reading and Happy learning!!!

Leave a comment