まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:servlet

久しぶりの Java プログラミングの機会があり、これまた久しぶりにサーブレットを作りました。POST されたデータを受け取って、バイナリデータを生成して、Content-Type をつけてストリームで返す、というものです。

この POST データの受け取り方をどうするかで(少しだけ)迷ったのですが、変数もその型も数も不定で受け取るような仕様だったので、深く考えずに JSON データを受け取ることにしました。普段の Node.js の時は特殊な事情がない限り JSON で受け取ることにしているので、その延長というか「まあイマドキは JSON だよね」くらいに考えていました。

・・・が、これが意外と苦戦。 誰かの参考になれば・・・、と思って、以下実際に作ったサーブレットの実装を紹介します。

まず最初はクライアント(呼び出し)側は普通にこんな感じの実装にしました。jQuery の AJAX を使ってタイムスタンプ値を含む JSON オブジェクトをポストしています。これを受け取って処理するサーブレット(/postdata)を作るのが今回の目的となります:
  :

$.ajax({
  type: 'POST',
  url: './postdata',
  data: { timestamp: ( new Date() ).getTime(), body: 'ハローワールド' },
  success: function( result ){
    console.log( result );
  },
  error: function( err ){
    console.log( 'error' );
  }
});
:

そしてそのサーブレットのコード部分が以下です:
public class PostdataServlet extends HttpServlet {
	
  @Override
  protected void doPost( HttpServletRequest req, HttpServletResponse res )
throws ServletException, IOException {
req.setCharacterEncoding( "UTF-8" ); try{
//. JSON テキストを全部取り出す BufferedReader br = new BufferedReader( req.getReader() ); String jsonText = br.readLine(); jsonText = URLDecoder.decode( json, "UTF-8" ); //System.out.println( jsonText );
//. JSON オブジェクトに変換 JSONParser parser = new JSONParser(); JSONObject jsonObj = ( JSONObject )parser.parse( jsonText );
//. JSON オブジェクトから特性の属性を取り出す
String body = ( String )jsonObj.get( "body" );
: }catch( Exception e ){ e.printStackTrace(); } } }

要は req.getParameter() などを使って特定のパラメータ値を取り出すわけではなく、req.getReader() を使ってポストされてきた全データ(今回の場合は JSON オブジェクトをテキスト化したもの)を受け取る必要があります。そして受け取ったテキストデータを(上記の例であれば JSON-Simple ライブラリを使って)JSON オブジェクト化した上でサーブレット内で取り扱う、という手法です。


・・・単純に JSON データを扱いたかっただけなんだけど、こんなに面倒だったっけ?

 

IBM Domino 上で Java サーブレットを動かすまでの設定と実装を紹介します。なお、今回は Domino バージョン 9 上で実行することを想定しています。


まずは Domino 上で Java サーブレットが動作するよう設定します。Domino 導入後、管理クライアントを開きます(ウェブ版でも構いません。その場合は webadmin.nsf を開きます)。サーバー設定文書を編集モードで開いて、"Internet Protocol" - "Domino Web Engine" - "Java servlet support" を "Domino Servlet Manager" に設定します。また必要に応じて Servlet URL path(デフォルトで /servlet)や Class path(同 domino/servlet)を編集して保存します。これで Domino サーバー側の設定は完了です:
2016102201


次に Java サーブレットを実装します。Domino 上の Java サーブレットは war ファイルを読み込む、といったことができないので、JDK でサーブレットの class ファイルを用意します。この辺り詳しくは以下のエントリも参照ください:
Eclipse(IDE) を使わずにJavaサーブレットを作る



上記エントリで紹介した HelloWorld サーブレットを動かしてもいいのですが、せっかくなので Domino のクラスを使って情報を取得して表示する内容にします。以下のソースコードを DominoInfo.java というファイル名で、UTF-8 で記述して保存します(青字部分で Domino クラスを使っています):
//. DominoInfo.java
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import lotus.domino.*;

public class DominoInfo extends HttpServlet{
  @Override
  protected void doGet( HttpServletRequest req, HttpServletResponse res ){
    try{
      NotesThread.sinitThread();
      Session session = NotesFactory.createSessionWithFullAccess();
      String username = session.getUserName();
      String version = session.getNotesVersion();
      
      res.setContentType( "text/plain; charset=UTF-8" );
      res.getWriter().println( username + " : " + version );
    }catch( Exception e ){
      e.printStackTrace();
    }finally{
      NotesThread.stermThread();
    }
  }
}

↑このサーブレットを実行するユーザー(つまりサーバー ID のユーザー)名と、実行中の Domino のバージョンを取得して表示する、という内容です。

これを javac でコンパイルします。このエントリでも紹介しましたが、javac のパラメータで UTF-8 エンコードであることと、ServletAPI.jar のクラスパスを指定する必要がありますが、更に加えて以下の2点を指定してコンパイルします:

(1) lotus.domino.* クラスを利用するので、Notes.jar のクラスパスを指定(以下のコマンド例では C:\IBM\Notes\jvm\lib\ext\Notes.jar に存在していると仮定)
(2) Domino 9 で使われている Java のバージョンは 6 (つまり JDK 1.6)なので、このバージョン互換でコンパイルするよう指定


具体的には以下のようなコマンドを実行します:
C:\tmp>javac -classpath c:\arc\servlet-api-3.1.jar;c:\IBM\Notes\jvm\lib\ext\Notes.jar -encoding utf-8 -source 1.6 -target 1.6 DominoInfo.java

コンパイルに成功すると DominoInfo.class というファイルが出来上がります。これを上記で設定した Domino の classpath(上の例だと domino/servlet)フォルダにコピーして、Domino を再起動します。

再起動後、ウェブブラウザで Domino サーバーの /servlet/DominoInfo にアクセスしてサーブレットを実行し、以下のような結果になることを確認します:
2016102202


↑サーバーを実行しているユーザーの名前(CN=Domino/O=Dot123)と、Domino サーバーのバージョン(Build V90_C06_12072012|December 07, 2012)が表示できました。 本来、これらの情報は Notes クライアントなどの特殊環境からでないと取得できないものですが、そのロジックを Java サーブレットという仕様でラッピングすることで HTTP アクセスで取得することができるようになったことが分かります。Notes/Domino データベースへの読み書きを実現するための1つの方法として使えます。

僕は普段 Java の IDE (統合開発環境)として Eclipse を使ってますが、久しぶりに IDE なしでサーブレットを作る機会がありました。結構ハマったこともあって、普段いかに Eclipse に頼っているのかを痛感しました・・・


今回やりたかったことは、ごくごくシンプルな話で、
 (1) サーブレットの Java ソースファイルを書いて、
 (2) JDK でビルドしてサーブレットの class ファイルを作る
という2つです。要はサーブレットの実行環境が特殊で war ファイルとかをデプロイできないため、 class ファイルをそのまま配置する必要がある、という(IBM Domino とかの)ケースを想定しています。

なお環境は Windows7 で、JDK 1.8 はダウンロード&インストール済みとします。



まず (1) のソースファイルは以下のような感じにしました。パッケージも指定していません。これを UTF-8 で記述して、HelloWorld.java というファイル名で保存します:
//. HelloWorld.java
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class HelloWorld extends HttpServlet{
  @Override
  protected void doGet( HttpServletRequest req, HttpServletResponse res ){
    try{
      res.setContentType( "text/plain; charset=UTF-8" );
      res.getWriter().println( "ハローワールド!" );
    }catch( Exception e ){
      e.printStackTrace();
    }
  }
}

↑特別なことは何もしてない、ごくごくシンプルな「ハローワールド!」です。


さて、これを (2) JDK でビルドして class ファイルに・・・という段階で何度かつまづきました。

まずは普通にそのままコンパイルしてみると、
C:\tmp>javac HelloWorld.java

HelloWorld.java:11: エラー: この文字は、エンコーディングMS932にマップできません
      res.getWriter().println( "繝上Ο繝シ繝ッ繝シ繝ォ繝会シ?" );

「ハローワールド!」という日本語文字列のエンコードでエラーが出てしまいました。ちゃんとソースコードは UTF-8 にしたつもりだったのに・・・

実は Windows 版の JDK の javac はソースコードが Shift-JIS で書かれているものとして動作するらしいのでした(なんちゅうクソ仕様・・・)。便利とは思えない仕様ですが、このエラーを回避するにはコマンドラインパラメータでソースコードのエンコードを指定必要があるのでした。

というわけで今度は UTF-8 を指定してコンパイル。すると別のエラーが大量に・・・
C:\tmp>javac -encoding utf-8 HelloWorld.java

HelloWorld.java:3: エラー: パッケージjavax.servletは存在しません
import javax.servlet.*;
^
HelloWorld.java:4: エラー: パッケージjavax.servlet.httpは存在しません
import javax.servlet.http.*;
^
HelloWorld.java:6: エラー: シンボルを見つけられません
public class DominoInfo extends HttpServlet{
                                ^
  シンボル: クラス HttpServlet
HelloWorld.java:8: エラー: シンボルを見つけられません
  protected void doGet( HttpServletRequest req, HttpServletResponse res ){
                        ^
  シンボル:   クラス HttpServletRequest
  :
  :

これら全てサーブレットの JAR ライブラリを(指定していないので)見つけられないことが原因のエラーです。これは普段の Eclipse 環境でも指定していることでしたが、すっかり忘れていました。

Tomcat あたりをダウンロード&インストールして、サーブレット JAR ファイル(servlet-api.jar とかいう名前だと思います)を見つけて取り出します(以下の例では c:\arc\servlet-api-3.1.jar というパスで保存しているものとして記述しています)。で、改めてこのファイルを classpath に指定してコンパイル:
C:\tmp>javac -classpath c:\arc\servlet-api-3.1.jar -encoding utf-8 HelloWorld.java
C:\tmp>

無事コンパイルできました。HelloWorld.class が出来ているはずです:
2016102201


・・・こ、こんなに難しかったっけ? (^^;


この応用で、作ったサーブレットを実際に IBM Domino 上で動かすための設定についてはこちら


仕事で「スレッドセーフなサーブレット」の話に遭遇したのでまとめておきます。


通常のサーブレットは java.servlet.http.HttpServlet を拡張して作ります。この方法で作成した場合、サーブレットはシングルインスタンスがマルチスレッドで動作します。サーブレットのインスタンスそのものは1つで、この1つのインスタンスが複数のリクエストを並行処理する、という挙動になります。並行処理している間はスレッドセーフではありません:

import javax.servlet.http.*;

public class MyServlet extends HttpServlet{
  :
  :
}

仮にこのサーブレットがリクエストされ、その実行が終了する前に別のリクエストを受けた場合、別のリクエストは前のサーブレットの終了を待たずに実行が始まります。大半のケースではこれが期待する挙動になると思っています。 ただしスレッドセーフではないので、例えばサーブレットの処理の中にデータベースへの更新処理が含まれているような場合、排他制御できるかどうかはそのデータベースシステム側に求められるか、データベース側が排他制御していないのであればサーブレットの中に独自の排他制御の仕組みを実装する必要があります。

ではサーブレットをスレッドセーフに作る方法はないのか? というのが今回の話題でした。

その結論がこちらです。javax.servlet.SingleThreadModel インターフェースを継承して作成したサーブレットはシングルスレッドモードになり、実行中は他のスレッドリクエストは待ち行列に入ります。結果としてスレッドセーフな処理が実現できますが、処理速度を考えるとこのサーブレットで実現する部分は可能な限り小さく作るべきであると思っています:

import javax.servlet.http.*;
import javax.servlet.*; public class MyServlet extends HttpServlet implements SingleThreadModel{ : : }

実際の処理ではデータベースだけではなく、どのリソースにスレッドセーフ性が求められるのか、を意識して実装する必要があるのですが、そこが明確になっているのであれば上記のような方法で実装そのものは簡単にできる、ということになります。後はそこだけを綺麗に分離して、小さく作れるかどうか、、ということになるのかな。




CentOS で(Eclipseを使って) Java の開発環境を整備するまでの手順を紹介します。
Eclipse を使うので、CentOS には X Window やデスクトップといったモジュールが導入されていることが必要です。


まず Eclipse のインストールそのものは簡単です。JDK を導入した上で最新版をダウンロードして展開してもいいですし、最新版でなくてもよければもっと簡単に yum でインストールすることもできます(以下、こちらの手順を使っている前提で紹介を続けます):
# yum install eclipse

yum を使う場合であれば JDK ごとインストールされてます。なお、2014/10/22 現在ですが、この方法でインストールされる Eclipse のバージョンは 3.6.1(Helios) でした。


加えてウェブアプリケーションの動作確認のため FireFox も導入しておきます:
# yum install firefox

インストールが完了したら、デスクトップのメニューから アプリケーション > プログラミング > Eclipse で起動できます:
2014102101


初回起動時はワークスペースをどのディレクトリに作るか聞かれます。特に理由がなければデフォルトのままでも構いません。"Use this as the default and do not ask again" にチェックを入れておくと、次回起動時にこの質問は聞かれなくなります:
2014102102


Eclipse が初回起動した時の画面です。チュートリアルなどは飛ばして、"Workbench" をクリックし、実際の作業画面に移ります:
2014102103


実際にコーディングを行う作業画面がこちらです。最初の段階では何もプロジェクトがないのでのっぺらぼうですが、ここに作業するプロジェクトを追加していくことになります:
2014102104


ただ、この段階ではまだ Eclipse に充分なプラグインが導入されていないため、Java のウェブアプリケーションを作るウィザードも使えませんし、デバッグ用のウェブアプリケーションサーバーも含まれていません。最低限必要な環境を追加していく必要があります。

Eclipse 作業画面のメニューから Help > Install New Software を選択して、プラグインの更新ダイアログを表示します。そして Work with: 欄に使っている Eclipse のバージョンに合わせた更新サイト(3.6.1 であれば http://download.eclipse.org/releases/helios)を指定します。しばらく待つと下に更新可能なプラグインの候補がカテゴリーに分類されて表示されます:
2014102105


ここから必要なものを選択してチェックを付けていきます。まずは "General Purpose Tools" に分類されている "Marketplace Client" にチェック: 
2014102106


続けて "Web, XML, and Java EE Development" に分類されている "Eclipse Java EE Developer Tools" と "Eclipse Web Development Tools" の両方に(上記と併せて3つ)チェックします:
2014102107


そして Next ボタンを2度クリックし、使用条件を確認した上で accept して、最後に Finish ボタンをクリックすると選択した3つのプラグインの導入が開始されます。プラグインの導入が完了すると Eclipse の再起動を促されるので、再起動("Restart Now")します。再起動後はこの3つのプラグインが有効になった状態で起動します。

この時点では Java アプリケーションプロジェクトを作成することはできるようになっていますが、まだデバッグ用のアプリケーションサーバーが用意されていません。Apache Tomcat などを別途導入してもいいのですが、ここでは簡単に導入できる Jetty をインストールして Eclipse 内から使えるようにします。


Eclipse の再起動後、今度はメニューから Help > Eclipse Marketplace を選択します(Marketplace も上記で導入しているので、再起動後からは使えるようになっているはずです)。

Marketplace ダイアログが表示されたら Find 欄に "Jetty" と入力して、Jetty 関連のマーケットモジュールを検索します:
2014102108


この結果表示されるモジュールの中から、以下の3つをインストールします:
(1) Eclipse Jetty
(2) Run-Jetty-Run
(3) WebLauncher

Marketplace では3つまとめてインストールすることができないので、これらを1つずつ探して "Install" ボタンをクリックする、と繰り返します。そして3つ全てインストールし終わったら、再度 Eclipse を再起動します。これで準備完了です。


では改めてこの環境で Java アプリケーションを作って動かしてみましょう。Eclipse のメニューから File > New > Project を選択してプロジェクトウィザードを出し、Web の下にある "Dynamic Web Project" を選択して "Next" をクリックします。プロジェクトの名称は適当に("TestWeb" など)入力します。他はデフォルトのまま "Finish" をクリックしてプロジェクトを作成します:
2014102109


このプロジェクトにテスト用の JSP ファイルを1つ追加します。プロジェクト名部分を右クリックして New > File を選択し、このプロジェクトの WebContent フォルダ内に test.jsp というファイルを追加します:
2014102110


追加作成した test.jsp を選択してエディタで開き、以下の様な内容を記入します。単純に現在時刻を表示するだけのページですが、Java を使っているので一応 Java ウェブアプリケーションと言えると思います:
2014102111


では作成したこのアプリケーションを実際に動かしてみましょう。プロジェクトを右クリックして Run As > Run Jetty を選択します:
2014102112


Eclipse 内で Jetty が(デフォルトでは 8080 番ポートで)起動します。これでこのプロジェクトが Jetty から参照できるようになっているので、同一マシンで FireFox を起動し、http://localhost:8080/TestWeb/test.jsp にアクセスしてみます:
2014102113
動作が確認できました。ちゃんと Java ウェブアプリケーションの開発や動作確認までできる環境が揃いました!



以下はオマケですが、せっかく Linux 版 Eclipse をインストールしたので、Windows 版ではできない Terminal 機能を紹介します。Eclipse 内のビューを使ってターミナルコンソールを起動する、というものです。

先程と同様にメニューから Help > Install New Software を選択して、今度は "General Purpose Tools" 以下の "Local Terminal" をチェックしてインストールして、Eclipse を再起動します:
2014102114


再起動後、Eclipse のメニューから Window > Show View > Other を選択し、Terminal の下にある Terminal を選択して OK をクリックします:
2014102115


すると画面右下に "Terminal" という名前のタブが1つ追加されます。まだ有効になっていませんが、ここがターミナルコンソールとして使えるウィンドウになります:
2014102116


実際にコンソールを使うにはタブ内の一番右のアイコン(マウスオーバーすると "New Terminal Connection in Current View" と表示される所)をクリックして、接続設定ダイアログを表示します。特に変更の必要はないので、そのまま OK ボタンをクリックします:
2014102117


ターミナルコンソールが有効になり、シェルとして使えるようになります。これで別窓のシェルにいちいち移動する手間が省けます:
2014102118


ただこの機能がまだ Incubation 段階だからなのかもしれませんが、このターミナルコンソールでは日本語が化けて表示されてしまうようです。日本語の入出力に依存するような自由な使い方はまだできない、という認識の元で使えそうです。






このページのトップヘ