Search code examples
javaspringaop

How proxyMode and scopeName works under the hood?


I am studying Spring framework and I saw some thing that I couldn't explain what happens actually. suppose we have this simple Service class:

@Service
@Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RandomNumberGenerator {

  double number;
  public RandomNumberGenerator() {
    System.out.println("Constructor is called!");
    number = Math.random();
  }

  public double getNumber() {
    return number;
  }
}

and the following controller that uses above service:

@RestController
public class NumberController {

  @Autowired
  RandomNumberGenerator numberGenerator;

  @GetMapping(path = "/number")
  public double getNumber(){
    System.out.println(System.identityHashCode(numberGenerator));
    return numberGenerator.getNumber();
  }
}

I postulated that with every request a new RandomNumberGenerator numberGenerator instance will be created and so I should have different numbers printed in the System.out.println(System.identityHashCode(numberGenerator)); but strangely I got the same number with every request but I can see with every request constructor of the RandomNumberGenerator is called!

for example you can see my console output:

257434344
Constructor is called!
257434344
Constructor is called!
257434344
Constructor is called!
257434344
Constructor is called!
257434344
Constructor is called!
257434344
Constructor is called!
257434344
Constructor is called!
257434344
Constructor is called!
257434344
Constructor is called!
257434344
Constructor is called!
257434344
Constructor is called!
257434344

my question is why I get the same number even though constructor is called? isn't System.out.println(System.identityHashCode(numberGenerator)) should returns different integers?


Solution

  • A lot has been written about how proxies are implemented in Spring, see my answers in the following posts:

    In short, with ScopedProxyMode.TARGET_CLASS, Spring will use CGLIB to generate a subclass of RandomNumberGenerator. The class will have a name like RandomNumberGenerator$$EnhancerByCGLIB$$d0daae41.

    This class overrides (almost) all the methods of RandomNumberGenerator to act as factories and delegators. Each of these overriden methods will produce a new instance (per request) of the real RandomNumberGenerator type and delegate the method call to it.

    Spring will create an instance of this new CGLIB class and inject it into your @Controller class' field

    @Autowired
    RandomNumberGenerator numberGenerator;
    

    You can call getClass on this object (one of the methods that is not overriden). You'd see something like

    RandomNumberGenerator$$EnhancerByCGLIB$$d0daae41

    indicating that this is the proxy object.

    When you call the (overriden) methods

    numberGenerator.getNumber()
    

    That CGLIB class will use Spring's ApplicationContext to generate a new RandomNumberGenerator bean and invoke the getNumber() method on it.


    The scopeName controls the scope of the bean. Spring will use RequestScope to handle these beans. If your controller called getNumber() twice, you should get the same value. The proxy (through the RequestScope) would internally cache the new, per-request object

    @GetMapping(path = "/number")
    public double getNumber(){
      System.out.println(numberGenerator.getNumber() == numberGenerator.getNumber()); // true
      return 123d;
    }
    

    If you had used a scope like "session", Spring would cache the real object across multiple requests. You can even use scope "singleton" and the object would be cached across all requests.


    If you really needed to, you can retrieve the actual instance with the techniques described here

    For example,

    Object real = ((Advised)numberGenerator).getTargetSource().getTarget();
    

    As for System.identityHashCode, you're calling it by passing the proxy object to it. There's only one proxy object, so the call will always return the same value.

    Note that identityHashCode is not guaranteed to return different values for different objects. Its javadoc states

    Returns the same hash code for the given object as would be returned by the default method hashCode(), whether or not the given object's class overrides hashCode(). The hash code for the null reference is zero.

    and hashCode()'s javadoc states

    It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results.