2008年5月29日木曜日

Spring 2.5 AbstractWizardFormControllerを使ってみる(3)

Validatorを使ってみます。

【手順概要】
InputValidatorを追加します。

SimpleWizardControllerでprotected void validatePage(..)をオーバーライドします。

page1form.jsp、page2form.jspにform:errorsタグを入れ込みます。

springapp2-servlet.xmlにvalidatorの定義を追加します。

【手順詳細】
InputValidatorを追加します。
package springapp2.service;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class InputValidator implements Validator {

public boolean supports(Class clazz) {
return AllValueForm.class.isAssignableFrom(clazz);
}

public void validate(Object obj, Errors errors) {
validateFirstPage((AllValueForm) obj, errors);
validateSecondPage((AllValueForm) obj, errors);

}

public void validateFirstPage(AllValueForm form, Errors errors) {
int id = form.getFirstId();
if (id == 0) {
errors.rejectValue("firstId", "", null,
"ID required (Other than 0).");
}
ValidationUtils.rejectIfEmpty(errors, "firstValue", "",
"Value required.");
}

public void validateSecondPage(AllValueForm form, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "nextValue", "",
"Value required.");
}

}

SimpleWizardControllerでprotected void validatePage(..)をオーバーライドします。
    @Override
protected void validatePage(Object command, Errors errors, int page,
boolean finish) {
AllValueForm form = (AllValueForm) command;
InputValidator validator = (InputValidator) getValidator();
// errors.setNestedPath("order");
switch (page) {
case 0:
validator.validateFirstPage(form, errors);
break;
case 1:
validator.validateSecondPage(form, errors);
}
}

page1form.jsp、page2form.jspにform:errorsタグを入れ込みます。(太字部分)
(page2form.jspは省略しています)
  <table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="right" width="20%">First id:</td>
<td width="20%">
<form:input path="firstId"/>
</td>
<td width="60%">
<form:errors path="firstId" cssClass="error"/>
</td>
</tr>
・・・略・・・
<form:errors path="firstValue" cssClass="error"/>


springapp2-servlet.xmlにvalidatorの定義を追加します。(太字部分)
・・・略・・・
<bean name="/page1form.htm"
class="springapp2.web.SimpleWizardController">
<property name="sessionForm" value="true" />
<property name="commandName" value="allValueForm" />
<property name="commandClass" value="springapp2.service.AllValueForm" />
<property name="validator">
<bean class="springapp2.service.InputValidator"/>
</property>

</bean>
<bean name="/page2form.htm"
class="springapp2.web.SimpleWizardController">
<property name="sessionForm" value="true" />
<property name="commandName" value="allValueForm" />
<property name="commandClass" value="springapp2.service.AllValueForm" />
<property name="validator">
<bean class="springapp2.service.InputValidator"/>
</property>

</bean>
・・・略・・・


以上。

「障害を許さない」プロジェクトが破綻する理由(2)

■「障害を許さない」スローガンの分析

ではなぜ障害は好ましくないと考えてしまうのでしょうか。今回は少し毛色を変えて「障害は許さない」という精神状態について分析してみましょう。

まず障害とは何でしょうか。

「常に予定外の出来事として発生するもの」
「通常の運用や業務を阻害するもの」
「不安を呼び覚ますもの」

「上司の思いつき」というのも一種の障害であることがよく分かりますね。
障害の原因という観点から整理してみましょう。

「ハードウェアの故障」
「ソフトウェアのバグ(時限系/タイミング系)」
「ソフトウェアのバグ(設計ミス)」
「ソフトウェアのバグ(ロジックの問題)」
「設計書のミス」

一次的にはそんなところでしょうか。

原因追求についてもう少しレベルを深くしてみましょう。
われわれ人間には実にさまざまな愛すべき欠陥があります。

「コミュニケーションミス」
「単純作業ミス」
「勘違い」
「思い込み」
「忘却」
「理解不足」

整理すると「人間の欠陥により」⇒「ハード/ソフトに欠陥が生じ」⇒「それによって予定外の通常業務を妨げる事象が発生し、不安を呼び覚まされる」ことが障害である、と言えます。

しかしこうしてみると「なあんだ。障害が起こるのなんてあったりまえのことじゃないか。所詮は人がやることでしょ」というふうに見えませんか?見えない?それは困った。

以上の文脈から言えば「障害が許せない」という人は「人間は確かに誤りを犯す。しかし障害は許せない」という考え方を持っていることになります。この考え方を二つに分けてみましょう。

(1)「自分のことはさておき他人の起こした障害は許せない」
(2)「自分の起こした障害だからこそ許せない」

(1)について。まあ確かに(例えば、ですよ)原子力発電所の運用をおろそかにされたりすると、これはちょっとけしからんと私でも思います。他人の安全を守るような仕事はきっちりやって頂きたい。
これを「他者糾弾タイプ」と定義します。

(2)は少し気の毒な考え方ですね。これを生真面目につきつめる人はうつ病まっしぐらかもしれません。適度な責任感を持って頂きたい。
こちらを「自己破滅タイプ」とします。

両者の資質を併せ持っているのが普通だと思います。後はそれぞれの傾向が強いかによってその人の性格を定義できるのではないでしょうか。

順列組み合わせによって4つのタイプが存在します。
タイプA【前者が強く、後者が弱い】
 ⇒ 自分のことは棚に上げて人のことをあれこれ言いつらうタイプ。
B【前者が強く、後者も強い】
 ⇒ 自分にも他人にも厳しいタイプ。
C【前者が弱く、後者も弱い】
 ⇒ テキトーな感じ。
D【前者が弱く、後者が強い】
 ⇒ 気の毒な感じですね。周りから利用されそう。

残念ながら悲惨なプロジェクトを推進する人には(また出世する人には)AやBのタイプが多いような気がします。いずれにせよある意味で正義感が強く、間違ったことが許せないタイプと言えるでしょう。こうしてみると、プロジェクトがどんどん悲惨になって行くのもむべなるかな、という気がしないではありません。恐らく短期的には「失敗が許せない」と胸を張っていうような人こそがはびこり「だって人間だもの」と弱々しい声でつぶやく人はどんどん駆逐されてゆくでしょうから。

とまあ今回はなんだか救いのない話になりましたが、実は私は希望を失っていません。短期的には他者糾弾傾向の強い人が幅をきかせるかもしれませんが、そのような人がプロジェクトを破壊する可能性もまた高い。長期的には妥当な判断が出来る人が成功すると私は信じています。

次回は「不安」という観点から「障害を許さない」というスローガンについて考えてみたいと思います。

Spring 2.5 AbstractWizardFormControllerを使ってみる(2)

「戻る」ボタンを実装します。

【手順概要】
page2form.jspにボタンを追加します。

confirmform.jspにボタンを追加します。

これだけ(!!)

【手順詳細】
page2form.jsp
formタグの中に以下を記載します。
<input type="submit" value="Previous" name="_target0">


confirmform.jsp
formタグの中に以下を記載します。
<input type="submit" value="Previous" name="_target1">


以上。素晴らしい。

#補足
前回投稿のspringapp2-servlet.xmlで、下記をコメントアウトしても問題なく動きました。
 <bean id="simpleWizardController"
class="springapp2.web.SimpleWizardController">
<!--
<property name="pages">
<list>
<value>page1form</value>
<value>page2form</value>
<value>confirmform</value>
</list>
</property>
-->

</bean>

逆にSimpleWizardControllerの以下をコメントアウトするとダメでした。
setPages(commands);

少し直感に反するなあ・・・

Spring 2.5 AbstractWizardFormControllerを使ってみる(1)

AbstractWizardFormController(以下AWFC)を使ってみます。
(情報がないので苦労しました・・・)
以下のようなフローを実現します。

入り口画面 ⇒ 設定画面1 ⇒ 設定画面2 ⇒ 設定確認画面 (⇒ 最初に戻る)

まずは小手調べに超基本的な動きのみを実装します。

【始めに】
SimpleFormController(以下SFC)は、画面を"formBackingObject"で描画し、"onSubmit"で送信されたデータを処理するという言わば単一画面でのインプット/アウトプット処理に特化したコントローラです。
それに対してAWFCは複数画面に渡るインプットを内容チェックしながら溜め込んで行く、文字通りウィザード形式の画面遷移をサポートするコントローラです。
「ウィザード形式」という抽象化が秀逸ですね。言われてみればショッピングカートもまさにウィザード形式になっています。
SFCでゴリゴリとウィザード形式の画面遷移を作成するよりも以下の点で優れています。

- 個々の画面にBeanを準備する必要がありません
 ⇒ 複数画面に渡って単一のBeanを共有できます。
- 「戻る」「進む」機能が単一のAWFCで処理できます。
 ⇒ この例でいえば「設定画面1」設定画面2」「設定確認画面」の遷移を単一のコントローラで制御できます。素晴らしい。

【手順概要】
入り口画面を作成します。

入り口画面用のコントローラをSFCで実装します。Beanは作成しません。

上記のSpring サーブレットXML定義ファイルを作成します。

AWFCで参照するBeanを作成します。

AWFCを継承したコントローラを作成します。

設定画面1~設定確認画面を作成します。

上記のSpring サーブレットXML定義ファイルを作成します。

補足「include.jsp」「web.xml」

【手順詳細】
入り口画面を作成します。
<%@ include file="/WEB-INF/jsp/include.jsp" %>

<html>
<head><title>Hello :: Wizard controller test</title></head>
<body>
<h1>Hello - Spring Application</h1>
<p>Message: <c:out value="${model.message}"/></p>
<br><a href="<c:url value="page1form.htm"/>">Input values</a>
</body>
</html>

入り口画面用のコントローラをSFCで実装します。Beanは作成しません。
package springapp2.web;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class EntranceController implements Controller {

protected final Log logger = LogFactory.getLog(getClass());
@Override
public ModelAndView handleRequest(HttpServletRequest arg0,
HttpServletResponse arg1) throws Exception {
String message = "WizardForm sample";
Map<String, Object> myModel = new HashMap<String, Object>();
myModel.put("message", message);
return new ModelAndView("entrance", "model", myModel);
}

}

上記のSpring サーブレットXML定義ファイル(springapp2-servlet.xml)を作成します。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<!-- the application context definition for the springapp DispatcherServlet -->

<bean name="/entrance.htm" class="springapp2.web.EntranceController" />

<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView">
</property>
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

</beans>

AWFCで参照するBeanを作成します。(まさにNothing specialです)
package springapp2.service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class AllValueForm {
protected final Log logger = LogFactory.getLog(getClass());
private int firstId;
private String firstValue;
private int nextId;
private String nextValue;

public String getNextValue() {
return nextValue;
}

public void setNextValue(String nextValue) {
this.nextValue = nextValue;
logger.info("nextValue set:" + nextValue);
}

public int getNextId() {
return nextId;
}

public void setNextId(int nextId) {
this.nextId = nextId;
logger.info("nextId set:" + nextId);
}

public int getFirstId() {
return firstId;
}

public void setFirstId(int firstId) {
this.firstId = firstId;
logger.info("firstId set:" + firstId);
}

public String getFirstValue() {
return firstValue;
}

public void setFirstValue(String firstValue) {
this.firstValue = firstValue;
logger.info("firstValue set:" + firstValue);
}
}

AWFCを継承したコントローラを作成します。
(本当にこれだけ!)
(5/30追記:processFinish(..)で返すのはforwardではなくredirectがよいです。forwardだとURLがpage1form画面になってしまい、リロードされると変なことになるので・・・)
package springapp2.web;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractWizardFormController;
import org.springframework.web.servlet.view.RedirectView;

import springapp2.service.AllValueForm;
import springapp2.service.InputValidator;

public class SimpleWizardController extends AbstractWizardFormController {

private static String[] commands = new String[] { "page1form", "page2form",
"confirmform" };

public SimpleWizardController() {
setPages(commands);
}

@Override
protected ModelAndView processFinish(HttpServletRequest arg0,
HttpServletResponse arg1, Object arg2, BindException arg3)
throws Exception {
String message = "Input done. (WizardForm sample)";
Map myModel = new HashMap();
myModel.put("message", message);
// return new ModelAndView("entrance", "model", myModel);
return new ModelAndView(new RedirectView("entrance.htm"), "model",
myModel);
}

@Override
protected void validatePage(Object command, Errors errors, int page,
boolean finish) {
AllValueForm form = (AllValueForm) command;
InputValidator validator = (InputValidator) getValidator();
// errors.setNestedPath("order");
switch (page) {
case 0:
validator.validateFirstPage(form, errors);
break;
case 1:
validator.validateSecondPage(form, errors);
}
}

}

設定画面1~設定確認画面を作成します。
それぞれのページのsubmitボタンにご注目ください。
<input type="submit" value="Next" name="_target1">
<input type="submit" value="Next" name="_target2">
<input type="submit" value="Finish" name="_finish">

'_targetN'のNが、SimpleWizardController.setPages(String[])した文字列配列の添え字を指します。
commands[0] ⇒ page1form
commands[1] ⇒ page2form
commands[2] ⇒ confirmform

また_finishボタンを押すとSimpleWizardController.processFinish(..)が呼ばれます。

page1form.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
<title>Page1</title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1>Page 1</h1>
<form:form method="post" commandName="allValueForm">
<table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="right" width="20%">First id:</td>
<td width="20%">
<form:input path="firstId"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
<tr>
<td align="right" width="20%">First value:</td>
<td width="20%">
<form:input path="firstValue"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
</table>
<br>
<input type="submit" value="Next" name="_target1">
</form:form>
<a href="<c:url value="entrance.htm"/>">Home</a>
</body>
</html>

page2form.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
<title>Page2</title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1>Page 2</h1>
<form:form method="post" commandName="allValueForm">
<table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="right" width="20%">Next id:</td>
<td width="20%">
<form:input path="nextId"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
<tr>
<td align="right" width="20%">Next value:</td>
<td width="20%">
<form:input path="nextValue"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
</table>
<br>
<input type="submit" value="Next" name="_target2">
</form:form>
<a href="<c:url value="entrance.htm"/>">Home</a>
</body>
</html>

confirmform.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
<title>Confirm</title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1>Confirm</h1>
<form:form method="post" commandName="allValueForm">
<table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="right" width="20%">First id:</td>
<td width="20%">
<c:out value="${allValueForm.firstId}"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
<tr>
<td align="right" width="20%">First value:</td>
<td width="20%">
<c:out value="${allValueForm.firstValue}"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
<tr>
<td align="right" width="20%">Next id:</td>
<td width="20%">
<c:out value="${allValueForm.nextId}"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
<tr>
<td align="right" width="20%">Next value:</td>
<td width="20%">
<c:out value="${allValueForm.nextValue}"/>
</td>
<td width="60%">
<!-- form:errors path="percentage" cssClass="error"/ -->
</td>
</tr>
</table>
<br>
<input type="submit" value="Finish" name="_finish">
</form:form>
<a href="<c:url value="entrance.htm"/>">Home</a>
</body>
</html>

上記のspringapp2-servlet.xmlに定義を追加します。(太字の部分です)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

<!-- the application context definition for the springapp DispatcherServlet -->

<bean name="/entrance.htm" class="springapp2.web.EntranceController" />

<bean id="simpleWizardController"
class="springapp2.web.SimpleWizardController">
<property name="pages">
<list>
<value>page1form</value>
<value>page2form</value>
<value>confirmform</value>
</list>
</property>
</bean>

<bean name="/page1form.htm"
class="springapp2.web.SimpleWizardController">
<property name="sessionForm" value="true" />
<property name="commandName" value="allValueForm" />
<property name="commandClass" value="springapp2.service.AllValueForm" />
</bean>
<bean name="/page2form.htm"
class="springapp2.web.SimpleWizardController">
<property name="sessionForm" value="true" />
<property name="commandName" value="allValueForm" />
<property name="commandClass" value="springapp2.service.AllValueForm" />
</bean>
<bean name="/confirmform.htm"
class="springapp2.web.SimpleWizardController">
<property name="sessionForm" value="true" />
<property name="commandName" value="allValueForm" />
<property name="commandClass" value="springapp2.service.AllValueForm" />
</bean>

<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView">
</property>
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

</beans>

補足「include.jsp」
<%@ page session="false"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

補足「web.xml」
<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.4" 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-app_2_4.xsd">
<servlet>
<servlet-name>springapp2</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>springapp2</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>

<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>

</web-app>

以上。

【参考】
Springに付属のJpetstoreサンプルから、OrderFormController.javaのソースと設定ファイル
The Spring Framework (Alan P Sexton)

2008年5月28日水曜日

「障害を許さない」プロジェクトが破綻する理由(1)

障害を許さないプロジェクトは簡単に破綻します。

現場経験の長い方なら、これだけでも何となく同意して頂けるかもしれませんね。

逆に経験のない方や、マネジメントの経験(特にユーザー寄り)が長い方は「何を言ってるんだ」と思われるかもしれません。障害を出さないのが当たり前だろう、と。

なぜ「障害を許さない」プロジェクトが破綻するのか。これから何回かに分けてその理由を述べて行きたいと思います。

■「障害を許さない」プロジェクトの特徴

障害は本当に好ましくないものでしょうか。「障害なんか起こらない方がいいに決まってるじゃないか」と思われた方、少し待ってください。確かに本番運用に入って起こる障害は好ましくない。エンドユーザーさんにも迷惑を掛けますし、現場の体力もかかる。しかしプロジェクトの途中で起こる障害はどうでしょうか。「プロジェクトだって同じだ。障害なんか起こらないでスムースに行くのが一番」という声が聞こえてきそうですね。

でも、私に言わせれば「障害が起こらない」というのは「開発者が仕事をしていない」のと同義なのです。言い換えれば開発者が熱心に仕事をすればするほど、障害が発生するものです。障害が発生しないプロジェクトの方がよほど怖い。それはどこかで開発/テストのプロセスが機能不全に陥っている証拠です。

テストは潜在的な欠陥を発見するためのプロセスです。問題がないことを確認するプロセスではない。ソフトウェアには必ず欠陥があります。欠陥を発見し、修正するためにテストサイクルがあります。
障害が出ることを好ましく思わないプロジェクトではこれが逆転します。本来どんどん障害を出すべきテストフェーズが、障害が起きないことを確認する為のテストフェーズにすり代わってしまうのです。

その結果、障害を許さないプロジェクトでは障害への対応方法が全く違ってきます。

障害の発生を推奨するプロジェクトでは、障害を修正した後に速やかに次のテストケースに移ることができます。目的は次の潜在障害の検知です。潜在障害を検知するためにテストですから、テストをどんどん実施するのが望ましい。テストフェーズが上手く回っているプロジェクトで仕事をするのは気持ちのよいものです。障害がどんどん直り、品質が向上していることが体感できます。

ところが、障害を許さないプロジェクトでは発生した障害が望ましくないものとして捉えられます。障害は発生してはならない。あってはならないことが起こってしまった。今後二度とこのようなことを起こさないために、何をすべきなのか。そして執拗な原因追求が始まります。
つまり障害がほとんど不祥事として扱われる。そして単純ミス、思い込み、レビュー漏れが不祥事の原因と考えられてしまいます。
その結果、本来の目的であるはずの潜在バグの洗い出し作業が後回しになり、現状のプロセスや開発者に対する粗探しが始まります。
コミュニケーションをしっかり取っていればこの障害は発生しなかった。では会議の数を増やそう。一見筋が通っているようにも見えます。しかし、はっきり言って手遅れかつ時間のムダと言えましょう。会議を増やすよりもむしろ再テストを行ったりテストケースを増やすほうがバグが効率的に潰せるのは明らかです。
「障害=不祥事」と捉えるプロジェクトではそのことに気がつかない。将来起こりうる障害を洗いだすことよりも、現状の粗探しに囚われてしまうのです。

繰り返しますが将来起こりうる障害を少なくするためには=ソフトウェアの品質を上げるためにはどんどん障害を出して修正して行くべきです。いくら設計書を読み返しても、ソースコードをチェックしても、会議を増やしても、障害はなくなりません

もちろん会議を増やすことによって実際に別の問題が見つかることがあるかもしれません。でもテストケースを増やしたり、テストを行うほうがよほど効率がよい。テストケースの検討を通じて障害が発見されることはめずらしくありません。またテストを行えば少なくともそのテストは上手く行ったという結果が必ず残ります。
障害の発生を許さないプロジェクトでは、調査や机上検証を執拗に繰り返したり、安心したいがためだけに一時的な文書をベンダーに作成させがちです。しかしこれらは潜在障害を洗い出すためには極めて非効率です。このような作業にリソースをつぎ込んでしまうとプロジェクトが疲弊し、結局品質の向上が後回しになってしまうのです。

(続く)

2008年5月27日火曜日

Springサンプルを拡張した感想

「Springを使うと、たかだかcreateとdeleteのためにあわせて20個以上のファイルを変更しなければならないんでしょ?何がうれしいの?」

正しいご指摘です。
確かにcreateもdeleteも極端を言えばServletひとつで出来てしまいます。単純に比較するとSpring上で開発する方が作業量が多い。
しかしながら実際に作業して頂けると分かると思うのですが、ServletやBeanでゴリゴリコーディングするよりも、開発の負担はかなり軽いように見えました。

「本当?わざわざ体力かけて勉強したから負け惜しみ言ってるんじゃないの?それとも勉強した成果がもったいないから無意識にSpringをかばってる?」

もちろん初期の学習体力はそれなりにかかります。それを乗り越えるとSpringが非常に風通しのよいフレームワークであることが分かります。

やはり各コンポーネントがきれいにオブジェクト指向的に分かれているのが大きいです。その結果、
 - DB系(ProductDAOとその実装)
 - ロジック系(ProductManagerとその実装)
 - Bean系
 - コントローラとJSPの描画系(特にtaglibですね)
 - MVCを関連付けるXML設定ファイル(ここも学習が必要です)
上記それぞれを独立して考えることが可能です。つまり、コンポーネントごとの関係とか副作用を机上で検討する必要がありません。また、JUnitで個々のコンポーネントをテストすることもできます。大規模なプロジェクトでも安心して開発できるデザインと言えます。
実際、Springのお作法が分かってからはほとんど悩むことがなく、サクサクと開発できました。

本当によく出来たフレームワークだと思います。
(喧伝されすぎてるなあ、というのはありますけどね)

Springサンプルに削除機能を作成する

続いてProductの削除機能です。
今度はサクサク行きました。注意点はcheckboxからのデータ取得だけです。
spring:bindでDeleteProducts.product.idをbindするとcheckboxで選択した値が取れません(選択されたものだけでなく、全てとれてしまいます)。
そのため"targets"というint配列をBeanに追加し、jsp上でid="targets"、name="targets"というタグを書き込んでいます。

【手順概要】

ProductDAOにインターフェースdeleteProductを追加します。

JdbcProductDaoでdeleteProductを実装します。

JdbcProductDaoTestsにテストメソッドを追加します。

ProductManagerにdeleteProductインターフェースを追加します。

SimpleProductManagerでdeleteProductメソッドを実装します。

DeleteProducts Beanを追加します。

DelteProductValidatorを追加します。

DeleteProductFormControllerを追加します。

deleteProduct画面を追加します。

springapp-servlet.xml、メッセージ、リンクにエントリーを追加します。

【手順詳細】
ProductDAOにインターフェースdeleteProductを追加します。
public void deleteProduct(Product prod);

JdbcProductDaoでdeleteProductを実装します。
    @Override
public void deleteProduct(Product prod) {
logger.info("Delete product: " + prod.getDescription());
int count = getSimpleJdbcTemplate().update(
"delete from products where id = :id",
new MapSqlParameterSource().addValue("id", prod.getId()));
logger.info("Deleted: " + count);
}

JdbcProductDaoTestsにテストメソッドを追加します。
    public void testDeleteProduct() {
List<Product> products = new ArrayList<Product>();
Product p1 = createProduct(200, "dummy10", 100.10);
Product p2 = createProduct(201, "dummy11", 100.20);
products.add(p1);
products.add(p2);
for (Product p : products) {
productDao.insertProduct(p);
}
List<Product> insertedProducts = productDao.getProductList();
boolean checked1 = false;
boolean checked2 = false;
for (Product p : insertedProducts) {
if (p.getId() == p1.getId()) {
dumpProduct(p);
dumpProduct(p1);
assertEquals(true, p.equals(p1));
checked1 = true;
}
if (p.getId() == p2.getId()) {
dumpProduct(p);
dumpProduct(p2);
assertEquals(true, p.equals(p2));
checked2 = true;
}
}
if (!checked1 || !checked2) {
fail("Product not inserted.");
}
productDao.deleteProduct(p1);
productDao.deleteProduct(p2);
List<Product> afterDelete = productDao.getProductList();
for (Product p : afterDelete) {
if (p.getId() == p1.getId()) {
fail("Product p1 not deleted.");
}
if (p.getId() == p2.getId()) {
fail("Product p2 not deleted.");
}
}
}

ProductManagerにdeleteProductインターフェースを追加します。
 public void deleteProduct(Product product);

SimpleProductManagerでdeleteProductメソッドを実装します。
 @Override
public void deleteProduct(Product product) {
productDao.deleteProduct(product);
}

DeleteProducts Beanを追加します。
package springapp.service;

import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class DeleteProducts {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private List<Product> products = null;
int [] targets = null; // ポイント

public void setProducts(List<Product> products) {
this.products = products;
}

public List<Product> getProducts() {
return products;
}

public int[] getTargets() {
return targets;
}

public void setTargets(int[] targets) {
this.targets = targets;
StringBuffer log = new StringBuffer();
log.append("Targets set:");
for(int i = 0; i < targets.length; i++) {
log.append(targets[i]);
log.append(":");
}
logger.info(log);
}
}

DelteProductValidatorを追加します。
package springapp.service;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class DeleteProductsValidator implements Validator {

/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

public boolean supports(Class clazz) {
return DeleteProducts.class.equals(clazz);
}

public void validate(Object obj, Errors errors) {
DeleteProducts dp = (DeleteProducts) obj;
logger.info("Validating ...");
int[] targets = dp.getTargets();
if (targets == null || targets.length == 0) {
logger.info("Delete target not selected.");
errors.rejectValue("products", "error.target-not-specified", null,
"Value required.");
}
logger.info("Validation end.");
}
}

DeleteProductFormControllerを追加します。
package springapp.web;

import java.util.List;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;
import springapp.service.DeleteProducts;
import springapp.service.ProductManager;

public class DeleteProductsFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

private ProductManager productManager;

public ModelAndView onSubmit(Object command) throws ServletException {
DeleteProducts dp = (DeleteProducts) command;
List<Product> products = dp.getProducts();
int[] targets = dp.getTargets();
for (int i = 0; i < targets.length; i++) {
for (Product product : products) {
if (targets[i] == product.getId()) {
productManager.deleteProduct(product);
logger.info("Deleted:" + product);
continue;
}
}
}
return new ModelAndView(new RedirectView(getSuccessView()));
}

protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
DeleteProducts dp = new DeleteProducts();
dp.setProducts(productManager.getProducts());
return dp;
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}

public ProductManager getProductManager() {
return productManager;
}

}

deleteProduct画面を追加します。
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head>
<title><fmt:message key="title"/></title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1><fmt:message key="deleteproducts.heading"/></h1>
<form:form method="post" commandName="deleteProducts">
<c:forEach items="${deleteProducts.products}" varStatus="row">
<spring:bind path="deleteProducts.products[${row.index}].id">
<input type="checkbox"
<!-- "id"ではチェックボックスの値が取得できません -->
name="targets"
id="targets"
value="<c:out value="${status.value}"/>" />
</spring:bind>
<spring:bind path="deleteProducts.products[${row.index}].description">
<c:out value="${status.value}"/>
</spring:bind>
<spring:bind path="deleteProducts.products[${row.index}].price">
<i>$<c:out value="${status.value}"/></i>
</spring:bind>
<br>
</c:forEach>
<form:errors path="products" cssClass="error"/>
<br>
<input type="submit" align="center" value="Execute">
</form:form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>

springapp-servlet.xml
    <bean name="/deleteproducts.htm" class="springapp.web.DeleteProductsFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="deleteProducts"/>
<property name="commandClass" value="springapp.service.DeleteProducts"/>
<property name="validator">
<bean class="springapp.service.DeleteProductsValidator"/>
</property>
<property name="formView" value="deleteproducts"/>
<property name="successView" value="hello.htm"/>
<property name="productManager" ref="productManager"/>
</bean>

メッセージ
deleteproducts.heading=Delete products :: SpringApp
error.target-not-specified=Target not specified!!!

hello.jspからのリンク
    <br><a href="<c:url value="deleteproducts.htm"/>">Delete products</a>

以上

Springサンプルに新規作成機能を作成する

Spring 2.5 付属のチュートリアル「Developing a Spring Framework MVC application step-by-step」に、Productの新規作成機能を追加します。

【手順概要】

ProductDAOにインターフェースinsertProductを追加します。

JdbcProductDaoでinsertProductを実装します。

Productにequalsメソッドを追加します。(テスト用)

JdbcProductDaoTestsにテストメソッドを追加します。

ProductManagerにinsertProductインターフェースを追加します。

SimpleProductManagerでinsertProductメソッドを実装します。

InsertProduct Beanを追加します。

InsertProductValidatoを追加します。

InsertProductFormControllerを追加します。

InsertProduct画面を追加します。

springapp-servlet.xml、メッセージ、リンクにエントリーを追加します。

【手順詳細】

ProductDAOにインターフェースinsertProductを追加します。
 public void insertProduct(Product prod);

JdbcProductDaoでinsertProductを実装します。
    public void insertProduct(Product prod) {
logger.info("Insert product: " + prod.getDescription());
int count = getSimpleJdbcTemplate().update(
"insert into products(id, description, price) values(:id, :description, :price)",
new MapSqlParameterSource().addValue("id", prod.getId())
.addValue("description", prod.getDescription())
.addValue("price", prod.getPrice()));
logger.info("Inserted: " + count);
}

Productにequalsメソッドを追加します。(テスト用です)
※ Doubleの比較には'=='ではなくequalsを使います。当たり前か。しかしTigerからdoubleとDoubleが等価的に使えるようになったので油断してしまいました。
 @Override
public boolean equals(Object obj) {
if (obj instanceof Product) {
Product prod = (Product)obj;
if (id != prod.getId()) {
logger.info("ID unmatch.");
return false;
}
if (!description.equals(prod.getDescription())){
logger.info("Description unmatch.");
return false;
}
if (!price.equals(prod.getPrice())) {
// '==' では比較できない
logger.info("Price unmatch.");
return false;
}
return true;
} else {
logger.info("Not Product instance.");
return false;
}
}

JdbcProductDaoTestsにテストメソッドを追加します。
やっててよかったJUnit。後で役に立ちました。
    private Product createProduct(int id, String desc, double price) {
Product ret = new Product();
ret.setId(id);
ret.setDescription(desc);
ret.setPrice(price);
return ret;
}
private void dumpProduct(Product p) {
logger.info(p);
}

public void testInsertProduct() {
List<Product> products = new ArrayList<Product>();
Product p1 = createProduct(100, "dummy1", 100.10);
Product p2 = createProduct(101, "dummy2", 100.20);
products.add(p1);
products.add(p2);
for (Product p : products) {
productDao.insertProduct(p);
}
List<Product> insertedProducts = productDao.getProductList();
boolean checked1 = false;
boolean checked2 = false;
for (Product p : insertedProducts) {
if (p.getId() == p1.getId()) {
dumpProduct(p);
dumpProduct(p1);
assertEquals(true, p.equals(p1));
checked1 = true;
}
if (p.getId() == p2.getId()) {
dumpProduct(p);
dumpProduct(p2);
assertEquals(true, p.equals(p2));
checked2 = true;
}
}
if (!checked1 || !checked2) {
fail("Product not inserted.");
}
}

ProductManagerにinsertProductインターフェースを追加します。
 public void insertProduct(Product product);

SimpleProductManagerでinsertProductメソッドを実装します。
 @Override
public void insertProduct(Product product) {
List<Product> products = getProducts();
int ids[] = new int[products.size()];
int i = 0;
// Uniqueキーをゴリゴリと作成
for (Product prod: products) {
ids[i] = prod.getId();
i++;
}
Arrays.sort(ids);
int last = ids[ids.length-1];
last++;
product.setId(last);
productDao.insertProduct(product);
}

InsertProduct Beanを追加します。Nothing specialというやつですね。
package springapp.service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class InsertProduct {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private String description;
private double price;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
logger.info("description:"+description+" set.");
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
logger.info("price:"+price+" set.");
}
}

InsertProductValidatorを追加します。
ちゃんと個々の項目にエラーがでます。
package springapp.service;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class InsertProductValidator implements Validator {

private int DEFAULT_MIN_PRICE = 1;
private int minPrice = DEFAULT_MIN_PRICE;

/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

public boolean supports(Class clazz) {
return InsertProduct.class.equals(clazz);
}

public void validate(Object obj, Errors errors) {
InsertProduct is = (InsertProduct) obj;
logger.info("Validating ...");
String desc = is.getDescription();
if (desc == null || desc.equals("")) {
logger.info("Description is null or empty string.");
errors.rejectValue("description", "error.description-not-specified", null,
"Value required.");
}
if (is.getPrice() <= minPrice) {
errors.rejectValue("price", "error.too-cheap",
new Object[] { new Integer(minPrice) },
"Value too low.");
}
logger.info("Validation end.");
}

public void setMinPrice(int i) {
minPrice = i;
}

public int getMinPrice() {
return minPrice;
}

}


InsertProductFormControllerを追加します。
package springapp.web;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.view.RedirectView;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.ServletException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.service.ProductManager;
import springapp.service.InsertProduct;
import springapp.domain.Product;

public class InsertProductFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private ProductManager productManager;

public ModelAndView onSubmit(Object command)
throws ServletException {
InsertProduct is = (InsertProduct) command;
logger.info("Will insert:" + is);
Product product = new Product();
product.setDescription(is.getDescription());
product.setPrice(is.getPrice());
productManager.insertProduct(product);
logger.info(
"returning from PriceIncreaseForm view to " + getSuccessView());
return new ModelAndView(new RedirectView(getSuccessView()));
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}
}

InsertProduct画面を追加します。
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<html>
<head>
<title><fmt:message key="title"/></title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1><fmt:message key="insertproduct.heading"/></h1>
<form:form method="post" commandName="insertProduct">
<table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td align="right" width="20%">Description</td>
<td width="30%">
<form:input path="description"/>
</td>
<td width="50%">
<form:errors path="description" cssClass="error"/>
</td>
</tr>
<tr>
<td align="right">Price</td>
<td>
<form:input path="price"/>
</td>
<td>
<form:errors path="price" cssClass="error"/>
</td>
</tr>
</table>
<br>
<input type="submit" align="center" value="Execute">
</form:form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>

springapp-servlet.xml、メッセージ、リンクにエントリーを追加します。
    <bean name="/insertproduct.htm" class="springapp.web.InsertProductFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="insertProduct"/>
<property name="commandClass" value="springapp.service.InsertProduct"/>
<property name="validator">
<bean class="springapp.service.InsertProductValidator"/>
</property>
<property name="formView" value="insertproduct"/>
<property name="successView" value="hello.htm"/>
<property name="productManager" ref="productManager"/>
</bean>

error.description-not-specified=Description not specified!!!

<br><a href="<c:url value="insertproduct.htm"/>">Insert product</a>

以上。少し慣れてきました。

2008年5月26日月曜日

Spring MVCでチェックボックスを使う(2)~解説

前の投稿のままではあまりに不親切なのでポイントを絞って解説します。

【SelectProducts.java】
 private int[] ids = null;

チェックボックスは配列に格納されるハズだという直感に従い int[] ids を定義しました。
(全体を精読できてはいませんがドキュメントにはこのような記載はなかったはず・・・)

【SelectInventorysFormController.java】
 protected ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
SelectProducts sp = (SelectProducts) command;
HttpSession session = request.getSession();
session.setAttribute("SelectProducts", sp);
return new ModelAndView(new RedirectView(getSuccessView()));
}

選択された"ids"を次画面に引き渡すため、オブジェクトをSessionに登録しておきます。

【selectproducts.jsp】
    <form:form method="post" commandName="selectProducts">
<c:forEach items="${selectProducts.products}" var="prod" varStatus="row">
<input type="checkbox" id="ids" name="ids" value="<c:out value="${prod.id}"/>">
<c:out value="${prod.description}"/>
<i>$<c:out value="${prod.price}"/></i><br><br>
</c:forEach>
<input type="submit" align="center" value="Execute">
</form:form>

ゴリゴリとチェックボックスを生成しています。
出来上がりは以下のようになります。
    <form id="selectProducts" action="/springapp/selectproducts.htm" method="post">
<input type="checkbox" id="ids" name="ids" value="1">
Lamp
<i>$10.0</i><br><br>
<input type="checkbox" id="ids" name="ids" value="2">
Table
<i>$10.0</i><br><br>
<input type="checkbox" id="ids" name="ids" value="3">
Chair
<i>$10.0</i><br><br>
<input type="submit" align="center" value="Execute">
</form>


【SetPrices.java】
 private List<Product> products = new ArrayList<Product>();
private int[] ids = null;
private String[] descriptions = null;
private double[] prices = null;
private static final int DEFALUT_NUM = 3;
private int position = 0;

後日検証する予定ですが、ids, descriptions, prices, positionは不要と思われます。

【SetPricesValidator.java】
 public void validate(Object obj, Errors errors) {
SetPrices ps = (SetPrices) obj;
for (Product product : ps.getProducts()) {
logger.info("Validating ...");
if (product.getPrice() == null) {
logger.info("Price is null.");
errors.rejectValue("prices", "error.price-not-specified", null,
"Value required.");
} else {
logger.info(product);
if (product.getPrice() <= minPrice) {
errors.rejectValue("prices", "error.too-cheap",
new Object[] { new Integer(minPrice) },
"Value too low.");
}
}
logger.info("Validation end.");
}
}

直感的に納得できるコーディングに落ち着きました。さすがSpringと思っています。

【SetPricesFormController】
 protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
SetPrices ret = new SetPrices();
SelectProducts sp = (SelectProducts) request.getSession().getAttribute(
"SelectProducts");
int[] i = sp.getIds();
for (Product product : productManager.getProducts()) {
for (int j = 0; j < i.length; j++) {
if (product.getId() == i[j]) {
ret.addProduct(product);
break;
}
}
}
return ret;
}

(List)SetPrices.getProducts()で取るための事前の仕込みです。
このようにしておけば後述の"<c:forEach items="${setPrices.products}" varStatus="row">"でJSPからアクセスできます。(これはJSTLのデザインが素晴らしいですね)
 public ModelAndView onSubmit(Object command) throws ServletException {
List<Product> products = ((SetPrices) command).getProducts();

for (Product product : products) {
productManager.setPrice(product.getId(), product.getPrice());
}

return new ModelAndView(new RedirectView(getSuccessView()));
}

絶対上手く行かないだろうと思っていたのでList<Product> products = ((SetPrices) command).getProducts(); でproductsが取れるのには驚きました。素晴らしい。

【setprices.jsp】
<form:form method="post" commandName="setPrices">
<c:forEach items="${setPrices.products}" varStatus="row">
<spring:bind path="setPrices.products[${row.index}].id">
<input type="hidden"
name="<c:out value="${status.expression}"/>"
id="<c:out value="${status.expression}"/>"
value="<c:out value="${status.value}"/>" />
</spring:bind>
<spring:bind path="setPrices.products[${row.index}].description">
<c:out value="${status.value}"/>
</spring:bind>
<spring:bind path="setPrices.products[${row.index}].price">
<input type="text"
name="<c:out value="${status.expression}"/>"
id="<c:out value="${status.expression}"/>"
value="<c:out value="${status.value}"/>" />
</spring:bind>
<br>
</c:forEach>
<form:errors path="prices" cssClass="error"/>

上記タグによって、以下のHTMLが生成されました。(全てにチェックを入れたパターン。もちろんチェックされ具合によって動的にリストが変わります。)
<form id="setPrices" action="/springapp/setprices.htm" method="post">
<input type="hidden"
name="products[0].id"
id="products[0].id"
value="1" />
Lamp
<input type="text"
name="products[0].price"
id="products[0].price"
value="10.0" />
<br>
<input type="hidden"
name="products[1].id"
id="products[1].id"
value="2" />
Table
<input type="text"
name="products[1].price"
id="products[1].price"
value="10.0" />
<br>
<input type="hidden"
name="products[2].id"
id="products[2].id"
value="3" />
Chair
<input type="text"
name="products[2].price"
id="products[2].price"
value="10.0" />
<br>
<br>
<input type="submit" align="center" value="Execute">
</form>

以上。

Spring MVCでチェックボックスを使う

前回はradioボタンでしたが、今回はチェックボックスを使ってみます。
コントローラはSimpleFormControllerを使います。
チェックボックスで受け付けるため、配列が入力されることが前提となります。
また件数可変の一覧表示画面から、spring:bindタグを使って動的にデータをBeanに入れています。

【手順概要】

変更対象選択用Bean(SelectProducts)を追加します。

SelectInventorysControllerを追加します。

変更対象複数選択画面を追加します。

springapp-servlet.xmlにエントリーを追加します。

メッセージを追加します。

リンクを追加します。

価格設定用Bean(SetPrices)を追加します。

バリデータ(SetPricesValidator)を追加します。

SetPricesFormControllerを追加します。

価格設定用画面を追加します。

メッセージを追加します。

springapp-servlet.xmlにエントリーを追加します。

【手順詳細】

SelectProducts
チェックボックスなので配列を取ります。
package springapp.service;

import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class SelectProducts {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

private List<Product> products = null;

private int[] ids = null;

public List<Product> getProducts() {
return products;
}

public void setProducts(List<Product> products) {
this.products = products;
}

public int[] getIds() {
return ids;
}

public void setIds(int[] ids) {
this.ids = ids;
StringBuffer log = new StringBuffer();
for(int i = 0; i < ids.length; i++) {
log.append("id[");
log.append(i);
log.append("]=");
log.append(ids[i]);
log.append("; ");
}
logger.info(log);
}
}

SelectInventorysFormControllerを追加します。
package springapp.web;

import org.springframework.validation.BindException;
import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.service.ProductManager;
import springapp.service.SelectProducts;

public class SelectInventorysFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private ProductManager productManager;

protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
SelectProducts selectProducts = new SelectProducts();
logger.info("productManager:" + productManager.getProducts().size());
selectProducts.setProducts(productManager.getProducts());
logger.info("selectProd:" + selectProducts.getProducts().size());
return selectProducts;
}

@Override
protected ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
logger.info("Executed.");
SelectProducts sp = (SelectProducts) command;
StringBuffer log = new StringBuffer();
log.append("SelectProduct.getIds():");
int[] i = sp.getIds();
for (int j = 0; j < i.length; j++) {
log.append("id[");
log.append(j);
log.append("]=");
log.append(i[j]);
log.append("; ");
}
logger.info(log);
HttpSession session = request.getSession();
session.setAttribute("SelectProducts", sp);
return new ModelAndView(new RedirectView(getSuccessView()));
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}

public ProductManager getProductManager() {
return productManager;
}

}

変更対象複数選択画面を追加します。
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head><title><fmt:message key="title"/></title></head>
<body>
<h1><fmt:message key="selectproducts.heading"/></h1>
<h3>Products</h3>
<form:form method="post" commandName="selectProducts">
<c:forEach items="${selectProducts.products}" var="prod" varStatus="row">
<input type="checkbox" id="ids" name="ids" value="<c:out value="${prod.id}"/>">
<c:out value="${prod.description}"/>
<i>$<c:out value="${prod.price}"/></i><br><br>
</c:forEach>
<input type="submit" align="center" value="Execute">
</form:form>
<br>
<a href="<c:url value="hello.htm"/>">back</a><br>
<br>
</body>
</html>

springapp-servlet.xmlにエントリーを追加します。
    <bean name="/selectproduct.htm" class="springapp.web.SelectInventoryFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="selectProduct"/>
<property name="commandClass" value="springapp.service.SelectProduct"/>
<property name="formView" value="selectproduct"/>
<property name="successView" value="setprice.htm"/>
<property name="productManager" ref="productManager"/>
</bean>

メッセージを追加します。
selectproducts.heading=Select Products :: SpringApp

トップページ(hello.jsp)にリンクを追加します。
<br><a href="<c:url value="selectproducts.htm"/>">Select products</a>

価格設定用Bean(SetPrices)を追加します。
妙にリッチな作りになっていますが(結果的に無意味だった)試行錯誤の結果です。
恐らく(面倒くさいので検証していませんが)setProducts(List<Product> products) だけでよいのではないかと思います。(あるべき姿は後日調査予定)
package springapp.service;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class SetPrices {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private List<Product> products = new ArrayList<Product>();
private int[] ids = null;
private String[] descriptions = null;
private double[] prices = null;
private static final int DEFALUT_NUM = 3;
private int position = 0;

public SetPrices(int num) {
initialize(num);
}

public SetPrices() {
initialize(DEFALUT_NUM);
}

private void initialize(int number) {
ids = new int[number];
descriptions = new String[number];
prices = new double[number];
}

public void setProducts(List<Product> products) {
this.products = products;
}

public void addProduct(Product prod) {
products.add(prod);
ids[position] = prod.getId();
descriptions[position] = prod.getDescription();
prices[position] = prod.getPrice();
position++;
}

public List<Product> getProducts() {
return products;
}

public int[] getIds() {
return ids;
}

public void setIds(int[] ids) {
this.ids = ids;
}

public String[] getDescriptions() {
return descriptions;
}

public void setDescriptions(String[] descriptions) {
this.descriptions = descriptions;
}

public double[] getPrices() {
return prices;
}

public void setPrices(double[] prices) {
this.prices = prices;
}
}

バリデータ(SetPricesValidator)を追加します。
package springapp.service;

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;

public class SetPricesValidator implements Validator {

private int DEFAULT_MIN_PRICE = 1;
private int minPrice = DEFAULT_MIN_PRICE;

/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

public boolean supports(Class clazz) {
return SetPrices.class.equals(clazz);
}

public void validate(Object obj, Errors errors) {
SetPrices ps = (SetPrices) obj;
for (Product product : ps.getProducts()) {
logger.info("Validating ...");
if (product.getPrice() == null) {
logger.info("Price is null.");
errors.rejectValue("prices", "error.price-not-specified", null,
"Value required.");
} else {
logger.info(product);
if (product.getPrice() <= minPrice) {
errors.rejectValue("prices", "error.too-cheap",
new Object[] { new Integer(minPrice) },
"Value too low.");
}
}
logger.info("Validation end.");
}
}

public void setMinPrice(int i) {
minPrice = i;
}

public int getMinPercentage() {
return minPrice;
}

public void setMaxPercentage(int i) {
minPrice = i;
}

public int getMaxPercentage() {
return minPrice;
}

}

SetPricesFormControllerを追加します。
package springapp.web;

import java.util.List;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import springapp.domain.Product;
import springapp.service.ProductManager;
import springapp.service.SelectProducts;
import springapp.service.SetPrices;

public class SetPricesFormController extends SimpleFormController {
/** Logger for this class and subclasses */
protected final Log logger = LogFactory.getLog(getClass());

private ProductManager productManager;

public ModelAndView onSubmit(Object command) throws ServletException {
List<Product> products = ((SetPrices) command).getProducts();

for (Product product : products) {
productManager.setPrice(product.getId(), product.getPrice());
}

return new ModelAndView(new RedirectView(getSuccessView()));
}

protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
SetPrices ret = new SetPrices();
SelectProducts sp = (SelectProducts) request.getSession().getAttribute(
"SelectProducts");
int[] i = sp.getIds();
for (Product product : productManager.getProducts()) {
for (int j = 0; j < i.length; j++) {
if (product.getId() == i[j]) {
ret.addProduct(product);
break;
}
}
}
return ret;
}

public void setProductManager(ProductManager productManager) {
this.productManager = productManager;
}

public ProductManager getProductManager() {
return productManager;
}

}

価格設定用画面を追加します。
恐らくはここがキモになります。
spring:bindタグを使っています。
参考サイト:Dynamic list binding in Spring MVC
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head>
<title><fmt:message key="title"/></title>
<style>
.error { color: red; }
</style>
</head>
<body>
<h1><fmt:message key="setprices.heading"/></h1>
<form:form method="post" commandName="setPrices">
<c:forEach items="${setPrices.products}" varStatus="row">
<spring:bind path="setPrices.products[${row.index}].id">
<input type="hidden"
name="<c:out value="${status.expression}"/>"
id="<c:out value="${status.expression}"/>"
value="<c:out value="${status.value}"/>" />
</spring:bind>
<spring:bind path="setPrices.products[${row.index}].description">
<c:out value="${status.value}"/>
</spring:bind>
<spring:bind path="setPrices.products[${row.index}].price">
<input type="text"
name="<c:out value="${status.expression}"/>"
id="<c:out value="${status.expression}"/>"
value="<c:out value="${status.value}"/>" />
</spring:bind>
<br>
</c:forEach>
<form:errors path="prices" cssClass="error"/>
<br>
<input type="submit" align="center" value="Execute">
</form:form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>

メッセージを追加します。
error.price-not-specified=Price not specified!!!
setprices.heading=Set Prices :: SpringApp

springapp-servlet.xmlにエントリーを追加します。
    <bean name="/setprices.htm" class="springapp.web.SetPricesFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="setPrices"/>
<property name="commandClass" value="springapp.service.SetPrices"/>
<property name="validator">
<bean class="springapp.service.SetPricesValidator"/>
</property>
<property name="formView" value="setprices"/>
<property name="successView" value="hello.htm"/>
<property name="productManager" ref="productManager"/>
</bean>

以上。疲れた・・・