はじめに

本記事では、Javaファイルを書かないJSPカスタムタグの作り方について説明します。「JSPの共通コンポーネントってどうやって作るんだ? 」「作り方調べてみたけど難しそう」こんなことはありませんでしたか? この記事を読めば「意外と簡単だな」と思ってもらえると思います。

私が参画したプロジェクトで得た知見をもとに、そもそもJSPカスタムタグとは何なのか、どのように作成・使用するのかについて記載します。またJSPのバージョンは3.1を想定しています。

JSPカスタムタグとは

JSPカスタムタグとは独自のロジック・出力を持つタグを作成できる機能であり、これによりJSPの処理を共通化できます。つまりJSPの共通コンポーネントを作成することができるということです。

共通コンポーネント化には以下のメリットが挙げられます。

  • 開発生産性向上:同じコードを何度も書く必要がなくなります。
  • デザイン一貫性の向上:カスタムタグ側でスタイルを指定することで、一貫したデザインを保つことができます。
  • 保守運用性の向上:仕様変更やバグ修正を行う際に影響範囲を特定しやすいです。

カスタムタグの作成・使用方法

カスタムタグの作成方法には、Javaクラスによる方法とタグファイルによる方法の2種類あります。

前者のJavaクラスによる方法は、複雑なロジックが実装しやすい反面、Javaの知識が必要であり実装量も多くなりがちです。またインターネットでカスタムタグの作成方法について調べるとこの方法が多く見つかります。これが「カスタムタグって難しそう」と思われる原因の一つだと私は考えています。

本記事では後者のタグファイルによる方法について説明していきます。タグファイルによる方法はいたってシンプルです。タグファイルを作ります。完成です。Javaファイルや設定用のxmlファイルは必要ありません。

ではタグファイルの中身はどう書いていくのか、作成したタグファイルはどのように使用するのかについて、具体例を交えて説明していきます。説明にあたり、以下のシンプルなログイン画面をJSPで作成することを考えます。

また作成するカスタムタグは以下2点です。

  • 入力フィールドを表すカスタムタグ
  • 入力フィールドにラベルを付与するカスタムタグ

コード例

ここでは入力フィールドにラベルを付与するカスタムタグとそれを使用しているJSPの一部を例に説明していきます。またソースコードは本記事の最後に記載しますので参考にしてください。

入力フィールドにラベルを付与するカスタムタグは以下のようになります。

<!-- (1) タグファイルであることの宣言 -->
<%@ tag language="java" pageEncoding="UTF-8"%>
<!-- (2) 使用するカスタムタグライブラリの指定 -->
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<!-- 属性 -->
<!-- (3-4) 属性の共通化 -->
<%@ include file="commonAttributes.tag"%>
<!-- (3) 属性の宣言 -->
<%@ attribute name="label" required="true" description="The label for the field" %>
<%@ attribute name="required" required="false" description="Whether the field is required" %>
<%@ attribute name="labelId" required="false" description="The id of the label" %>
<%@ attribute name="labelCssClass" required="false" description="The css class of the label" %>

<!-- (4) 本体処理 -->
<div
  id="<c:out value='${id}'/>"
  class="field <c:out value='${cssClass}'/>"
>
  <label
    id="<c:out value='${labelId}'/>"
    class="field-label <c:out value='${labelCssClass}'/>"
  >
    <c:out value='${label}'/>
    <c:if test='${required}'>
      <span class="required">*</span>
    </c:if>
  </label>
  <jsp:doBody/>
</div>

JSPは以下のようになります。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!-- (2) 使用するカスタムタグライブラリの指定 -->
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<%@ taglib prefix="custom" tagdir="/WEB-INF/tags"%>

<!DOCTYPE html>
<html>
<head>
  <!-- 省略 -->
</head>
<body>
  <h1>ログイン画面</h1>
  <form action="/login" method="post">
    <!-- (5) カスタムタグの使用 -->
    <custom:fieldWithLabel label="ユーザーID" required="true">
      <custom:text name="userId" maxlength="10" />
    </custom:fieldWithLabel>
    <custom:fieldWithLabel label="パスワード" required="true">
      <custom:text name="password" maxlength="10" />
    </custom:fieldWithLabel>
    <button type="submit">ログイン</button>
  </form>
</body>
</html>

(1) タグファイルであることの宣言

まずはじめに、このファイルがタグファイルであることを宣言します。tagディレクティブを宣言することで実現できます。

<%@ tag language="java" pageEncoding="UTF-8"%>

tagディレクティブには以下のプロパティがあります。

プロパティ 概要
language スクリプト言語。JSPではJavaが唯一のスクリプト言語であるためJavaを指定。
pageEncoding タグファイルの文字コード。

(2) 使用するカスタムタグライブラリを指定する

カスタムタグライブラリを使用したい場合、taglibディレクティブを宣言することで実現できます。

カスタムタグライブラリはフレームワーク等で配布されているライブラリと自作のカスタムタグライブラリの2種類に分けられます。

配布されているライブラリを使用する場合はuriプロパティを指定します。自作のカスタムタグライブラリの場合はtagdirプロパティにタグファイルを格納しているディレクトリを指定します。

<!-- 配布されているライブラリ(ここではJSTLのコアタグライブラリを例としている) -->
<%@ taglib prefix="c" uri="jakarta.tags.core"%>

<!-- 自作のカスタムタグライブラリ -->
<%@ taglib prefix="custom" tagdir="/WEB-INF/tags"%>

taglibディレクティブには以下のプロパティがあります。

プロパティ 概要
prefix カスタムタグを利用する場合のプレフィックス。
uri タグライブラリを指定するためのURI。
tagdir タグファイルが格納されているディレクトリ。タグファイルで実現されるカスタムタグを利用する場合にuriプロパティの代わりに指定。

(3) 属性の宣言と指定方法

カスタムタグが受け取る属性を宣言します。属性には以下3種類があります。それぞれの宣言方法とタグファイル側での使用方法、またカスタムタグを呼び出すJSP側での指定方法について説明します。

  1. 単純属性
  2. フラグメント属性
  3. 動的属性

(3-1) 単純属性

単純属性は文字列や数字など静的な値をカスタムタグに引き渡すための機能です。単純属性はattributeディレクティブを利用して宣言できます。

タグファイル側での単純属性の使用はEL式および<c:out>を使って行います。

<!-- 宣言 -->
<%@ attribute name="label" required="true" description="The label for the field" %>

<!-- 使用 -->
<label>
  <c:out value='${label}'/>
</label>

またJSP側で指定する場合は以下のように属性名と指定する値を記載します。

<custom:fieldWithLabel label="ユーザーID">

attributeディレクティブには以下のプロパティがあります。

プロパティ 概要
name 属性名。
required 必須項目であるか。
description 属性の説明。
fragment フラグメント属性であるか。デフォルトはfalseなので単純属性の場合は未設定。

(3-2) フラグメント属性

フラグメント属性は動的なJSP要素をカスタムタグに引き渡すための機能です。フラグメント属性の宣言方法は2種類あります。

attributeディレクティブを利用

一つは単純属性と同じようにattributeディレクティブを利用する方法です。ただしfragmentプロパティにはtrueを指定します。

タグファイル側で使用するにはjsp:invokeを記載し、fragmentプロパティに属性名を指定します。

<!-- 宣言 -->
<%@ attribute name="field" required="true" description="The field" fragment="true" %>

<!-- 使用 -->
<jsp:invoke fragment="field"/>

またJSP側でフラグメント属性を指定する場合は以下のようにjsp:attributeを記載して指定します。

<custom:fieldWithLabel label="ユーザーID" required="true">
  <jsp:attribute name="field">
    <custom:text name="userId" maxlength="10" />
  </jsp:attribute name="field">
</custom:fieldWithLabel>
ボディ部として指定

もう一つはカスタムタグ使用時にボディ部に指定されたJSP要素をカスタムタグ内で使用する方法です。フラグメント属性が1つのみである場合のみ使用できます。

属性を宣言する必要はありません。タグファイル側で使用するにはjsp:doBodyを記載します。

<!-- 宣言は不要 -->

<!-- 使用 -->
<jsp:doBody/>

またJSP側では以下のような記載になります。

<custom:fieldWithLabel label="ユーザーID" required="true">
  <custom:text name="userId" maxlength="10" />
</custom:fieldWithLabel>

(3-3) 動的属性

動的属性は事前に定義されていない属性をカスタムタグに引き渡すための機能です。これによりattributeディレクティブで宣言していない属性も使用することができます。また動的属性として指定できる値は単純な文字列のみであり、フラグメント属性のように動的なJSP要素を指定することはできません。

動的属性はtagディレクティブのdynamic-attributesプロパティを利用して宣言します。動的属性として指定された値はdynamic-attributesプロパティに指定された名前でMap形式で保持されます。

タグファイル側で使用するにはc:forEach等を利用して使用できます。

<!-- 宣言 -->
<%@ tag dynamic-attributes="attrMap" %>

<!-- 使用 -->
<ul>
  <c:forEach var="entry" items="${attrMap}">
    <li>${entry.key}: ${entry.value}</li>
  </c:forEach>
</ul>

またJSP側では単純属性と同じように指定できます。

<custom:company name="TIS" location="Tokyo"/>

(3-4) 属性の共通化

カスタムタグを作成するにあたって、複数のカスタムタグに共通の属性がある場合、別タグファイルに切り出すことができます。

共通属性を管理するタグファイルにはattributeディレクティブのみを記載します。共通属性を使用するにはincludeディレクティブを使用し、fileプロパティに共通属性を管理するタグファイルへのパスを指定することで実現できます。

<%@ include file="commonAttributes.tag"%>

(4) 本体処理

本体処理には出力したいJSPを記載します。HTML要素、他のカスタムタグ、属性を使用して組み立てます。

<!-- 本体処理 -->
<div>
  <label
    id="<c:out value='${labelId}'/>"
    class="field-label <c:out value='${labelCssClass}'/>"
  >
    <c:out value='${label}'/>
    <c:if test='${required}'>
      <span class="required">*</span>
    </c:if>
  </label>
  <jsp:doBody/>
</div>

(5) カスタムタグを使用する

自作したカスタムタグの使用方法はフレームワーク等で配布されているライブラリと違いはなく、taglibディレクティブで指定したプレフィックスを指定して使用します。

<custom:fieldWithLabel label="ユーザーID" required="true">
  <custom:text name="userId" maxlength="10" />
</custom:fieldWithLabel>

タグファイル作成のポイント

タグファイルの作成・使用方法について理解が深まったかと思います。

ここでは、私がタグファイル作成を実践してみてポイントだと考えたことを3つご紹介します。

任意属性は未指定の場合に注意する

任意属性を出力する場合、未指定の場合は空文字が出力されます。つまりid=""のようになるわけです。ただし属性によっては空文字が指定されることで不都合が生じる場合があります。例に挙げたid属性では空文字が指定されると画面表示は問題ないですが、コンソールでid属性が一意でない旨のwarningが発生してしまいます。

解決策としては未指定の場合は属性の宣言自体行わないように制御することが挙げられます。具体的には以下のようにid属性の宣言を<c:if>で囲むことで実現できます。

<input
  type="text"
  <c:if test='${not empty id}'>id="<c:out value='${id}'/>"</c:if>
  name="<c:out value='${name}'/>"
  maxlength="<c:out value='${maxlength}'/>"
  class="text-field <c:out value='${cssClass}'/>"
/>

画面テンプレートタグを作成する

画面開発にあたり全画面で共通してくる要素がある場合がほとんどです。例えばheadタグに記載するCSSやJavaScriptの設定、画面見出しのスタイル等です。これらを記載した画面テンプレートタグを作成することで、各画面のJSPに記載する必要がなくなり実装が簡単になります。

画面テンプレートタグは以下のようになります。

<%@ tag language="java" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<!-- 属性 -->
<%@ attribute name="pageId" required="true" description="The id of the page" %>
<%@ attribute name="pageHeader" required="true" description="The page header" %>

<!-- 本体処理 -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8"/>
  <title>サンプルシステム</title>
  <!-- 共通のCSSとJavaScript -->
  <link rel="stylesheet" type="text/css" href="/css/common.css"/>
  <link rel="javascript" type="text/javascript" href="/js/common.js"/>
  <!-- ページ固有のCSSとJavaScript -->
  <link rel="stylesheet" type="text/css" href="/css/<c:out value='${pageId}'/>.css"/>
  <link rel="javascript" type="text/javascript" href="/js/<c:out value='${pageId}'/>.js"/>
</head>
<body>
  <h1 class="page-header"><c:out value="${pageHeader}"/></h1>
  <jsp:doBody/>
</body>
</html>

テンプレートタグを使用したJSPは以下のようになります。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<%@ taglib prefix="custom" tagdir="/WEB-INF/tags"%>

<custom:template pageId="sample" pageHeader="ログイン画面">
  <form action="/login" method="post">
    <custom:fieldWithLabel label="ユーザーID" required="true">
      <custom:text name="userId" maxlength="10" />
    </custom:fieldWithLabel>
    <custom:fieldWithLabel label="パスワード" required="true">
      <custom:text name="password" maxlength="10" />
    </custom:fieldWithLabel>
    <button type="submit">ログイン</button>
  </form>
</custom:template>

カスタムタグ側でエスケープ処理をする

単純属性の使用で、属性の設定値の出力には<c:out>を使うことを説明しました。そうすることで属性に指定された値がエスケープされてから出力されるようになります。これはクロスサイトスクリプティング(XSS)対策として必要な対応です。

このように実装することで、カスタムタグを利用している以上JSP側でエスケープする必要がなくなります。

おわりに

タグファイルを利用することでJSPカスタムタグが簡単に作成できることが伝わったかなと思います。JSP開発効率化の助けになれば幸いです。

ソースコード

最後に本記事で扱ったログイン画面を実現するためのソースコードについて記載します。

ディレクトリ構成は以下のようになります。pagesディレクトリにはJSPファイル、tagsディレクトリにはタグファイルを格納しました。

src
 `-- main
     `-- webapp
          `-- WEB-INF
              |-- pages
              |    `-- sample.jsp
               `- tags
                   |-- text.tag
                   |-- fieldWithLabel.tag
                    `- commonAttributes.tag
text.tag

<%@ tag language="java" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<!-- 属性 -->
<%@ include file="commonAttributes.tag"%>
<%@ attribute name="name" required="true" description="The name of the field" %>
<%@ attribute name="maxlength" required="true" description="The maximum length of the field" %>
<%@ attribute name="value" required="false" description="The value of the field" %>

<!-- 本体処理 -->
<input
  type="text"
  id="<c:out value='${id}'/>"
  name="<c:out value='${name}'/>"
  class="text <c:out value='${cssClass}'/>"
  maxlength="<c:out value='${maxlength}'/>"
  value="<c:out value='${value}'/>"
/>
fieldWithLabel.tag

<%@ tag language="java" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<!-- 属性 -->
<%@ include file="commonAttributes.tag"%>
<%@ attribute name="label" required="true" description="The label for the field" %>
<%@ attribute name="required" required="false" description="Whether the field is required" %>
<%@ attribute name="labelId" required="false" description="The id of the label" %>
<%@ attribute name="labelCssClass" required="false" description="The css class of the label" %>

<!-- 本体処理 -->
<div
  id="<c:out value='${id}'/>"
  class="field <c:out value='${cssClass}'/>"
>
  <label
    id="<c:out value='${labelId}'/>"
    class="field-label <c:out value='${labelCssClass}'/>"
  >
    <c:out value='${label}'/>
    <c:if test='${required}'>
      <span class="required">*</span>
    </c:if>
  </label>
  <jsp:doBody/>
</div>
commonAttributes.tag

<%@ attribute name="id" required="true" description="The id" %>
<%@ attribute name="cssClass" required="false" description="The css class" %>
sample.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<%@ taglib prefix="custom" tagdir="/WEB-INF/tags"%>

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8"/>
  <title>サンプルシステム</title>
  <!-- 共通のCSSとJavaScript -->
  <link rel="stylesheet" type="text/css" href="/css/common.css"/>
  <link rel="javascript" type="text/javascript" href="/js/common.js"/>
  <!-- ページ固有のCSSとJavaScript -->
  <link rel="stylesheet" type="text/css" href="/css/sample.css"/>
  <link rel="javascript" type="text/javascript" href="/js/sample.js"/>
</head>
<body>
  <h1>サンプルシステム</h1>
  <form action="/login" method="post">
    <custom:fieldWithLabel label="ユーザーID" required="true">
      <custom:text name="userId" maxlength="10" />
    </custom:fieldWithLabel>
    <custom:fieldWithLabel label="パスワード" required="true">
      <custom:text name="password" maxlength="10" />
    </custom:fieldWithLabel>
    <button type="submit">ログイン</button>
  </form>
</body>
</html>
出力されるHTML

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8"/>
  <title>サンプルシステム</title>
  <!-- 共通のCSSとJavaScript -->
  <link rel="stylesheet" type="text/css" href="/css/common.css"/>
  <link rel="javascript" type="text/javascript" href="/js/common.js"/>
  <!-- ページ固有のCSSとJavaScript -->
  <link rel="stylesheet" type="text/css" href="/css/sample.css"/>
  <link rel="javascript" type="text/javascript" href="/js/sample.js"/>
</head>
<body>
  <h1>サンプルシステム</h1>
  <form action="/login" method="post">
    <div id="" class="field">
      <label id="" class="field-label">ユーザーID<span class="required">*</span></label>
      <input type="text" id="" name="userId" class="text-field" maxlength="10" value=""/>
    </div>
    <div id="" class="field">
      <label id="" class="field-label">パスワード<span class="required">*</span></label>
      <input type="password" name="password" class="text-field" maxlength="10" value="" />
    </div>
    <button type="submit">ログイン</button>
  </form>
</body>
</html>