この記事で実装する RPC メカニズムは極めて単純なもので、サーバー上にある一連の JavaScript 関数を、あたかも 1 つの JavaScript エンジンでクライアント・コードとサーバー・コードの両方を実行しているかのように、Web ブラウザーから呼び出せるようにします。しかしそのためには、サーバー・サイドのルーチンと同じ名前とパラメーターを持つクライアント・サイドのルーチンが必要になります。
クライアント・サイドのルーチンは、Ajax を使用して、実際の処理が行われるサーバーにパラメーターを送信します。サーバー・サイドの関数は Java サーブレットによって呼び出され、その結果は JSON フォーマットでクライアントに返されます。するとクライアント・サイドのルーチンが Ajax レスポンスを評価し、JSON ストリングを JavaScript オブジェクトに再び変換してから、アプリケーションに返すという筋書きです。
アプリケーション開発者である読者は、ユーザー・インターフェースの作成と、サーバーで実行される関数の作成に専念してください。Ajax や RPC の問題については、アプリケーション開発者が対処する必要はありません。この記事がタグ・ファイルという形で提供している JavaScript コード生成プログラムを JSP ページで使えば、クライアント・サイドのルーチンが自動的に生成されるからです。この仕組みを理解してもらうために、まずはサンプル・アプリケーションから取り掛かります。
この記事のサンプル・アプリケーションでは、アプリケーションをホストする Java EE サーバーを実行する JVM を監視するために、Java Management API を使用します。ユーザー・インターフェースを構成するのは単一の Web ページです。このページには、Java クラスの数、メモリー使用量、ガーベッジ・コレクターのアクティビティー、スレッドの数といった、さまざまなインジケーターが表示されます。
ページに表示される情報は、Ajax によって取得されて HTML テーブルに挿入されます (図 1 を参照。図 1 を拡大表示するには、ここをクリックしてください)。アプリケーションをもっと興味深いものにするため、この Web ページには、メモリーを指定の秒数の間、割り当てるためのフォームが含まれています。また、このページからは JVM ガーベッジ・コレクター (GC) を起動することもできます。
図 1. サンプル・アプリケーション
サーバー・サイドでは、アプリケーションは JavaScript ファイルを使用します。この JavaScript ファイルには、Ajax を利用して Web ブラウザーから呼び出される関数があり、関数の呼び出しには連載第 1 回に記載したスクリプト・ランナーが使用されます。このスクリプト・ランナーは、URL が .jss 拡張子で終わるリクエストを処理する単純なサーブレットです。サーブレットはサーバー上で対応する JavaScript ファイルを検索し、見つかったファイルを Java スクリプト API を使って実行します。
リスト 1 に、表と MonitorPage.jsp ファイルのフォームを記載します。それぞれのデータ・セルには ID があるため、表の内容は JavaScript で更新することができます。<body> タグの onload 属性を使用して init() という名前の JavaScript 関数を呼び出すと、この関数がアプリケーションのクライアント・サイドの部分を初期化します。他の 2 つの関数、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 | JSON レスポンスを保存するための JavaScript 変数 (オプション) の名前 |
method | HTTP メソッド (設定可能なメソッドは 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 の表のデータ・セル内に情報を挿入する別の関数に渡されます。async は true に設定されることから、生成された getInfo() 関数はリクエストを送信すると直ちにリターンし、showInfo() 関数が Ajax コールバックによって呼び出されます、
allocMem() は validator 属性を使ってユーザー入力を検証する関数です。valid() 関数が 2 つのパラメーターのうちのどちらか 1 つにでも false を返した場合、リモート呼び出しはキャンセルされ、アラート・メッセージが表示されます。この場合、async は false になるため、gc() 関数はレスポンスを待ってから制御を返します。すると init() 関数が getInfo() を呼び出して UI を初期化するとともに、getInfo() を setInterval() に渡すことで、1 秒おきに getInfo() が呼び出されて Web ページが最新の情報を表示するようになります。
rpc.tag という名前のタグ・ファイルとして実装されたカスタム・タグが <js:rpc> ですが、この <js:rpc> によって生成されたコードをリスト 3 に記載します。生成されたそれぞれの関数が使用する 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 関数があります。そのうちの 1 つ、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 を参照) は、JavaScript で実装された run() メソッドを実行する Java スレッドを作成します。このコードは java.lang.reflect.Array クラスの newInstance() メソッドを使って byte 配列を作成してから、java.lang.Thread.sleep() を呼び出します。すると、この byte 配列がガーベッジ・コレクションの対象となります。
リスト 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 は、JVM のガーベッジ・コレクターを呼び出す gc() 関数です。
リスト 6. MonitorScript.jss の gc() 関数
function gc() {
java.lang.System.gc();
}
|
前のセクションでは、<js:rpc> タグによって生成される JavaScript コードについて説明しました。生成されるコードを最小限かつ単純にするため、新規リクエスト・オブジェクトを作成する操作や HTTP リクエストを送信する操作など、XMLHttpRequest API に関連するすべての操作は xhr.js という別個の JavaScript ファイルに配置しました。このセクションでは、XHR オブジェクトのメソッドについて説明します。
XHR() コンストラクター (リスト 7 を参照) は 3 つの引数 (method、url、async) を取り、これらのパラメーターをプロパティーとして保存します。前のリクエストがまだアクティブである場合、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 リクエストの送信時に、リクエストに組み込むリクエスト・パラメーターとリクエスト・ヘッダーを追加することができます。この 2 つのメソッドは、新規リクエストが作成されると同時に使用できるようになります。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 を参照) は、パラメーターをエンコードし、リクエストをオープンし、ヘッダーを追加し、async が true の場合には Ajax コールバックを設定した上で、HTTP リクエストを送信します。async が false の場合には、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() によって、リスト 11 にコードが記載された showRequestInfo() 関数が呼び出されます。この関数はウィンドウを開き、そのウィンドウ上にリクエストの情報を出力します。つまり、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 という変数が設定されるようにしています。_freeze が true に設定されると、リクエストの情報は更新されません。また、ユーザーがエラー・ウィンドウを閉じたとしても、ウィンドウが再び開くことはありません。コードを変更した後、アプリケーションのページを更新表示するだけで、エラーがまだ発生しているのか、あるいは修正されたかどうかを確認することができます。
htmlEncode() 関数 (リスト 12 を参照) はストリング・パラメーターを引数に取り、&、<、および > の文字をそれぞれ &、<、> に置き換えます。
リスト 12. xhr.js の htmlEncode() 関数
function htmlEncode(value) {
return value ? value.replace(/&/g, "&")
.replace(/</g, "<").replace(/>/g, ">") : "";
}
|
このセクションでは、クライアント・サイドの RPC メカニズムを実装する JavaScript 関数に関して、これらの関数を生成する JSP タグ・ファイルについて説明します。また、それぞれの関数に対応するサーバー・サイドの関数がどのように呼び出され、結果がどのように返されるかについても説明します。しかしまず最初に、セキュリティーに関する注意点を考慮しなければなりません。
サーバー・サイドのスクリプトは通常のリソースのように扱えるので、これらのスクリプトへのアクセスは、Java EE の標準的なセキュリティー手法を使って制限することができます。通常は、web.xml ファイルに指定されたセキュリティー制約に応じてスクリプトにアクセスする権利を有するロールを 1 つ以上定義することになります。
スクリプトへのアクセスを制限するかどうかに関わらず、Web クライアントからサーバー・サイド・スクリプトの関数を呼び出せるようであってはなりません。どの JavaScript 関数を RPC メカニズムによって呼び出せるかを制御する簡単な方法は、これらの関数を Set コレクションに保持することです。リスト 13 に示す AuthorizedCalls Bean は、許可された呼び出しを管理するスレッド・セーフなメソッドを提供します。
リスト 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 を参照) には 2 つの属性 (function と script) があり、これらの属性の値が AuthorizedCalls の add() メソッドに渡されます。さらに、スクリプトの相対 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);
%>
|
セキュリティー関連の側面として分析しなければならない極めて重要なもう 1 つの事項は、リモートで呼び出される関数のパラメーターをサーバー・サイドではどのように処理するかという点です。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 ファイルを呼び出します。そして xhrVar と paramList という 2 つの JSP 変数を設定し、指定の名前とパラメーターを使ってクライアント・サイドの JavaScript 関数を生成します。
xhrVar はサーバー・サイドで使用される変数であり、生成される JavaScript コード全体で使用される JavaScript 変数の名前を保持します。この JavaScript コードが、Web ブラウザーで実行されるコードです。xhrVar 変数の値は関数の名前と “XHR” というストリングで構成されます。例えば関数が getInfo() の場合、JSP 変数の値 (そして JavaScript 変数の名前) は getInfoXHR となります。
もう 1 つの JSP 変数である paramList は、『(』と『)』で囲まれた function 属性によって指定されたパラメーターのリストを保持します。例えば関数が allocMem(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 オブジェクトを作成します。続いてこのタグ・ファイルは、Web ブラウザーで対応するサーバー・サイドの関数を呼び出すために使用できる JavaScript 関数を生成します。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()" ...> 要素には、JSON レスポンスを処理するための showInfo(json, "info"); が含まれます。リスト 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 ブラウザーで呼び出すたびに、URL が .jss で終わる Ajax リクエストを送信するために XHR オブジェクトが使用されます。さらに、サーバー・サイドで呼び出す必要がある関数のシグニチャーが、Ajax-Call という名前の HTTP ヘッダーに指定されます。.jss リクエストを処理するのは、連載第 1 回に記載したサーブレット、JSServlet です。
サンプル・アプリケーションの MonitorScript.jss が要求されたときに JSServlet が実際に実行するスクリプトは、init.jss、MonitorScript.jss、および finalize.jss の 3 つです。init.jss スクリプト (リスト 19 を参照) は Java ストリングであるリクエストのパラメーターを受け取り、これらのパラメーターを JavaScript ストリングに変換して param オブジェクトのプロパティーとして保存します。init.jss スクリプトも同じく、Bean を取得するための関数や、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");
|
Ajax-Call ヘッダーは authorizedCalls.contains() を使用して確認されるため、関数を実行するには eval() を使用するのが堅実です。
この記事では、サーバーとクライアントの両方で JavaScript コードに依存する Ajax アプリケーションと Java アプリケーションで RPC を使用する方法を説明しました。また、JavaScript で Java インターフェースを実装する方法、Java 配列を作成して JavaScript コードでスレッドを開始する方法、さらにデータ・フィードに接続する際に Ajax リクエストのライフサイクルを管理する方法についても説明しました。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Sample application for this article | jsee_part2_src.zip | 23KB | HTTP |
- 「JavaScript EE: 第 1 回 サーバー・サイドで JavaScript ファイルを実行する」では、javax.script API を使用して JavaScript ファイルをコンパイルし、実行する方法、Java オブジェクトをスクリプト変数としてエクスポートする方法、そしてサーバー・サイドで実行する JavaScript ファイルを作成する方法を説明しています。
- 記事「Ajax and Java development made simpler, Part 1」(developerWorks、2008年4月) でも、JSP ファイルで動的に JavaScript コードを生成するという概念を探っています。
- Web 2.0 開発のツールと情報が満載の developerWorks Web development ゾーンにアクセスしてください。
- developerWorks Ajax resource center には、Ajax 関連の記事が次々と追加されています。Ajax アプリケーションを今すぐ始めるのに役立つ資料もここから入手できます。
Andrei Cioroianu は、カスタム Java EE 開発および Ajax/JSF コンサルティング・サービスを提供する会社、Devsphere のシニア Java 開発者兼コンサルタントです。彼への連絡には、www.devsphere.com のコンタクト・フォームを利用してください。