javaspring-bootcglibspring-framework-beans

Why is Lazy creation of beans leading to classes with different References/hashCodes in SpringBoot?


Why do bean of classes created using @Lazy annotation have different reference compared to the normal Autowired objects of the same class. I am aware that @Lazy provides bean instance when it is required but i thought it would always be the same reference.

I have written below tests to demonstrate the same.

A simple counter service :

@Service
public class CounterService {

  private final AtomicInteger counter = new AtomicInteger();


  public void inc() {
    counter.incrementAndGet();
  }

  public int getCounter() {
    return counter.get();
  }

}

Tests without use of Lazy :

public class CounterServiceTest extends BaseITTest {

  @Autowired
  private CounterService counterService;
  @Autowired
  private ApplicationContext context;

  @Test
  void testBeansReference() {
    CounterService contextCounterService = context.getBean(CounterService.class);

    counterService.inc();
    contextCounterService.inc();

    System.out.println(counterService.getCounter() + " / " + contextCounterService.getCounter());
    System.out.println("counterService --> obj: " + counterService + ", hashCode: " + counterService.hashCode());
    System.out.println("contextCounterService --> obj: " + contextCounterService + ", hashCode: " + contextCounterService.hashCode());
    System.out.println("== Check: " + (counterService == contextCounterService));
    System.out.println(".equals() Check: " + (counterService.equals(contextCounterService)));
  }
  
}

Output :

2 / 2
counterService --> obj: com.flock.appointment.service.CounterService@750c23a3, hashCode: 1963729827
contextCounterService --> obj: com.flock.appointment.service.CounterService@750c23a3, hashCode: 1963729827
== Check: true
.equals() Check: true

Tests without use of Lazy :

public class CounterServiceLazyTest extends BaseITTest {

  @Lazy
  @Autowired
  private CounterService counterService;
  @Autowired
  private ApplicationContext context;

  @Test
  void testBeansReference() {
    CounterService contextCounterService = context.getBean(CounterService.class);

    counterService.inc();
    contextCounterService.inc();

    System.out.println(counterService.getCounter() + " / " + contextCounterService.getCounter());
    System.out.println("counterService --> obj: " + counterService + ", hashCode: " + counterService.hashCode());
    System.out.println("contextCounterService --> obj: " + contextCounterService + ", hashCode: " + contextCounterService.hashCode());
    System.out.println("== Check: " + (counterService == contextCounterService));
    System.out.println(".equals() Check: " + (counterService.equals(contextCounterService)));
  }

}

Output :

2 / 2
counterService --> obj: com.flock.appointment.service.CounterService@259c6ab8, hashCode: -1833445830
contextCounterService --> obj: com.flock.appointment.service.CounterService@259c6ab8, hashCode: 631007928
== Check: false
.equals() Check: false

Solution

  • Will try to help, but let me preface by saying I'm not an expert on Spring or anything close to it.

    When using @Autowired and @Lazy (or @Inject and @Lazy), Spring will not inject the requested instance itself, naturally since you want it to be lazily initialized. Instead, it creates a "lazy proxy" object for it. The purpose of the proxy is defer the initialization of the real dependency for as long as possible.

    Think of a Lazy proxy as a Mockito spy, or a java.lang.reflect.Proxy. It has the same interface as the original object that it is proxying to. It also has a bit of "middleware" in it (a.k.a InvocationHandler implementation), which is responsible for initializing the "real dependency" as soon as the first invocation is made on the proxy. From that point on, all method calls on the proxy are simply forwarded to the downstream dependency.

    For some good reason I suppose, Spring has its own Proxy implementation at https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/cglib/proxy/Proxy.java (rather than using Java's built-in Proxy). This is the "CGLIB" stuff you were referring to. Browse around that class, the whole module, as well as the parent module, to learn more about it.

    Check out the docs on the Java built-in Proxy too https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html

    A better alternative to using @Lazy is probably to use Provider<Foo> fooProvider or similar, rather than @Lazy @Autowired Foo foo. The downside is that you have to call fooProvider.get() or such but the advantage is that you don't need to explain anyone how on earth @Lazy even works.

    Finally, the hashCodes and toStrings questions you posed - the @Lazy injected instance (proxy) probably proxies the toString() call to the downstream dependency, but does not proxy the hashCode() and equals() methods. Not entirely sure why they would choose to do that, but, that's what it looks like to me.

    Hope it helps lead you toward the real answer, cheers!