自分にとって覚えにくい事柄であるJSPカスタムタグのことを備忘録的に残して、試験対策を行う。
JSPの標準で<jsp:useBean><jsp:getProperty>などのタグがサポートされていますが、
その他、独自にタグを定義する事ができます。これのことをカスタムタグと呼びます。
標準タグライブラリである JSTL(JavaServer Pages Standard Tag Libirary)と
その実装であるJakarta Taglibsもある意味、カスタムタグではあるのですが、
分類上は、カスタムタグとはいいません。
カスタムタグを使用する際の文法は以下の通りです。
タグボディが無い場合
<(接頭辞):(カスタムタグ名) (属性1)="..." (属性2)="..." ... />
タグボディがある場合
<(接頭辞):(カスタムタグ名) (属性1)="..." (属性2)="..." ... > ボディ部 </(接頭辞):(カスタムタグ名)>
たとえば、接頭辞が「myTag」で、カスタムタグ名が「time」の場合、
<myTag:time />
のようになります。
カスタムタグを使用するには幾つかのことを行う必要性があります。
以上のことを行うことによりカスタムタグを利用できるようになります。
タグライブラリを構成する個々のカスタムタグの実体は、タグハンドラと呼ばれるJavaクラスです。
タグハンドラは、大きく分けて2種類に分類することができます。
JSP1.2以前のAPIを用いて作成されたタグハンドラのことをクラッシックタグハンドラと呼んでいます。
クラッシックタグハンドラは、javax.servlet.jsp.tagextパッケージで提供されるTagインタフェース、
IterationTagインタフェース、BodyTagインタフェースのいずれかを実装して作成されたタグハンドラになります。
JSP2.0において、javax.servlet.jsp.tagextパッケージに新たに追加されたSimpleTagインタフェースを実装して作成された
タグハンドラを、シンプルタグハンドラと呼んでいます。
以上のことをまとめると、以下の図のようになります。

最上位のインタフェースであるJspTagインタフェースは、クラッシクタグハンドラとシンプルタグハンドラを
統合する意味で導入されたインタフェースで、メソッドや定数は一切定義されていません。
従来からのクラッシクタグハンドラをつかって、カスタムタグを構築する開発者にとって重要なメソッドは、
の3つです。
doStartTag()、doEndTag()は、Tagインタフェースに定義されているメソッドで、
それぞれカスタムタグの「開始タグ」「終了タグ」に対応して呼び出されます。

doAfterBody()は、IterationTagインタフェースで定義されているメソッドで、タグボディの評価の後に呼び出されるインタフェースです。
JSPページに書かれたカスタムタグは、コンテナ(たとえば、Tomcat)によって、
タグハンドラを呼び出すコードに変換されて実行されます。
ご存じのとおり、JSPページそのものは、コンテナ(たとえば、Tomcat)によってServletに変換されます。
カスタムタグが存在する場合、タグハンドラを呼び出すコードに変換されるわけです。
以降の説明については、とても複雑なので、随時、注釈を入れて理解をしていきます。
また、JaJakartaプロジェクトのTagのJavadocの図を見ながら進めると非常に判り易かったので、
それもみて理解すべきでしょう。(下図、参照)

http://www.jajakarta.org/tomcat/servletapi/servletapi-4.0/docs-ja/javax/servlet/jsp/tagext/Tag.html
実行時、JSPページの中に記述されたカスタムタグのハンドラとなるクラスをインスタンス化するのは、コンテナ(たとえばTomcat)です。
JavadocのINIT PROTOCOLの図上では、new()という部分になります。
newされてインスタンスが生成されました。
newされたインスタンスは、あるデフォルト値をプロパティとして持ったインスタンスです。
(Properties have Dafault values)と書かれている部分がそれです。
クラスインスタンスが作成された時、フィールドには何かしらの初期値を持っているはずです。
一般的なJavaBeanを想像してください。フィールドに初期値を持たせて宣言しているはずです。
その後、コンテナ(たとえばTomcat)は、setPageContextメソッドを呼び出し、PageContextオブジェクトをセットします。
これによりクラスインスタンスは、コンテナ(たとえばTomcat)がセットしてくれたPageContexオブジェクトにアクセスすることができるようになります。
→フィールドとして、PageContextオブジェクトを持たなければなりません。
また、親タグが存在する場合には、コンテナ(たとえばTomcat)がsetParent()メソッドを呼び出し、親タグをセットします。
これによりクラスインスタンスは、コンテナ(たとえばTomcat)がセットしてくれたTagオブジェクトにアクセスすることができるようになります。
→フィールドとして、PageContextオブジェクトを持たなければなりません。
さらにタグに属性が定義されている場合には、その属性に対応するsetterメソッドを呼び出して、ハンドラーオブジェクトのフィールドを初期化します。
ここまでのことは、INIT PROTOCOLの図の中では、 setPageContext()、setParent()、setters()と書かれていいる部分がそれです。
下の方に、サンプルプログラムが書いてあるのですが、プロパティとして、pageContext、parentTagがあるのに
着目してください。
<helloTag name="xxx" state="xxx">Body</helloTag>
こういったカスタムタグが欲しい場合、
フィールド(プロパティ)には、
PageContex pageContext;
Tag parentTag = null;
String name = "";
String state = "";
のようなフィールドがあって、コンテナ(たとえばTomcat)が自動的にJavaBeanオブジェクト(タグハンドラ)のオブジェクトを作ってくれるわけです。
コンテナ(たとえばTomcat)が、>(タグの閉じ記号)を検出したら、doStartTagメソッドが呼び出してくれます。
doStartTagメソッドが、コンテナ(たとえばTomcat)に呼び出された際には、
属性に対応するsetter()、pageContext、parentTagに関する情報は、
コンテナ(たとえばTomcat)よって、フィールドに書きこまれているので、あとはそれを利用するたけです。
このdoStartTagが返す戻り値によって、コンテナ(たとえばTomcat)がタグボディを評価するか、
タグボティを評価せずにdoEndTagメソッドを呼び出すのかが決定されます。
タグボディを評価する場合には、タグボディの評価結果を新しく作成した出力バッファに追加した後、doAfterBody()メソッドを呼び出します。
doAfterBody()メソッドは、出力バッファに書きだされた内容を読むことができるので、あとはそれを処理するだけです。
ここでも、doAfterBody()メソッドの戻り値によって、コンテナ(たとえばTomcat)が、
再度タグボディを評価するか、もしくはdoEndtagメソッドを呼び出すかどうかが決定されます。
カスタムタグによる処理が終了すると、コンテナ(たとえばTomcat)は、
release()メソッドを呼び出して、フィールド値をクリアにします。
| 条件 | 実装 |
| タグボディのデータを操作しないクラッシックタグハンドラ | Tagインタフェースを実装 |
| IterationTagインタフェースを実装 | |
| タグボディのデータを操作するクラッシクタグハンドラ | BodyTagインタフェースを実装 |
だんだんと辻褄があって、理解出来てきました。
doStartTag()、doAfterBody()、doEndTag()の3つのライフサイクルメソッドは、int型を返すメソッドです。
これらのメソッドから返される値に応じて、コンテナ(たとえばTomcat)は次にどのような処理をすれば良いのかという振る舞いを決定します。
| メソッド名 | 戻り値 | 振る舞い |
| doStart()メソッド | SKIP_BODY | タグボディ部は評価しない |
| EVAL_BODY_INCLUDE | タグボディを評価する | |
| EVAL_BODY_BUFFERED | 新しいバッファを作成、タグボディは、タグハンドラで処理する | |
| doAfterBody()メソッド | SKIP_BODY | タグボディを再評価しない |
| EVAL_BODY_AGAIN | タグボディを再評価する。 | |
| doEndTag()メソッド | SKIP_PAGE | ページの残り部分を評価しない |
| EVAL_PAGE | ページの残り部分を評価する |
EVAL_BODY_BUFFERD を返却するようにカスタムタグを作った場合、コンテナ(たとえばTomcat)は、
setBodyContent()メソッドとdoInitBody()メソッドを呼び出して、出力バッファを新しく作成します。
EVAL_BODY_INCLUDEを返却するようにカスタムタグを作った場合、コンテナ(たとえばTomcat)は、画面上にそのままタグを出力します。
SKIP_BODYを返却するようにカスタムタグを作った場合、コンテナ(たとえばTomcat)は、タグボディ部を無視します。(たとえ、記述があったとしても)
doAfterBody()メソッドは、タグボディが評価された後に呼び出されます。
このメソッドがSKIP_BODYを返却するようにカスタムタグを作成した場合、タグボディの再評価は実施されず終了タグの評価に移行します。
EVAL_BODY_AGAINを返すようにカスタムタグを作成した場合、タグボディを再評価します。
いつかは、SKIP_BODYを返すように実装しないと、無限ループのようにボディの繰り返しを再評価しづづけるため、注意が必要です。
doEndTag()メソッドは、終了タグの検出時に呼び出されます。
このメソッドが、SKIP_PAGEを返却するようにカスタムタグを作成した場合、ページの残りの部分は評価されません。(書かれていても無視される。)
このメソッドが、EVAL_PAGEを返却するようにカスタムタグを作成した場合、ページの残りの部分を評価します。
「ボディ部を持たない」、もっとも簡単なカスタムタグハンドラを作ってみます。
最初に作成するカスタムタグは、「hello」という文字列を出力するだけのカスタムタグを作ります。
JSPコードの中では、次のように使用します。
<(接頭辞):hello />
package com.techch.tagsample;
import java.io.IOException;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.Tag;
/**
* HelloTagクラス.
*
* @author YS
*/
public class HelloTag implements Tag {
/** ページコンテキスト */
private PageContext pageContext = null; // ①
/** 親タグ */
private Tag parentTag = null; // ②
/* (非 Javadoc)
* @see javax.servlet.jsp.tagext.Tag#doEndTag()
*/
@Override
public int doEndTag() throws JspException {
try {
pageContext.getOut().print("hello"); // ③
} catch (IOException e) {
throw new JspException(e.getMessage()); // ④
}
return EVAL_PAGE; // ⑤
}
/* (非 Javadoc)
* @see javax.servlet.jsp.tagext.Tag#doStartTag()
*/
@Override
public int doStartTag() throws JspException {
return SKIP_BODY; // ⑥
}
/* (非 Javadoc)
* @see javax.servlet.jsp.tagext.Tag#getParent()
*/
@Override
public Tag getParent() {
return parentTag;
}
/* (非 Javadoc)
* @see javax.servlet.jsp.tagext.Tag#release()
*/
@Override
public void release() {
}
/* (非 Javadoc)
* @see javax.servlet.jsp.tagext.Tag#setPageContext(javax.servlet.jsp.PageContext)
*/
@Override
public void setPageContext(PageContext pageContext) {
this.pageContext = pageContext;
}
/* (非 Javadoc)
* @see javax.servlet.jsp.tagext.Tag#setParent(javax.servlet.jsp.tagext.Tag)
*/
@Override
public void setParent(Tag parentTag) {
this.parentTag = parentTag;
}
}
Tagインタフェースで定義されているメソッドは以下の通りになります。
| No | メソッド | 意味 |
| 1 | doEndTag() | タグの終了時に呼び出されるメソッド。返り値はタグの後に続くJSPページを評価して実行するかどうかを指定する。「Tag.EVAL_PAGE」の場合は評価し、「Tag.SKIP_PAGE」の場合は評価しない。 |
| 2 | doStartTag() | タグの開始時に呼び出されるメソッド。返り値はタグのボディ部を評価して実行するかどうかを指定する。「Tag.EVAL_BODY_INCLUDE」の場合は評価し、「Tag.SKIP_BODY」の場合は評価しない。 |
| 3 | getParent() | タグを囲んでいる親タグを返す。 |
| 4 | release() | タグで設定されたリソースを解放する。 |
| 5 | setPageContext(PageContext pageContext) | JSPページで使用する様々なリソース・データ等を管理する「javax.servlet.jsp.PageContext」を設定する。 |
| 6 | setParent() | タグを囲んでいる親タグを設定する。 |
ソースコードを添付したので、ダウンロードしながら見ると判りやすいです。
①にて、クラスメンバ変数として「ページコンテキスト」を定義しています。コンテナがセットしれくれるPageContextオブジェクトの入れ先です。まあ、おまじない的なものとして考えて良いです。
②にて、クラスメンバ変数として、「親タグ」を定義しています。コンテナがセットしれくれる親タグ(Tag)オブジェクトです。これもまた、おまじない的なものとして考えて良いです。
③の部分は、doEndTag()メソッド内の部分ですから、タグの終了時にコンテナから呼び出されるメソッドです。
JSPページへの出力は、PageContextのgetOut()メソッドで取り出される「javax.servlet.jsp.JspWriter」インスタンスを利用します。
このクラスは「java.io.PrintWriter」とほぼ同じメソッドを提供していますので、サーブレットでの出力処理と同じように行う事ができます。
④の部分は、例外の置き換えです。doEndTag()メソッドは、JspExceptionしかスローすることができません。
一方、JspWriter.print()メソッドは、IOExceptionをスローします。
このため、例外をラッピングしなおして、スローする必要があります。
⑤のreturn文は、タグの後に続くJSPページを評価するかどうかを返却します。
通常、'Tag.EVAL_PAGE'を返却して、以降のJSPページも評価を実施します。
⑥のreturn文は、doStart()メソッドの定義です。
このメソッドは、タグの開始時にコンテナから呼び出されるメソッドです。
今回作るのは、ボディ部を持たないタグハンドラですので、'Tag.SKIP_BODY'を返却します。
残りのメソッドである'getParent()'メソッド、'release()'メソッド、'setPageContext()'メソッド、'setParent()'メソッドは、
一般的に、このように記載します。
続いてタグハンドラディスクリプタ(タグの設定ファイル、TLDファイル)を作成します。
タグハンドラディスクリプタ(以降、TLDファイル)は、
カスタムタグを定義し、そのタグをタグハンドラクラスに結び付けるためのXML形式の定義ファイルです。
TLDファイルのルート要素は、<taglib>であり、子要素<tag>を用いてタグの定義を記述します。
具体的には、<name>要素に任意のタグ名を定義し、そのタグに対応したハンドラクラスの
完全修飾名を<tag-class>要素に記述します。
<tag>
<name>タグの名前</name>
<tag-class>ハンドラクラスの完全修飾名</tag-class>
<body-content>タグボディの内容タイプ</body-content>
</tag>
<body-content>要素には、タグボディに指定可能な内容タイプを定義します。
この要素に指定できるのは、以下の4つの値のいずれかになります。
| No | 値 | 意味 |
| 1 | JSP | JSPコード |
| 2 | scriptless | テンプレートテキスト、EL式、JSPアクション(デフォルト) |
| 3 | tagdependent | テンプレートテキスト(タグボディの内容はタグハンドラによって処理される) |
| 4 | empty | 空(タグボディを持たない) |
設定ファイルには、以下のようなおまじないを必ずつけます。
<?xml version="1.0" encoding="UTF-8" ?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">
次のように書くように解説しているサイトがあります。
間違いでは無いのですが、DTDを使った古い書き方です。JSP2.0では、XMLSchemaで書きます。
【古い書き方】
<?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN" "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">
TLDファイルの形式については、
を参照してください。
参照してくれると判りますが、必須項目とオプション項目というのがあります。
<taglib>要素には、以下の子要素が必須になります。
| No | 要素名 | 説明 |
| 1 | <tlib-version> | このタグライブラリが機能するJSPのバージョン。必須。 |
| 2 | <short-name> | 省略名。必須 |
残りの要素は必須では無いのですが、<tag>要素が無いと定義している意味がありません。
<tag>要素の子要素の必須は以下になります。
| No | 要素名 | 説明 |
| 1 | <name> | タグ名。必須 |
| 2 | <tag-class> | タグハンドラクラスの完全修飾名。必須 |
| 3 | <body-content> | タグボディの内容に関する定義。必須 |
というわけで、必要最低限のTLDファイルは以下のようになります。
<?xml version="1.0" encoding="UTF-8" ?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">
<tlib-version>1.1</tlib-version>
<short-name>myTag</short-name>
<tag>
<name>hello</name>
<tag-class>com.techch.tagsample.HelloTag</tag-class>
<body-content>empty</body-content>
</tag>
</taglib>
このファイルを、WEB-INF/ または、WEB-INF/tldフォルダ内に配置します。