首页 > 解决方案 > proxyMode 和 scopeName 如何在幕后工作?

问题描述

我正在研究 Spring 框架,我看到了一些我无法解释实际发生的事情。假设我们有这个简单的服务类:

@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))应该返回不同的整数吗?

标签: javaspringaop

解决方案


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

简而言之,ScopedProxyMode.TARGET_CLASSSpring 将使用CGLIB生成RandomNumberGenerator. 该类将具有类似的名称RandomNumberGenerator$$EnhancerByCGLIB$$d0daae41

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

Spring 将创建这个新 CGLIB 类的实例并将其注入到您的@Controller类的字段中

@Autowired
RandomNumberGenerator numberGenerator;

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

RandomNumberGenerator$$EnhancerByCGLIB$$d0daae41

表示这是代理对象。

当您调用(覆盖)方法时

numberGenerator.getNumber()

该 CGLIB 类将使用 SpringApplicationContext生成一个新RandomNumberGeneratorbean 并getNumber()在其上调用方法。


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

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

如果您使用了“会话”之类的范围,Spring 将跨多个请求缓存真实对象。您甚至可以使用范围“单例”,并且该对象将被缓存在所有请求中。


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

例如,

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

至于System.identityHashCode,您通过将代理对象传递给它来调用它。只有一个代理对象,因此调用将始终返回相同的值。

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

为给定对象返回与默认方法返回的相同的哈希码hashCode(),无论给定对象的类是否覆盖hashCode()。空引用的哈希码为零。

andhashCode()的 javadoc状态

要求如果两个对象根据方法不相等equals(java.lang.Object),则对两个对象中的每一个调用 hashCode 方法必须产生不同的整数结果。


推荐阅读