获取 CDI 托管 bean 实例的规范方法:BeanManager#getReference() 与 Context#get()

Posted

技术标签:

【中文标题】获取 CDI 托管 bean 实例的规范方法:BeanManager#getReference() 与 Context#get()【英文标题】:Canonical way to obtain CDI managed bean instance: BeanManager#getReference() vs Context#get() 【发布时间】:2013-12-01 15:21:39 【问题描述】:

我认为,当只有Bean<T> 开头(基于Class<T> 创建)时,有两种通用方法可以通过BeanManager 获取自动创建的CDI 托管bean 实例:

    作者BeanManager#getReference(),更常见于 sn-ps:

    Bean<TestBean> bean = (Bean<TestBean>) beanManager.resolve(beanManager.getBeans(TestBean.class));
    TestBean testBean1 = (TestBean) beanManager.getReference(bean, bean.getBeanClass(), beanManager.createCreationalContext(bean));
    

    Context#get(),在 sn-ps 中很少出现:

    Bean<TestBean> bean = (Bean<TestBean>) beanManager.resolve(beanManager.getBeans(TestBean.class));
    TestBean testBean2 = beanManager.getContext(bean.getScope()).get(bean, beanManager.createCreationalContext(bean));
    

实际上,它们最终做的事情完全相同:返回对当前 CDI 托管 bean 实例的代理引用,如果该 bean 实例在作用域中不存在,则自动创建它。

但他们的做法有点不同:BeanManager#getReference() 总是创建一个全新的代理实例,而 Context#get() 重用现有的代理实例(如果之前已经创建)。当上述代码在现有TestBean 实例的操作方法中执行时,这一点很明显:

System.out.println(testBean1 == testBean2); // false
System.out.println(testBean1 == this); // false
System.out.println(testBean2 == this); // true

Context#get() 的 javadoc 在这方面非常明确:

返回某个上下文类型的现有实例或通过调用 Contextual.create(CreationalContext) 创建一个新实例并返回新实例。

虽然BeanManager#getReference() 的javadoc 在这方面不够明确:

获取某个bean的上下文引用以及该bean的某个bean类型。

这让我很困惑。你什么时候使用一个或另一个?对于这两种方式,您无论如何需要一个Bean&lt;T&gt; 实例,bean 类和bean 范围很容易从中获得,这是作为附加参数所必需的。我无法想象为什么在这种特定情况下需要从外部提供它们。

我可以想象Context#get() 的内存效率更高,因为它不会不必要地创建另一个引用相同底层 bean 实例的代理实例,而只是查找并重用现有的代理实例。

这让我想到了以下问题:BeanManager#getReference() 究竟什么时候比Context#get() 更有用?它更常显示在 sn-ps 中,并且更经常被推荐为解决方案,但它只会不必要地创建一个新代理,即使已经存在一个代理。

【问题讨论】:

【参考方案1】:

beanManager#getReference 为您提供客户端代理的新实例,但客户端代理会将方法调用转发到特定上下文的当前上下文实例。 一旦您获得代理并保留它,将在当前实例(例如当前请求)上调用方法调用。 如果上下文实例不可序列化,这也很有用 - 客户端代理将在您反序列化后重新连接。

BeanManager#getContext 无需客户端代理即可获取目标实例。您可能仍会在类名中看到 Weld 的代理,但它是提供拦截和装饰的增强子类。如果 bean 没有被拦截或修饰,这将是给定 bean 的一个普通实例。

通常 (1) 更合适,除非您有特殊用例需要直接访问目标实例(例如访问其字段)。

或者换句话说

1) BeanManager#getReference 将返回一个“上下文引用”,其中包含 bean 的正常范围代理。 如果一个bean有@SessionScoped作为

@SessionScoped User user;

然后上下文引用用户将“指向”每次调用的当前会话的相应用户实例(“上下文实例”)。 从两个不同的网络浏览器对user.getName() 的两次不同调用会给你不同的答案。

2) Context#get() 将返回一个没有正常作用域代理的内部“上下文实例”。这通常不是用户应该自称的。如果您以这种方式获得“Bob”的User user 并将其存储在@ApplicationScoped bean 或静态变量中, 那么它将永远是用户“Bob”——即使是来自其他浏览器的网络请求!您将获得一个直接的、非代理的实例。

【讨论】:

我已经更新了答案,希望现在你会得到真正的答案。有时链接非常适合答案 回到具体问题,好吧,Context#get() 因此实际上没有返回任何代理。我相信我被增强的子类误导了。这现在确实更有意义。如果您确实需要一个可序列化的代理,请使用BeanManager#getReference()。如果您不需要可序列化代理和/或需要通过反射探索实例,请使用Context#get()。谢谢你的回答! 是的,你是绝对正确的第三条评论。不用说谢谢,不客气 也认为打印出来的答案 - 没有摘要的链接在那里毫无用处。【参考方案2】:

这在将 CDI 与 javafx 集成时非常有用,问题是我需要引用正确的作用域对象,而不是依赖作用域的代理...

我使用了生产者方法来获取一个 javaFX 节点,该节点像这样注入到控制器中:

@Inject
@ApplicationScoped
@FXMLFile("javafx/wares.fxml")
@FXMLController(WaresController.class)
Parent wares;

但是当使用 BeanManager#getReference() 代理时,我得到“吃掉”FXMLLoader 设置的所有值,getContext.get() 方法解决了它。

谢谢

【讨论】:

【参考方案3】:

我有一个 Singleton,我使用 getReference() 方法来获取引用。即使单例已经初始化,通过 getReference() 创建的代理在每次使用 getReference() 时都会调用 @PostConstruct。

@Startup
@ApplicationScoped
@Singleton

@PostConstruct
private void initialize() 

通过切换到 getContext().get() 方法,不再进行不必要的@PostConstruct 代理调用。

【讨论】:

有趣的副作用。我相信这一定是使用的 CDI 实现中的一个错误。您使用的是哪个 CDI impl/version?

以上是关于获取 CDI 托管 bean 实例的规范方法:BeanManager#getReference() 与 Context#get()的主要内容,如果未能解决你的问题,请参考以下文章

将 JSF 托管 bean 迁移到 CDI 托管 bean

CDI 托管 bean 和 JSF 托管 bean 可以相互通信吗?

使用 CDI(上下文和依赖注入)支持 bean 而不是托管 Bean

如何从过滤器中获取 SessionScoped CDI bean?

是否有可能在运行时检测CDI托管bean的范围?

JSF-2.3 找不到我的 @Named CDI-1.2 托管 bean