proxyMode 和 scopeName 是如何工作的?

How proxyMode and scopeName works under the hood?

我正在研究 Spring 框架,我看到了一些我无法解释实际发生的事情。 假设我们有这个简单的服务 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;
  }
}

和以下使用上述服务的控制器:

@RestController
public class NumberController {

  @Autowired
  RandomNumberGenerator numberGenerator;

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

我假设每次请求都会创建一个新的 RandomNumberGenerator numberGenerator 实例,因此我应该在 System.out.println(System.identityHashCode(numberGenerator)); 中打印不同的数字,但奇怪的是我每次请求都得到相同的数字,但我可以看到 RandomNumberGenerator 的每个请求构造函数都被调用了!

例如你可以看到我的控制台输出:

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

我的问题是为什么即使调用了构造函数我得到的是相同的数字? System.out.println(System.identityHashCode(numberGenerator)) 不应该 returns 不同的整数吗?

关于如何在 Spring 中实现代理已经写了很多,请参阅我在以下帖子中的回答:

简而言之,使用ScopedProxyMode.TARGET_CLASS,Spring将使用生成RandomNumberGenerator的子class。 class 的名称类似于 RandomNumberGenerator$$EnhancerByCGLIB$$d0daae41.

这个 class 覆盖(几乎)所有 RandomNumberGenerator 的方法来充当工厂和委托者。这些重写方法中的每一个都将生成一个真实 RandomNumberGenerator 类型的新实例(每个请求)并将方法调用委托给它。

Spring 将创建此新 CGLIB class 的实例并将其注入到您的 @Controller class' 字段

@Autowired
RandomNumberGenerator numberGenerator;

您可以在此对象上调用 getClass(未被覆盖的方法之一)。你会看到类似

的东西

RandomNumberGenerator$$EnhancerByCGLIB$$d0daae41

表示这是代理对象

当您调用(覆盖的)方法时

numberGenerator.getNumber()

CGLIB class 将使用 Spring 的 ApplicationContext 生成一个新的 RandomNumberGenerator bean 并在其上调用 getNumber() 方法。


scopeName 控制 bean 的范围。 Spring 将使用 RequestScope 来处理这些 bean。如果你的控制器两次调用 getNumber(),你应该得到相同的值。代理(通过 RequestScope)将在内部缓存新的、每个请求的对象

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

如果您使用了像“会话”这样的范围,Spring 会在多个请求中缓存真实对象。您甚至可以使用范围“单例”,对象将在所有请求中缓存。


如果确实需要,您可以使用此处描述的技术检索实际实例

  • Is it possible to unproxy a Spring bean?

例如,

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

至于 System.identityHashCode,您通过将代理对象传递给它来调用它。只有一个代理对象,所以调用总是 return 相同的值。

请注意,identityHashCode 不保证 return 不同对象的不同值。它的 javadoc 状态

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.

hashCode()'s javadoc状态

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.