Springについて_「SpringFramework超入門〜やさしくわかるWebアプリ開発〜」を読んで
目次
- 目次
- 概要
- SpringFrameworkとは
- アプリケーションのレイヤ化
- DispatcherServlet
- リクエストハンドラメソッド
- サニタイズ
- Thymeleaf(タイムリーフ)
- Viewからのリクエストパラメータの受け取り方
- POJO
- バリデーションチェック
- form-backing bean(Formクラス)
- プレースホルダ
- ジェネリッククラス
- トランザクション
- application.properties
- Flash Scope(フラッシュスコープ)
- アノテーション
- リダイレクト
- 【メモ】クライアントに同画面を返却する場合でも、Formの状態によって処理の走らせ方を変える
- 所感
概要
Springについて学ぶ為にSpringFramework超入門〜やさしくわかるWebアプリ開発〜
*1を読んだので、重要そうなポイントを以下に纏める。
SpringFrameworkとは
Javaアプリケーション開発におけるフレームワーク。単に「Spring」とも呼ばれる。
開発が楽になる様々な機能を提供してくれていて、機能毎に下記の様なプロジェクトが存在する。
SpringBoot
Springアプリケーションを煩雑な設定をせず迅速に作成する為の機能を提供している。
Springが多機能になりすぎて、逆に開発をスタートする際のコストが大きくなってしまったという課題を解決してくれる。
Springプロジェクト
Spring MVC
Webアプリを簡単に作成する為の機能を提供。
Spring Data
データアクセスの為の機能を提供。
Spring Batch
バッチ処理機能を提供。
Spring Security
認証・認可の機能を提供。
Springコア
Spring DI
依存性注入の機能を提供。
Spring AOP
アスペクト指向プログラミングの機能を提供。
アプリケーションのレイヤ化
アプリケーションを理解・管理しやすくする為に、アプリケーション全体をひと纏まりとして捉えるのではなく、役割によってレイヤ化する。
DDD(ドメイン駆動開発)で定義されているレイヤは下記3層。また、各レイヤは複数のコンポーネントを包括する。
- Application層
- Domain層
- Infrastructure層
Application層とInfrastructure層はDomain層に依存して良いが、Domain層は他2層から疎な状態で、再利用可能でなくてはならない。
つまり、Domain層に変更が加わった際に他2層に変更が生じても良いが、他2層に変更が生じた際にDomain層に変更が生じてはいけない。
Application層
クライアントとの入出力となるUIを提供したり、リクエストを基にドメイン層を呼び出すなど、アプリケーションを構築する為のレイヤ。
この層はなるべく薄く保たれるべきなので、ビジネスルールを含んではいけない。
包括するコンポーネントは下記の通り。
Controller
クライアントからのRequestを受け取り、Domain層の適したServiceを呼び出す。
処理結果をViewに返却する。
View
クライアントへのUIを提供する。
Form
クライアントとController間でやり取りするデータを格納する入れ物。
具体的には、HTMLのformタグ内のデータを格納。(「Form」という名前の由来は恐らくこれ。)
Domain層がApplication層に依存しない為に、Application層→Domain層にデータを引き渡す際は、Formのまま渡すのではなく、Domain層のDomain Objectに変換してから引き渡すべき。また、その変換処理はApplication層(Controller)で行う。
Domain層
アプリケーションのコアとなるビジネスルール(業務処理)を担当し、Entityに対するサービス処理を提供するレイヤ。
包括するコンポーネントは下記の通り。
Service
業務処理を提供する。
Domain Object
- 業務処理を行う中で発生する管理すべきデータの入れ物となるクラス。
- その1種である
Entity
は、DBとアプリがデータをやり取り(アプリ→DB
,DB→アプリ
)する際の、データの入れ物となるクラス。- Application層のFormに格納されていたデータがEntityに格納される。
DAO,DTOデザインパターン
のDTO
がこれに当たる。- フィールドとアクセッサ(ゲッター,セッター)で構成され、クラス名はDBテーブル名と同じ名前にする。
- DBテーブルの1レコードが1クラスに対応し、クラスが保有する各フィールドが対象レコードの各フィールド(各カラムの値)に対応する。
Repository(リポジトリ)
- DBとアプリがデータをやり取り(
アプリ→DB
,DB→アプリ
)する際に、DBへのCRUD処理機能を提供するインターフェース。 - 実体はInfrastructure層の
RepositoryImpl
で実装するため、 どのようなデータアクセスが行われているかについての情報は持たない。DAO,DTOデザインパターン
のDAO
がこれに当たる。
Infrastructure層
Domain Object(Entityなど)にCRUD処理(永続化)を提供するレイヤ。
包括するコンポーネントは下記の通り。
RepositoryImpl
- Domain層のRepositoryインターフェースを実装したクラスで、実際のCRUD処理を提供する。
- 必ず、インターフェースを定義したうえで実装する。
- 使用する側(Domain層)のクラスが、クラス依存ではなくインターフェース依存でRepositoryを利用できるようにし、依存性を低くする為。
Repository
インターフェースがCrudRepository
クラス(Springが提供)を継承する事で、自動的にCRUDメソッドが利用可能になる。
O/R Mapper
にSpringDataJDBCを使用する場合は、実装したRepository(インターフェース)から自動生成されるため、実装不要。
O/R Mapper
DispatcherServlet
フロントコントローラ
と呼ばれ、クライアントからの全てのリクエストを受信してくれる。フロントコントローラパターン
と呼ばれる、デザインパターンの1種。
- リクエストを受信した後に、リクエストURLを基に然るべき
リクエストハンドラメソッド
を呼び出す。
リクエストハンドラメソッド
- MVCの
Controller
の中に定義されるメソッドで、クライアントからのリクエストを処理する。 - リクエストURLとリクエストメソッド(
GET
orPOST
)をアノテーションで記載する事で、リクエスト毎に正しいリクエストハンドラメソッド
を呼び出せる。 - 戻り値として、クライアントに返却すべき
View
の名前をDispatcherServlet
にreturnする事で、DispatcherServlet
はその名前に応じたView
に、HTMLを生成させクライアントに返却させる。
サニタイズ
- 危険なコードやデータを変換や除去する事で、無力化する処理。
- 例えば、Webサイトの入力フォームに悪意あるコード等が入力された際に、無力化する。
Thymeleaf(タイムリーフ)
SpringMCV
フレームワークにて推奨されている、動的にView
を生成する為のテンプレートエンジン。- HTMLベースのテンプレと、動的データをバインド(組み合わせる)してViewを構成する。
- JavaのWebアプリの
View
としてはJSP
がよく利用されるが、JSP
ファイルはWebブラウザが読み込めないため、開発段階でWebブラウザにてViewを表示して明示確認できないという課題が有る。 - 一方で、
Thymeleaf
はHTMLベースのため、ファイルをWebブラウザ上で明示確認しながら開発を進める事ができる。- そのため、UI(View)デザイナーとの分業が簡単になる。
Viewからのリクエストパラメータの受け取り方
@RequestParam
アノテーションを使う方法とForm
クラスを用いる方法の2種類有る。
@RequestParamアノテーションを利用する方法
- リクエストハンドラメソッドの引数に
@RequestParam
アノテーションを付与する事で、リクエストパラメータを引数として受け取る事ができる。View
のname
(入力項目名)と同じ引数名を宣言する。
- リクエストパラメータが増えるほど引数も増えるので、冗長になってしまう可能性が有る。
Formクラスを利用する方法
- Formは、リクエストパラメータを格納する為のクラス。
- リクエストパラメータを纏めて引き渡す事ができるので、パラメータ数が増えても冗長にならない。
POJO
で実装する。View
のリクエストパラメータのname
と、Form
クラスの変数名を同一名にする事で、リクエストパラメータが自動でForm
クラスのフィールドに格納される。- リクエストパラメータはフィールドの型に自動変換される。
POJO
Plain Old Java Object
の略。- 「ポジョ」と読むらしい。
java.lang.Object
を継承して、それ以外何も継承していない、シンプルなJavaクラスの事。java.lang.Object
は、全てのJavaクラスが暗黙的に自動で継承しているので、つまりは、明示的には何も継承していないクラスの事。
バリデーションチェック
大きく、単項目チェック
と相関項目チェック
の2つに分かれる。
単項目チェック
相関項目チェック
- 複数の入力項目(フィールド)に対して同時に行う入力チェックの事。
- 例えば、左の入力項目が奇数、かつ、右側の入力項目が偶数、の様なモノ。
- 自分で新たなアノテーションを作成するか、
Validator
インターフェースを実装するかの2択。
form-backing bean(Formクラス)
- バリデーションを行う際に必要となる、HTMLの
form
タグにバインドするForm
クラスのインスタンスの事。 @ModelAttribute
アノテーションを付与したメソッドでform-backing bean
を作成する。@ModelAttribute
アノテーションが付与されたメソッドは、そのクラス(Controller)のリクエストハンドラメソッドの実行前に呼ばれ、returnするform-backing bean(Formオブジェクト)
はmodel.addAttribute(form)
相当の処理が実行され、Model(ControllerからViewに渡される、表示データ等が格納されるオブジェクト)
に格納される。- Modelに格納される際、デフォルトではリクエストスコープで格納されるので、クライアントへのResponse後に自動削除される。
プレースホルダ
実際の内容(値)を後から挿入する為に、とりあえず仮に確保した場所の事。
ジェネリッククラス
クラスを定義する際に、クラス名の右側に
と記載されたクラスの事。 T
は「型パラメータ」と呼ばれる。<>
内の文字はT
でなくても良いらしい。class GenericClass<T> { private T data; public void setData(T value) { this.data = value; } public T getData() { return data; } }
クラスを利用する際に、
T
の部分に型を指定すると、クラス内のT
が指定した型に置き換わる。型は参照型(クラスやインターフェースなど)しか指定できず、基本型(intなど)を指定してしまうとコンパイルエラーになる。
下記例では、
String
型に置き換わる。GenericClass<String> genericClass = new GenericClass<String>;
トランザクション
複数の処理をひと纏まりにしたモノで、トランザクションが完了した際に、処理が全て成功していれば保存(コミット)、1つでも失敗していればトランザクションで行った処理を全て巻き戻す(ロールバック)する。
トランザクションの開始と修了の範囲を「トランザクション境界」と呼び、Domain層(MVCモデルにおけるModel)の入り口であるService(ServiceImplクラス)の中で定義する。
トランザクションを管理したいクラスやメソッドに@Transactional
アノテーションを付与すれば、トランザクションが自動で管理される。
application.properties
SpringBootプロジェクトにおいて、例えばDBへの接続情報等の環境設定を行う為のファイル。
Flash Scope(フラッシュスコープ)
1回のリダイレクトの間のみ有効なスコープ。
アノテーション
Springでは、「@」に続いて特定のキーワードを記載する形式でソースコードにアノテーション(注釈)を記載する事で、Springが標準で提供している何かしらの機能を対象のソースコードに付与する事ができる。
例えば、AOP(アスペクト思考プログラミング)を実現する際や、DIを実現する際などに使用する。
本書で勉強する際に頻出度が高かった一部のアノテーションをいかにメモる。
全体を通して使われるモノ
@Autowired
DI(依存性の注入)を担うアノテーションで、自動生成したインスタンスを、アノテーションを付与した変数に自動代入(注入)してくれる。これを行う事で、「new」キーワードによるインスタンス生成を記載しなくてよくなるため、newされる側のクラスに対する依存性が下がる。
Controllerクラスで使われるモノ
@Controller
クラスに付与することで、Spirngのコンポーネントとして認識される。
@RequestMappng
クラスやクラス内のメソッドに付与する事で、リクエストURLに対してどのメソッドの処理を実行するかのマッピングを行える。
アノテーションの第一引数にURLキーワード、第二引数にHTTPリクエストのメソッドを記載する。
@GetMapping
GETリクエストを処理する@RequestMappngの簡略アノテーション(「@RequestMapping」の第二引数に「Get」を指定した場合と同じ)。
@PostMapping
POSTリクエストを処理する@RequestMappngの簡略アノテーション(「@RequestMapping」の第二引数に「Post」を指定した場合と同じ)。
@ModelAttribute
メソッドか引数に付与する事で、それぞれ違った効果を発揮する。
メソッドに付与した場合は、リクエストハンドラメソッドが呼び出される前に、そのメソッドが呼ばれ、メソッドの戻り値がリクエストスコープでModelに格納される。form-backing bean
の初期化処理等に利用される。
引数に付与した場合は、リクエストハンドラメソッドが呼び出される前に、その引数に
リダイレクト
クライアントからの任意のReqに対応するリクエストハンドラメソッドでクライアントへのResの画面名をreturnする代わりに、他のリクエストハンドラメソッドに処理を引き継ぐ為に、他リクエストハンドラメソッドが対応するURLをreturnする事で、仮想的なReqを投げるイメージ。
例えば、本書のQuizアプリの中では、下記の様に使用していた。
// クイズを1件登録。 @PostMapping("/insert") // 「@Validated」アノテーションで「QuizForm」オブジェクトをバリチェックした結果が「BindingResult」インターフェースに格納される。 public String insert(@Validated QuizForm quizForm, BindingResult bindingResult, Model model, RedirectAttributes redirectAttributes) { // Formの中身をEntityに詰め替える。 Quiz quiz = makeQuiz(quizForm); // バリチェック。 if(!bindingResult.hasErrors()) { service.insertQuiz(quiz); redirectAttributes.addFlashAttribute("complete", "登録が完了しました。"); return "redirect:/quiz"; }else { // バリチェックがエラーになった場合は、一覧表示処理を呼ぶ。 return showList(quizForm, model); } }
上記は、クイズを登録する際のリクエストハンドラメソッドだが、11行目が該当箇所。
画面名をreturnせず、/quiz
というURLに対するReqが飛んで来た時と同じ処理を後続で走らせている。
【メモ】クライアントに同画面を返却する場合でも、Formの状態によって処理の走らせ方を変える
クライアントに同じ画面を返却する場合でも、Modelに詰めてるFormをどういう状態で返却したいかに応じて処理の走らせ方を変えている事に気付いたので、以下にメモを残す。
対象部分のソースは下記。
@Controller @RequestMapping("/quiz") public class QuizController { @Autowired QuizService service; // 「form-backing bean」の初期化。 // 「ModelAttribute」アノテーションを付与する事で、リクエストハンドラメソッドが実行される前に毎回このメソッドが実行される。 // returnされるFormクラスは、model.addAttribute(form)相当の処理が実行され、「quizForm」という名前でModelに追加される。 // 今回は、Formインスタンス内のラジオボタンの初期値設定のみ行っている。Formインスタンス内の他フィールドについては、後続処理の中で設定する。 @ModelAttribute public QuizForm setUpForm() { QuizForm form = new QuizForm(); // ラジオボタンの初期値設定。 form.setAnswer(true); return form; } // クイズの一覧を表示する。 @GetMapping public String showList(QuizForm quizForm, Model model) { // 新規登録設定。 quizForm.setNewQuiz(true); // クイズの一覧を取得する。 Iterable<Quiz> list = service.selectAll(); // 表示用「Model」への格納。 model.addAttribute("list", list); model.addAttribute("title", "登録用フォーム"); // クライアントに返却するView名(HTMLのファイル名)をreturnする。 return "crud"; } // クイズを1件登録。 @PostMapping("/insert") // 「@Validated」アノテーションで「QuizForm」オブジェクトをバリチェックした結果が「BindingResult」インターフェースに格納される。 public String insert(@Validated QuizForm quizForm, BindingResult bindingResult, Model model, RedirectAttributes redirectAttributes) { // Formの中身をEntityに詰め替える。 Quiz quiz = makeQuiz(quizForm); // バリチェック。 if(!bindingResult.hasErrors()) { service.insertQuiz(quiz); redirectAttributes.addFlashAttribute("complete", "登録が完了しました。"); return "redirect:/quiz"; }else { // バリチェックがエラーになった場合は、一覧表示処理を呼ぶ。 return showList(quizForm, model); } } }
ユーザがクイズ登録機能を利用した際に、入力したクイズデータがバリデーションチェックを通るか弾かれるかによって、処理の走らせ方を変えている。
具体的には、41行目と44行目。
処理の走り方の違い
それぞれどういう処理の走らせ方をしているか、簡単に記載する。
①バリチェックが通った時の処理
- 41行目で
/quiz
というURLに対してリダイレクト。 - 12行目の
setUpForm()
メソッドが呼ばれる。 - リクエストハンドラメソッドとして、20行目の
showList(QuizForm quizForm, Model model)
メソッドが呼ばれる。
②バリチェックで弾かれた時の処理
- 44行目で、Formオブジェクトを引数として渡して
showList(QuizForm quizForm, Model model)
メソッドを呼ぶ。 - 20行目の
showList(QuizForm quizForm, Model model)
メソッドが走る。
①と②の具体的な違い
具体的には、①と②では下記2点が大きく異なる。
- Formオブジェクトを初期化する、12行目の
setUpForm()
メソッドが呼ばれるかどうか。- ①では呼ぶが、②では呼ばない。
- クライアントに返却するView名をreturnする20行目の
showList(QuizForm quizForm, Model model)
メソッドに、Formオブジェクトを渡せるか渡せないか。- ①では渡せないが、②では渡せる。
処理の走らせ方を変えた意図
結論から言うと、Resした後のUIに、先ほどユーザが入力したデータを表示させたいかどうかによって①と②を使い分けているのだと理解した。
バリチェックが通った時は、UI上のクイズデータ入力フォーム等を初期状態(空文字等)にしておきたいので、先ほどユーザが入力したデータをResに含める必要が無い。つまり、Formオブジェクトは初期化されていてほしいという事。
一方で、バリチェックに弾かれた時は、UI上のクイズデータ入力フォーム等には、先ほどユーザが入力したデータを表示しておきたいので、Formオブジェクトは初期化してはいけないし、先程のReqで受け取ったFormオブジェクトをそのまま持ち回りたい(引数としてshowList(QuizForm quizForm, Model model)
メソッドに渡したい)という事。