正确使用有状态 Bean 和 Servlet
Posted
技术标签:
【中文标题】正确使用有状态 Bean 和 Servlet【英文标题】:Correct usage of Stateful Beans with Servlets 【发布时间】:2010-12-28 10:56:56 【问题描述】:我们目前有一个注入到 Servlet 中的 Stateful bean。问题是有时我们在有状态 bean 上执行方法时会得到一个Caused by: javax.ejb.ConcurrentAccessException: SessionBean is executing another request. [session-key: 7d90c02200a81f-752fe1cd-1]
。
public class NewServlet extends HttpServlet
@EJB
private ReportLocal reportBean;
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try
String[] parameters = fetchParameters(request);
out.write(reportBean.constructReport(parameters));
finally
out.close();
在上面的代码中,constructReport
将检查它是否需要打开到报告中指定的数据库的新连接,然后根据指定参数构建的查询构造 HTML 格式的报告。
我们之所以选择使用有状态 bean 而不是无状态 bean,是因为我们需要打开与未知数据库的数据库连接并对其执行查询。对于无状态 bean,重复打开和关闭与每个注入的 bean 实例的数据库连接似乎非常低效。
【问题讨论】:
【参考方案1】:关于ConcurrentAccessException 的更多细节:根据 EJB 规范,对 SLSB 的访问由应用程序同步。服务器。但是,SFSB 并非如此。确保 SFSB 不被同时访问的负担落在了应用程序开发人员的肩上。
为什么?好吧,SLSB 的同步仅在实例级别是必需的。也就是说,SLSB 的每个特定实例都是同步的,但您可能在一个池中或集群中的不同节点上有多个实例,不同实例上的并发请求不是问题。不幸的是,对于 SFSB,这并不容易,因为实例的钝化/激活以及跨集群的复制。这就是规范不强制执行此操作的原因。如果您对该主题感兴趣,请查看this dicussion。
这意味着从 servlet 使用 SFSB 很复杂。具有来自同一会话的多个窗口的用户,或在渲染完成之前重新加载页面可能导致并发访问。理论上,在 servlet 中完成的对 EJB 的每次访问都需要在 bean 本身上同步。我所做的是创建一个 InvocationHandler 来同步特定 EJB 实例上的所有调用:
public class SynchronizationHandler implements InvocationHandler
private Object target; // the EJB
public SynchronizationHandler( Object bean )
target = bean;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
synchronized( target )
// invoke method to the target EJB
然后,在您获得对 EJB 的远程引用之后,您使用 SynchronizationHandler
包装它。这样您就可以确保不会从您的应用程序同时访问此特定实例(只要它仅在一个 JVM 中运行)。您还可以编写一个常规的包装类来同步 bean 的所有方法。
我的结论是:尽可能使用 SLSB。
编辑:
这个答案反映了 EJB 3.0 规范(第 4.3.13 节):
不允许客户端对有状态会话进行并发调用 目的。如果客户端调用的业务方法正在 当另一个客户端调用来自相同或不同的调用时的实例 客户端,到达有状态会话 bean 类的同一个实例, 如果第二个客户端是 bean 的业务接口的客户端,则 并发调用可能会导致第二个客户端收到 javax.ejb.ConcurrentAccessException
此类限制已在 EJB 3.1 中删除(第 4.3.13 节):
默认情况下,允许客户端并发调用有状态的 会话对象和容器需要序列化这样的 并发请求。
[...]
Bean 开发人员可以选择指定并发客户端 禁止对有状态会话 bean 的请求。这是使用 @AccessTimeout 注释或访问超时部署描述符 值为 0 的元素。在这种情况下,如果客户调用的业务 当另一个客户端调用调用时,方法正在实例上进行, 从相同或不同的客户端,到达相同的实例 有状态会话 bean,如果第二个客户端是 bean 的客户端 业务接口或无接口视图,并发调用 必须导致第二个客户端收到一个 javax.ejb.ConcurrentAccessException
【讨论】:
嗯,一旦通过 JNDI 查找获得对 SFSB 的引用,就不需要同步了,对吧?在这种情况下,Java EE 规范保证每次查找都会返回一个新的 SFSB 实例。 @fnt 每次查找都会返回一个新实例,但如何使用它取决于您。容器不会序列化对实例的调用,如果不小心,可能会导致 ConcurrentAccessException。 我的意思是,如果您不存储对 SFSB 的引用(无论是 servlet 实例字段还是任何其他共享资源),每次都可以安全地查找 SFSB。在这种情况下,您描述的 SynchronizationHandler 是多余的。 你错了。所有对无状态和有状态的调用都是序列化的。请阅读 EJB 规范的“4.3.13 序列化会话 Bean 方法”中的更多内容,其中说:“容器序列化对每个有状态和无状态会话 bean 实例的调用。” @MartinAndersson 好点!显然,EJB 3.0 和 3.1 规范在并发访问 SFSB 方面有所不同。我在答案中添加了注释。谢谢!【参考方案2】:这不是有状态会话 bean (SFSB) 的用途。它们旨在保存会话状态,并绑定到用户的 http 会话以保存该状态,就像直接在会话中存储状态的重量级替代方案一样。
如果你想保持数据库连接之类的东西,那么有更好的方法来解决它。
最好的选择是使用连接池。您应该始终使用连接池,并且如果您在应用程序服务器中运行(如果您使用的是 EJB,那么您就是),那么您可以轻松地使用应用程序服务器的数据源配置来创建一个连接池,并在 stateless 会话 bean (SLSB) 中使用它。
【讨论】:
AFAIK,在应用服务器上创建数据源时,您必须事先知道数据库存在。上述连接是针对用户指定我们必须连接的数据库进行的。 这对我来说似乎非常不安全。【参考方案3】:在您提供一些代码和堆栈跟踪之前,我建议您考虑使用connection pool。 如果您所说的“未知数据库”是指由最终用户提供参数的数据库,因此无法预先配置连接池,您仍然可以使用连接池概念,而不是每次都打开一个新连接。
另外,请this thread。
【讨论】:
【参考方案4】:会话 bean 不能同时使用,就像 skaffman 所说的那样,它们旨在处理与客户端会话对应的状态,并且通常存储在每个客户端的会话对象中。
重构以使用数据库池来处理对资源的并发请求是可行的方法。
同时,如果您只需要这个微不足道的使用,您可以同步对constructReport的调用,如下所示:
synchronised (reportBean)
out.write(reportBean.constructReport(parameters));
请注意,如果相对于您的客户数量,constructReport 花费大量时间,这不是解决方案。
【讨论】:
【参考方案5】:你永远不应该同步 servlet 或 ejb 访问,因为这会导致请求排队,如果你有 N 个并发用户,最后一个用户将等待很长时间并且经常得到超时响应!!!同步方法不是出于这个原因!!!
【讨论】:
以上是关于正确使用有状态 Bean 和 Servlet的主要内容,如果未能解决你的问题,请参考以下文章
Spring 上下文中的有状态 bean 和无状态 bean