关于上下文、作用域和会话

发布者:    |      

在JSR-299中,上下文非常重要,以至于在尝试了几次之后,它实际上被纳入了名称中。那么,让我们看看上下文和作用域到底是什么。当我们想要了解实现细节时,我们会使用Weld作为参考,但核心内容是独立于实现的,并遵循规范规则。这篇博客文章可以算是一篇“傻瓜式的上下文和作用域”介绍,并进行了一些简化。如果你想要更正式的方法,你可能需要阅读详细的规范。

上下文

上下文就像一个标签货架。有名为@RequestScoped、@SessionScoped等的货架。当你使用特定作用域的bean时,会从该货架(或如果没有则创建并放置在那里)获取一个bean实例,这样你就可以保证在下次引用时(在相同的生命周期内)获取到相同的实例。

作为一个有趣的事实可以提到,实际上对于特定作用域可以有多个货架(只要在任何给定时间内只有一个处于活动状态)。给能找到这个用途的人5分钟的名誉。我认为这与曾经是规范一部分的子活动有关。

作用域

作用域是bean和上下文之间的链接。不同的上下文/作用域有不同的生命周期,这决定了你的东西应该放在架子上多长时间。当作用域达到其生命周期的结束时(例如,会话上下文的内容在会话过期时被销毁等),货架会自动清理。在Seam中,你可以几乎将任何bean放入任何作用域,但在CDI中,特定作用域的bean总是会最终放在相应的货架上。一个bean只有一个作用域。

代理

代理是包装器。有不同类型的代理,有占位代理用于注入点上的常规作用域bean,它在调用方法之前查找真实实例,还有围绕bean类真实实例的拦截器和内容等包裹的代理。

幕后

那么,当你有像这样的东西时,实际上会发生什么呢

@Model
public class Foo
{
   @Inject Bar bar;

   public String getPong()
   {
       return bar.pong();
   }
}

@SessionScoped
public class Bar implements Serializable
{
    public String pong()
    {
        return "pong";
    };
}

启动时,Weld会为Foo和Bar创建Bean。Foo是一个@Named @RequestScoped Bean(@Model注解),而Bar是一个@SessionScoped Bean。此时并没有创建实际的实例,但会有一个代理注入到

@Inject Bar bar;

其中,它可以在需要时查找Bar实例。此时,请求上下文和会话上下文都是空的。现在,让我们看看魔法所在。当你在一个xhtml页面中做如下操作时

#{foo.pong}

EL解析器就会介入,并得出我们需要以命名(EL可用)的Bean foo 为起点的结论。BeanManager将解析这个Bean,并注意到Foo是@RequestScoped,所以它会向RequestContext请求一个实例。由于请求上下文是空的,因此会生成一个实例,使用Foo-bean作为模板。这个实例被放在请求上下文中,以防在同一个请求期间有人需要它。

调用该实例上的getPong()方法会触发Bar的代理。由于Bar是@SessionScoped,BeanManager会咨询会话上下文以获取一个实例。由于那里没有,会创建一个实例,并将其放在会话上下文中以供以后参考。这个实例是调用其pong()方法的实例。

当请求结束时,请求上下文被销毁,并调用Foo的任何析构函数,但由于会话上下文仍然存活(会话没有终止),Bar的实例仍然存活。

现在,同一个webapp的另一个请求到来。请求作用域再次为空,并且我们有一个新的foo实例,但这次bar已经在会话上下文中找到,我们得到相同的Bar实例。正如我们所预期的,因为我们有一个新的请求(新鲜请求上下文),但同一个旧会话(旧的会话上下文)。这样,我们可以在上下文之间混合注入 - 我们可以在@SessionScoped中注入@RequestScoped Bean,反之亦然,并且代理将确保在上下文中击中(或创建)正确的实例。

上下文未激活

并不是所有的上下文总是激活的。从@ApplicationScoped Bean中获得上下文未激活异常有点棘手,但请求上下文仅在存在活跃的HTTP请求时可用,会话上下文仅在存在活跃的HTTP会话时可用。这意味着在这些上下文中访问Bean是不允许的,例如,从MDB中。为什么是这样呢?

以下是Weld特有的内容,以请求上下文为例:只有一个请求上下文。真的。你的webapp的5000个并发用户共享同一个请求上下文实例。是的,实际上下文数据存储在ThreadLocal字段中,这意味着(幸运的是)每个用户线程在该字段中都有自己的数据。

BeanStore是一个接口,而它特定的实现被注入请求上下文的ThreadLocal字段,这个实现封装了一个HTTP servlet请求。这意味着向那个BeanStore添加东西实际上是将它放入请求中,清除BeanStore会从请求中删除属于那个BeanStore的所有键。请求属性名称会通过一个命名方案过滤,因此以@RequestScoped类名称开头的属性被认为属于那个BeanStore。

这在实践中意味着servlet请求必须存在,才能作为BeanStore在请求上下文中的后端存储工作。上下文的活跃标志只是用来指示,在这个点上,Weld已经用真正的BeanStore填充了该上下文。

所以实际上,你不能直接将东西放在@RequestScoped的架子上。你只能将东西放在Weld放在架子上(由servlet请求支持的BeanStore)的盒子里。盒子的放置与上下文的激活耦合,盒子的移除与上下文的去激活耦合。这些激活和去激活由监视请求创建和销毁的servlet请求监听器处理。

当我们谈论请求作用域时,上下文是短暂的,每当请求开始时就会在架子上放置一个新的盒子,并在请求结束时清除它。但是关于会话作用域呢?故事非常相似,只是属于会话bean存储的物品在请求结束时不会销毁。当下一个请求到来时,会话上下文会通过包装HTTP会话的bean存储器重新填充。由于之前的请求也使用了HTTP会话作为后端存储,所以放在会话上下文中的东西在以@SessionScoped类名开头的会话属性中继续存在。正如你所注意到的,对于请求和会话上下文,命名都起着重要的作用。还有一些其他的东西在HTTP请求和会话属性周围飘浮,我们不想在我们清除bean存储时意外地触碰到不属于我们的东西。

重要的是要认识到,限制并不是Welds实现的限制。当然,可能有其他方法来实现上下文,使它们看起来总是可用的,但根据定义,它们可能不是语义上正确的。你也可以通过不直接写入底层HTTP会话来实现一些东西,但是当涉及到会话复制时,这是一个好事。

值得一提的是,由于规范说会话上下文必须存活于会话无效化(当你有直接由HTTP会话支持的bean存储,并在无效化后尝试访问它时这不是好事)的情况下,会话上下文中内置了一种缓冲机制,在初始化时加载并在会话在某些时候出现问题时缓存东西。

亲爱的,我们需要谈谈

对话上下文实际上类似于一个命名的子会话上下文。@RequestScoped的架子只能有一个存放物品的盒子,但在@ConversationScoped的架子上有许多盒子。盒子的标签是会话ID。对话可以是瞬时的(默认值)和在这个情况下,它表现得非常像请求上下文,只存在于一个HTTP请求期间。这并不是很有用,因此可以将对话提升为非瞬时的(长时间运行)。这是通过在可注入的对话bean上调用begin()或begin(String)方法来实现的。当请求结束时,对话管理器根据对话的transient标志来决定对话上下文的命运。如果是瞬时的,则销毁对话上下文;如果不是,则保持原样,并将生成的(或分配的)会话ID作为HTTP参数传递到下一个请求。

对话管理器也是决定我们是否应该从一个空白的、瞬时的对话和对话上下文开始,或者当一个请求到来时是否应该恢复一个现有的对话的人。在传入的HTTP请求中寻找cid参数。没有参数表示瞬时会话,但如果有一个值为cid的值,比如说1,对话管理器就会从其存储中取下标签为1的盒子,并将其加载到对话上下文中。实际上,它用适合匹配该会话ID的对话-bean存储器包装HTTP会话(再次强调,命名扮演着重要角色),并在对话实例中预先填充cid和transient标志设置为false。我们可以自由地切换对话,提升和降低它们,HTTP会话将通过其属性来镜像这些上下文,适合匹配对话的名称。

如果你认为可以通过修改传递的cid参数进行URL重写,那么请随意。由于你的自己的会话被用作后端存储,你唯一能做的就是切换到另一个自己的对话(或者因为cid未知而崩溃)。你不能从另一个用户那里访问对话数据,即使规范说不能跨越HTTP会话边界。

值得一提的是,随着现代浏览器的普及,获取新的会话变得越来越困难。标签浏览等特性带来了一种副作用,即会话和cookie在浏览器实例间共享。以前通常只需要启动一个新的浏览器,但如今通常需要使用隐身/私密浏览来测试同一品牌浏览器的会话边界。

结论

希望您现在对这个问题有了更深入的理解。您可能还想查阅有关如何编写自定义上下文和作用域类型的规范。您还可以查看位于org.jboss.weld.context包中的Weld源代码,了解BeanManagerImpl是如何进行解析的。


返回顶部