見出し画像

JHipsterでThymeleafを使ってみた

みなさんこんにちは、@ultaroです!

最近はReact.jsやVue.jsなどSPAでWebアプリケーションフロントエンドを作成するのが一般的になっていますね。ただ、SPAを使うとJavaScriptをおぼえないといけない、Node.js環境を整えないといけない、React.jsやVue.jsのアップデートサイクルに追随しないといけない、という問題もありアプリの特性によっては安心感のある技術で作りたいシーンもあるはずです。このような変化に対応できるプロジェクトメンバーが揃っていれば問題ないのですが、そうでない場合もしばしば。基幹系システムで外部環境に依存したくない、でもJHipsterのような便利なツールも活用したい、そんな想いから「JHipsterでThymeleafを使ったアプリの構築をしたい」ということで調べてみました。

JHipsterはインタラクティブにいくつかの質問に答えるだけで、簡単にアプリケーションが作れてしまう便利な開発ツールキットです。公式ページの「Server Side Options」にも「Thymeleaf」の名前が載っています。よしよし、使えそうだな?!

……と思い調査しましたが、結論としては、JHipster標準のフロントエンドフレームワーク(React/Vue.js/Angular)を選択した場合のように、自動でUIの生成を行うことはできないようです。

しかし自分でThymeleafのテンプレートやコントローラを用意すれば、Thymeleafのアプリケーションが動きます。

さて、ではどのように構築していけばいいのか、今回検証したサンプルアプリ作成の実際の手順をご紹介します。


前提条件

今回の検証では以下のバージョンを利用しています。

  • Java 17.0.10

  • JHipster 8.3.0

1.JHipsterプロジェクトの作成

次の通り質問に回答し、JHipsterプロジェクトを構築します。 今回、回答の必要が無いものはエンターキーを押してスキップしています。
また?What is your default Java package name?への回答は本来は”com.mycompany.myapp”のようなパッケージ名になります。本記事ではわかりやすくするために[my_package_name]と記載しています。

? What is the base name of your application? --> thymeleafsample
? Which *type* of application would you like to create? --> Monolithic application (recommended for simple projects)
? Besides Junit, which testing frameworks would you like to use? --> (skip)
? Do you want to make it reactive with Spring WebFlux? --> No
? Which *type* of authentication would you like to use? --> HTTP Session Authentication (stateful, default Spring Security
mechanism)
? Which *type* of database would you like to use? --> SQL (H2, PostgreSQL, MySQL, MariaDB, Oracle, MSSQL)
? Which *production* database would you like to use? --> PostgreSQL
? Which *development* database would you like to use? --> PostgreSQL
? Which cache do you want to use? (Spring cache abstraction) --> Ehcache (local cache, for a single node)
? Do you want to use Hibernate 2nd level cache? --> Yes
? Which other technologies would you like to use? --> (skip)
? Which *Framework* would you like to use for the client? --> React
? Besides Jest/Vitest, which testing frameworks would you like to use? --> (skip)
? Do you want to generate the admin UI? --> Yes
? Would you like to use a Bootswatch theme (https://bootswatch.com/)? --> Default JHipster
? What is your default Java package name? --> [my_package_name]
? Would you like to use Maven or Gradle for building the backend? --> maven
? Would you like to enable internationalization support? --> No
? Please choose the native language of the application --> English

さて、ここでのポイントが2点あります。

💡Point1


? Which type of application would you like to create?
の回答として「Monolithic application」を選びます。

「Microservice application」だとバックエンドのみ、
「Microservice gateway」だとフロントエンドのみになるので、Thymeleafを使う前提であればフロントエンドとバックエンドの両方が生成される「Monolithic application」を選びます。

すると自動的にThymeleafが使える(mavenであればpom.xmlに依存関係が追加される)ようになります。

💡Point2


? Which Framework would you like to use for the client?
の回答として今回は「React」を選択しました。

画面はThymeleafで作るんじゃないの?ここでなぜReactを選んでいるの?と思われるでしょう。

はい、その通りです。しかしReactを選んでいることには理由があります。

JHipsterで作成したアプリケーションは認証を行わないと動作確認ができません。このため認証機能を作る必要があります。

認証機能を作る方法としては自分で実装するか、JHipsterに自動生成させるかの二択になります。

今回はThymeleafを使ったアプリの構築が出来るかの確認をまずやりたいので、実装の手間を減らすためにJHipsterに自動生成してもらうことにしました。そのため今回はReactを選んでいます。(他のフロントエンドフレームワークでも構いません)

そうではなく、自分で認証画面を作る場合は「No client」を選択してゴリゴリ実装してください。

2.Thymeleafのテンプレート作成

JHipsterの質問に答えてプロジェクトが生成されたら、次に、自分自身でThymeleafのテンプレートを作成します。 src/main/resources/templatesディレクトリ内にテンプレートファイルを追加します。

今回は以下のように簡単なhtmlを定義しました。

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Hello</title>
        <meta charset="utf-8" />
    </head>
    <body>
        <h1>HELLOOOOOO</h1>
        <h2><span th:text="${message}"></span></h2>
    </body>
</html>

3.コントローラでテンプレートを返す

次に、Thymeleafのテンプレートを返すようなコントローラを作成します。 JHipsterで生成されるREST APIのコントローラはsrc/main/java/[my_package_name]/web/rest 以下に生成されますが、今回はsrc/main/java/[my_package_name]/web/controllerというディレクトリを作成してコントローラを配置することにします。 message変数に"Hello World!"という文字列を設定するだけの簡単なコントローラです。

package [my_package_name].controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HelloController {
@GetMapping("/thymeleaf/hello")
    public String helloWorld(Model model) {
        model.addAttribute("message", "Hello World!!");
        return "hello";
    }
}

4.権限設定

src/main/java/[my_package_name]/config/SecurityConfiguration.java のfilterChainメソッドで、権限が設定されています。 今回追加するパス"/thymeleaf/hello"にも権限を追加したいですが、今後パスが増えるたびに権限設定を行うのは面倒なので、"/thymeleaf"から始まるパスに権限を追加します。
(+部分が今回追加分。他は自動生成されたコードです)

(省略)
@Configuration
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfiguration {
(省略)
@Bean
    public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
        http
            (省略)
            .authorizeHttpRequests(
                authz ->
                    // prettier-ignore
                authz
                    .requestMatchers(mvc.pattern("/index.html"), mvc.pattern("/*.js"), mvc.pattern("/*.txt"), mvc.pattern("/*.json"), mvc.pattern("/*.map"), mvc.pattern("/*.css")).permitAll()
                    .requestMatchers(mvc.pattern("/*.ico"), mvc.pattern("/*.png"), mvc.pattern("/*.svg"), mvc.pattern("/*.webapp")).permitAll()
                    .requestMatchers(mvc.pattern("/app/**")).permitAll()
                    .requestMatchers(mvc.pattern("/i18n/**")).permitAll()
                    .requestMatchers(mvc.pattern("/content/**")).permitAll()
                    .requestMatchers(mvc.pattern("/swagger-ui/**")).permitAll()
                    .requestMatchers(mvc.pattern("/api/authenticate")).permitAll()
                    .requestMatchers(mvc.pattern("/api/register")).permitAll()
                    .requestMatchers(mvc.pattern("/api/activate")).permitAll()
                    .requestMatchers(mvc.pattern("/api/account/reset-password/init")).permitAll()
                    .requestMatchers(mvc.pattern("/api/account/reset-password/finish")).permitAll()
                    .requestMatchers(mvc.pattern("/api/admin/**")).hasAuthority(AuthoritiesConstants.ADMIN)
                    .requestMatchers(mvc.pattern("/api/**")).authenticated()
                    .requestMatchers(mvc.pattern("/v3/api-docs/**")).hasAuthority(AuthoritiesConstants.ADMIN)
                    .requestMatchers(mvc.pattern("/management/health")).permitAll()
                    .requestMatchers(mvc.pattern("/management/health/**")).permitAll()
                    .requestMatchers(mvc.pattern("/management/info")).permitAll()
                    .requestMatchers(mvc.pattern("/management/prometheus")).permitAll()
                    .requestMatchers(mvc.pattern("/management/**")).hasAuthority(AuthoritiesConstants.ADMIN)
+                   .requestMatchers(mvc.pattern("/thymeleaf/**")).authenticated()
            )
(省略)
        return http.build();
    }
}

6.フィルターチェーン設定

フィルターチェーンの設定を変更します。 JHipsterはSPAを前提としているため、特定のパス以外はindex.htmlへリクエストがフォワードされるように設定されています。 今回は"/thymeleaf/hello"というパスでレンダリングをしたいので、src/main/java/[my_package_name]/web/filter/SpaWebFilter.java に以下のように追記します。(フィルターチェーン設定も権限設定と同じくパスごとに増えると煩雑なので、"/thymeleaf"で始まるパスを一律設定します)

package [my_package_name].web.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.web.filter.OncePerRequestFilter;
public class SpaWebFilter extends OncePerRequestFilter {
/**
     * Forwards any unmapped paths (except those containing a period) to the client {@code index.html}.
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        // Request URI includes the contextPath if any, removed it.
        String path = request.getRequestURI().substring(request.getContextPath().length());
        if (
+           !path.startsWith("/thymeleaf") &&
            !path.startsWith("/api") &&
            !path.startsWith("/management") &&
            !path.startsWith("/v3/api-docs") &&
            !path.contains(".") &&
            path.matches("/(.*)")
        ) {
            request.getRequestDispatcher("/index.html").forward(request, response);
            return;
        }
        filterChain.doFilter(request, response);
    }
}

これで、/thymeleaf/helloへのリクエストに対してhello.htmlテンプレートをもとにHTMLレンダリングされるようになります。
これでThymeleafを使用したWebアプリの準備が整いました。

7.リンクの追加

最後に、"/thymeleaf/hello"へ飛ぶためのリンクを用意します。Reactを選択したので、src/main/webapp/app/modules/home/home.tsxに以下のようにaタグを追記します。

(省略)
 export const Home = () => {
   const account = useAppSelector(state => state.authentication.account);
return (
     <Row>
       <Col md="3" className="pad">
         <span className="hipster rounded" />
       </Col>
       <Col md="9">
         <h1 className="display-4">Welcome, Java Hipster!</h1>
         <p className="lead">This is your homepage</p>
+        <a href="/thymeleaf/hello">Go to Hello page</a>
        (省略)
        );
};
(省略)

8.動作確認

ではブラウザから確認してみましょう。 今回は開発用DBにPostgreSQLを選択したので、dockerを立ち上げておきます。(開発用DBにH2を選択した場合は不要です。)

docker compose -f src/main/docker/postgresql.yml up -d


mavenを使用しているので、./mvnwでアプリケーションを起動します。 http://localhost:8080/ でトップページにアクセスできます。

トップページ(http://localhost:8080/)

そしてログインした後に" Go to Hello page"リンクを押すと……

今回Thymeleafで作成したページ(http://localhost:8080/thymeleaf/hello)

設定したページに遷移できました!

まとめ


このように、JHipsterで自動生成されたコードをもとに自分自身で実装を追加していけば、Thymeleafを使用すること可能です。JHipsterでThymeleafを使いたい!という場合は、ぜひ参考にしてみてください。