内容


JavaScript EE,第 2 部分

用 Ajax 调用远程 JavaScript 函数

使用 Ajax 和 Java 代码实现用于 JavaScript 的 RPC

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: JavaScript EE,第 2 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:JavaScript EE,第 2 部分

敬请期待该系列的后续内容。

RPC 机制非常简单。服务器上有一组 JavaScript 函数,现在希望能从 Web 浏览器调用它们,就像有一个 JavaScript 引擎在同时执行客户机代码和服务器代码一样。因此,需要有一些客户端例程,这些例程应该与服务器端的对应例程有相同的名称和参数。

客户端例程将使用 Ajax 把它们的参数传递到服务器,在服务器上进行真正的处理。一个 Java servlet 将调用服务器端函数,并使用 JSON 格式将结果返回到客户机。然后,客户端例程计算 Ajax 响应,将 JSON 字符串转换回 JavaScript 对象,后者被返回到应用程序。

作为应用程序开发人员,可以将精力放在构建用户界面和服务器中执行的函数上。您不需要处理 Ajax 或 RPC 问题,因为本文以标记文件的形式提供了一个 JavaScript 代码生成器,您可以在 JSP 页面使用它来自动生成客户端例程。为了理解其中的工作原理,我们从一个示例应用程序开始。

开发一个 JVM 监视应用程序

本文中的示例应用程序使用 Java Management API 监视运行托管应用程序的 Java EE 服务器的 JVM。用户界面由一个简单的 Web 页面组成,该页面显示各种不同的指示器,例如 Java 类的数量、内存消耗、垃圾收集器的活动和线程的数量。

这些信息通过 Ajax 获得,并插入到一个 HTML 表中(如图 1 所示;单击 这里 查看图 1 的放大图)。更有趣的是,这个 Web 页面包含一个表单,通过该表单可以为给定的秒数分配内存。还可以调用 JVM 的垃圾收集器(GC)。

图 1. 示例应用程序
显示示例应用程序界面的截图
显示示例应用程序界面的截图

在服务器端,应用程序使用一个 JavaScript 文件,Web 浏览器将借助 Ajax 并使用本系列第 1 部分提供的脚本运行器调用该文件中的函数。这是一个简单的 servlet,它处理 URL 以 .jss 扩展名结束的所有请求。该 servlet 查找服务器上相应的 JavaScript 文件,并使用 Java Scripting API 执行它。

创建 Web 页面

清单 1 显示 MonitorPage.jsp 文件中的表和表单。每个数据单元有一个 ID,以便用 JavaScript 更新表中的内容。<body> 标记的 onload 属性用于调用一个名为 init() 的 JavaScript 函数,该函数将初始化应用程序的客户端部分。另外两个函数 allocMem()gc() 是在用户单击按钮时调用的。

清单 1. MonitorPage.jsp 的 HTML 代码
<html>
<head>
    ...
    <style type="text/css">
        th { text-align: left; }
        td { text-align: right; }
        .space { margin: 10px; }
    </style>
</head>
<body onload="init()">
    <table border="1" cellpadding="5">
        <tr>
            <th colspan=2>Classes</th>
            <th colspan=2>Heap Memory</th>
            <th colspan=2>Non-Heap Memory</th>
            <th colspan=2>Garbage Collector</th>
            <th colspan=2>Threads</th>
        </tr>
        <tr>
            <th>Loaded</th>
            <td id="info.classes.loaded"></td>
            <th>Used</th>
            <td id="info.memory.heap.used"></td>
            <th>Used</th>
            <td id="info.memory.nonHeap.used"></td>
            <th>Count</th>
            <td id="info.gc.count"></td>
            <th>Live</th>
            <td id="info.threads.live"></td>
        </tr>
        <tr>
            <th>Unloaded</th>
            <td id="info.classes.unloaded"></td>
            <th>Committed</th>
            <td id="info.memory.heap.committed"></td>
            <th>Committed</th>
            <td id="info.memory.nonHeap.committed"></td>
            <th>Time</th>
            <td id="info.gc.time"></td>
            <th>Peak</th>
            <td id="info.threads.peak"></td>
        </tr>
    </table>
    <br>
    <form name="monitorForm">
        Size: <input name="size" size="10">
        <span class="space"></span>
        Seconds: <input name="seconds" size="4">
        <span class="space"></span>
        <button type="button"
            onclick="allocMem(this.form.size.value, this.form.seconds.value)"
        >Allocate Memory</button>
        <span class="space"></span>
        <button type="button" onclick="gc()">Collect Garbage</button>
    </form>
</body>
</html>

MonitorPage.jsp 文件(见清单 2)使用一个名为 <js:rpc> 的定制标记来生成客户端 JavaScript 函数,这些函数调用服务器端对应的函数。<js:rpc> 标记有以下属性:

script将在服务器上执行的脚本的 URL
function被远程调用的 JavaScript 函数的签名
validator一个可选表达式,在 Web 浏览器中将计算该表达式以验证函数的参数
jsonVar一个可选 JavaScript 变量的名称,该变量用于存储 JSON 响应
methodHTTP 方法,可以是 GET 或 POST
async表明应该异步还是同步使用 XMLHttpRequest 的一个布尔属性
清单 2. MonitorPage.jsp 的 JavaScript 代码
<%@ taglib prefix="js" tagdir="/WEB-INF/tags/js" %>

<html>
<head>
    <title>Monitor</title>
    <script src="xhr.js" type="text/javascript">
    </script>
    <script type="text/javascript">
        <js:rpc function="getInfo()" script="MonitorScript.jss"
                method="GET" async="true" jsonVar="json">
            showInfo(json, "info");
        </js:rpc>

        <js:rpc function="allocMem(size, seconds)" script="MonitorScript.jss"
                validator="valid('Size', size) && valid('Seconds', seconds)"
                method="POST" async="true"/>

        <js:rpc function="gc()" script="MonitorScript.jss"
                method="POST" async="false">
            alert("Garbage Collected");
        </js:rpc>

        function showInfo(obj, id) {
            if (typeof obj == "object") {
                for (var prop in obj)
                    showInfo(obj[prop], id + "." + prop);
            } else {
                var elem = document.getElementById(id);
                if (elem)
                    elem.innerHTML = htmlEncode(String(obj));
            }
        }

        function valid(name, value) {
            if (!value || value == "") {
                alert(name + " is required");
                return false;
            }
            var n = new Number(value);
            if (isNaN(n) || n <= 0 || Math.floor(n) != n) {
                alert(name + " must be a positive integer.");
                return false;
            } else
                return true;
        }

        function init() {
            getInfo();
            setInterval(getInfo, 1000);
        }
    </script>
    ...
</head>
...
</html>

<js:rpc></js:rpc> 之间的 JavaScript 代码将用于处理 Ajax 响应。例如,对于 getInfo() 函数,json 响应被传递到另一个名为 showInfo() 的函数,该函数递归地遍历对象树,将信息插入到 HTML 表的数据单元中。由于 asynctrue,生成的 getInfo() 函数将在发送请求后立即返回,然后通过 Ajax 回调调用 showInfo() 函数。

allocMem() 函数使用 validator 属性验证用户输入。如果对于两个参数中的任何一个参数,valid() 函数返回 false,那么取消远程调用,并显示一条警告消息。由于在这里 asyncfalsegc() 函数在返回控制前会一直等待响应。init() 函数调用 getInfo() 来初始化 UI,并将同一个函数传递到 setInterval(),以便每秒钟都调用该函数刷新 Web 页面的信息。

清单 3 包含用 <js:rpc> 定制标记产生的代码,它被实现为一个名为 rpc.tag 的标记文件。每个生成的函数使用一个 XHR 对象,对象的原型可以在 MonitorPage.jsp 在其头部导入的 xhr.js 文件中找到。本文后面将提供 rpc.tag 和 xhr.js 文件的源代码。

清单 3. 生成的 JavaScript 函数
var getInfoXHR = new XHR("GET", "MonitorScript.jss", true);

function getInfo() {
    var request = getInfoXHR.newRequest();
    getInfoXHR.addHeader("Ajax-Call", "getInfo()");
    function processResponse() {
        if (getInfoXHR.isCompleted()) {
            var json = eval(request.responseText);
            showInfo(json, "info");
        }
    }
    getInfoXHR.sendRequest(processResponse);
}


var allocMemXHR = new XHR("POST", "MonitorScript.jss", true);

function allocMem(size, seconds) {
    if (!(valid('Size', size) && valid('Seconds', seconds)))
        return;
    var request = allocMemXHR.newRequest();
    allocMemXHR.addHeader("Ajax-Call", "allocMem(size, seconds)");
    allocMemXHR.addParam("size", size);
    allocMemXHR.addParam("seconds", seconds);
    function processResponse() {
        if (allocMemXHR.isCompleted()) {
        }
    }
    allocMemXHR.sendRequest(processResponse);
}


var gcXHR = new XHR("POST", "MonitorScript.jss", false);

function gc() {
    var request = gcXHR.newRequest();
    gcXHR.addHeader("Ajax-Call", "gc()");
    function processResponse() {
        if (gcXHR.isCompleted()) {
            alert("Garbage Collected");
        }
    }
    gcXHR.sendRequest(processResponse);
}

编写服务器端脚本

MonitorScript.jss 文件包含 3 个在服务器上执行的 JavaScript 函数。getInfo() 函数(见清单 4)使用 Java Management API 获得与类、内存和线程相关的 JVM 信息。全部数据被打包到一个对象树中,在对 Ajax 请求的响应中将以 JSON 的形式返回该对象树。getInfo() 函数不必做任何特别的事情。在接下来的小节中,您将看到如何实现 RPC 机制。

清单 4. MonitorScript.jss 的 getInfo() 函数
importClass(java.lang.management.ManagementFactory);

function getInfo() {
    var classes = ManagementFactory.getClassLoadingMXBean();
    var memory = ManagementFactory.getMemoryMXBean();
    var heap = memory.getHeapMemoryUsage();
    var nonHeap = memory.getNonHeapMemoryUsage();
    var gc = ManagementFactory.getGarbageCollectorMXBeans();
    var threads = ManagementFactory.getThreadMXBean();

    var gcCount = 0;
    var gcTime = 0;
    for (var i = 0; i < gc.size(); i++) {
        gcCount += gc.get(i).collectionCount;
        gcTime += gc.get(i).collectionTime;
    }

    return {
        classes: {
            loaded: classes.loadedClassCount,
            unloaded: classes.unloadedClassCount,
        },
        memory: {
            heap: {
                init: heap.init,
                used: heap.used,
                committed: heap.committed,
                max: heap.max
            },
            nonHeap: {
                init: nonHeap.init,
                used: nonHeap.used,
                committed: nonHeap.committed,
                max: nonHeap.max
            }
        },
        gc: {
            count: gcCount,
            time: gcTime
        },
        threads: {
            live: threads.threadCount,
            peak: threads.peakThreadCount
        }
    };
}

allocMem() 函数(见清单 5)创建一个 Java 线程,该 Java 线程执行一个用 JavaScript 实现的 run() 方法。代码使用 java.lang.reflect.Array 类的 newInstance() 方法创建一个 byte 数组,并调用 java.lang.Thread.sleep()。然后,数组就可以执行垃圾收集。

清单 5. MonitorScript.jss 的 allocMem() 函数
function allocMem(size, seconds) {
    var runnable = new java.lang.Runnable() {
        run: function() {
            var array = new java.lang.reflect.Array.newInstance(
                    java.lang.Byte.TYPE, size);
            java.lang.Thread.sleep(seconds * 1000);
        }
    }
    var thread = new java.lang.Thread(runnable);
    thread.start();
}

清单 6 显示 gc() 函数,该函数调用 JVM 的垃圾收集器。

清单 6. MonitorScript.jss 的 gc() 函数
function gc() {
    java.lang.System.gc();
}

构建 XMLHttpRequest 包装器

在上一小节,您看到了由 <js:rpc> 标记生成的 JavaScript 代码。为了简化生成的代码,所有与 XMLHttpRequest API 相关的操作,例如创建新的请求对象或发送 HTTP 请求,都被放到一个单独的名为 xhr.js 的 JavaScript 文件中。本节展示 XHR 对象的方法。

初始化请求

XHR() 构造函数(见清单 7)带有 3 个参数(methodurlasync),它将这些参数存储为属性。newRequest() 方法终止仍在活动的前一个请求,用 delete 操作符释放内存,并创建一个新的 XMLHttpRequest 实例。该行为适合使用 Ajax 连接到一个数据提要或保存用户输入的应用程序。通常情况下,您只对装载或保存最新的数据感兴趣。

清单 7. xhr.js 的 XHR() 和 newRequest() 函数
function XHR(method, url, async) {
    this.method = method.toUpperCase();
    this.url = url;
    this.async = async;
}

XHR.prototype.newRequest = function() {
    var request = this.request;
    if (request) {
        request.onreadystatechange = function() { };
        if (request.readyState != 4)
            request.abort();
        delete request;
    }
    request = null;
    if (window.ActiveXObject)
        request = new ActiveXObject("Microsoft.XMLHTTP");
    else if (window.XMLHttpRequest)
        request = new XMLHttpRequest();
    this.request = request;
    this.sent = false;
    this.params = new Array();
    this.headers = new Array();
    return request;
}

清单 8 显示 addParam()addHeader() 方法,通过这两个方法可以添加包括在 HTTP 请求中的请求参数和头部。一旦创建新的请求后,便可以使用这些方法。如果还没有创建 XMLHttpRequest 对象,或者已经发送了请求,那么 checkRequest() 函数将抛出一个异常。

清单 8. xhr.js 的 checkRequest()、addParam() 和 addHeader() 函数
XHR.prototype.checkRequest = function() {
    if (!this.request)
        throw "Request not created";
    if (this.sent)
        throw "Request already sent";
    return true;
}

XHR.prototype.addParam = function(pname, pvalue) {
    this.checkRequest();
    this.params[this.params.length] = { name: pname, value: pvalue };
}

XHR.prototype.addHeader = function(hname, hvalue) {
    this.checkRequest();
    this.headers[this.headers.length] = { name: hname, value: hvalue };
}

发送请求

sendRequest() 函数(见清单 9)将参数编码,打开请求,添加头部,当 asynctrue 时设置 Ajax 回调,并发送 HTTP 请求。如果 asyncfalse,则在 send() 之后调用回调。

清单 9. xhr.js 的 sendRequest() 函数
XHR.prototype.sendRequest = function(callback) {
    this.checkRequest();
    var query = "";
    for (var i = 0; i < this.params.length; i++) {
        if (query.length > 0)
            query += "&";
        query += encodeURIComponent(this.params[i].name) + "="
            + encodeURIComponent(this.params[i].value);
    }
    var url = this.url;
    if (this.method == "GET" && query.length > 0) {
        if (url.indexOf("?") == -1)
            url += "?";
        else
            url += "&";
        url += query;
    }
    this.request.open(this.method, url, this.async);
    for (var i = 0; i < this.headers.length; i++)
        this.request.setRequestHeader(
                this.headers[i].name, this.headers[i].value);
    if (this.async)
        this.request.onreadystatechange = callback;
    var body = null;
    if (this.method == "POST") {
        body = query;
        this.request.setRequestHeader("Content-Type",
            "application/x-www-form-urlencoded");
    }
    this.sent = true;
    this.request.send(body);
    if (!this.async)
        callback();
}

在 Ajax 回调中,可以使用 isCompleted()(见清单 10)验证请求的状态。

清单 10. xhr.js 的 isCompleted() 函数
XHR.prototype.isCompleted = function() {
    if (this.request && this.sent)
        if (this.request.readyState == 4)
            if (this.request.status == 200)
                return true;
            else
                this.showRequestInfo();
    return false;
}

报告错误

如果服务器端发生错误,则 isCompleted() 调用 showRequestInfo(),清单 11 显示了后者的代码。该函数打开一个窗口,并输出请求的信息:HTTP 方法、URL、参数、头部和响应。

清单 11. xhr.js 的 showRequestInfo() 函数
var xhrErrorWindow = null;

XHR.prototype.showRequestInfo = function() {
    if (xhrErrorWindow && (xhrErrorWindow.closed || xhrErrorWindow._freeze))
        return;
    xhrErrorWindow = window.open("", "XHRError", "menubar=no, resizable=yes, "
            + "scrollbars=yes, width=600, height=600");
    var doc = xhrErrorWindow.document;
    doc.writeln("<p align='right'>");
    doc.writeln("<button onclick='window._freeze = true'>Freeze</button>")
    doc.writeln("</p>");
    doc.writeln(htmlEncode(this.method + " " + this.url));
    doc.writeln("<pre>" + this.request.status + "</pre>");
    doc.writeln("Parameters:");
    doc.writeln("<pre>");
    for (var i = 0; i < this.params.length; i++) {
        doc.writeln(htmlEncode(this.params[i].name
                + "=" + this.params[i].value));
    }
    doc.writeln("</pre>");
    doc.writeln("Headers:");
    doc.writeln("<pre>");
    for (var i = 0; i < this.headers.length; i++) {
        doc.writeln(htmlEncode(this.headers[i].name
                + "=" + this.headers[i].value));
    }
    doc.writeln("</pre>");
    doc.writeln("Response:");
    var response = this.request.responseText;
    doc.writeln(response);
    doc.close();
    xhrErrorWindow.focus();
}

如果之前的 HTTP 错误已经打开了错误窗口,那么 focus() 调用将使之成为当前窗口,如果再三发生 HTTP 错误,那么会导致可用性问题。在这种情况下,由于每秒钟都会刷新窗口的内容,导致无法使用滚动,因此也难以对错误进行分析。

为了修复这些可用性问题,showRequestInfo() 函数增加了一个按钮,当单击此按钮时,该按钮设置一个名为 _freeze 的变量。如果 _freezetrue,则不刷新请求的信息。此外,如果用户关闭了错误窗口,该窗口将不再重新打开。在对代码做出更改之后,只需刷新应用程序的页面,就可以验证错误是否仍然存在还是已经被修复。

htmlEncode() 函数(见清单 12)以一个字符串作为参数,并分别用 &amp;&lt;&gt; 替换 &<> 字符。

清单 12. xhr.js 的 htmlEncode() 函数
function htmlEncode(value) {
    return value ? value.replace(/&/g, "&amp;")
        .replace(/</g, "&lt;").replace(/>/g, "&gt;") : "";
}

实现 JavaScript-RPC 机制

本节介绍生成 JavaScript 函数的 JSP 标记文件,这些 JavaScript 函数实现 RPC 机制的客户端部分。您还将看到如何调用与这些函数对应的服务器端函数,以及如何返回结果。但是首先了解一些关于安全性的注意事项。

授权函数调用

服务器端脚本可以视作常规的资源,可以使用 Java EE 的标准安全性过程限制对它们的访问。根据 web.xml 文件中指定的安全性约束,通常会定义一个或多个角色,这些角色的用户将拥有访问这些脚本的适当权限。

无论是否限制对脚本的访问,Web 客户机应该不能调用服务器端脚本的任何函数。要控制可以通过 RPC 机制调用哪些 JavaScript 函数,一种简单的方式是在一个 Set 集合中维护它们。AuthorizedCalls bean(见清单 13)提供了一些线程安全的方法,用于管理这个经过授权的调用的集合。

清单 13. AuthorizedCalls 类
package jsee.rpc;

import java.util.*;

public class AuthorizedCalls implements java.io.Serializable {
    private Set<String> calls;
    
    public AuthorizedCalls() {
        calls = new HashSet<String>();
    }
    
    protected String encode(String scriptURI, String functionName) {
        return scriptURI + '#' + functionName;
    }
    
    public synchronized void add(String scriptURI, String functionName) {
        calls.add(encode(scriptURI, functionName));
    }

    public synchronized void remove(String scriptURI, String functionName) {
        calls.remove(encode(scriptURI, functionName));
    }

    public synchronized boolean contains(
            String scriptURI, String functionName) {
        return calls.contains(encode(scriptURI, functionName));
    }
    
    public String toString() {
        return calls.toString();
    }
    
}

本文的示例应用程序需要授权从 JSP 页面进行的调用。authorize.tag 文件(见清单 14)有两个属性(functionscript),这两个属性的值被传递到 AuthorizedCallsadd() 方法。此外,任何相对的脚本 URI 被转换为绝对 URI,以确保可以用 URI 惟一地标识每个脚本。由于 AuthorizedCalls 实例存储在 session 作用域,因此在限制某些用户对脚本的访问的情况下,只能以授权用户的身份执行服务器端函数。

清单 14. authorize.tag 文件
<%@ attribute name="function" required="true" rtexprvalue="true" %>
<%@ attribute name="script" required="true" rtexprvalue="true" %>

<jsp:useBean id="authorizedCalls"
    class="jsee.rpc.AuthorizedCalls" scope="session"/>
<%
    String functionName = (String) jspContext.getAttribute("function");
    String scriptURI = (String) jspContext.getAttribute("script");
    if (!scriptURI.startsWith("/")) {
        String base = request.getRequestURI();
        base = base.substring(0, base.lastIndexOf("/"));
        scriptURI = base + "/" + scriptURI;
    }
    authorizedCalls.add(scriptURI, functionName);
%>

另一个需要分析的与安全性相关的方面是,如何在服务器端处理远程调用函数的参数。一种常见的想法是在 Web 浏览器中将 JavaScript 对象编码为 JSON 字符串,然后将它们发送到服务器,在服务器上则可以使用 eval() 轻松地进行解码。然而,这个想法犯了一个很大的错误。这样会使恶意用户乘机注入将在服务器上执行的代码。

本文中的示例代码只允许通过 Ajax 提交的原语类型(例如字符串和数字)的参数。在服务器端,它们被视作字符串,必要时由 JavaScript 引擎自动将它们转换成数字。如果需要更复杂的类型,不应该依靠 eval() 在服务器上进行解码,而应该使用自己的编码/解码方法。

使用标记文件生成 JavaScript 代码

本文第一节提供的 MonitorPage.jsp 文件使用 <js:rpc> 标记(见清单 15)生成调用服务器端函数的 JavaScript 例程。

清单 15. 在 MonitorPage.jsp 中使用 <js:rpc>
<js:rpc function="getInfo()" script="MonitorScript.jss"
        method="GET" async="true" jsonVar="json">
    showInfo(json, "info");
</js:rpc>

<js:rpc function="allocMem(size, seconds)" script="MonitorScript.jss"
        validator="valid('Size', size) && valid('Seconds', seconds)"
        method="POST" async="true"/>

<js:rpc function="gc()" script="MonitorScript.jss"
        method="POST" async="false">
    alert("Garbage Collected");
</js:rpc>

清单 16 包含 rpc.tag 文件,该文件实现定制标记。该标记文件声明它的属性和使用的 JSP 库,用 <js:authorize> 调用 authorize.tag 文件,设置两个 JSP 变量 xhrVarparamList,并生成具有给定名称和参数的客户端 JavaScript 函数。

对于在生成的 JavaScript 代码(将在 Web 浏览器中执行)中使用的 JavaScript 变量,服务器端使用一个 xhrVar 变量维护该变量的名称。xhrVar 变量的值由函数名和 XHR 字符串组成。例如,如果 functiongetInfo(),那么这个 JSP 变量的值(以及 JavaScript 变量的名称)为 getInfoXHR

另一个 JSP 变量 paramList 将保存通过 () 之间的 function 属性提供的参数列表。例如,如果 functionallocMem(size, seconds)paramList 变量将保存 size, seconds 列表。

清单 16. rpc.tag 文件
<%@ attribute name="function" required="true" rtexprvalue="true" %>
<%@ attribute name="script" required="true" rtexprvalue="true" %>
<%@ attribute name="validator" required="false" rtexprvalue="true" %>
<%@ attribute name="jsonVar" required="false" rtexprvalue="true" %>
<%@ attribute name="method" required="false" rtexprvalue="true" %>
<%@ attribute name="async" required="true" rtexprvalue="true"
    type="java.lang.Boolean" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="js" tagdir="/WEB-INF/tags/js" %>

<js:authorize script="${script}" function="${function}"/>

<c:set var="xhrVar" value="${fn:trim(fn:substringBefore(function, '('))}XHR"/>
<c:set var="paramList"
    value="${fn:substringBefore(fn:substringAfter(function, '('), ')')}"/>

var ${xhrVar} = new XHR("${method}", "${script}", ${async});

function ${function} {
    <c:if test="${!empty validator}">
        if (!(${validator}))
            return;
    </c:if>
    var request = ${xhrVar}.newRequest();
    ${xhrVar}.addHeader("Ajax-Call", "${function}");
    <c:forEach var="paramName" items="${paramList}">
        <c:set var="paramName" value="${fn:trim(paramName)}"/>
        ${xhrVar}.addParam("${paramName}", ${paramName});
    </c:forEach>

    function processResponse() {
        if (${xhrVar}.isCompleted()) {
            <c:if test="${!empty jsonVar}">
                var ${jsonVar} = eval(request.responseText);
            </c:if>
            <jsp:doBody/>
        }
    }

    ${xhrVar}.sendRequest(processResponse);
}

rpc.tag 生成的 JavaScript 代码的第一行创建 XHR 对象。然后,该标记文件生成一些 JavaScript 函数,这些函数可在 Web 浏览器中用于调用相应的服务器端函数。如果 validator 属性有一个非空值,则生成的代码中会包括一个 JavaScript 表达式,以决定是否可以进行远程调用。

接下来,使用 newRequest() 函数初始化一个新的 XMLHttpRequest 对象,该对象存储在一个名为 request 的本地 JavaScript 变量中。生成的代码将添加 Ajax-Call 头部,它的值就是函数的签名。然后,将参数添加到 XHR 对象。清单 17 包含生成的 allocMem() 函数的代码。

清单 17. 生成的 allocMem() 函数
var allocMemXHR = new XHR("POST", "MonitorScript.jss", true);

function allocMem(size, seconds) {
    if (!(valid('Size', size) && valid('Seconds', seconds)))
        return;
    var request = allocMemXHR.newRequest();
    allocMemXHR.addHeader("Ajax-Call", "allocMem(size, seconds)");
    allocMemXHR.addParam("size", size);
    allocMemXHR.addParam("seconds", seconds);
    function processResponse() {
        if (allocMemXHR.isCompleted()) {
        }
    }
    allocMemXHR.sendRequest(processResponse);
}

完成 XHR 对象的初始化后,rpc.tag 文件生成一个名为 processResponse() 的 Ajax 回调。该函数验证 Ajax 请求是否完成,如果提供了 jsonVar 属性,则还会计算响应。

对于 JSP 页面中在 <js:rpc></js:rpc> 之间的内容,将使用 <jsp:doBody/> 将其包括在 Ajax 回调中。例如,MonitorPage.jsp 的 <js:rpc function="getInfo()" ...> 元素包含 showInfo(json, "info");,用于处理 JSON 响应。清单 18 显示该代码在由 rpc.tag 生成的 getInfo() 函数中的位置。

清单 18. 生成的 getInfo() 函数
var getInfoXHR = new XHR("GET", "MonitorScript.jss", true);

function getInfo() {
    var request = getInfoXHR.newRequest();
    getInfoXHR.addHeader("Ajax-Call", "getInfo()");
    function processResponse() {
        if (getInfoXHR.isCompleted()) {
            var json = eval(request.responseText);
            showInfo(json, "info");
        }
    }
    getInfoXHR.sendRequest(processResponse);
}

调用脚本的函数

每当在 Web 浏览器中调用一个生成的函数时,将使用 XHR 对象发送一个 URL 以 .jss 结束的 Ajax 请求。此外,对于必须在服务器端调用的函数,必须以名为 Ajax-Call 的 HTTP 头部的形式提供它的签名。.jss 请求由本系列第一部分提供的一个名为 JSServlet 的 servlet 处理。

当示例应用程序的 MonitorScript.jss 被请求时,JSServlet 实际执行 3 个脚本:init.jss、 MonitorScript.jss 和 finalize.jss。init.jss 脚本(见清单 19)获取请求参数(这些参数是 Java 字符串),将它们转换成 JavaScript 字符串,并将参数存储为 param 对象的属性。init.jss 脚本还提供用于访问和设置 bean 的函数。

清单 19. init.jss 文件
var debug = true;
var debugStartTime = java.lang.System.nanoTime();

var param = new Object();
var paramValues = new Object();

function initParams() {
    var paramNames = request.getParameterNames();
    while (paramNames.hasMoreElements()) {
        var name = paramNames.nextElement();
        param[name] = String(request.getParameter(name));
        paramValues[name] = new Array();
        var values = request.getParameterValues(name);
        for (var i = 0; i < values.length; i++)
            paramValues[name][i] = String(values[i]);
    }
}

initParams();

function getBean(scope, id) {
    return eval(scope).getAttribute(id);
}

function setBean(scope, id, bean) {
    if (!bean)
        bean = eval(id);
    return eval(scope).setAttribute(id, bean);
}

由于所有 3 个脚本都在同一个上下文中执行,finalize.jss(见清单 20)可以使用 init.jss 和 MonitorScript.jss 的变量和函数。finalize.jss 脚本获取 Ajax-Call 头部,验证调用是否被授权,使用 eval() 调用脚本的函数,并使用 toSource() 将返回的对象转换成 JSON 字符串。由于函数参数是作为请求参数传输的,可以从 param 对象获得它们的值。

清单 20. finalize.jss 文件
var ajaxCall = request.getHeader("Ajax-Call");
if (ajaxCall != null) {
    var authorizedCalls = getBean("session", "authorizedCalls");
    if (authorizedCalls.contains(request.requestURI, ajaxCall)) {
        var ajaxResponse = eval("with(param) " + ajaxCall);
        if (ajaxResponse)
            print(ajaxResponse.toSource());
    }
}

var debugEndTime = java.lang.System.nanoTime();
if (debug)
    println("// Time: " + (debugEndTime - debugStartTime) + " ns");

使用 eval() 执行函数是安全的,因为使用 authorizedCalls.contains()Ajax-Call 头部进行了验证。

结束语

在本文中,您学习了如何在 Ajax 和同时在服务器和客户机上使用 JavaScript 代码的 Java 应用程序中使用 RPC。您还看到了如何用 JavaScript 实现 Java 接口,如何在 JavaScript 代码中创建 Java 数组和启动线程,以及如何在连接到数据提要时管理 Ajax 请求的生命周期。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development, Java technology
ArticleID=397455
ArticleTitle=JavaScript EE,第 2 部分: 用 Ajax 调用远程 JavaScript 函数
publish-date=06152009