使用Servlet正确使用有状态Bean

我们目前有一个注入Servlet的有状态bean。 问题是有时我们得到一个Caused by: javax.ejb.ConcurrentAccessException: SessionBean is executing another request. [session-key: 7d90c02200a81f-752fe1cd-1] Caused by: javax.ejb.ConcurrentAccessException: SessionBean is executing another request. [session-key: 7d90c02200a81f-752fe1cd-1]在有状态bean上执行方法时。

 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将检查是否需要打开与Report中指定的数据库的新连接,之后在根据指定参数构建的查询中构建HTML中的Report。

我们选择在无状态bean上使用有状态bean的原因是因为我们需要打开与未知数据库的数据库连接并对其执行查询。 对于无状态bean,使用每个注入的bean实例重复打开和关闭数据库连接似乎非常低效。

这不是有意使用的有状态会话bean(SFSB)。 它们被设计为保持对话状态,并且被绑定到用户的http会话以保持该状态,就像直接在会话中存储状态的重量级替代。

如果你想保存数据库连接之类的东西,那么有更好的方法来实现它。

最好的选择是使用连接池。 您应该始终使用连接池,如果您在应用程序服务器内运行(如果您使用的是EJB,那么您可以使用它),那么您可以轻松地使用appserver的数据源配置来创建连接池,并使用在无状态会话bean(SLSB)中。

有关ConcurrentAccessException的更多详细信息:根据EJB规范,应用程序会同步对SLSB的访问。 服务器。 但是,SFSB并非如此。 确保不同时访问SFSB的负担在应用程序开发人员的肩上。

为什么? 好吧,只有在实例级别才需要同步SLSB。 也就是说,SLSB的每个特定实例都是同步的,但是您可能在池中或群集中的不同节点上有多个实例,并且不同实例上的并发请求不是问题。 遗憾的是,由于实例的钝化/激活以及群集中的复制,这对于SFSB来说并不那么容易。 这就是规范没有强制执行此操作的原因。 如果您对该主题感兴趣,请查看此讨论 。

这意味着从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 Developer可以选择指定禁止对有状态会话bean的并发客户端请求。 这是使用值为0的@AccessTimeout批注或访问超时部署描述符元素完成的。在这种情况下,如果客户端调用的业务方法正在实例上进行另一个客户端调用的调用,来自相同或不同的客户端,到达有状态会话bean的同一个实例,如果第二个客户端是bean的业务接口或无接口视图的客户端,并发调用必须导致第二个客户端接收到javax.ejb.ConcurrentAccessException

在您提供一些代码和堆栈跟踪之前,我建议您考虑使用连接池 。 如果“未知数据库”是指最终用户提供参数的数据库,因此无法预先配置连接池,您仍然可以使用连接池概念,而不是每次都打开新连接。

还有, 这个post 。

会话bean不能同时使用,例如skaffman说它们旨在处理与客户端会话相对应的状态,并且通常存储在每个客户端的会话对象中。

重构使用数据库池来处理对资源的并发请求是可行的方法。

与此同时,如果你需要的只是这个简单的用途,你可以将调用同步到constructReport,如下所示:

 synchronised (reportBean) { out.write(reportBean.constructReport(parameters)); } 

请注意,如果constructReport相对于您的客户端数量占用大量时间,则这不是解决方案。

你永远不应该同步servlet或ejb访问,因为这导致请求队列,如果你有N个并发用户,最后一个将等待很长时间,并经常获得超时响应! Syncronize方法不是出于这个原因!