見出し画像

【SpringBoot+Docker】Web MVC ベース その④ ~テンプレートとコントローラーを開発しやすくするヘルパーコンポーネントを作る~

前回前々回で Spring Boot の構造について説明を行いました。
基本ですので知っている人にとっては目新しいものではありませんでした。

しかし、初学者にとってはよく分からなかったと思います。
コメントなり、そのために Twitter を始めましたので、質問を投げかけてもらえれば答えたいと思いますし、補足を追記したりして内容を充実させたいと思っています。
遠慮なく質問して下さい。

今回は便利なんですが使われているところをあまり見たことがない Expression Object Dialect やテクニックを使って、テンプレートとコントローラーを開発しやすくするヘルパーコンポーネントを紹介します。

この記事は Java、Docker、Visual Studio Code(以下、vscode)、シェルなどのプログラミングに関する基本的な技術を知っている、または、調べれば分かる程度の知識がある前提で書いています。
分かりづらいところがあったらコメントでお知らせ下さい。

今日のノウハウ

テンプレートとコントローラーで指定するパス関係の文字列を
ヘルパークラスに一元化する

普通にテンプレートとコントローラーを開発しているとパスを指定しているところがサンプルのようになります。

■UsersController.java

  @GetMapping("/users")                                             // /users にマッピング
  public String indexPage(Model model) {                            // model はビューへオブジェクト受け渡す役
    ※省略

  @PostMapping("/users/{id}/update")                                // /users/ユーザーID/update にマッピング。@PathVariable で引数とマッピング
  public String update(@PathVariable Long id, UserForm userForm) {  // ユーザーID を id に受け取る。
    ※省略
    return "redirect:/users";                                       // リダイレクト先を指定する
  }

■show.html

  <a href="/users">index</a> |

■edit.html

  <form th:action="@{/users/{id}/update(id=${userForm.id})}"
      method="post" th:object="${userForm}">                    <!-- /users/ID/update に post。form 内でuserFormを参照 -->

"/users" がコントローラーでは @GetMapping やリダイレクト先を指定する return "redirect:/users" で使われています。
テンプレートにも index ページへのリンクで使われています。
"/users/{id}/update" もコントローラーでは @PostMapping とテンプレートでは form の th:action で使われています。
すべて文字列なので間違わないように注意深くコーディングする必要があります。

また、form の th:action に指定する Thymeleaf の式 "@{/users/{id}/update(id=${userForm.id})}" は致し方ないのですが少々面倒です。
id 一つだからこのくらいですが、クエリーストリングで三つも指定することになったらとても書いていられません。

これらをメソッドにできると便利そうです。
メソッドにできればコンパイル時や実行時エラーになってくれ、品質も上がるはずです。

仮に UsersHelper クラスを作りそこにメソッドを定義したとして、return しているところはメソッドになっていれば呼べばいいだけです。
しかし、@GetMapping などアノテーションのところはどうでしょうか。
実は Spring 式言語(SpEL)が使えます。
こんなふうに書けるということです。

@GetMapping("#{usersHelper.indexPageMappingPath()}")              // /users にマッピング

Thymeleaf 側はどうでしょうか。
Expression Object Dialect 機能を使用すると実現できます。
こんなふうに書けます。

<a th:href="${#usersHelper.indexPagePath()}">index</a> |          <!-- /users にリンク -->

当社ではヘルパーコンポーネントと呼んでいますが、これを作成する方法を紹介します。

今回も事前にソースコードを用意しています。

簡単ですが以前の記事に動かし方を書いています。
含まれている DDL を使ってデータベースを作成する必要がありますのでそちらを参照して下さい。

それでは実際に見ていきます。

ヘルパーコンポーネントクラスを作成する。

ヘルパークラスは次のような単純なクラスです。
ここにパスに関する情報を一元管理し、テンプレートやコントローラーで使用するようにします。

@Component                                                                    // コンポーネントとして扱う
public class UsersHelper {

  public String indexPageMappingPath() {                                      // indexPage() へのマッピングパス
    return "/users";
  }

  ※省略

  public String updateMappingPath() {                                         // update() へのマッピングパス
    return "/users/{id}/update";
  }

  ※省略

  public String redirectIndexPage() {                                         // indexPage() へのリダイレクト
    return "redirect:" + indexPagePath();
  }

  ※省略

  public String indexPagePath() {                                             // index ページのパス
    return indexPageMappingPath();
  }

  ※省略

  public String updatePath(long id) {                                         // update のパス
    return updateMappingPath().replace("{id}", String.valueOf(id));
  }

  ※省略

クラスに @Component をつけることでコンポーネントとして Spring Boot の管理下に置かれます。

ヘルパーをコントローラーで使用する。

@RequiredArgsConstructor と private final を使ってコントローラーにヘルパーを注入します。
ヘルパーは Spring Boot の管理下に置かれているので、コンストラクターを介してインスタンスを注入することができます。

@Controller                                                         // コントローラーとして扱う
@RequiredArgsConstructor                                            // 必須フィールドのコンストラクターを生成
public class UsersController {

  ※省略

  private final UsersHelper usersHelper;                            // users ヘルパーを注入

そして、@GetMapping などの引数に SpEL で埋め込むことができます。
return しているところはメソッドを呼ぶだけです。

  @GetMapping("#{usersHelper.indexPageMappingPath()}")              // /users にマッピング
  public String indexPage(Model model) {                            // model はビューへオブジェクト受け渡す役
    ※省略

  @PostMapping("#{usersHelper.updateMappingPath()}")                // /users/ユーザーID/update にマッピング。@PathVariable で引数とマッピング
  public String update(@PathVariable Long id, UserForm userForm) {  // ユーザーID を id に受け取る。
    ※省略
    return usersHelper.redirectIndexPage();                         // リダイレクト先を指定する
  }

実は SpEL で埋め込みだけならインスタンスの注入は不要です。
API を開発するようになるとそうなるんですが Web MVC では注入すると覚えて下さい。

Expression Object Dialect 機能を設定する。

次にテンプレートで使用できるようにします。
Thymeleaf の Expression Object Dialect 機能を使います。
設定すると言ってもクラスを作成するだけです。
サンプルに入っていますのでそのまま使って下さい。

@Component                                                                  // コンポーネントとして扱う
@RequiredArgsConstructor                                                    // 必須フィールドのコンストラクターを生成
public class ExpressionObjectDialect implements IExpressionObjectDialect {  // IExpressionObjectFactory を提供する宣言

  private final UsersHelper usersHelper;                                    // users ヘルパーを注入

  ※省略

  @Override
  public IExpressionObjectFactory getExpressionObjectFactory() {            // IExpressionObjectFactory を返す

    Map<String, Object> expressionObjects = new HashMap<>();                // ヘルパーを Map で管理する
    expressionObjects.put("usersHelper", this.usersHelper);

  ※省略

クラスに @Component をつけてコンポーネントとして扱います。
IExpressionObjectDialect を実装して、IExpressionObjectFactory を提供します。
@RequiredArgsConstructor と private final を使ってヘルパーを注入します。

そして、IExpressionObjectFactory を返しているところで、ヘルパーを Map で管理しています。
この Map に登録すると登録した名前でテンプレート側で使えるようになります。

ヘルパーをテンプレートで使用する。

以上の設定で次のようにテンプレート側で呼び出すことができるようになります。

■show.html
  <a th:href="${#usersHelper.indexPagePath()}">index</a> |          <!-- /users にリンク -->

■edit.html
  <form th:action="${#usersHelper.updatePath(userForm.id)}"
      method="post" th:object="${userForm}">                    <!-- /users/ID/update に post。form 内でuserFormを参照 -->

SpEL とは若干記法が違うので注意して下さい。

まとめ

ヘルパーコンポーネントクラスは少々冗長ですが、マッピングパス、パス、リダイレクトパスを一元管理します。
コントローラーやテンプレートでこれらを使用することで統一した方法でパスをプログラミングすることができます。
コンパイル時や実行時にエラー検出可能な仕組みになるため品質が上がります。

あまり活用されているのを見ませんし、ウェブで検索しても出て来ません。
もしかすると普通に使われている技術なのかも知れませんが、みなさんに共有したいと思います。
活用して下さい。

補足:基本クラス可できるか。

サンプルではテンプレートパスも管理しています。
次のようにリソース名を管理することで一元管理化が進みます。

@Component                                                                    // コンポーネントとして扱う
public class UsersHelper {

  public String resourceName() {
    return "users";
  }

  public String indexPageMappingPath() {                                      // indexPage() へのマッピングパス
    return "/" + resourceName();
  }

  public String indexPageTemplate() {                                         // indexPage() のテンプレート名
    return resourceName() + "/index";
  }

さて、このアイデアの元は Rails や Laravel です。
ここまでくると勘の良い人なら基本クラス化ができることに気付くんじゃないでしょうか。
実際、可能です。
もちろん当社では基本クラス化しています。
実際にはインタフェースを使用します。
基本クラス化できるとリソース名を返すメソッドを実装するだけで各リソースのヘルパーコンポーネントが作成できるようになります。

この記事が気に入ったらサポートをしてみませんか?