1

我想基于javax.enterprise.event.Event接口实现事件处理程序。目前它运行良好,但仅限于单个会话。我的环境是JBoss-7.1.1.FinalJSF2。我可以在这个简化(工作)示例中演示这个问题:

@NamedBean 中,我定义了一个Event<MyEjb>并且可以触发事件。

@Inject private Event<MyEJB> ejbChanged;

public void test()
{
  ejbChanged.fire(new MyEjb());
}

@Named @SessionScoped这在Bean中被识别,我可以轻松地处理它

public void
       onChanged(@Observes(notifyObserver = Reception.IF_EXISTS) final MyEjb ejb)
{
  logger.info("onChanged "+ejb.toString());
}

但不幸的是,仅在属于当前用户会话onChanged的 Bean 中调用。@SessionScoped但我也想访问属于其他用户会话的 Bean。这个 CDI 事件处理可能吗?

4

2 回答 2

3

Markus Dahm 在此处的博客条目中描述的解决方案

他将事件发送到 Web 应用程序的所有活动会话的解决方案包括扩展现有的 CDI 观察者模式以支持全局事件。

以下是相关代码:

首先,我们需要一个 GlobalevHttpSessionController 来注册和处理所有 HTTP 会话并将所有事件委托给会话:

@ApplicationScoped
public class GlobalevHttpSessionController {
  public static final String 
    EVENT_ATTRIBUTE_NAME = "HttpSessionControllerEvent";

  private final List _httpSessions = new ArrayList();

  public List getHttpSessions() {
    return new ArrayList(_httpSessions);
  }

  public void addSession(final HttpSession httpSession) {
    _httpSessions.add(httpSession);
  }

  public void removeSession(final HttpSession httpSession) {
    _httpSessions.remove(httpSession);
  }

  public void fireEvent(final GlobalEvent eventObject) {
    for (final HttpSession session : _httpSessions) {
      fireEvent(session, eventObject);
    }
  }

  private void fireEvent(final HttpSession session, final GlobalEvent eventObject) {
    try {
      final List globalEvents = getGlobalEvents(session);

      globalEvents.add(eventObject);
    } catch (final Exception e) {
      throw new IllegalStateException("fireEvent", e);
    }
  }

  private synchronized List getGlobalEvents(final HttpSession session) {
    List globalEvents = (List) session.getAttribute(EVENT_ATTRIBUTE_NAME);

    if (globalEvents == null) {
      globalEvents = new ArrayList();
      session.setAttribute(EVENT_ATTRIBUTE_NAME, globalEvents);
    }

    return globalEvents;
  }
}

其中 GlobalEvent 只是所有全局事件的简单可序列化超类:

public abstract class GlobalEvent implements Serializable {
  private static final long serialVersionUID = 1L;
}

接下来,我们需要一个 HTTP 侦听器来添加和删除活动的客户端会话:

@WebListener
public class GlobalevHttpSessionListener implements HttpSessionListener {
  @Inject
  private GlobalevHttpSessionController _httpSessionController;

  public void sessionCreated(final HttpSessionEvent se) {
    _httpSessionController.addSession(se.getSession());
  }

  public void sessionDestroyed(final HttpSessionEvent se) {
    _httpSessionController.removeSession(se.getSession());
  }
}

为了将事件分派给客户端,他选择使用 JSF PhaseListener 查找传入事件。其他解决方案,例如使用过滤器,也是可能的。侦听器查找全局事件并使用 CDI bean 管理器将它们分派到本地会话。管理器实例是通过 JNDI 查找获得的(根据规范,bean 管理器必须由 java:comp/BeanManager 下的容器绑定)。不幸的是,我们不能在这里使用 CDI 注入,因为阶段监听器不是由 CDI 实例化,而是由 Java Server Faces (JSF) 框架实例化的。然而,该框架确实提供了我们通过 FacesContext 对象访问 HTTP 会话所需的所有信息

public class GlobalevEventPhaseListener implements PhaseListener {
  public void beforePhase(final PhaseEvent event) {
    final FacesContext facesContext = event.getFacesContext();
    final HttpSession httpSession =   
        JSFUtil.getHttpSession(facesContext);

    if (httpSession != null) {
      final List globalEvents = getGlobalEvents(httpSession);

      if (!globalEvents.isEmpty()) {
        fireEvents(globalEvents);
      }
    }
  }

  private void fireEvents(final List globalEvents) {
    final BeanManager beanManager = lookBeanManager();

    if (beanManager != null) {
      try {
        for (final GlobalEvent devaGlobalEvent : globalEvents) {
          beanManager.fireEvent(devaGlobalEvent);
        }
      } catch (final Exception e) {
        throw new IllegalStateException("fireEvents", e);
      }
    }
  }

  @Override
  public PhaseId getPhaseId() {
    return PhaseId.RENDER_RESPONSE; // RESTORE_VIEW;
  }

  private BeanManager lookBeanManager() {
    try {
      final Object obj = 
        new InitialContext().lookup("java:comp/BeanManager");

      return (BeanManager) obj;
    } catch (final Exception e) {
           throw new 
            IllegalStateException("Lookup bean manager", e);
    }

    return null;
  }

  private synchronized List getGlobalEvents(final HttpSession httpSession) {
    final List events = (List) httpSession.getAttribute(
        GlobalevHttpSessionController.EVENT_ATTRIBUTE_NAME);
    final List result = new ArrayList();

    if (events != null) {
      result.addAll(events);
      events.clear();
    }

    return result;
  }
}

最后,我们需要在 faces-config.xml 中注册我们的监听器:

<lifecycle>
  <phase-listener>
de.akquinet.jbosscc.globalev.listener.GlobalevEventPhaseListener
  </phase-listener>
</lifecycle>

代码也可以在github 上找到

感谢马库斯!

于 2012-10-02T14:54:02.807 回答
0

我曾经想过这个,但后来,这种方法的可扩展性如何?如果您有数千个活动会话怎么办?事件的回调方法必须非常轻量级,因为对于您触发的每个事件,您在每个上下文中都有数千个方法调用。

您并不总是需要通知所有活动会话,您可以扩展此机制并让某些会话订阅特定事件,以便方法调用的数量在您的控制之下。

您应该做什么取决于您的要求,如果数据更新非常频繁,您最好让会话 bean 轮询应用程序范围的 bean 以进行更改,这样,会话 bean 仅在来自的实际请求时刷新其数据用户。这将只更新当前正在使用数据的那些 bean。

同样,也许有人可以帮助我解决这个问题,但我觉得这不会导致可扩展的设计,如果有一天你决定使用负载平衡并且你有一个服务器集群并且你需要将一个事件传播到所有活动会话在所有服务器等中。我觉得规范没有实现这一点是有正当理由的。您应该将服务器的工作量最小化到用户需要的程度,考虑您可以使用属于离开计算机去喝杯咖啡的用户的事件更新多少会话 bean,等等。

于 2013-02-08T13:23:15.017 回答