从 p:remoteCommand 的 oncomplete 处理程序调用 JavaScript 函数 - 使用一些 JavaScript 代码模拟相同的函数

Posted

技术标签:

【中文标题】从 p:remoteCommand 的 oncomplete 处理程序调用 JavaScript 函数 - 使用一些 JavaScript 代码模拟相同的函数【英文标题】:Invoking a JavaScript function from oncomplete handler of p:remoteCommand - simulating the same using some JavaScript code 【发布时间】:2015-05-01 19:38:51 【问题描述】:

警告:虽然这个问题涵盖了带有大量 Java 代码 sn-ps 的长文本信息,但它只是针对 javascript/jQuery 和一些 PrimeFaces 的东西(只是 <p:remoteCommand>)作为在开头的介绍部分提到。


我从 WebSockets (Java EE 7 / JSR 356 WebSocket API) 收到如下 JSON 消息。

if (window.WebSocket) 
    var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");

    ws.onmessage = function (event) 
        jsonMsg=event.data;
        var json = JSON.parse(jsonMsg);        
        var msg=json["jsonMessage"];

        if (window[msg]) 
            window[msg](); //It is literally interpreted as a function - updateModel();
        
    ;

在上面的代码中,event.data 包含一个 JSON 字符串 "jsonMessage":"updateModel"。因此,msg 将包含一个字符串值,即updateModel

在下面的代码段中,

if (window[msg]) 
    window[msg](); //It is literally interpreted as a JavaScript function - updateModel();

window[msg](); 导致调用与 <p:remoteCommand> 关联的 JavaScript 函数(进而调用与 <p:remoteCommand> 关联的 actionListener="#bean.remoteAction")。

<p:remoteCommand name="updateModel"
                 actionListener="#bean.remoteAction" 
                 oncomplete="notifyAll()"
                 process="@this"
                 update="@none"/>

update="@none" 不一定需要。


收到此消息后,我需要通知所有关联的客户端有关此更新。我使用以下 JavaScript 函数来执行此操作,该函数与上述 &lt;p:remoteCommand&gt;oncomplete 处理程序相关联。

var jsonMsg;

function notifyAll() 
    if(jsonMsg) 
       sendMessage(jsonMsg);
    

请注意,变量jsonMsg 已经在第一个 sn-p 中分配了一个值 - 它是一个全局变量。 sendMessage() 是另一个 JavaScript 函数,它实际上通过 WebSockets 向所有关联的客户端发送有关此更新的通知,这在本问题中不需要。


这很好用,但有没有办法在以下情况下发挥作用

if (window[msg]) 
    window[msg]();

    //Do something to call notifyAll() on oncomplete of remote command.

这样notifyAll() 函数可以直接通过一些 JavaScript 代码调用(当前附加到 &lt;p:remoteCommand&gt;oncomplete 并且预期的 JavaScript 代码(甚至其他东西)应该模拟这个 oncomplete)基本上无需依赖全局 JavaScript 变量 (jsonMSg)?


编辑:我试图解决的问题(它可能被认为是附加信息)。

例如,当管理员对名为 Category 的 JPA 实体进行一些更改(通过 DML 操作)时,将触发实体侦听器,进而引发 CDI 事件,如下所示。

@ApplicationScoped
public class CategoryListener 

    @PostPersist
    @PostUpdate
    @PostRemove
    public void onChange(Category category) throws NamingException 
        BeanManager beanManager = (BeanManager) InitialContext.doLookup("java:comp/BeanManager");
        beanManager.fireEvent(new CategoryChangeEvent(category));
    

不用说实体Category是用注释@EntityListeners(CategoryListener.class)指定的。

只是一个侧面说明(完全偏离主题):通过 JNDI 查找获取 BeanManager 的实例,如前面代码 sn-p 中所做的那样是临时的。具有 Weld 版本 2.2.2 最终版本的 GlassFish Server 4.1 无法注入 CDI 事件 javax.enterprise.event.Event&lt;T&gt;,该事件应该按如下方式注入。

@Inject
private Event<CategoryChangeEvent> event;

然后,可以参考上面的相关代码sn-p如下触发事件。

event.fire(new CategoryChangeEvent(category));


在web项目中观察到这个事件如下。

@ApplicationScoped
public class RealTimeUpdate 

    public void onCategoryChange(@Observes CategoryChangeEvent event) 
        AdminPush.sendAll("updateModel");
    

管理员使用自己的端点如下(AdminPush.sendAll("updateModel"); 在其中手动调用)。

@ServerEndpoint(value = "/AdminPush", configurator = ServletAwareConfig.class)
public final class AdminPush 

    private static final Set<Session> sessions = new LinkedHashSet<Session>();

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) 
        if (Boolean.valueOf((String) config.getUserProperties().get("isAdmin"))) 
            sessions.add(session);
        
    

    @OnClose
    public void onClose(Session session) 
        sessions.remove(session);
    

    private static JsonObject createJsonMessage(String message) 
        return JsonProvider.provider().createObjectBuilder().add("jsonMessage", message).build();
    

    public static void sendAll(String text) 
        synchronized (sessions) 
            String message = createJsonMessage(text).toString();
            for (Session session : sessions) 
                if (session.isOpen()) 
                    session.getAsyncRemote().sendText(message);
                
            
        
    

此处仅允许管理员使用此端点。使用onOpen() 方法中的条件检查阻止所有其他用户创建 WebSocket 会话。

foreach 循环内的session.getAsyncRemote().sendText(message); 向管理员发送通知(以 JSON 消息的形式)关于实体 Category 中所做的这些更改。

如第一个代码 sn-p 所示,window[msg](); 调用与应用程序范围 bean - actionListener="#realTimeMenuManagedBean.remoteAction" 关联的操作方法(通过如前所示的 &lt;p:remoteCommand&gt;)。

@Named
@ApplicationScoped
public class RealTimeMenuManagedBean 

    @Inject
    private ParentMenuBeanLocal service;

    private List<Category> category;
    private final Map<Long, List<SubCategory>> categoryMap = new LinkedHashMap<Long, List<SubCategory>>();
    // Other lists and maps as and when required for a dynamic CSS menu.

    public RealTimeMenuManagedBean() 

    @PostConstruct
    private void init() 
        populate();
    

    private void populate() 
        categoryMap.clear();
        category = service.getCategoryList();

        for (Category c : category) 
            Long catId = c.getCatId();
            categoryMap.put(catId, service.getSubCategoryList(catId));
        
    

    // This method is invoked through the above-mentioned <p:remoteCommand>.
    public void remoteAction() 
        populate();
    

    // Necessary accessor methods only.

只有在actionListener="#realTimeMenuManagedBean.remoteAction" 完全完成时才应通知所有其他用户/客户(位于不同面板上 - 除了管理面板) - 不得在操作方法完成之前发生 - 应通知通过&lt;p:remoteCommand&gt;oncomplate 事件处理程序。这就是采取两个不同端点的原因。


其他用户使用自己的端点:

@ServerEndpoint("/Push")
public final class Push 

    private static final Set<Session> sessions = new LinkedHashSet<Session>();

    @OnOpen
    public void onOpen(Session session) 
        sessions.add(session);
    

    @OnClose
    public void onClose(Session session) 
        sessions.remove(session);
    

    @OnMessage
    public void onMessage(String text) 
        synchronized (sessions) 
            for (Session session : sessions) 
                if (session.isOpen()) 
                    session.getAsyncRemote().sendText(text);
                
            
        
    

@OnMessage注解的方法发挥作用,如上图通过&lt;p:remoteCommand&gt;oncomplete发送消息。

那些客户端使用以下 JavaScript 代码从上述应用程序范围的 bean 中获取新值(该 bean 已经被管理员从数据库中充分查询过。因此,没有必要通过以下方式再次荒谬地查询它每个单独的客户端(管理员除外)。因此,它是一个应用程序范围的 bean)。

if (window.WebSocket) 
    var ws = new WebSocket("wss://localhost:8181/ContextPath/Push");
    ws.onmessage = function (event) 
        var json = JSON.parse(event.data);
        var msg = json["jsonMessage"];

        if (window[msg]) 
            window[msg]();
        
    ;

    $(window).on('beforeunload', function () 
        ws.close();
    );

结合以下&lt;p:remoteCommand&gt;.

<p:remoteCommand name="updateModel"
                 process="@this"
                 update="parentMenu"/>

其中parentMenu - 由此&lt;p:remoteCommand&gt; 更新的组件是容器JSF 组件&lt;h:panelGroup&gt;id,其中包含一个带有一堆&lt;ui:repeat&gt;s 的纯CSS 菜单。

希望这能让场景更清晰。


更新:

这个问题已经根据&lt;p:remoteCommand&gt; 准确回答了here(至于具体问题,唯一的问题是删除对全局 JavaScript 变量的依赖,如本问题的介绍部分所述)。

【问题讨论】:

我很难看到/理解您要解决的问题。您基本上是在来回发送推送消息。为什么?要求真的如下:“只推送到一个客户端,完成后,将完全相同的消息推送到所有其他客户端”?我想知道您为什么不首先推送给所有客户。 一个快速的想法可能是在调用updateModel 时将jsonMsg 作为请求参数发送,而updateModel 将在args var 中重新发送jsonMsg,因此oncomplete 将从 args 对象中获取该 var,因为它已经在 oncomplete 参数中可用。这样你会限制对 jsonMsg 的访问,并且它只能在 oncomplete 函数中使用! @BalusC :这个观察是正确的。这就是在这种情况下需要的方式 - 当相关数据库中进行一些更改时(由管理员。只有管理员被允许(或具有特权)进行这些更改),所有其他客户端(匿名用户和/或经过身份验证的用户)用户)将在进行这些更改后立即收到通知,换句话说,只有在管理员(及时)进行这些更改后才必须通知所有其他用户 - 仅在该数据库操作完成后成功提交 - 不是在提交之前那么明显。 我理解那部分。但是,如果请求只能来自管理员,为什么不首先推送给所有客户端呢? @BalusC :唯一的管理员和所有其他用户都有不同的端点。不允许其他用户使用属于该管理员唯一的管理员的端点(即没有其他用户可以在此端点中拥有 WebSocket 会话。他们被阻止。所有其他用户使用单独的端点点代替)。更新由给定的&lt;p:remoteCommand&gt; 进行,当它的actionListener="#bean.remoteAction" 被触发时(即&lt;p:remoteCommand&gt;),只有当管理员接收到 WebSocket 消息时才会触发(或执行)(例如:通过 JPA 实体侦听器) )。 【参考方案1】:

我不认为我了解您问题的各个方面,但无论如何我会尽力提供一些帮助。请注意,我不知道 PrimeFaces,所以我所做的只是阅读文档。

我的理解是,你试图摆脱全局变量。但恐怕,我认为这是不可能的。

这里的问题是,PrimeFaces 不允许您将远程调用的调用透明地传递给 oncomplete 调用(除非您将其传递给 Bean 的 Java 代码,然后再传回 UI ,而这通常不是你想要的)。

但是,我希望,你可以非常接近它。

第 1 部分,JS 早早回归

还请注意,对 Java 和 JavaScript 可能存在一些误解。

Java 是多线程的,可以并行运行多个命令,而 JavaScript 是单线程的,通常从不等待某事完成。异步处理是获得响应式 Web-UI 的必要条件。

因此,您的 remoteCommand 调用(从 JS 端看)将(通常是异步情况)在 oncomplete 处理程序被调用之前很久就返回。这意味着,如果window[msg]() 返回,您还没有完成remoteCommand

那么你想用下面的代码管理什么

if (window[msg]) 
    window[msg]();

    //Do something to call notifyAll() on oncomplete of remote command.
    dosomethinghere();

会失败。当remoteCommand 返回时,dosomethinghere() 不会被调用(因为 JS 不想等待某个事件,这可能永远不会发生)。这意味着,当 Ajax 请求刚刚打开到远程(Java 应用程序)时,dosomethinghere() 将被调用。

要在 Ajax 调用完成后运行某些内容,必须在 oncomplete 例程(或 onsuccess)中完成。这就是它存在的原因。

第 2 部分,验证 msg

请注意window[msg]() 的不同之处。如果您不能完全信任推送的消息,这可能会被认为有点危险。 window[msg]() 本质上运行任何以变量msg 的内容命名的函数。例如,如果 msg 恰好是 close,则将运行 window.close(),这可能不是您想要的。

您应该确保msg 是一个预期的词,并拒绝所有其他词。示例代码:

var validmsg =  updateModel:1, rc:1 

[..]

if (validmsg[msg] && window[msg])
  window[msg]();

第 3 部分:如何并行处理多个 JSON 消息

全局变量有一些缺点。只有一个。如果您碰巧在 WebSocket 上收到另一条 JSON 消息,而前一条消息仍在 remoteCommand 中处理,这将覆盖前一条消息。所以notifyAll() 会看到新消息两次,旧消息丢失。

经典的竞争条件。你必须做的是,创建一个类似注册表的东西来注册所有的消息,然后将一些值传递给notifyAll() 来告诉哪些注册的消息应该被处理。

只需稍作改动,您就可以并行(此处)或串行(第 4 部分)处理消息。

首先,创建一个能够区分消息的计数器。也是一个存储所有消息的对象。我们声明所有我们期望的有效消息(参见第 2 部分):

var jsonMsgNr = 0;
var jsonMessages = ;
var validmsg =  updateModel:1 

现在每次我们收到一条消息时添加一条消息:

if (window.WebSocket) 
    var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");

    ws.onmessage = function (event) 
        var jsonMsg = event.data;
        var json = JSON.parse(jsonMsg);        
        var msg=json["jsonMessage"];

        if (validmsg[msg] && window[msg]) 
            var nr = ++jsonMsgNr;
            jsonMessages[nr] =  jsonMsg:jsonMsg, json:json ;

为了能够将nr 传递给NotifyAll(),需要将一个附加参数传递给Bean。我们就叫它msgNr

            // Following might look a bit different on older PrimeFaces
            window[msg]([name:'msgNr', value:nr]);
        
    

也许查看https://***.com/a/7221579/490291 以了解更多关于以这种方式传递值的信息。

remoteAction bean 现在获得了一个附加参数 msgNr 传递,它必须通过 Ajax 传回。

不幸的是,我不知道(抱歉)它在 Java 中的外观。因此,请确保您对 AjaxCall 的回答再次将 msgNr 复制出来。

另外,由于文档对这个主题很安静,我不确定参数是如何传递回oncomplete 处理程序的。根据 JavaScript 调试器,notifyAll() 有 3 个参数:xhdrpayloadpfArgs。不幸的是,我无法设置测试用例来了解情况。

因此函数看起来有点像(请耐心等待):

function notifyAll(x, data, pfArgs) 
   var nr = ???; // find out how to extract msgNr from data

   var jsonMsg = jsonMessages[nr].jsonMsg;
   var json = jsonMessages[nr].json;
   jsonMessages[nr] = null;  // free memory

   sendMessage(jsonMsg);

   dosomething(json);

如果将其拆分为两个函数,则可以从应用程序的其他部分调用notifyAll()

function notifyAll(x, data, unk) 
   var nr = ???; // find out how to extract msgNr from data

   realNotifyAll(nr);


function realNotifyAll(nr) 
  if (!(nr in jsonMessages)) return;

  var jsonMsg = jsonMessages[nr].jsonMsg;
  var json = jsonMessages[nr].json;
  delete jsonMessages[nr];  // free memory

  sendMessage(jsonMsg);

  dosomething(json);

这里有些东西有点多余。例如,您可能不需要jsonMessages 中的json 元素,或者想再次解析json 以节省一些内存,以防json 非常大。但是,该代码并不是最佳的,而是易于根据您的需要进行调整。

第 4 部分:序列化请求

现在来进行序列化的更改。通过添加一些信号量,这很容易。 JavaScript 中的信号量只是变量。这是因为只有一个全局线程。

var jsonMsgNr = 0;
var jsonMessages = ;
var validmsg =  updateModel:1 
var jsonMsgNrLast = 0;           // ADDED

if (window.WebSocket) 
    var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");

    ws.onmessage = function (event) 
        var jsonMsg = event.data;
        var json = JSON.parse(jsonMsg);        
        var msg=json["jsonMessage"];

        if (validmsg[msg] && window[msg]) 
            var nr = ++jsonMsgNr;
            jsonMessages[nr] =  jsonMsg:jsonMsg, json:json ;

            if (!jsonMsgNrLast)     // ADDED
                jsonMsgNrLast = nr;  // ADDED
                window[msg]([name:'msgNr', value:nr]);
            
        
    


function realNotifyAll(nr) 
  if (!(nr in jsonMessages)) return;

  var jsonMsg = jsonMessages[nr].jsonMsg;
  var json = jsonMessages[nr].json;
  delete jsonMessages[nr];  // free memory

  sendMessage(jsonMsg);

  dosomething(json);

  // Following ADDED
  nr++;
  jsonMsgNrLast = 0;
  if (nr in jsonMessages)
    
      jsonMsgNrLast = nr;
      window[jsonMessages[nr].json.msg]([name:'msgNr', value:nr]);
    

注意:jsonMsgNrLast 可能只是一个标志(真/假)。但是,将当前处理的数字放在变量中可能会有所帮助。

话虽如此,如果sendMessagedosomething 发生故障,则会出现饥饿问题。所以也许你可以把它交错一下:

function realNotifyAll(nr) 
  if (!(nr in jsonMessages)) return;

  var jsonMsg = jsonMessages[nr].jsonMsg;
  var json = jsonMessages[nr].json;
  delete jsonMessages[nr];  // free memory

  nr++;
  jsonMsgNrLast = 0;
  if (nr in jsonMessages)
    
      jsonMsgNrLast = nr;
      // Be sure you are async here!
      window[jsonMessages[nr].json.msg]([name:'msgNr', value:nr]);
    

  // Moved, but now must not rely on jsonMsgNrLast:
  sendMessage(jsonMsg);
  dosomething(json);

这样,当sendMessage 正在运行时,AJAX 请求就已经发送出去了。如果现在dosomething 出现 JavaScript 错误或类似错误,消息仍会被正确处理。

请注意:所有这些都是在没有任何测试的情况下输入的。可能存在语法错误或更糟。对不起,我尽力了。如果您发现错误,编辑就是您的朋友。

第 5 部分:从 JS 直接调用

现在,所有这些都到位并进行了序列化运行,您始终可以使用realNotifyAll(jsonMsgNrLast) 调用以前的notifyAll()。或者您可以在列表中显示jsonMessages 并选择任意数字。

通过跳过对window[jsonMessages[nr].json.msg]([name:'msgNr', value:nr]);(及以上window[msg]([name:'msgNr', value:nr]);)的调用,您还可以停止Bean 处理并使用通常的JQuery 回调按需运行它。为此创建一个函数并再次更改代码:

var jsonMsgNr = 0;
var jsonMessages = ;
var validmsg =  updateModel:1 
var jsonMsgNrLast = 0;
var autoRun = true;        // ADDED, set false control through GUI

if (window.WebSocket) 
    var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");

    ws.onmessage = function (event) 
        var jsonMsg = event.data;
        var json = JSON.parse(jsonMsg);        

        if (validmsg[msg] && window[msg]) 
            var nr = ++jsonMsgNr;
            jsonMessages[nr] =  jsonMsg:jsonMsg, json:json ;

            updateGuiPushList(nr, 1);

            if (autoRun && !jsonMsgNrLast) 
                runRemote(nr);
            
        
    


function realNotifyAll(nr) 
  if (!(nr in jsonMessages)) return;

  var jsonMsg = jsonMessages[nr].jsonMsg;
  var json = jsonMessages[nr].json;
  delete jsonMessages[nr];  // free memory

  updateGuiPushList(nr, 0);

  jsonMsgNrLast = 0;
  if (autoRun)
    runRemote(nr+1);

  // Moved, but now must not rely on jsonMsgNrLast:
  sendMessage(jsonMsg);
  dosomething(json);


function runRemote(nr) 
  if (nr==jsonMsgNrLast) return;
  if (nr in jsonMessages)
    
      if (jsonMsgNrLast)  alert("Whoopsie! Please wait until processing finished"); return; 
      jsonMsgNrLast = nr;

      updateGuiPushList(nr, 2);

      // Be sure you are async here!
      window[jsonMessages[nr].json.msg]([name:'msgNr', value:nr]);
    

现在您可以使用runRemote(nr) 开始处理并使用realNotifyAll(nr) 调用完成函数。

函数updateGuiPushList(nr, state)state=0:finished 1=added 2=running 是对GUI 代码的回调,它会更新屏幕上等待处理的推送列表。设置autoRun=false停止自动处理,设置autoRun=true自动处理。

注意:将autoRunfalse 设置为true 后,当然需要用最低的nr 触发一次runRemote

【讨论】:

我暂时避免接受答案。我需要相当长的时间才能回到这个话题。感谢您提供详细的答复。

以上是关于从 p:remoteCommand 的 oncomplete 处理程序调用 JavaScript 函数 - 使用一些 JavaScript 代码模拟相同的函数的主要内容,如果未能解决你的问题,请参考以下文章

p:remoteCommand 销毁 @ViewScoped 托管 bean

python OnCom.py

错误:QueryReqWrap.onresolve 处的 querySrv ENODATA _mongodb._tcp.blog-cluster-0hb5z.mongodb.net [as oncom

数据核增强失败

onPause() 和 onStop() 不起作用

Git和XCode:Git内部版本号脚本