ServletはJavaの開発元であるSun Microsystems社(2010年にOracle社が吸収合併; 以下Sunと記述)が開発した、サーバ上で動作するJavaプログラムです。 ここではServletアプリがデータをクライアントから取得する方法と、クライアントに出力する方法を解説します。 Servletの仕様はSunが公開しています(注3)

Servletの特徴

Servletの機能を一言で言えば、クライアントからのリクエストを処理して、その結果をレスポンスとしてクライアントに返すことです。 ServletはJavaプログラムです。 その証拠に拡張子には .java が使われます。 Servletと同様な仕組みとしてCGIがありますが、ServletはCGIに比して次のような利点があります。

  • ライフサイクル管理
    一度ロードされるとメモリ上に残っているため、2回目以降の処理が速い。 ローディングに際し最初に一度だけ実施する処理や、メモリから削除される直前に実施する処理などが定義できる。(CGIでは要求があるたびに毎回ロードされる)
  • マルチスレッド
    メモリ上の1つのプログラムで複数のリクエストを処理するので、使用メモリサイズが少なくてすむ。 (CGIでは10のリクエストがあれば10のプログラムがロードされる)
  • セッション管理
    同一のクライアントとの一連の処理の間、データを保持しておく仕組み(セッション管理)がある。(CGIではアプリケーションプログラムが自分で保持しなければならない)

単にCGIをJavaで書いただけではServletにはなりません。 HttpServletクラスを継承し、次項で述べるServletコンテナの機能を用いて処理を行うのがServletです。

(注3) SunのServlet仕様書v2.4は以下からダウンロードできます。
http://download.oracle.com/otndocs/jcp/servlet-2.4-fr-spec-oth-JSpec/
また以下にServlet仕様書v2.2の日本語訳があります。
http://www.cresc.co.jp/tech/java/ServletSpecificationV2.2/index.htm

Servletコンテナ

Servletコンテナ

Servletでの処理の中心的な役割を果たす、いわばエンジン部分をServletコンテナと言います。 Servletでの処理はプログラマーが書いたServletアプリケーションとServletコンテナとが協力しあって実行されます。 以下では省略してそれぞれ「アプリ」、「コンテナ」と言うことにします。 前述のライフサイクル管理やセッション管理などの機能はコンテナによって実現されます。

コンテナはクライアントからのリクエストを受け取るとアプリを呼びます。 画面からユーザが入力したデータはコンテナが保管しており、アプリからの求めに応じて渡します。 またコンテナはアプリが出力したデータをレスポンスとしてクライアントに送ります。

したがって、Servlet処理システムを構築するには次の点が重要です。これさえ知っていれば、基本的なシステムを作ることができます。 以下にそれぞれの具体的なやり方を示します。

  • コンテナの呼び出し方
  • コンテナからの入力データの受け取り方
  • コンテナへの出力データの渡し方

コンテナの呼び出し方

コンテナの呼び出し方と書きましたが、実はアプリがコンテナを呼び出すのではなく、コンテナがアプリを呼び出すと言った方が正しいでしょう。 というのはアプリはコンテナのHttpServletクラスを継承(取込&拡張)し、その中のいくつかのメソッドをオーバライド(再定義)することで、コンテナと結びつくからです。

コンテナの呼び出し方

すなわち、コンテナはリクエストを受け取るとdoGetまたはdoPostというメソッドを呼び出すように作られています。 GETメソッドのリクエストなら前者、POSTメソッドのリクエストなら後者を呼び出すのです。 HEADメソッドに対応したdoHeadと言うメソッドもちゃんとあります。

説明だけではわかりにくいので、実際のサンプルを見ていただきましょう。

アプリのソースコード

アプリのサンプルソースコードを示します(注4)。 以下このソースを使ってコンテナとの関わりを見て行きましょう。これは完全なソースでちゃんと動作します。


図解: サンプルソースコード

(注4) このサンプルソースにはセキュリティ上の問題があります。 このままの形でインターネットで公開するとXSS(クロスサイトスクリプティング)という攻撃にさらされる危険があるのでご注意ください。 もちろん自分のパソコン上で動かしている分には全く問題ありません。このサンプルの問題点は入力された文字を何のチェックもせずにそのまま出力していることです。 そのままではなくhtmlタグの開始文字である"<"などを消去したり、別のものに変換したりして出力すれば問題は解消されます。

ソース全体の動作

これからサンプルソースについて各行ごとに説明して行きます。これまでの概念的な説明と違って細かい話が多くなりますが、頑張ってフォローしてください。 まずはサンプルソース全体の動きを説明します。

(1)ではGreetingServletというクラスを定義しています。このクラスはHttpServletを継承しています。継承することでアプリはコンテナの様々な機能が利用できるようになります。継承について忘れてしまった方は、本章末の コラム にごく簡単な説明がありますのでご参照ください。
(2)ではdoPostメソッドを定義しています。このメソッドは継承元のHttpServletクラスで既に定義済なので、ここで定義するとオーバライド(再定義)することになります。
(3)から(5)はこのアプリにさせたい処理です。ここでは入力データを受け取ってdataという変数に代入しています。
(6)から(9)は処理結果を出力画面のHTML文として書き出しています。「こんにちはxxさん」というメッセージが表示されます。

(2)のdoPostメソッドはHttpServletRequest型とHttpServletResponse型の2つの引数を持っています。前者はクライアントからのリクエストに関する情報とそれを収集する処理を集めたもの(オブジェクト)で、後者はクライアントにレスポンスを返すときに利用する情報と処理を集めたもの(オブジェクト)です。このサンプルではそれぞれにreqとresと言う名前を付けました。 入力データを扱うメソッドはServletRequestに属しているのでreqを使い、出力データを扱うメソッドはServletResponseに属しているのでresを用います。 すなわち、(3)のsetCharacterEncodingメソッドと、(4)のgetParameterメソッドではreqを使い、(5)のsetContentTypeメソッドや、(6)のgetWriterメソッドではresを使います。

ところで、HttpServletクラスの中のオーバライドされる前のdoGetやdoPostメソッドの処理はどうなっていると思いますか。ちょっと考えてみてください……。
答えは「この処理はサポートされていない」という趣旨のメッセージを表示する、です。この部分はアプリがオーバライドしないときに実行されるからです。

入力データの受け取り方

サンプルソースの入力データの受け取りの部分を再度示します。まず(3)ではsetCharacterEncodingメソッドで受け取る文字データのコード系を指定しています。 ここで "Windows-31J" とはシフトJISコードのことです。日本語のデータを受け取るときは必ずこのメソッドでコード系を指定する必要があります(指定しないと文字バケします)。 次に(4)ではgetParameterメソッドで入力データを取得しています。このメソッドはStringを返すので、サンプルソースではString型の変数dataで受け取っています。


図解: 入力データ受け取り部分

入力データの受け取り方

(4)のgetParameterメソッドの引数には入力パラメーター名を書きます。入力パラメーター名とは入力画面のHTML文のテキストボックスに付けた名前です。 そのときのHTML文を再度示しましょう。以下のようにname="yourName"と指定しています。このyourNameが入力パラメーター名です。


図解: 入力パラメーター名

複数の入力データの取得

もし入力欄が複数あれば、それごとに異なる入力パラメーター名を付けることで、アプリはHTMLの入力画面でユーザが入力した情報をすべて取得することができます。


図解: 複数の入力欄

例えば上のようなHTML文のデータを受け取るには、次のようなアプリを記述します。


図解: 複数の入力データ受け取り

次は1つの入力パラメーター名に複数の値が入る場合の例です。 HTMLの入力画面で使用する「リストボックス」では、次の例のようにmultiple属性を指定すると1つのパラメーター名に複数の値を指定することができます。


図解: 1つのパラメーター名に複数の値指定

この場合はgetParameterメソッドの代わりにgetParameterValuesメソッドを使います。Valuesと複数形になっていることに注目してください。 このメソッドはStringの配列を返します。これを取得して出力するソースの例を書いてみましょう。


図解: 複数の入力データ取得のソース

またパラメーター名が不明なときに、それを問い合わせるgetParameterNamesというメソッドがあります。

出力データの渡し方

出力データを扱う前にあらかじめコンテントタイプを指定しておく必要があります。コンテントタイプとはファイルの種類のことで、MIME(マイム)形式で指定します。 MIME(Multipurpose Internet Mail Extension)は、名前からもわかるように当初は電子メールにテキスト以外のデータを添付する方法として規定されました。html形式の場合はtext/htmlと設定します。

コンテントタイプの指定はsetContentTypeメソッドで行います。コンテントタイプがtextのときはさらに文字コードの指定も必要です。指定がないと西欧言語を表すのに適したISO-8859-1と見なされます。 したがって日本語の文字を出力するには必ず指定する必要があります(指定しないと文字バケします)。文字コードの指定はcharsetパラメーターで行います。

実際の出力はprintWriterというオブジェクトが行います。出力の前にこのオブジェクトを取得しておく必要があります。

以上の説明を基に、再度サンプルソースの出力データの渡し方の部分を見てみましょう。(5)ではtext/htmlタイプのデータのコード系を指定し、 (6)ではPrintWriterクラスのオブジェクトを取得しoutと名付けています。


図解: 出力データの渡し方

PrintWriterクラスのoutオブジェクトが取得できればあとは簡単です。 PrintWriterのメソッドであるprintlnでHTML文を出力していけば良いのです。 (8)で使っているdataは(4)で取得した入力データで、ユーザ名が入っています。


図解: HTML文の出力

バイナリデータの出力

バイナリデータの出力

前項の説明は、実はテキストデータを出力するときの話でした。 バイナリデータを出力するには次のようにします。

まずコンテントタイプでバイナリデータの種類を指定します。 以下はjpeg形式のイメージファイルを指定する例です。

図解: バイナリデータの種類の指定

次に出力を行うオブジェクトを取得します。バイナリデータを出力するのはPrintWriterオブジェクトではなく、ServletOutputStreamオブジェクトです。 そして、これを取得するのはgetWriterメソッドではなく、getOutputStreamメソッドです。


図解: バイナリオブジェクトの取得

ただし、1回の出力では文字データ用のgetWriterか、バイナリデータ用のgetOutputStreamかどちらか一方しか使えません(両方出すとIllegalStateException例外がスローされます)。

バイナリデータの出力はServletOutputStreamが継承しているOutputStreamクラスのwriteメソッドを使用します。


図解: バイナリデータの出力

以下はsample.jpgというイメージファイルを読んで、そのまま出力するプログラムの例です。 ここではファイル読込の部分は詳しくは説明しませんが、 getResourceAsStreamメソッドでInputStreamのオブジェクトであるinを取得し、それをもとにBufferedInputStreamのオブジェクトbinを生成しています。詳細はJavaの参考書をご覧ください。


図解: バイナリデータの出力の例

Servletの起動から終了まで

前述のように、Servletは最初のリクエスト時にロードされ起動されると、以後Servletコンテナがアプリを破棄するまでメモリに常駐し続けます。 このロードから破棄の過程で、コンテナは次のようなメソッドを呼び出します。アプリはこれらのメソッドをオーバライド(再定義)して、Servletの機能を実現していきます。

  • initメソッド
    コンテナはアプリが初めてロードされたときに一度だけinitメソッドを呼び出します。アプリはこのメソッドをオーバライドして、カウンタの初期値の設定などいわゆる初期化の処理を記述します。 アプリの初期化の処理がない場合はオーバライドする必要はありません。
  • serviceメソッド
    コンテナはクライアントからのリクエストがあるたびにserviceメソッドを呼び出します。 アプリがこのメソッドをオーバライドしていると、その処理が実行されますが、オーバライドしていないと、serviceメソッドはリクエストの種類に応じてdoXXXを呼び出します。 通常、アプリはserviceはオーバライドせずに、doXXXの方をオーバライドして、Servletに行わせたい処理を記述します。
  • destroyメソッド
    コンテナはアプリを破棄する時にdestroyメソッドを呼び出します。アプリはこのメソッドをオーバライドしてカウンタのセーブなど終了時の処理を記述します。 アプリの終了時の処理がない場合はオーバライドする必要はありません。

Servletのライフサイクル

Servletのライフサイクル

Servletアプリのライフサイクルをまとめると次のようになります。
  1. アンロード状態にあるアプリに対しクライアントからのリクエストがあると、コンテナはそのアプリをメモリ上にロードします。 ロードされたアプリ内で定義されたinitメソッドが実行され、ロード状態になります。 アプリがinitメソッドをオーバライドしていなければ、コンテナで用意した初期化処理だけが行われます。
  2. serviceメソッドが実行されます。 serviceメソッドがオーバライドされていなければ、リクエストの種類に応じてdoXXXメソッドが実行されます。 処理が終了してもそのアプリはメモリ上に残ったままで、再度リクエストが来ると、またserviceメソッドが実行されます。 状態はロード状態のままです。
  3. Servletコンテナの終了時にdestroyメソッドが実行され、アンロード状態に戻ります。 アプリがこのメソッドをオーバライドしていなければ何も実行されません。

リクエストヘッダーの取得

HTTPリクエストはリクエスト行とヘッダー部、およびボディ部からなっているのでした。このヘッダー部の情報を取得するのがgetHeaderメソッドです。このメソッドの仲間には以下の3種があります。

String getHeader(String headerName)
   headerNameで指定されたヘッダー項目の情報を1つだけ返す

Enumeration getHeaders(String headerName)
   headerNameで指定されたヘッダー項目の情報を列挙オブジェクトとして返す

Enumeration getHeaderNames()
   ヘッダー項目の名前を列挙オブジェクトとして返す

以下にgetHeaderの使い方の例を示します。以下の例で指定しているacceptはHTTPのヘッダー項目の1つで、クライアント側で受け入れ可能なコンテントタイプをMIME形式で示すものです。 HTTPヘッダー項目の種類については以下を参照してください。
http://www.atmarkit.co.jp/fnetwork/rensai/netpro01/header-fields.html


図解: getHeader

次はヘッダーの各項目の名前が判らない場合に、getHeaderNamesメソッドを使ってヘッダー情報を取得するソースの例です。


図解: getHeaderNamesによるヘッダー情報の取得

結果は以下のようになりました。 この内容は使っているブラウザによって異なります。


図解: ヘッダー情報の取得結果

確認問題

確認問題に答えていただくにあたって、メソッドの記述の仕方を説明します。
Aと言うクラス(またはインターフェース)のBと言うメソッドを示すときは、区切りに # を使ってA#Bと表します。 A.Bのように区切りに .(ピリオド)を使っても良いのですが、こうするとAがクラス名ではなくそのオブジェクトのように見えるのでクラス(またはインターフェース)名のときは # を使うこととします。
では問題です。

(1) HTTPのPOSTメソッドで呼び出されるメソッドを仮引数も含めて書いてください。
(2) ユーザがHTML画面に入力した情報を取得するにはどんなメソッドを使いますか。
(3) HTML画面の出力に使うPrintWriterクラスのオブジェクトを取得するメソッドは何ですか。
(4) アプリのロード時、終了時に呼ばれるメソッドはそれぞれ何ですか。
(5) クライアントからのリクエストのたびに呼ばれるメソッドは何ですか。
またそのメソッドがアプリでオーバライドされないときに呼ばれる、HTTPリクエストの種類に対応したメソッドを2つ挙げてください
(6) HTTPリクエストのヘッダーを取得するメソッドは何ですか。

解答

(1) doPost(HttpServletRequest req, HtteServletResponse res)
(2) ServletRequest#getParameterメソッド。 単にgetParameter、またはreq.getParameterでも正解です
(3) ServletResponse#getWriterメソッド。 単にgetWriter、またはres.getWriterでも正解です
(4) initメソッドとdestroyメソッド
(5) serviceメソッド、doGetメソッドとdoPostメソッド
(6) HttpServletRequest#getHeaderメソッド。 単にgetHeader、またはreq.getHeaderでも正解です

コラム(継承)

ここでは継承の働きを簡単に説明します。

図解: 継承

図をご覧ください。クラスAとBとがあります。Aにはメソッド1が、Bにはメソッド2が定義されています。そしてextends Aで示されるようにBはAを継承しています。

クラスA内のプログラム(これは図には書かれていません)は自クラスのメソッド1を呼び出すことができます。同様にクラスB内のプログラムは自クラスのメソッド2を呼び出すことができます。ここまではアタリマエですね。

しかしアタリマエでないことがあります。それは、クラスB内のプログラムがメソッド1も呼び出せることです。これがAを継承した効果です。このように継承することで元のクラスが持つ(メソッド1などの)機能をそっくり引き継いで、さらに(メソッド2などを定義することで)クラスの機能を拡張することができるのです。

前述のサンプルソースに当てはめるとAはコンテナが用意したHttpServletクラスです。アプリがこれを継承してクラスBすなわちGreetingServletクラスを作ることで、Aで定義されているメソッドを自由に使うことができます。

ここでは説明を簡単にするために可視性(アクセス修飾子)については無視しました。さらに詳しく知りたい方は同著者の以下の記事をご覧ください。
Web開発のためのJava入門 7章:クラスの継承

ページの先頭へ