はじめに
JSP&サーブレットで web アプリケーションを作成する際に学習した事項を絞って覚書にしました。あまり触らなくなりましたが、基本的な部分を忘れかけていたので、ここに覚書として残しておきます。
最新の情報では無い場合があります。
Tomcat
Tomcat はオープンソースのサーブレットコンテナ。サーブレットコンテナとは、端的に言うと「JSP に対応したウェブサーバ」のこと。Tomcat では、サーバ上のプログラムを「アプリケーション」という単位で管理しており、一つのサーバの中に複数のアプリケーションを配備することができる。
アプリケーションフォルダの設定
- webapps フォルダに配置する方法
アプリケーション(WAR ファイルまたは展開されたフォルダ)を Tomcat の webapps ディレクトリに配置する。Tomcat は起動時にこのフォルダ内のアプリケーションを自動的に検出して展開する。 - 外部設定ファイルを使用する方法
Tomcat にアプリケーションフォルダの場所を設定するには、次のフォルダに設定ファイルを作ればよい。- フォルダ
Tomcat をインストールしたフォルダ> conf > Catalina > localhost - 設定ファイル
docBase
では、サーバ上でファイルが置かれているフォルダを指定する。絶対パスでない場合は、Tomcat の webapps フォルダからの相対パスになる。また、path
では、ブラウザからアクセスするときに使う URL 上のパスを指定する。URL は、「http://【ホスト名】/【path 名】/」となる。
xml:設定ファイルの例 <?xml version='1.0' encoding='utf-8'?> <Context docBase="C:\example" path="/example"/>
- フォルダ
WEB-INF フォルダ
アプリケーションフォルダの下には様々なフォルダを作ることができるが、WEB-INF という名前を付けると特別な意味を持つフォルダになる。一般に、このフォルダには、次のようなファイルやフォルダが配置される。
- web.xml
デプロイメントディスクリプタ - classes
jar 形式ではないクラスファイルを配置するフォルダ - lib
jar 形式のライブラリを配置するフォルダ
WEB-INF の中はブラウザから URL で直接アクセスすることはできない。なので、データファイルなどを置く場所に使ってもよい。
web.xml
web.xml はデプロイメントディスクリプタと呼ばれるファイルで、アプリケーションごとの設定を記述するものである。よく使われる設定の要素は次のとおり。なお、サーブレットの設定は、@WebServlet
アノテーションでもできる。
- welcome-file-list
- error-page
エラーが発生したときに表示するページを指定するもの。ページの指定はアプリケーションフォルダのルートからの相対パスになる。 - servlet
- servlet-mapping
- resource-ref
- security-constraint
- login-config
アプリケーションのリロード
アプリケーションの状態をリセット(リロード)する場合、次の URL にアクセスすればアプリケーションごとにリロードできる。
http://localhost/manager/reload?path=/【アプリケーション名】
停止させる場合はreload
の代わりにstop
、開始はstart
を指定する。
ここにアクセスし、Tomcat をインストールするときに指定したユーザ名とパスワードを入力すれば実行される。
サーブレットの初期化
サーブレットのpublic void init()
メソッドを定義すると、それがTomcat起動時に呼び出される。複数のサーブレットに定義されている場合は、@WebServlet
アノテーションの引数loadOnStartup
の値が小さい順に実行される(web.xmlでサーブレットの設定をしている場合は、web.xmlに記述することもできる)。
標準アクションタグ
JSP ページの中で使える特別な機能を持ったタグのこと。
<jsp:include>
ページ外のリソースを現在のページ位挿入するために用いる。
<jsp:include page="挿入リソースURL(相対URLで指定する)"/>
include ディレクティブでは、JSP ページに最初にアクセスがあった時(サーブレットに変換される時)に挿入処理が静的に行われる。一方で、<jsp:include>では、実行時に挿入処理が動的に行われる。よって、後者では挿入する側とされる側それぞれの出力結果が毎回結合されてクライアントに返される。
<jsp:useBean>
JavaBeans のオブジェクトを新規に作ったり、各スコープからとってきたりする。
<jsp:useBean id="オブジェクト名" [scope="オブジェクトのスコープ"] class="クラス名">
初期化コード
</jsp:useBean>
または、
<jsp:useBean id="オブジェクト名" [scope="オブジェクトのスコープ"] class="クラス名"/>
id 属性の値を参照変数としてスクリプト中で使う。class 属性の値は、FQCN で指定する。初期化コードは、オブジェクトが新規に作られた時のみ実行される。
<jsp:getProperty>
<jsp:useBean>などで作られたオブジェクトのプロパティを文字列へ変換してクライアントへ送信する。対象のプロパティにはゲッターが定義されていなければならない。戻り値がオブジェクトの場合は、その toString()メソッドが呼ばれる。
<jsp:getProperty name="オブジェクト名" property="プロパティ名"/>
<jsp:setProperty>
<jsp:useBean>などで作られたオブジェクトのプロパティを変更するためのもの。対象のプロパティにはセッターが定義されていなければならない。
<jsp:setProperty name="オブジェクト名" property="プロパティ名" value="セットする値"/>
value 属性の値は、一旦 String 型として解釈され、その後セッターの引数のデータ型に合わせて変換される(基本データ型、String 型、ラッピングクラス型が対象)。
<jsp:setProperty name="オブジェクト名" property="プロパティ名" param="フォームのパラメータ名"/>
param 属性を使ってフォームの name 属性で指定したパラメータ名を指定すると、フォームから送られてきた情報を使ってプロパティを変更することができる。パラメータ名とプロパティ名が等しい場合、省略可能。
<jsp:setProperty name="オブジェクト名" property="*"/>
プロパティ名と同じ名前のフォームから送られたパラメータがあった場合、それらすべてが各プロパティにセットされる。
EL
「式言語」とも呼ばれる。${}
で囲まれた式を評価し、その結果を出力する。演算式や評価式の結果を出力するために使用され、アクションタグに属性の値として与えることもできる。すでにスコープに格納されたオブジェクトを取り出す場合には、<jsp:useBean>タグで宣言無しに、次の例のように直接呼び出せる。各スコープを明示的に指定する場合は、暗黙オブジェクトを使って次の例のように呼び出す。スコープを明示しなければ、page、request、session、application の順で各スコープを検索する。
${ 5 }
${ 1 + 2 * 3}
${user.name}
${array[1]}
${pageScope.user.name}
${requestScope.user.name}
${sessionScope.user.name}
${applicationScope.user.name}
${user.age >= 20}
${((user.gender == 0) && (user.age >= 20)) || (user.gender != 0)}
${(100 < 10) ? "Yes" : "No"}
タグライブラリ(主に JSTL について)
スクリプトの代わりに処理を行うことができるタグを種類ごとにまとめたもの。JSTL では、プレゼンテーションロジックの実装に必要な、反復、条件分岐のような頻繁に使用する機能を共通化し、標準的に使えるようにしてある。
EL と組み合わせることにより、JSP ページの中から極力スクリプトレットを排除し、コードを見やすくすることができる。
JSTL のアクションタグの対象は、スコープに格納されたオブジェクトである。
利用のための設定
- カスタムタグの定義が入っている jar ファイルを入手して、「webapp のルートディレクトリ/WEB-INF/lib」ディレクトリに配置する
- JSP ページ中で使いたいカスタムタグを以下のように taglib ディレクティブで宣言する
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<c:set>
変数に値をセットする。文字列や数値をセットできる。
<c:set var="変数名" value="セットする値" [scope="オブジェクトのスコープ]/>
変数 var に value 属性の値をセットする。デフォルトでは page スコープ。
<c:set value="セットする値" target="(オブジェクトの)変数名" property="プロパティ名"/>
target 属性で指定したオブジェクトのプロパティに値をセットする。
<c:remove>
スコープに格納された変数を削除する。
<c:remove var="変数名" [scope="オブジェクトのスコープ"]/>
<c:out>
変数にセットされた値を出力する。escapeXml 属性を明示しなければ、デフォルトでエスケープ処理ができる。一方、EL ではエスケープ処理はされない。
<c:out value="変数名" [escapeXml="true | false"] [default="valueがnullであった場合の値"]/>
<c:forEach>
繰り返し処理を行う。
<c:forEach [var="変数名"] begin="開始値" end="終了値" [step="ステップ値"] [varStatus="ステータスオブジェクト変数名"]
...
</c:forEach>
var の変数名は、Java の for 文の繰り返し変数と同様の働きをする。ステータスオブジェクトのプロパティには次のようなものがあり、EL の表記で参照できる。
プロパティ名 | 役割 |
---|---|
current | 現在 var で参照しているオブジェクトの参照 |
index | 0から始まるループ回数 |
count | 1から始まるループ回数 |
first | ループの開始フラグ(開始直後であれば true) |
last | ループの終了フラグ(ループの最後であれば true) |
begin | begin 属性で指定された値 |
end | end 属性で指定された値 |
step | step 属性で指定された値 |
<c:forEach [var="オブジェクト参照名"] items="オブジェクトの集合" [begin="開始する順序番号"] [end="終了する順序番号"] [step="ステップ値"] [varStatus="ステータス変数名"]>
...
</c:forEach>
Collection 型、Iterator 型、Enumeration 型、Map 型、配列型、カンマ区切りの文字列については、構文2の方法で、集合に含まれるオブジェクトの数だけループすることができる。Map 型では、名前とキーのセットが var にセットされるので、key と value というプロパティを使って、それぞれ参照できる。
<c:forTokens>
文字列を区切り文字で区切り、先頭から繰り返し参照していく。例えば、abc,d,ef,g,hij
という文字列を,
で区切った場合、abc
、d
、ef
、g
、hij
に区切られ、順次繰り返し参照される。
<c:forTokens items="文字列" delims="区切り文字" [begin="開始する順序番号"] [end="終了する順序番号"] [step="ステップ値"] [var="変数名"] [varStatus="ステータス変数名"]>
...
</c:forTokens>
<c:if>
単純条件分岐。test 属性の評価式の値が true であれば、タグの中身を評価する。var 属性で指定した変数名に、評価式の結果を代入し、スコープに格納することもできる。
<c:if test="評価式 [var="変数名"] [scope="オブジェクトのスコープ"]>
...
</c:if>
<c:choose>・<c:when>・<c:otherwise>
複数条件分岐。<c:when>タグ内に、条件に当てはまる場合の記述を行い、どれにも当てはまらない場合の記述を<c:otherwise>タグ内に記述する。
<c:choose>
<c:when test="評価式A">
...
</c:when>
<c:when test="評価式B">
...
</c:when>
...
<c:otherwise>
...
</c:otherwise>
</c:choose>
<base>タグ
フォワードを行うと、実際表示されているページのURLとブラウザに表示されているURLの間でずれが生じることがある。このときにJSPページの中でURLを相対パスで指定すると、問題が起こることがある。これを解決するために、<base>
タグを利用する。<head>タグ内でhref属性に基準となるURLを絶対パスで指定して記述することで、そのページ内で指定された相対パスは、「その絶対パス+相対パス」として扱われる。
二重サブミットの防止
二重サブミットとは、同じフォームから同じデータが2回以上送信されること。ユーザが何度も「送信」ボタンを押してしまうことや、データを送信した後で「戻る」ボタンで入力画面に戻りもう一度送信することで同じデータが複数追加されてしまう。
これを防止するためには、登録画面を開いた時の時間情報を隠しパラメータに保管し、この時間情報を持ったフォームは一度しか登録を受け付けないようにする。全く同じ時間に別々のユーザが画面を開く可能性があるので、時間情報に加えてユーザ ID をキーとする。
日時データの処理
Java では、日時データを扱うためのクラスがDate
とCalendar
の2種類ある。
Date
クラスは日時データを単なる値として扱う場合に用いる。
一方で、Calendar
クラスは年月日などを指定して日付をセットする場合に用いる。
Date クラス
-
new
すると、現在の時刻がオブジェクトにセットされる。
Date date = new Date();
-
Date
クラスのgetTime
メソッドは、グリニッジ標準時における 1970 年 1 月 1 日午前 0 時を 0 として、そこからの経過時間をミリ秒単位で表した long 型の整数でオブジェクトの持つ時間データを返す
Calendar クラス
- オブジェクトを生成するには、
Calendar
クラスのgetInstance
クラスメソッドを用いる。すると、Date
クラスと同様な時間データがセットされる。
Calendar cal = Calendar.getInstance();
-
Calendar
型のオブジェクトから年月日を取得するには、get
メソッドを用いる。この場合において、get(Calendar.MONTH)
で取得する月は、1 月から 12 月に対して 0 から 11 の値が返される(つまり、得た値に 1 を足せば通常のカレンダー通りの月となる)ので注意
int intYear = cal.get(Calendar.YEAR);
int intMonth = cal.get(Calendar.MONTH) + 1;
int intDate = cal.get(Calendar.DATE);
-
Calendar
型のオブジェクトに年月日をセットするには、set
メソッドを用いる。この場合においては、get メソッドとは逆に 1 を引く必要がある
cal.set(Calendar.YEAR, 2024);
cal.set(Calendar.MONTH, 10 - 1);
cal.set(Calendar.DATE, 21);
Date と Calendar の変換
-
Calendar
クラスのgetTime
メソッドを用いて、Calendar
型からDate
型に変換することができる。
Date date = cal.getTime();
-
Calendar
クラスのsetTime
メソッドを用いて、Date 型から Calendar 型に変換することができる。
cal.setTime(date);
日時データから文字列表現への変換
-
java.text
パッケージのSimpleDateFormat
クラスが便利。new
するときに表示フォーマットを指定し、そのオブジェクトのformat
メソッドに Date 型のオブジェクトを渡すと、文字列表現に変換される
SimpleDateFormat df = new SimpleDateFormat("yyyy年 MM月 dd日");
Date date = new Date();
String strDate = df.format(date);
文字列データから日時データへの変換
String strDate = "2024-10-30";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = dateFormat.parse(strDate);
和暦
Java6 から標準で和暦対応の Calendar クラスが提供されている。また、Java12 からは令和にも対応している(それ以前の ver でもパッチを当てれば対応可)。日本では明治6年からグレゴリオ暦を導入しているため、明治 6 年 1 月 1 日以後の日付をサポートしている。
和暦対応クラス JapaneseImperialCalendar のオブジェクト取得方法
// 方法1
// この方法では、システムのデフォルトロケールを和暦ロケールにしている。
Locale.setDefault(new Locale("ja", "JP", "JP"));
Calendar cal = Calendar.getInstance();
// 方法2
Calendar cal = Calendar.getInstance(new Locale("ja", "JP", "JP"));
JapaneseImperialCalendar はアクセス修飾子が public ではないため、上記の方法でオブジェクトを取得しなければならない。
元号の数値対応は次のとおり。
明治 1
大正 2
昭和 3
平成 4
令和 5
任意の年月日の JapaneseImperialCalendar のオブジェクト取得方法
GregorianCalendar クラスを使用する。例は次のとおり。
Locale.setDefault(new Locale("ja", "JP", "JP"));
Calendar cal = Calendar.getInstance();
// 西暦2024年(令和6年)5月10日の日付を指定。月は1月から12月に対して0から11が対応しているので、5月であれば4を指定する。
GregorianCalendar gregorianCalendar = new GregorianCalendar(2024, 5 - 1, 10);
// 和暦に変換
cal.setTime(gregorianCalendar.getTime());
System.out.println(cal.getClass().getName());
System.out.println(cal.get(Calendar.ERA));
System.out.println(cal.get(Calendar.YEAR));
System.out.println(cal.get(Calendar.MONTH) + 1);
System.out.println(cal.get(Calendar.DATE));
SimpleDateFormat を使用した和暦へのフォーマット
ロケールを和暦用ロケールに設定しておけば、SimpleDateFormat で行うことができる。
Locale.setDefault(new Locale("ja", "JP", "JP"));
DateFormat dateFormat = new SimpleDateFormat("GGGGyyyy年M月d日");
// JapaneseImperialCalendarからのフォーマット。和暦でフォーマットされる。
Calendar cal = Calendar.getInstance();
System.out.println(dateFormat.format(cal.getTime()));
// GregorianCalendarからのフォーマット。きちんと和暦でフォーマットされる。
GregorianCalendar gregorianCalendar = new GregorianCalendar(2024, 5 - 1, 10);
System.out.println(dateFormat.format(gregorianCalendar.getTime()));
クッキー(Cookie)
クッキーは、ウェブブラウザとサーバー間で情報を保存・管理するための小さなデータ片のこと。セッション管理や状態管理などに用いられる。
基本的な特性
- 名前と値
各クッキーは名前と値のペアで構成される - 有効期限
クッキーには有効期限が設定されており、指定された時間が過ぎると自動的に削除される - ドメインとパス
クッキーは特定のドメインとパスに関連付けられ、その範囲内でのみ送信される - セキュリティ属性
Secure や HttpOnly といった属性を設定することで、クッキーの安全性を向上させることができる
注意点
クッキーにはサイズ制限があり、通常は 1 つのクッキーあたり 4096 バイト程度。
基本操作
クッキーの作成
クッキーを作成するには、javax.servlet.http.Cookie クラスを使用する。クッキーの名前と値を設定し、レスポンスに追加する。
Cookie cookie = new Cookie("cookieName", "cookieValue"); // ("クッキー名", "クッキー値")
cookie.setMaxAge(3600); // 有効期限を1時間に設定。単位は秒。
response.addCookie(cookie);
クッキーの取得
クッキーを取得するには、HttpServletRequest オブジェクトの getCookies()メソッドを使用する。このメソッドは、クッキーの配列を返す。
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("cookieName")) {
String value = cookie.getValue(); // クッキーの値を取得
// 使用する処理
}
}
}
クッキーの削除
クッキーを削除するには、クッキーの有効期限を過去に設定します。対象のクッキーを見つけて、setMaxAge(0)を呼び出し、レスポンスに追加します。
// 削除したいクッキーの名前
String cookieName = "myCookie";
// クッキーを取得
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
// 有効期限を過去に設定
cookie.setMaxAge(0);
cookie.setPath("/"); // パスを設定(必要に応じて調整)
response.addCookie(cookie); // クッキーをレスポンスに追加
break; // 削除対象が見つかったらループを抜ける
}
}
}
}
ハイパーリンクを使ったデータ送信
いくつかの候補(リンク)からユーザに1つ選んでもらうような場合や、移動先のページに何か情報を引き継ぎたいような場合に利用する。一般的には、次のような形式となる。
<a href="送信先URL?パラメータ名1=値1&パラメータ名2=値2...">ボディ</a>
フォーム送信時における javascript を利用したバリデーション
form タグにおいて、onsubmit にバリデーションをする関数をイベントハンドラとして登録する。バリデーションに引っかかった場合、false を返すようにしておき、同時にアラートを表示させるようにする。各種のバリデーション例は以下のコードのとおり。
<body>
<form action="index.html" onsubmit="return check();">
<input type="text" name="myId" id="myId">
<input type="password" name="myPassword1" id="myPassword1">
<input type="password" name="myPassword2" id="myPassword2">
<select name="mySelection" id="mySelection">
<option value="0">---選択してください---</option>
<option value="1">select1</option>
<option value="2">select2</option>
</select>
<label><input type="radio" name="myRadio" id="myRadio1" value="1">option1</label>
<label><input type="radio" name="myRadio" id="myRadio2" value="2">option2</label>
<label><input type="radio" name="myRadio" id="myRadio3" value="3">option3</label>
<input type="text" name="myNumber" id="myNumber">
<input type="text" name="myHalfAlphanumeric" id="myHalfAlphanumeric">
<textarea name="myTextArea" id="myTextArea"></textarea>
<input type="submit" value="送信">
</form>
<script src="script.js"></script>
</body>
function check() {
// ID入力欄
// 空欄チェック
const myId = document.querySelector("#myId");
if (myId.value === "") {
alert("idは空欄にできません");
return false;
}
// パスワード入力欄
// 空欄チェック
const myPassword1 = document.querySelector("#myPassword1");
const myPassword2 = document.querySelector("#myPassword2");
if (myPassword1.value === "") {
alert("パスワードは空欄にできません");
return false;
} else if (myPassword1.value !== myPassword2.value) {
// 確認用パスワードのチェック
alert("確認用のパスワードが違います");
return false;
}
// セレクトボックス
// 候補を選択していることのチェック
const mySelection = document.querySelector("#mySelection");
if (mySelection.value === "0") {
alert("セレクトボックスの候補を選択してください");
return false;
}
// ラジオボタン
// 候補を選択していることのチェック
const myRadio = document.querySelector('input[name="myRadio"]:checked');
if (!myRadio) {
alert("ラジオボタンの候補を選択してください");
return false;
}
// 入力内容が数値かどうかのチェック
// 入力内容を数値に変換したあと、それが数であることを確認する。
// Number.isFinite()は、暗黙的な型変換を行わずに有限数かどうかを判定する。
const myNumber = document.querySelector("#myNumber");
if (!Number.isFinite(Number(myNumber.value))) {
alert("数値を入力してください");
return false;
}
// 入力内容が半角英数かのチェック
const myHalfAlphanumeric = document.querySelector("#myHalfAlphanumeric");
if (!myHalfAlphanumeric.value.match(/^[A-Za-z0-9]*$/)) {
alert("半角英数字で入力してください");
return false;
}
// 文字列長の制限
// 全角も半角もそれぞれ一文字として数えられる
const myTextArea = document.querySelector("#myTextArea");
if (myTextArea.value.length > 10) {
alert("文字数が制限(10文字)を超えています");
return false;
}
// フォームを送信してよいかの確認
return confirm("この内容で登録します。よろしいですか?");
}
ファイルアップロード・ダウンロード
サーブレットバージョンチェック
サーブレットAPI3.0以降はファイルアップロード・ダウンロードのパッケージがデフォルトで含まれるようになったので、Tomcat8・javaSE8環境の場合はどうか確認した。
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
JSP version: <%= JspFactory.getDefaultFactory().getEngineInfo().getSpecificationVersion() %><br>
Servlet Specification: <%= application.getMajorVersion() %>.<%= application.getMinorVersion() %><br>
</body>
</html>
結果は、
- JSP version: 2.3
- Servlet Specification: 3.1
となり、新たにライブラリをダウンロードする必要はなかった。
アップロード機能
要点
- 画面のフォームでは、
enctype="multipart/form-data"
属性を付ける - inputのtypeは
file
とする - サーブレットクラスに
@MultipartConfig
アノテーションをつける -
request.getPart()
メソッドでPartオブジェクトを取得する - Partは複数取得も可。その場合は受け取る変数をコレクション(リストとか)にする
-
getSubmittedFilename()
メソッドでクライアントから示されたファイルの名前を取得する。Servlet API3.1以降は使える - ファイルをアップロードするフォルダを作成しておき、
getServletContext().getRealPath()
メソッドでパスを取得する - Partオブジェクトの
getSize()
メソッドでファイルサイズを取得。ファイルサイズが0より大きいかどうかで取得の例外処理をする - Partオブジェクトの
write()
メソッドでファイル書き出し
<form action="UploadServlet" method="POST" enctype="multipart/form-data">
<p>
<input type="file" name="upfile" />
</p>
<p>
<input type="submit" value="upload" />
</p>
</form>
package controller;
import java.io.File;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
@WebServlet("/UploadServlet")
// multipart/form-data形式のリクエストに対応できるようにする
@MultipartConfig
public class UploadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// partオブジェクトの取得。getPart()の引数は、フォームのname属性の値。
Part part = request.getPart("upfile");
// 名前の取得
String partName = part.getName(); // パートの名前を取得。getPartの引数と同じ。
String submittedFileName = part.getSubmittedFileName(); // クライアントから送信されるファイル名を取得
// アップロードするフォルダ
String path= getServletContext().getRealPath("/upload");
// ファイルサイズ取得
long fileSize = part.getSize();
// ファイル書き出し。取得できていないとfileSizeは0なので、それで例外処理。
if (fileSize > 0) {
part.write(path + File.separator + submittedFileName);
}
// ページ遷移
request.getRequestDispatcher("/uploadFile.jsp").forward(request, response);
}
}
ダウンロード
- webアプリケーションルート内のファイルをおいているパスを表すFileオブジェクトを作成
- ファイル読み込み用バッファの用意
- 拡張子からcontentTypeを取得
-
response.setContentType
でcontentTypeを出力 -
response.setHeader
でファイル名の送信。このとき、attachment
を指定しなければ、ブラウザに直接表示されてしまう場合があるため注意 - ServletOutputStreamに対して、FileInputStreamからファイル内容を送信する
- 画面側では、このサーブレットへのハイパーリンクなどを用いてユーザにダウンロードさせる
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// webアプリケーションルート内のファイルをおいているパスを表すFileオブジェクトを作成
ServletContext context = getServletContext();
String downloadFilePath = context.getRealPath("/upload/");
File downloadFile = new File(downloadFilePath, "test.csv");
// ファイルのダウンロード処理
// ファイル読み込み用バッファ
byte buffer[] = new byte[4096];
String fileName = downloadFile.getName();
// 拡張子からcontentTypeを取得
FileDataSource fds = new FileDataSource(fileName);
String contentType = fds.getContentType();
// contentTypeを出力
response.setContentType(contentType);
// ファイル名の送信
response.setHeader("Content-disposition", "attachment; filename=\"" + fileName +"\"");
// ファイル内容の送信
ServletOutputStream sos = response.getOutputStream();
FileInputStream fis = new FileInputStream(downloadFile);
int size;
while ((size = fis.read(buffer)) != -1) {
sos.write(buffer, 0, size);
}
fis.close();
sos.close();
}
JDBC によるデータベースアクセス
JDBC は、Java プログラムからデータベースへの接続・切断、SQL の実行、結果の取得を行うためのクラスやメソッドを提供している。データベースによる差異は JDBC ドライバが吸収してくれるため、どのデータベースを扱うときも同じ手順でプログラムを作成することができる。
JDBC 利用の概要
- JDBC ドライバのインストール
- JDBC ドライバのロード
- データベースへの接続
使用するドライバをロードし、データベースへのコネクション(接続)を確立する - データベースアクセス処理
SQL 文を文字列として組み立てて、データベースに送信する。このとき、コネクションを使う - 切断処理
コネクションを開放する
JDBC ドライバのインストール
jar ファイルを「webapp のルートディレクトリ/WEB-INF/lib」ディレクトリに配置する。
JDBC ドライバのロード
JDBC を利用するためには、各データベース用の JDBC ドライバをロードしておく必要がある。JDBC4.0 以降は自動ロードされるが、明示的にロードする場合は、次のようにする。
Class.forName("各データベース用のドライバのクラス名");
クラス名は、例えば、MySQL の場合はcom.mysql.jdbc.Driver
である。
データベースへの接続
java.sql.DriverManager
クラスのgetConnection()
メソッドを用いて接続をする(コネクションオブジェクトを得る)。
Connection con = DriverManager.getConnection(接続先URL文字列, ユーザ名, パスワード);
接続先 URL 文字列は、次のような文字列である。
jdbc:データベースシステム名://ホストアドレス:ポート番号/データベース名?パラメータ=値...
例えば、次のとおり。
jdbc:mysql://db.hoge.co.jp:5000/exampledb?useUnicode=true&characterEncoding=UTF-8
データベースアクセス処理
SQL の実行
SQL 文を記述した文字列引数にしてコネクションオブジェクトのprepareStatement()
メソッドによりPreparedStatement
オブジェクトを作成する。次に、そのオブジェクトのset+データ型
メソッドを用いてプレースホルダと呼ばれる変数部分に値をセットし、最後にexecuteUpdate()
やexecuteQuery()
メソッドを用いて SQL を実行する。なお、プレースホルダへの代入時には、エスケープ処理がなされる。例えば、次のようにする。
PreparedStatement pstmt = con.prepareStatement("UPDATE employee SET department=? WHERE number=?");
pstmt.setString(1, "総務");
pstmt.setInt(2, 99);
pstmt.executeUpdate();
結果の取得
SELECT 文を実行した場合、結果として返されるテーブルは ResultSet オブジェクトとして得られる。行と列を指定してデータを取り出すこととなる。
行を指定するには、「カーソル」と呼ばれる「現在どの行を参照しているのか?」という情報を表すものを使う。カーソル移動のためのメソッドは次のようなものがある。なお、初期状態ではどの行も参照していない状態である。多くはnext()
メソッドを while 文の条件部分に用いて順次参照していく。
メソッド名 | 機能 |
---|---|
boolean first() | 参照行を先頭に移動 |
boolean next() | 参照行を1行下に移動。移動先の行がなければ false を返す。 |
boolean previous() | 参照行を1行上に移動。移動先の行がなければ false を返す。 |
boolean last() | 参照行を最終行に移動 |
列を指定するには、get+データ型("カラム名")
メソッドを利用する。例えば、integer の値を取得するには、getInt()
メソッドを利用する。
切断処理
最後は、ResultSet(検索の場合)、PreparedStatement 及び Connection それぞれのオブジェクトのclose()
メソッドを用いてクローズをする。ただし、ResultSet オブジェクトと PreparedStatement オブジェクトは、Connection オブジェクトをクローズすれば自動的にクローズされる。
DAO パターン、DTO パターンを用いたデータベース処理
DAO パターン
DAO パターンとは、データベースとアプリケーションの間のデータのやり取り専用の DAO と呼ばれるクラスを用意し、アプリケーションとデータベースの関連をまとめて最小にするデザインパターンのこと。
DAO の中では、データベースへの接続部分と切断部分をそれぞれメソッドにまとめておき、その他データベースへの登録、更新、検索を行うメソッドを定義する。
データベースへの接続メソッドでは、コネクションを返すところまでを担当する。
切断メソッドでは、コネクションを受け取り、それをクローズするところまでを担当する。
各種データベースへの登録、更新、検索メソッドでは、コネクションの接続・切断メソッドを利用しつつ、その他の SQL 実行の処理を行う。このとき、検索条件や登録、更新する DTO を引数として受け取ったり、検索結果を DTO(あるいはそのリスト)としてアプリケーションに返すことをする。
DTO パターン
DTO パターンとは、データベースにおける行とアプリケーションにおけるオブジェクト相互の変換過程において用いられるデータを抽象化した JavaBeans 形式のクラス(DTO)によってデータのやり取りを行うデザインパターンのこと。
DTO は、アプリケーション側で使いやすい形で準備する。必要なフィールド変数とそのゲッター・セッターを持ったクラスとなる。
処理の流れ
データ登録であれば、アプリケーションが登録したい情報を DTO として DAO に渡し、DAO の中で DTO をもとに SQL 文を組み立てデータベースに登録を行い、成否をアプリケーションに返す、という流れとなる。
データの検索であれば、アプリケーションが検索条件を DAO に渡し、DAO がデータベースから取り出した情報を元に DTO を作り、それをアプリケーションに返す、という流れとなる。
DataSource を利用した DB 接続
前述の方法では、DriverManager を利用してコネクションを取得していたが、一般的には、DataSource と JNDI という仕組みを使ってコネクションを取得する方が良いとのこと。
DataSource を用いれば、リソース情報を外部ファイルに記述することができるほか、コネクションプーリングの機能も使える。
DataSource の設定
アプリケーションごとに設定する場合を記述する。(サーバーごとに設定したい場合は、Tomcat 側の設定ファイルに記述を追加する。)
アプリケーションのコンテキストの META-INF/に、context.xml
というファイルを作成し、次のように記述する。
<Context>
<Resource name="jdbc/mydb"
auth="Container"
type="javax.sql.DataSource"
username="dbuser"
password="dbpassword"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://db.hoge.co.jp:5000/exampledb?useUnicode=true&characterEncoding=UTF-8
"/>
</Context>
name
は、データソースを取得するときに使用する名前。慣習的にjdbc/
を前につけ、任意の名前を記述する。
url
は、JDBC ドライバの URL。前述の「データベースの接続」の項で DriverManager に渡したものと同じ。
DataSouce の利用方法
- InitialContext の作成
- DataSource の取得
- DataSource から Connection オブジェクトを取得する
実際の利用部分のコード例は次のとおり。
// リソース名を表すフィールド変数(前項で設定したnameと同じ)
private final static String resourceName = "jdbc/mydata";
// 1. InitialContextの作成
InitialContext ic = new InitialContest();
// 2. DataSourceの取得
DataSource ds = (DataSource)ic.lookup("java:comp/env/" + resourceName);
// 3. DataSourceからConnectionオブジェクトを取得する
Connection con = ds.getConnection();
なお、デフォルトの実装クラスはorg.apache.tomcat.dbcp.dbcp2.BasicDataSource
(Apach Common DBCP)となる。
フォームで送られてきたデータの文字コード
フォームから送られてくる文字コードは、文字エンコーディングを識別するための情報がないために、サーブレットコンテナが勝手にiso-8859-1という文字エンコーディングのデータとしてUnicodeに変換しようとする。
したがって、データを正しく扱うための処理が必要となる。
このとき、フィルターという機能を使えば効率的に処理ができる。
eclipseを使用しているなら、プロジェクトに対して新規に「フィルター」を作成する(その他>Webの中にある)。次にパッケージ名を「filter」、クラス名を「EncodingFilter」として「次へ」をクリック。フィルター・マッピングを編集して、「/*」にする。これで、このプロジェクトのすべてのサーブレットが対象となる。作成したフィルタークラスのdoFilter()
メソッド内に次の処理を記述すれば完了。
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=UTF-8");
chain.doFilter(request, response);
java Mailによるメール送信
- あらかじめ
javax.mail
のjarファイルを取得し、ビルドパスに追加しておく - サーブレット側では、PropertiesオブジェクトにSMTPサーバの設定を持たせる。内容は外部ファイルに書き込んでおき、クラスパスの通ったフォルダに配置しておく。そのInputStreamを
this.getClass().getClassLoader().getResourceAsStream()
で取得し、情報をロードしている - 認証情報をjava.mailの`Sessionに渡して、Sessionオブジェクトを作成する
- MimeMessageでメール情報を作成する
-
Transport.send()
で送信する - なお、gmailを用いる場合は、アプリパスワードを取得して利用しなければならない
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// メール送信情報の設定
request.setCharacterEncoding("UTF-8");
String subject = request.getParameter("subject");
String body = request.getParameter("body");
// SMTPサーバの設定
// properitesファイルに書き込んでおいた設定を読み込む
Properties properties = new Properties();
try (InputStream input = this.getClass().getClassLoader().getResourceAsStream("config.properties")) {
// プロパティファイルを読み込み
properties.load(input);
} catch (IOException e) {
e.printStackTrace();
}
// プロパティの取得
String fromEmail = properties.getProperty("fromEmail");
String toEmail = properties.getProperty("toEmail");
String username = properties.getProperty("username"); // メールアカウントのユーザー名
String password = properties.getProperty("password"); // メールアカウントのパスワード
// 認証情報の設定
Session session = Session.getInstance(properties, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
try {
// メールメッセージの作成
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(fromEmail));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
message.setSubject(subject);
message.setText(body);
// メールの送信
Transport.send(message);
System.out.println("メールが正常に送信されました。");
} catch (MessagingException e) {
e.printStackTrace();
}
}
mail.smtp.auth=true
mail.smtp.starttls.enable=true
mail.smtp.host=smtp.gmail.com
mail.smtp.port=587
セキュリティ関連
クロスサイトスクリプティング
悪意のある第三者が、そのサイトを閲覧したユーザのブラウザ上で任意のJavaScriptプログラムを実行できてしまうこと。この脆弱性は、次のような状況が重なった場合に問題となる。
- ユーザからの入力に含まれるHTML特殊文字の処理が適切に行われていない
- ユーザからの入力文字列を出力する(ブラウザに渡す)処理が含まれる
- ブラウザがJavaScriptを実行可能である
例えば、簡易的な掲示板などで、悪意のあるユーザがJavaScriptを埋め込んだリンクを書き込むなどし、誰かがそれをクリックするとクッキーの値を悪意のあるユーザのもとへ集める、といったことができる。
対策は、入力時にHTML特殊文字(「<」「&」「"」など)を適切に変換する処理を施すことである。JSTLの\タグを利用することも有効な手段である。
SQLインジェクション
リクエストパラメータから取り出した内容を、そのままデータベースへのSQL文の一部として送っているときに発生する。
例えば、条件(WHERE句)部分に「or true」などの文字が入力されると、すべてのデータを読みだされる危険がある。
対策は、入力をチェックし、想定外の値が指定されたときにきちんとエラーとなるようにしておくこと。また、シングルクォーテーションなどの特殊文字については、エスケープ処理が必要。
フィルターを使った認証処理
認証処理は、文字コードの項で触れたフィルターを使えば、効率的に実装できる。
秘密にしたいページをURLパターンに登録し(ログイン用のページは除いておく)、認証用のBean(ログインページで認証済みかどうかを表すフラグのフィールドを持つBean。セッションに登録する。)で認証済みかを判定し、フィルタを通過させるかどうかという処理をさせる。フィルタを通過できなかった場合は、ログインページなどにリダイレクトさせる。
動作テスト
システムが正しく動作するかどうかを確認すること。システムが一通り組み上がったら、なるべく早い時期にテストを行い、問題になりそうなところを洗い出しておくと良い。完璧なテストというものはないが、妥協できるところとできないところを区別してアプリケーションの規模や開発期間、人員、予算などによってリソースを割り当てる。
システムの動作テスト
- web アプリケーションの場合は、「画面の遷移」と「入力項目」が主なテスト対象となる
- テスト担当者、ドキュメント担当者、ユーザーが関わるのも良いこと
- テスト項目を一覧にしたものを「テスト仕様」という。テスト仕様に従ってテスティングする
【ユーザー管理機能】
・ユーザ一覧画面
新規画面へのリンク
編集画面へのリンク
トップページへのリンク
ログアウトへのリンク
システム管理者以外が開けないこと
・新規画面
ログイン名
通常の入力で正しく登録できること
空欄のまま送信できないこと
・・・
氏名
通常の入力で正しく登録できること
空欄のまま送信できないこと
・・・
送信ボタンの動作
戻るボタンの動作
・・・
- 「どのバージョン」で「どのテストを行ったか」を管理する。コードの修正を繰り返すたびにテストを行うから。不具合があれば、操作手順やエラーメッセージ、再現方法を記録する
- コードが修正された場合は、修正部分の再テストを行う。他の部分も影響を受けている可能性があるので注意
テスト項目
画面遷移のテスト
- リンク切れがないか
まず、画面の遷移図に従って、すべての経路をテストする。次に、各画面のリンクをすべてテストし、リンク切れがないか確認する。 - 上の階層に戻るリンクがあるか
- フォームデータの送信
通常の入力での遷移と不正な入力での遷移をテストする。各入力項目に関する詳細なテストは、画面ごとにまとめて行ったほうが良い。
画面のチェック
- 表示内容のチェック
- JavaScript の動作チェック
- レイアウトの崩れがないか
表示するデータが極端に多い場合や、極端に少ない場合、データが空の場合などに問題がないかどうかをチェックする。また、ブラウザの幅を狭めたり広げたりしてレイアウトが崩れないことを確認する。
入力項目のテスト
様々な値を入力してエラーが生じないかをテストする。次のようなパターンが考えられる。
- 空欄
- 数値
上限値、下限値、その隣接値。ゼロ、マイナス。大きい値、小さい値。 - 文字数
データベースのカラムに保存可能な文字数を超えた場合に正しく処理されるか。 - 全角半角
文字種を区別している場合に、両方のパターンをテストして正しく処理されるか。 - HTML タグ
入力欄に HTML タグが入力されたときにレイアウトが崩れるなどの不具合が生じないかどうかを確認する。 - SQL インジェクション対策
入力欄にシングルクオートやセミコロンなどの文字を入力してデータベースのエラーが生じないことを確認する。これらの文字をデータベースに保存するには、エスケープシーケンスに置き換える必要がある。PreparedStatement オブジェクトに setString メソッドを使って文字列を指定した場合は、自動的にエスケープされる。
各種状態のチェック
- データが空の状態
運用開始時など、データが空の状態で不具合が生じないかどうか - 排他処理
同じデータに複数の端末からアクセスして編集操作を行い、排他処理が正しく行われることを確認する。 - 二重サブミット
フォームを送信した後、ブラウザの戻るボタンをクリックして、もう一度フォームを送信し、二重サブミット対策が正しく機能しているかどうかを確認する。
参考文献
はじめてのJSP&サーブレットプログラミング/アイティーブースト 著/秀和システム
JSP業務アプリケーション短期開発入門/竹形 誠司 著/ラトルズ
標準JSP サーブレット教科書/片山 幸雄 著/ソフトバンククリエイティブ
Java+MySQL+Tomcatで始めるWebアプリケーション構築入門/竹形 誠司 著/ラトルズ