ラベル DI の投稿を表示しています。 すべての投稿を表示
ラベル DI の投稿を表示しています。 すべての投稿を表示

2012年4月21日土曜日

Hibernate でデータアクセス(2)

前回の続きです。

サービスレイヤー
DAO レイヤーと連携しながら、コントローラーの要求に応じたサービスを提供するレイヤーです。DAO レイヤーと同様(というか Spring Framework における開発の基本?)、インターフェースを定義し、その実装クラスを作ります。

AccountService.java(インターフェース)
新規アカウントを作成する makeAccount() と UserProfile を取得する getProfile() を定義してます。

package wrider.service;

import java.util.Map;

import wrider.model.IdCard;
import wrider.model.UserProfile;

public interface AccountService {
  
  public Map makeAccount(UserProfile userProfile);
  
  public UserProfile getProfile(IdCard idCard);

}

AccountServiceImpl.java(実装クラス)
@Service アノテーションでこのクラスがサービスコンポーネントであることを示しています。尚、ProfileManager インターフェースの実装クラス ProfileManagerImple には、@Repository アノテーションを付けてリポジトリーコンポーネントであることを示しています。

[servlet-name]-servlet.xml で <context:component-scan/> が有効になっている場合、Spring Framework は、@Autowired されたフィールドの型に該当するインターフェースの実装クラスを探して DI します。実装クラスに付けた @Service や @Repository は、この時の目印になります。

ProfileManager フィールドに @Autowired を付けて、 ProfileManagerImpl を DI しています。

makeAccount()メソッドは、コントローラーから受け取った UserProfile オブジェクトを profileManager の callAddUserProfile()メソッドに渡し、処理結果を Map で受け取ります。getProfile()メソッドは、コントローラーから IdCard オブジェクトを受け取り、profileManager の getProfile()を呼び出す際、パラメーターとして渡します。

package wrider.service;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import wrider.dao.ProfileManager;
import wrider.model.IdCard;
import wrider.model.UserProfile;

@Service
public class AccountServiceImpl implements AccountService {
  
  @Autowired
  private ProfileManager profileManager;
  
  public Map<String, Object> makeAccount(UserProfile userProfile) {
    return this.profileManager.callAddUserProfile(userProfile);
  }
  
  public UserProfile getProfile(IdCard idCard) {
    return this.profileManager.getUserProfile(idCard);
  }
  
}

コントローラー
テーブルスキーマの定義から DAO レイヤー、サービスレイヤーと遡って、ようやくコントローラーに辿りつきました。『CGLIB Proxy で捕捉エリアを拡大』までに散々手を加えた AccountController クラスに、先述した AccountService を利用するメソッドを追加します。

AccountController.java(抜粋)
まず、@Autowired で AccountService を DI しています。/account/login.html および /account/register.html に対する GET リクエストを受けた場合は、単純に各 URL に対応するビュー名を返すだけです。
package wrider.controller;
    :
@Controller
@SessionAttributes({"idCard", "userProfile"})
public class AccountController {
  
  @Autowired
  private AccountService accountService;
  
  @ModelAttribute("idCard")
  public IdCard makeIdCard() {
    return new IdCard();
  }
  
  @ModelAttribute("userProfile")
  public UserProfile makeUserProfile() {
    return new UserProfile();
  }
    :
  @RequestMapping(value="/account/login.html", method=RequestMethod.GET)
  public String login() {
    return "account/login";
  }
  
    :
  @RequestMapping(value="/account/register.html", method=RequestMethod.GET)
  public String register() {
    return "account/registrationForm";
  }
    :
}

login()メソッド:
ログインフォームからの POST リクエストで実行される login()メソッドは、引数として フォームへの入力データを含む IdCard とバリデーション結果を格納する BindingResult のほか、@ModelAttribute でアノテートされた UserProfile オブジェクトを受け取ります。これに accountService.getProfile(idCard) の実行結果がバインドされます。UserProfile オブジェクトは、@SessionAttributes に登録されているため、セッションが有効である間は維持されます。認証成功後、取得した UserProfile を ModelAndView オブジェクトに登録して /welcome/home.html にリダイレクトします。

@RequestMapping(value="/account/login.html", method=RequestMethod.POST)
  public ModelAndView login(
      @ModelAttribute("userProfile") UserProfile userProfile,
      @Valid IdCard idCard,
      BindingResult br) {
    
    ModelAndView mav = new ModelAndView();
    
    if (br.hasErrors()) {
      mav.getModel().putAll(br.getModel());
      mav.setViewName("account/login");
    }
    else {
      userProfile = accountService.getProfile(idCard);
      if (userProfile == null) {
        mav.setViewName("account/login");
      }
      else {
        mav.addObject("userProfile", userProfile);
        mav.setViewName("redirect:../welcome/home.html");
      }
    }
    return mav;
  }

尚、引数の順序について若干の注意が必要です。リファレンス“16.3.3.1 Supported method argument types”の最後に『BindingResult(またはError)引数は、モデルオブジェクト(引数)の直後に続かなければならない。Spring は、先行する各モデルオブジェクトのために BindingResult インスタンスを生成する』という旨が書いてあります。確かに、BindingResult が @Valid でアノテートした引数の後に記述されていないと「バリデーション結果を入れる場所が無い」と怒られます。

register()メソッド:
accountService.makeAccount(userProfile) の実行結果は、HashMap型で返されます。キー「result」には成功(true)/失敗(false)、「newpid」には割り当てられた pid(profileId)が入ります。アカウント作成が成功した場合は /welcome/home.html にリダイレクトし、失敗の場合は再度、アカウント作成フォームを表示します。

@RequestMapping(value="/account/register.html", method=RequestMethod.POST)
  public ModelAndView register(
      @Valid UserProfile userProfile, 
      BindingResult br) {
    
    ModelAndView mav = new ModelAndView();
    
    if (br.hasErrors()) {
      mav.getModel().putAll(br.getModel());
      mav.setViewName("account/registrationForm");
    }
    else {
      
      HashMap<String, Object>notice = (HashMap<String, Object>)accountService.makeAccount(userProfile);
      
      if (Boolean.parseBoolean(notice.get(Items.RESULT).toString())) {
        mav.setViewName("redirect:/welcome/home.html");
      }
      else {
        mav.setViewName("account/registrationForm");
      }
      
    }
    
    return mav;
  }

次回は、Hibernate, c3p0 関連の設定を予定しています。

2012年4月2日月曜日

DI による Validator の再利用


今回の課題は「Validator と MessageSource」で作成したパーツを再利用した登録フォームにします。

データオブジェクトの再利用
とりあえず登録フォームの入力項目は以下のように考えました。
  • メールアドレス: email
  • パスワード: password
  • 名字: firstName
  • 名前: lastName
この中で「メールアドレス」と「パスワード」は前に作った IdCard と重複しているので、登録フォーム用のデータオブジェクト(UserProfile)には、これを再利用することにします。

UserProfile.java
このクラスは IdCard クラスを extends しています。名字(firstName)と名前(lastName)は、プロパティとそれらに対応する setter/getter を定義していますが、メールアドレス(email)とパスワード(password)については、スーパークラスの setter/getter を呼び出しています。
package wrider.model;

public class UserProfile extends IdCard {
  
  private String lastName;
  
  public String getLastName() {
    return this.lastName;
  }
  
  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
  
  private String firstName;
  
  public String getFirstName() {
    return this.firstName;
  }
  
  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }
  
  public String getEmail() {
    return super.getEmail();
  }
  
  public void setEmail(String email) {
    super.setEmail(email);
  }
  
  public String getPassword() {
    return super.getPassword();
  }
  
  public void setPassword(String password) {
    super.setPassword(password);
  }

}

Validator の再利用
Validator は、IdCard 用の IdCardValidator を DI(Dependency injection)します。

UserProfileValidator.java
リファレンスの“6.2 Validation using Spring's Validator interface”に習い、コンストラクター引数として IdCardValidator を DI しています。この方法を Constructor-based DI というらしいです。

上記 UserProfile クラスと同じように登録フォーム独自の項目である「名字」と「名前」の未入力チャックとエラー情報の reject を ValidationUtilsrejectIfEmptyOrWhitespace(..) メソッドで行い、「メールアドレス」と「パスワード」に関しては、DI した IdCardValidator をinvokeValidator(..) メソッドで呼び出して検証させます。
package wrider.validator;

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

import wrider.model.UserProfile;

public class UserProfileValidator implements Validator {
  
  private final Validator idCardValidator;
  
  private static final String ERROR_FIELD_EMPTY = "error.field.empty";
  
  public UserProfileValidator(Validator idCardValidator) {
    this.idCardValidator = idCardValidator;
  }
  
  public boolean supports(Class<?> clazz) {
    return UserProfile.class.isAssignableFrom(clazz);
  }
  
  public void validate(Object target, Errors errors) {
    
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", ERROR_FIELD_EMPTY);
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", ERROR_FIELD_EMPTY);
    
    UserProfile userProfile = (UserProfile)target;
    
    ValidationUtils.invokeValidator(idCardValidator, userProfile, errors);
    
  }
}

Validator の登録
新しく作成した UserProfileValidator を applicationContext.xml に登録します。

applicationContext.xml(抜粋)
リファレンスの“4.4.1.1 Constructor-based dependency injection”に習い <constructor-arg> 要素で userProfileValidator ビーンのコンストラクター引数として idCardValidator を DI するよう指示しています。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    :
  http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
  
  <!-- Validator -->
  <bean id="idCardValidator"
    class="wrider.validator.IdCardValidator"/>
  
  <bean id="userProfileValidator"
    class="wrider.validator.UserProfileValidator">
    <constructor-arg ref="idCardValidator"/>
  </bean>
    :
</beans>

エラーコードの修正
今回は IdCard と UserProfile のバリデーションエラーに共通のエラーコードで対応したいので、以下のように errors.xml を修正しました。

修正前 [エラーコード].[オブジェクト名].[フィールド名]
 ↓
修正後 [エラーコード].[フィールド名]

errors.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment>field and form level error messages</comment>
  <entry key="error.field.empty.email">必須です</entry>
  <entry key="error.field.illigal.email">あなたのメルアドです</entry>
  <entry key="error.field.empty.password">必須です</entry>
  <entry key="error.field.illigal.password">内容をお確かめ下さい</entry>
  <entry key="error.field.empty.firstName">必須です</entry>
  <entry key="error.field.empty.lastName">必須です</entry>
  <entry key="error.form.invalid.idCard">入力ミスがあります</entry>
  <entry key="error.form.invalid.userProfile">入力ミスがあります</entry>
</properties>

registrationForm.jsp(抜粋)
登録フォームの jsp です。とりあえず先の login.jsp に名字と名前を追加して、<form:form> タグの modelAttribute と、POST リクエストを投げる先を変えただけです。
<body>
  <h1>WriDer's Demo Site</h1>
  <form:form action="register.html" method="POST" modelAttribute="userProfile">
    <label for="email">メールアドレス</label>
    <input type="text" id="email" name="email" value="${userProfile.email}"/>
    <span class="red"><form:errors path="email"/></span>
    <br />
    <label for="password">パスワード</label>
    <input type="password" id="password" name="password" value="${userProfile.password}"/>
    <span class="red"><form:errors path="password"/></span>
    <br />
    <label for="firstName">名字</label>
    <input type="text" id="firstName" name="firstName" value="${userProfile.firstName}"/>
    <span class="red"><form:errors path="firstName"/></span>
    <br />
    <label for="lastName">名前</label>
    <input type="text" id="lastName" name="lastName" value="${userProfile.lastName}"/>
    <span class="red"><form:errors path="lastName"/></span>
    <br />
    <input type="submit" value="決定"/>
  </form:form>
</body>

コントローラーの変更
今回の登録フォームも AccountController クラスで対応します。@RequestMapping アノテーションで /account/register.html に対する GET/POST リクエストを振り分けています。POST で実行される register メソッドで userProfileValidator を呼び出し、バリデーションを行っています。

また、@SessionAttributes, @Autowired, @ModelAttribute アノテーション、および import に UserProfile に関する記述を追加しています。

AccountController.java(抜粋)
package wrider.controller;
    :
import wrider.model.UserProfile;

@Controller
@SessionAttributes({"idCard", "userProfile"})
public class AccountController {
  
  @Autowired
  private Validator idCardValidator;
  
  @Autowired
  private Validator userProfileValidator;

    :
  @ModelAttribute("userProfile")
  public UserProfile makeUserProfile() {
    return new UserProfile();
  }
    :

  @RequestMapping(value="/account/register.html", method=RequestMethod.GET)
  public String register() {
    return "account/registrationForm";
  }
  
  @RequestMapping(value="/account/register.html", method=RequestMethod.POST)
  public ModelAndView register(UserProfile userProfile, BindingResult br) {
    
    this.userProfileValidator.validate(userProfile, br);
    
    ModelAndView mav = new ModelAndView();
    mav.getModel().putAll(br.getModel());
    mav.setViewName("account/registrationForm");
    return mav;
  }
    :
}

実行
図はブラウザーで /account/register.html にアクセスし、「名前」を未入力にして submit した後の画面です。

POST リクエストを受けた Spring Framework は IdCard インスタンスに「メールアドレス」と「パスワード」、UserProfile インスタンスに「名字」「名前」をセットし、AccountController の register(..)メソッドを呼び出します。同メソッドは UserProfileValidator のバリデーション結果を BindingResult から取り出して ModelAndView にセットします。

Java は元々、コンポーネントを再利用する仕掛けを持っていますが、Spring Framework が提供する DI(または IoC)の仕掛けを使えば、依存関係を記した XML(やアノテーション)に従って、インスタンス化や注入を行ってくれます。使いたいコンポーネントを直に import して new する頻度を減らせるので、うまく使いこなせば大規模アプリケーションの改修なども楽になるかも、と感じました。