目次


JavaScript EE

第 1 回 サーバー・サイドで JavaScript ファイルを実行する

Ajax および Java EE アプリケーションで javax.script API を使用する方法を学ぶ

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: JavaScript EE

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:JavaScript EE

このシリーズの続きに乞うご期待。

典型的な Ajax アプリケーションでは、クライアント・サイドで JavaScript を使用し、サーバー・サイドでは Java などの別の言語を使用します。そのため開発者は、一部のルーチンについては Web ブラウザーでは JavaScript を使用して実装し、サーバーには別の言語を使用して実装するというように、実装を 2 回繰り返さなければならなくなります。このような二重にコーディングが必要となる問題は、JavaScript をサーバー・サイドの Java コードと組み合わせて使用し、javax.script API を介してスクリプト言語のフルサポートを得ることで解決できます。さらに JDK (Java SE Development Kit) 6 にはすでに Mozilla の Rhino JavaScript エンジンが組み込まれているため、セットアップの必要はありません。

この連載の第 1 回目の記事では、JavaScript ファイルを Java EE アプリケーション内で実行できるようにする単純なスクリプト・ランナーを使用します。スクリプトは、JSP ページで使用する applicationsessionrequestresponse などのいわゆる「暗黙オブジェクト」にアクセスします。大部分のサンプルは再利用可能なコードで構成されているので、これらのサンプルを利用することで、独自のアプリケーションでもサーバーで簡単に JavaScript を使い始められます。

javax.script API の使用

このセクションでは javax.script API の概要を説明します。Java オブジェクトにアクセスするスクリプトを実行する方法、Java コードから JavaScript 関数を呼び出す方法、そしてコンパイル後のスクリプトにキャッシング・メカニズムを実装する方法を学んでください。

スクリプトを実行する

javax.script API の使い方は極めて単純です。まず始めに ScriptEngineManager インスタンスを作成し、このインスタンスによって ScriptEngine オブジェクト (リスト 1 を参照) を取得します。オブジェクトの取得には、以下のいずれかのメソッドを使用します。

  • getEngineByName()
  • getEngineByExtension()
  • getEngineByMimeType()
リスト 1. ScriptEngine インスタンスの取得
import javax.script.*;
...
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
...
engine.eval(...);

getEngineFactories() を使用して、使用可能なスクリプト・エンジンのリストを取得することもできます。現在、JDK 6 にバンドルされているのは JavaScript エンジンだけですが、ScriptEngineManager は JSR-223 Scripting for the Java Platform (「参考文献」を参照) をサポートするサード・パーティー・エンジンの検出メカニズムを実装します。必要な作業は、スクリプト・エンジンの JAR ファイルを CLASSPATH に配置すればよいだけです。

javax.script.ScriptEngine インスタンスを取得すれば、eval() を呼び出してスクリプトを実行できるようになります。また、Bindings インスタンスを eval() メソッドに渡して、Java オブジェクトをスクリプト変数としてエクスポートすることも可能です。リスト 2 に、ScriptDemo.java サンプルを記載します。このサンプルでは、demoVarstrBuf という 2 つの変数をエクスポートし、DemoScript.js スクリプトを実行した後、変数を取得してそれぞれの変更後の値を出力します。

リスト 2. ScriptDemo.java サンプル
package jsee.demo;

import javax.script.*;
import java.io.*;

public class ScriptDemo {

    public static void main(String args[]) throws Exception {
        // Get the JavaScript engine
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        // Set JavaScript variables
        Bindings vars = new SimpleBindings();
        vars.put("demoVar", "value set in ScriptDemo.java");
        vars.put("strBuf", new StringBuffer("string buffer"));
        
        // Run DemoScript.js
        Reader scriptReader = new InputStreamReader(
            ScriptDemo.class.getResourceAsStream("DemoScript.js"));
        try {
            engine.eval(scriptReader, vars);
        } finally {
            scriptReader.close();
        }
        
        // Get JavaScript variables
        Object demoVar = vars.get("demoVar");
        System.out.println("[Java] demoVar: " + demoVar);
        System.out.println("    Java object: " + demoVar.getClass().getName());
        System.out.println();
        Object strBuf = vars.get("strBuf");
        System.out.println("[Java] strBuf: " + strBuf);
        System.out.println("    Java object: " + strBuf.getClass().getName());
        System.out.println();
        Object newVar = vars.get("newVar");
        System.out.println("[Java] newVar: " + newVar);
        System.out.println("    Java object: " + newVar.getClass().getName());
        System.out.println();
    }
    
}

DemoScript.js ファイル (リスト 3 を参照) に記載されている printType() という名前の関数は、各スクリプト変数の型を出力するために使用します。このサンプルは strBuf オブジェクトの append() メソッドを呼び出し、demoVar の値を変更して newVar という名前の新しいスクリプト変数を設定します。

printType() に渡されたオブジェクトが getClass() メソッドを持つオブジェクトの場合、このオブジェクトは obj.getClass().name によってクラス名を取得する Java オブジェクトということになります。この JavaScript の式は、このオブジェクトの java.lang.Class インスタンスの getName() メソッドを呼び出します。オブジェクトに getClass メンバーがなければ、printType() は、すべてのJavaScript オブジェクトが持つ toSource() メソッドを呼び出します。

リスト 3. DemoScript.js サンプル
println("Start script \r\n");

// Output the type of an object
function printType(obj) {
    if (obj.getClass)
        println("    Java object: " + obj.getClass().name);
    else
        println("    JS object: " + obj.toSource());
    println("");
}

// Print variable
println("[JS] demoVar: " + demoVar);
printType(demoVar);

// Call method of Java object
strBuf.append(" used in DemoScript.js");
println("[JS] strBuf: " + strBuf);
printType(strBuf);

// Modify variable
demoVar = "value set in DemoScript.js";
println("[JS] demoVar: " + demoVar);
printType(demoVar);

// Set a new variable
var newVar = { x: 1, y: { u: 2, v: 3 } }
println("[JS] newVar: " + newVar);
printType(newVar);

println("End script \r\n");

リスト 4 に、ScriptDemo.java サンプルの出力を記載します。この出力でまず気付くのは、demoVar は JavaScript の String としてエクスポートされる一方、strBuf の型は java.lang.StringBuffer のままであるということです。プリミティブ変数と Java ストリングは、ネイティブ JavaScript オブジェクトとしてエクスポートされます。その他すべての Java オブジェクト (配列を含む) は、変更されずにそのままエクスポートされます。

リスト 4. ScriptDemo.java の出力
Start script

[JS] demoVar: value set in ScriptDemo.java
    JS object: (new String("value set in ScriptDemo.java"))

[JS] strBuf: string buffer used in DemoScript.js
    Java object: java.lang.StringBuffer

[JS] demoVar: value set in DemoScript.js
    JS object: (new String("value set in DemoScript.js"))

[JS] newVar: [object Object]
    JS object: ({x:1, y:{u:2, v:3}})

End script

[Java] demoVar: value set in DemoScript.js
    Java object: java.lang.String

[Java] strBuf: string buffer used in DemoScript.js
    Java object: java.lang.StringBuffer

[Java] newVar: [object Object]
    Java object: sun.org.mozilla.javascript.internal.NativeObject

スクリプトの実行後、エンジンはすべての変数 (新しい変数を含む) を取り、逆の変換を行って JavaScript プリミティブおよびストリングを Java オブジェクトに変換します。他の JavaScript オブジェクトは、sun.org.mozilla.javascript.internal.NativeObject などのエンジン固有の内部 API を使用する Java オブジェクトのなかにラップされます。

標準 API だけを使用できるように、Java コードと実行スクリプトとの間でのすべてのデータ交換は、プリミティブ変数とストリング、そして Java オブジェクト (JavaScript コード内でも難なくアクセスできるプロパティーとメソッドを持つ Java オブジェクト (Bean など)) によって行ってください。簡単に言えば、Java コードでネイティブ JavaScript オブジェクトにアクセスしようとしてはいけないということです。代わりに、JavaScript コード内の Java オブジェクトを使用してください。

関数を呼び出す

前のサンプルで、JavaScript から Java メソッドを呼び出せることは明らかにしたので、今度は Java コードから JavaScript 関数を呼び出す方法を説明します。まずは、呼び出したい関数が含まれるスクリプトを実行する必要があります。次に、ScriptEngine インスタンスを javax.script.Invocable にキャストします。これによって、invokeFunction()invokeMethod() が使えるようになります。スクリプトが Java インターフェースのすべてのメソッドを実装している場合には、getInterface() を使って、スクリプト言語でコーディングされたメソッドを持つ Java オブジェクトを取得することもできます。

InvDemo.java サンプル (リスト 5 を参照) は、demoFunction() ルーチンが含まれる InvScript.js というスクリプトを実行します。この Java サンプルは、ScriptEngine インスタンスを Invocable にキャストした後、関数の名前とパラメーターをエンジンの invokeFunction() メソッドに渡します。すると、demoFunction() によって値が返されます。

リスト 5. InvDemo.java サンプル
package jsee.demo;

import javax.script.*;
import java.io.*;

public class InvDemo {

    public static void main(String args[]) throws Exception {
        // Get the JavaScript engine
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");
        
        // Run InvScript.js
        Reader scriptReader = new InputStreamReader(
            InvDemo.class.getResourceAsStream("InvScript.js"));
        try {
            engine.eval(scriptReader);
        } finally {
            scriptReader.close();
        }
        
        // Invoke a JavaScript function
        if (engine instanceof Invocable) {
            Invocable invEngine = (Invocable) engine;
            Object result = invEngine.invokeFunction("demoFunction", 1, 2.3);
            System.out.println("[Java] result: " + result);
            System.out.println("    Java object: "
                    + result.getClass().getName());
            System.out.println();
        } else
            System.out.println("NOT Invocable");
    }
    
}

InvScript.js ファイル (リスト 6 を参照) には、demoFunction() ルーチン、そして前のスクリプト・サンプルで使用した printType() 関数が含まれます。

リスト 6. InvScript.js サンプル
println("Start script \r\n");

function printType(obj) {
    if (obj.getClass)
        println("    Java object: " + obj.getClass().name);
    else
        println("    JS object: " + obj.toSource());
    println("");
}

function demoFunction(a, b) {
    println("[JS] a: " + a);
    printType(a);
    println("[JS] b: " + b);
    printType(b);
    var c = a + b;
    println("[JS] c: " + c);
    printType(c);
    return c;
}

println("End script \r\n");

InvDemo.java の出力 (リスト 7 を参照) を見ると、数値パラメーターが JavaScript オブジェクトに変換され、demoFunction() が返す値は Java オブジェクトとして取得されることがわかります。このような変換は、プリミティブとストリングに対してしか行われません。それ以外のオブジェクトについては、そのままの形で JVM と JavaScript エンジンとの間で受け渡しされます。

リスト 7. InvDemo.java の出力
Start script

End script

[JS] a: 1
    JS object: (new Number(1))

[JS] b: 2.3
    JS object: (new Number(2.3))

[JS] c: 3.3
    JS object: (new Number(3.3))

[Java] result: 3.3
    Java object: java.lang.Double

javax.script.Invocable はオプションのインターフェースなので、スクリプト・エンジンによっては実装されていない場合もあることに注意してください。JDK 6 に付属の JavaScript エンジンであれば、このインターフェースをサポートします。

スクリプトをコンパイルする

スクリプトを実行するたびにスクリプトの解釈が行われるのでは、CPU リソースを無駄に消費します。同じスクリプトを何度も実行する場合には、JDK 6 の JavaScript エンジンがサポートする別のオプション・インターフェース、javax.script.Compilable が提供するメソッドを使用してスクリプトをコンパイルすると、実行時間を大幅に短縮することができます。

CachedScript クラス (リスト 8 を参照) はスクリプト・ファイルを取り、ソース・コードが変更されたときにだけコンパイルし直します。getCompiledScript() メソッドがスクリプト・エンジンの compile() を呼び出すと、javax.script.CompiledScript オブジェクトが返されます。このオブジェクトの eval() メソッドが、スクリプトを実行します。

リスト 8. CachedScript クラス
package jsee.cache;

import javax.script.*;
import java.io.*;
import java.util.*;

public class CachedScript {
    private Compilable scriptEngine;
    private File scriptFile;
    private CompiledScript compiledScript;
    private Date compiledDate;

    public CachedScript(Compilable scriptEngine, File scriptFile) {
        this.scriptEngine = scriptEngine;
        this.scriptFile = scriptFile;
    }
    
    public CompiledScript getCompiledScript()
            throws ScriptException, IOException {
        Date scriptDate = new Date(scriptFile.lastModified());
        if (compiledDate == null || scriptDate.after(compiledDate)) {
            Reader reader = new FileReader(scriptFile);
            try {
                compiledScript = scriptEngine.compile(reader);
                compiledDate = scriptDate;
            } finally {
                reader.close();
            }
        }
        return compiledScript;
    }

}

ScriptCache クラス (リスト 9 を参照) はコンパイル済みスクリプトのリポジトリーを実装するために、java.util.LinkedHashMap オブジェクトを使用します。このマップの初期容量はキャッシュされるスクリプトの最大数に設定され、負荷係数は 1 に設定されます。この 2 つのパラメーターにより、cacheMap の再ハッシュが必要になることは決してありません。

デフォルトでは、LinkedHashMap クラスのエントリーには挿入順が適用されます。マップのエントリーにデフォルトの順序ではなくアクセス順を適用するには、LinkedHashMap() コンストラクターの 3 番目のパラメーターを true に設定する必要があります。

キャッシュの最大容量に達すると、removeEldestEntry() メソッドが true を返すようになります。そのため、新しいコンパイル済みスクリプトがキャッシュに追加される度に、エントリーは自動的に cacheMap から削除されます。

LinkedHashMap の自動削除メカニズムをアクセス順と組み合わせて使うことで、新規スクリプトの追加時に ScriptCache は、いっぱいになったキャッシュから長時間未使用 (LRU) スクリプトを確実に削除するようにします。

リスト 9. ScriptCache クラス
package jsee.cache;

import javax.script.*;

import java.io.*;
import java.util.*;

public abstract class ScriptCache {
    public static final String ENGINE_NAME = "JavaScript";
    private Compilable scriptEngine;
    private LinkedHashMap<String, CachedScript> cacheMap;

    public ScriptCache(final int maxCachedScripts) {
        ScriptEngineManager manager = new ScriptEngineManager();
        scriptEngine = (Compilable) manager.getEngineByName(ENGINE_NAME);
        cacheMap = new LinkedHashMap<String, CachedScript>(
                maxCachedScripts, 1, true) {
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > maxCachedScripts;
            }
        };
    }

    public abstract File getScriptFile(String key);

    public synchronized CompiledScript getScript(String key)
            throws ScriptException, IOException {
        CachedScript script = cacheMap.get(key);
        if (script == null) {
            script = new CachedScript(scriptEngine, getScriptFile(key));
            cacheMap.put(key, script);
        }
        return script.getCompiledScript();
    }
    
    public ScriptEngine getEngine() {
        return (ScriptEngine) scriptEngine;
    }
    
}

次のセクションではこの ScriptCache クラスを使用して、抽象メソッド getScriptFile() を実装し、getScript() によってキャッシュからコンパイル済みスクリプトを取得します。

スクリプト・ランナーの作成

このセクションでは、URL とスクリプトとのマッピングを実装する単純な Java サーブレットを作成する方法を説明します。このサーブレットは、サーバー・サイドのスクリプトを Web ブラウザーから呼び出せるようにするだけでなく、いくつかの Java EE オブジェクトを変数として公開して JavaScript コードで使用できるようにします。このセクションではさらに、単一の JavaScript エンジンで複数の並行スクリプトを実行できるようにするスクリプト・コンテキストの使用方法も説明します。

サーブレットを初期化する

このサーブレット・クラスの名前は JSServlet です。このクラスの init() メソッド (リスト 10 を参照) は複数の構成パラメーターを取って ScriptCache オブジェクトを作成します。サーブレットのスクリプト・キャッシュは、特定の URI にマッピングされたスクリプト・ファイルのパスを取得するために getRealPath() を使用します。

リスト 10. JSServlet の init() メソッド
package jsee.servlet;

import javax.script.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import jsee.cache.*;

public class JSServlet extends HttpServlet {
    private String cacheControlHeader;
    private String contentTypeHeader;
    private ScriptCache scriptCache;
    
    public void init() throws ServletException {
        ServletConfig config = getServletConfig();
        cacheControlHeader = config.getInitParameter("Cache-Control");
        contentTypeHeader = config.getInitParameter("Content-Type");
        int maxCachedScripts = Integer.parseInt(
                config.getInitParameter("Max-Cached-Scripts"));
        scriptCache = new ScriptCache(maxCachedScripts) {
            public File getScriptFile(String uri) {
                return new File(getServletContext().getRealPath(uri));
            }
        };
    }
    ...
}

リスト 11 には、サーブレットのパラメーターが記載されています。これらのパラメーターは web.xml ファイルに指定されているものです。Cache-Control ヘッダーはスクリプト・キャッシュとは何の関係もありません。ヘッダーに関するパラメーターは両方 (Cache-Control と Content-Type) とも、サーブレットが返す HTTP レスポンスに組み込まれます。no-cache 値はブラウザーに、サーブレットのレスポンスをキャッシュに入れないように指示します。レスポンスは text/plain として処理する必要があるからです。

リスト 11. web.xml ファイル
<web-app ...>

    <servlet>
        <servlet-name>JSServlet</servlet-name>
        <servlet-class>jsee.servlet.JSServlet</servlet-class>
        <init-param>
            <param-name>Cache-Control</param-name>
            <param-value>no-cache</param-value>
        </init-param>
        <init-param>
            <param-name>Content-Type</param-name>
            <param-value>text/plain</param-value>
        </init-param>
        <init-param>
            <param-name>Max-Cached-Scripts</param-name>
            <param-value>1000</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>JSServlet</servlet-name>
        <url-pattern>*.jss</url-pattern>
    </servlet-mapping>

</web-app>

リスト 11 を見るとわかるように、*.jss パターンはサーブレットにマッピングされます。これはつまり、JSServlet は、URL が .jss 拡張子で終わるすべてのリクエストを処理するということです。ユーザーが Web ブラウザーでこのような URL を入力するか、.jss リンクをクリックすると、ブラウザーは HTTP リクエストを Web サーバー (例えば、Apache) に送信します。この Web サーバーは、受信したリクエストをサーブレット・コンテナー (例えば、Tomcat) に渡すように構成されている必要があります。サーブレット・コンテナーが Web サーバーとしても機能する場合には、それ以上の構成は必要ありません。

サーブレット・コンテナーは URL が .jss で終わるリクエストを受け取ると、service() メソッドを呼び出します。このメソッドは、JSServletjavax.servlet.http.HttpServlet から継承したもので、リクエストの HTTP メソッドに応じて doGet() または doPost() のいずれかを呼び出します。どちらのメソッドも、このセクションの後で説明するように JSServlet によってオーバーライドされます。

スクリプトのコンテキストを使用する

それぞれのスクリプト・エンジン・インスタンスには、put() メソッドで変数を保管できるデフォルトのコンテキストがあり、実行されたスクリプトの出力は、デフォルトで System.out に送信されます。サーバー環境では、それぞれに固有のコンテキストを持つ複数の並行スクリプトを実行しなければならないことがあります。このニーズに対応するのが、ScriptContext インターフェースと SimpleScriptContext 実装を提供する、javax.script API です。

Mozilla の Rhino JavaScript エンジンはマルチスレッド化エンジン (囲み記事を参照) であり、同じコンテキストを共有する並行スクリプトを実行できますが、ここで必要としているのは、エンジン・スコープとさまざまなスレッドで実行されるスクリプトの出力とを分離することです。つまり、HTTP リクエストごとに新しい ScriptContext インスタンスを作成しなければなりません。

リスト 12 に、JSServlet クラスの createScriptContext() メソッドを記載します。このメソッドはスクリプトの実行時に、スクリプトの出力を response オブジェクトのライターに送信するようにコンテキストの writer プロパティーを設定します。これはつまり、スクリプトで print() または println() に渡すすべてのものは、サーブレットのレスポンスに組み込まれることを意味します。

さらに、createScriptContext() はスクリプト・コンテキストの setAttribute() メソッドによって以下のスクリプト変数を定義します。

表 1. JSServlet が実行するスクリプトで使用可能な変数
スクリプト変数説明
configサーブレットの javax.servlet.ServletConfig インスタンス
applicationWeb アプリケーションの javax.servlet.ServletContext インスタンス
sessionjavax.servlet.http.HttpSession オブジェクト
requestjavax.servlet.http.HttpServletRequest オブジェクト
responsejavax.servlet.http.HttpServletResponse オブジェクト
outレスポンスを出力するために使用される java.io.PrintWriter オブジェクト
factoryスクリプト・エンジンの javax.script.ScriptEngineFactory

factory 変数は、JavaScript エンジンに関する情報 (言語バージョンやエンジン・バージョンなど) を取得するために使用することができます。これ以外の変数の役割は、そのそれぞれが JSP ページで果たす役割と同じです。

リスト 12. JSServlet の createScriptContext() メソッド
public class JSServlet extends HttpServlet {
    ...
    protected ScriptContext createScriptContext(
            HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        ScriptContext scriptContext = new SimpleScriptContext();
        scriptContext.setWriter(response.getWriter());
        int scope = ScriptContext.ENGINE_SCOPE;
        scriptContext.setAttribute("config", getServletConfig(), scope);
        scriptContext.setAttribute("application", getServletContext(), scope);
        scriptContext.setAttribute("session", request.getSession(), scope);
        scriptContext.setAttribute("request", request, scope);
        scriptContext.setAttribute("response", response, scope);
        scriptContext.setAttribute("out", response.getWriter(), scope);
        scriptContext.setAttribute("factory",
                scriptCache.getEngine().getFactory(), scope);
        return scriptContext;
    }
    ...
}

runScript() メソッド (リスト 13 を参照) は、キャッシュからコンパイル済みスクリプトを取得し、その特定のスクリプト・コンテキストをパラメーターとして渡して eval() メソッドを呼び出します。

リスト 13. JSServlet の runScript() メソッド
public class JSServlet extends HttpServlet {
    ...
    protected void runScript(String uri, ScriptContext scriptContext)
            throws ScriptException, IOException {
        scriptCache.getScript(uri).eval(scriptContext);
    }
    ...
}

リクエストを処理する

HTTP リクエストの URL にマッピングされたスクリプトを実行するには、上記の runScript() メソッドを呼び出すだけで十分ですが、実際のアプリケーションでは、スクリプトを実行する前に何らかの初期化を行い、スクリプトの実行後には何らかのファイナライゼーションを行わなければならない場合があります。

複数のスクリプトを同じコンテキストで連続して実行することは可能です。例えば、あるスクリプトが一連の変数と関数を定義し、続いて別のスクリプトが、同じコンテキストで実行された前のスクリプトの変数と関数を使用して何らかの処理を行うといった具合です。

サーブレットの handleRequest() メソッド (リスト 14 を参照) は、まず HTTP ヘッダーを設定し、init.jss スクリプトを実行します。続いてリクエストの URI からコンテキスト・パスを削除し、取得した URI を持つスクリプトを実行してから、今度は finalize.jss という名前の別のスクリプトを実行します。

リスト 14. JSServlet の handleRequest() メソッド
public class JSServlet extends HttpServlet {
    ...
    protected void handleRequest(
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        if (cacheControlHeader != null)
            response.setHeader("Cache-Control", cacheControlHeader);
        if (contentTypeHeader != null)
            response.setContentType(contentTypeHeader);
        ScriptContext scriptContext = createScriptContext(request, response);
        try {
            runScript("/init.jss", scriptContext);
            String uri = request.getRequestURI();
            uri = uri.substring(request.getContextPath().length());
            try {
                runScript(uri, scriptContext);
            } catch (FileNotFoundException x) {
                response.sendError(404, request.getRequestURI());
            }
            runScript("/finalize.jss", scriptContext);
        } catch (ScriptException x) {
            x.printStackTrace(response.getWriter());
            throw new ServletException(x);
        }
    }
    ...
}

handleRequest() を呼び出すのは、JSServletdoGet() および doPost() メソッドです (リスト 15 を参照)。

リスト 15. JSServlet の doGet() および doPost() メソッド
public class JSServlet extends HttpServlet {
    ...
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        handleRequest(request, response);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        handleRequest(request, response);
    }

}

サーバー・サイドのスクリプトの開発

このセクションには、サーバー・サイドのサンプル・スクリプトを記載します。これらのサンプルから、リクエスト・パラメーターを取得する方法、JavaBean のプロパティーにアクセスする方法、そして JSON レスポンスを生成する方法がわかります。

プリプロセッシングとポストプロセッシング

前のセクションで、JSServlet は init.jss (リスト 16 を参照) を呼び出してから、要求されたスクリプトを実行することがわかったはずです。スクリプトの実行時間を測定したい場合には、このスクリプトで以下のように変数に開始時刻を保管するという方法を使えます。

リスト 16. init.jss スクリプト
var debug = true;
var debugStartTime = java.lang.System.nanoTime();

こうすることによって、finalize.jss (リスト 17 を参照) で実行時間を計算できるようになります。計算された時間は、JSServlet が有効な JSON レスポンスを生成できるように、JavaScript コメントとして出力されます。

リスト 17. finalize.jss スクリプト
var debugEndTime = java.lang.System.nanoTime();
if (debug)
    println("// Time: " + (debugEndTime - debugStartTime) + " ns");

この連載では今後、init.jss と finalize.jss にさらにコードを追加していきます。

リクエスト・パラメーターを取得する

JSServlet を利用して呼び出されたスクリプトは、Java オブジェクトを返す request.getParameter()request.getParameterValues() を使用してリクエスト・パラメーターにアクセスすることができます。もっと簡潔な構文で、Java のストリングと配列の代わりに JavaScript オブジェクトを操作したいという場合には、以下の行を init.jss に追加すれば (リスト 18 を参照)、簡単にその目的を達成することができます。

リスト 18. init.jss でのリクエスト・パラメーターの取得
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();

一例として、ParamDemo.jss という名前のスクリプトを要求するとします。その場合の URL はリスト 19 のとおりです。

リスト 19. スクリプトを要求する場合のサンプル URL
http://localhost:8080/jsee/ParamDemo.jss?firstName=John&lastName=Smith

ParamDemo.jss (リスト 20 を参照) では、param.firstNameparam.lastName を使って 2 つのリクエスト・パラメーターを取得することができます。

リスト 20. ParamDemo.jss サンプル
println("Hello " + param.firstName + " " + param.lastName);

JavaBean にアクセスする

Web アプリケーションの applicationsessionrequest の各スコープに保管する Bean インスタンスは、それぞれ ServletContextHttpSessionHttpServletRequestgetAttribute() メソッドと setAttribute() メソッドを使用して取得したり、置換したりすることができます。また、getBean() および setBean() 関数 (リスト 21 を参照) を使用することも可能です。スクリプトが関数を呼び出せるように、これらの関数は init.jss ファイルに配置してください。scope パラメーターは、以下のいずれかのストリングになります。

  • "application"
  • "session"
  • "request"
リスト 21. init.jss の getBean() および setBean() 関数
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);
}

例えば、session スコープには DemoBean (リスト 22 を参照) のインスタンスを維持するとします。

リスト 22. DemoBean.java サンプル
package jsee.demo;

public class DemoBean {
    private int counter;

    public int getCounter() {
        return counter;
    }

    public void setCounter(int counter) {
        this.counter = counter;
    }

}

BeanDemo.jss スクリプト (リスト 23 を参照) は importPackage(Packages.jsee.demo) によって、JavaBean が含まれるパッケージをインポートします。このスクリプトは続いて getBean() によって session スコープから Bean インスタンスを取得しようとします。Bean が見つからない場合には、BeanDemo.jss はオブジェクトを作成し、このオブジェクトを setBean() によって session スコープに配置します。最後に、BeanDemo.jss スクリプトは Bean の counter プロパティーをインクリメントして出力します。

リスト 23. BeanDemo.jss サンプル
importPackage(Packages.jsee.demo);
var bean = getBean("session", "demo");
if (!bean) {
    bean = new DemoBean();
    setBean("session", "demo", bean);
}
bean.counter++;
println(bean.counter);

javajavaxorgeducom、または net が先頭に付いたパッケージをインポートする場合には、importPackage() Packages 接頭部を使う必要はありません。また、importClass() を使用して個々のクラスをインポートすることもできます。

JSON レスポンスを返す

リスト 24 は、JavaScript エンジンに関する情報とスクリプトの URI が含まれる JSON レスポンスを生成する例です。この例では、JavaScript 構文を使用して json オブジェクトを作成します。そして toSource() メソッドによって、このオブジェクトのソース・コードを String として取得します。

リスト 24. JsonDemo.jss サンプル
var json = {
    engine: { 
        name: String(factory.engineName),
        version: String(factory.engineVersion),
        threading: String(factory.getParameter("THREADING"))
    }, 
    language: { 
        name: String(factory.languageName),
        version: String(factory.languageVersion)
    },
    script: {
        uri: String(request.requestURI)
    }
};

println(json.toSource());

この例で factory および request のプロパティーから取得するすべての Java オブジェクトは、 JavaScript オブジェクトに変換する必要があります。そうでないと、toSource() は正常に動作しません。リスト 25 に、このスクリプトの出力を記載します。

リスト 25. JsonDemo.jss の出力
({language:{name:"ECMAScript", version:"1.6"}, 
engine:{name:"Mozilla Rhino", threading:"MULTITHREADED", 
version:"1.6 release 2"}, script:{uri:"/jsee/JsonDemo.jss"}})

まとめ

この記事では、javax.script API による JavaScript ファイルのコンパイル方法と実行方法を説明しました。さらに、java.util.LinkedHashMap をベースに LRU キャッシュを実装する方法、Java オブジェクトをスクリプト変数としてエクスポートする方法、URL とスクリプトのマッピングを実装する方法、そしてサーバー・サイドで実行する JavaScript ファイルを作成する方法も説明しました。連載の次回の記事では、Ajax でリモート JavaScript 関数を呼び出す方法を学びます。お楽しみに。


ダウンロード可能なリソース


関連トピック

  • JSR-223 Scripting for the Java Platform は、Java アプリケーションでスクリプト言語を使用する際のすべての詳細を網羅した仕様です。
  • Java Scripting Programmer's Guide では、Java Scripting API を紹介し、この記事に記載した以外のサンプル・コードを記載しています。
  • javax.script パッケージには、Java Scripting API のすべてのクラスおよびインターフェースが含まれています。
  • Web 2.0 開発のツールと情報が満載の WdeveloperWorks Web development ゾーンにアクセスしてください。
  • developerWorks Ajax resource center には、Ajax 関連の記事が次々と追加されています。Ajax アプリケーションを今すぐ始めるのに役立つ資料もここから入手できます。
  • Mozilla Rhino は、JDK (Java SE Development Kit) 6 にバンドルされている JavaScript エンジンです。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development, Java technology
ArticleID=367274
ArticleTitle=JavaScript EE: 第 1 回 サーバー・サイドで JavaScript ファイルを実行する
publish-date=12162008