Search code examples
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
  • Note that output in second test(with lazy) is 2, which proves that even though reference is different, the classes are still singleton which is expected as other wise output of wouldn't be 2/2.
  • I am also not sure in second test why object.toString() are producing same outputs even though hashCode is different and also == checks says false. Isn't number in end of toString() hex of hashCode, and that hex should be different as hashCodes printed are different ?
  • I had observed the instance of counterService in test 2 in debugger and the instance was enhanced by CGLIB, I read about CGLIB and it made me think most of the things are due to it but I did not understand it properly how all of this is happening, Also I would like to know in detail exactly how is spring internally doing this.

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!