Javalinを調べたときのメモ

# 概要

**Javalinとは**

JavalinはJavaおよびKotlin用の軽量Web Frameworkです。[Spark Framework] (http://sparkjava.com/)というWeb Frameworkからforkされ、2017年5月にバージョン0.0.1、2019年8月現在は3.4.1がリリースされています。
以下に主な特徴をGitHubから引用します。（日本語訳はグーグル翻訳です）

> https://github.com/tipsy/javalin
> 
> Javalin is more of a library than a framework. Some key points:
>
 * You don't need to extend anything
 * There are no @Annotations
 * There is no reflection
 * There is no other magic; just code.

> Javelinは、フレームワークというよりもライブラリです。 いくつかのキーポイント：
> 
* 何も拡張する必要はありません
* @Annotationsはありません
* リフレクションはありません
* 他にマジックはありません。 コードだけ

この記事の前半は、最小構成で"Hello World"を表示するアプリケーションの実装について、後半は簡単なWebアプリケーションの実装で調べたこと／試したことのメモになります。

**環境**

* Windows 10 Professional 1903
* OpenJDK 12.0.2
* Javalin 3.4.1
    * Jetty-server 9.4.19
    * Jetty-webapp 9.4.19
* Intellij IDEA 2019.2

**参考**

* [Javalin - A simple web framework for Java and Kotlin] (https://javalin.io/)
* [Spark - A micro framework for creating web applications in Kotlin and Java 8 with minimal effort] (http://sparkjava.com/)
* [Embedding Jetty] (https://www.eclipse.org/jetty/documentation/current/embedding-jetty.html)

# 最小構成でHello World

* 参考チュートリアル : [Setting up Javalin with Maven] (https://javalin.io/tutorials/maven-setup)

## プロジェクト構成

通常のMavenプロジェクトです。

```text
[project_root]
 |
 `--- /src
 |      |
 |      `--- /main
 |      |      |
 |      |      `--- /java
 |      |      |      |
 |      |      |      `--- com.example.demo (package)
 |      |      |            |
 |      |      |            `--- App.java
 |      |      |
 |      |      `--- /resources
 |      |
 |      `--- /test
 |             |
 |             `--- /java
 |             |
 |             `--- /resources
 |
 `--- pom.xml
```

## pom.xml

最小構成のJavalinアプリケーションに必要な依存関係はjavalinとslf4j-simpleです。

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo-java12-javalin3</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo java12 javalin 3.4.1</name>
    <description>Demo project for Javalin 3 with Java12</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>12</java.version>
        <maven.clean.version>3.1.0</maven.clean.version>
        <maven.resources.version>3.1.0</maven.resources.version>
        <maven.compiler.version>3.8.1</maven.compiler.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <maven.compiler.showWarnings>true</maven.compiler.showWarnings>
        <maven.jar.version>3.1.1</maven.jar.version>
        <maven.assembly.version>3.1.1</maven.assembly.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.javalin</groupId>
            <artifactId>javalin</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.26</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-clean-plugin</artifactId>
                <version>${maven.clean.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>${maven.resources.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven.compiler.version}</version>
                <configuration>
                    <compilerArgs>
                        <arg>-Xlint:all</arg>
                    </compilerArgs>
                    <release>${java.version}</release>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>${maven.jar.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>${maven.assembly.version}</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>com.example.demo.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
```

## アプリケーション起動ポイントの実装

Appクラス(クラス名は任意)のmainメソッドに、Javalinアプリケーションの起動ポイントを実装します。

* (1) : Javalinアプリケーションのインスタンスを初期設定で生成しポート7000で起動します。
    * createメソッドにJavalinConfigクラスのインスタンスを渡すことでアプリケーションをカスタマイズできます。
* (2) : Javalinアプリケーションに、パス`/`にGETリクエストハンドラを追加します。

```java
import io.javalin.Javalin;

public class App {
  public static void main(String ... args) {
    // (1)
    Javalin app = Javalin.create().start(7000);
    // (2)
    app.get("/", ctx -> ctx.result("Hello World"));
  }
}
```

## IDEから起動

App.mainメソッドを実行するとJavalinが起動し、以下のようなログがコンソールに出力されます。ログに出力されている通り私の開発環境では起動にかかる時間は1秒未満でした。
`http://localhost:7000` にアクセスすると"Hello World"というtext/plainのレスポンスが返ります。

```text
[main] INFO io.javalin.Javalin - 
           __                      __ _
          / /____ _ _   __ ____ _ / /(_)____
     __  / // __ `/| | / // __ `// // // __ \
    / /_/ // /_/ / | |/ // /_/ // // // / / /
    \____/ \__,_/  |___/ \__,_//_//_//_/ /_/

        https://javalin.io/documentation

[main] INFO org.eclipse.jetty.util.log - Logging initialized @827ms to org.eclipse.jetty.util.log.Slf4jLog
[main] INFO io.javalin.Javalin - Starting Javalin ...
[main] INFO io.javalin.Javalin - Listening on http://localhost:7000/
[main] INFO io.javalin.Javalin - Javalin started in 731ms \o/
```

## jarファイルから起動

ビルドするとtargetディレクトリ下に`demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar`というjarファイルが生成されます。
Javalinアプリケーションは埋め込みJettyを利用しているので`java -jar <jarファイル>`でWebアプリケーションを起動することができます。

```bat
> java --version
openjdk 12.0.2 2019-07-16
OpenJDK Runtime Environment (build 12.0.2+10)
OpenJDK 64-Bit Server VM (build 12.0.2+10, mixed mode, sharing)

> java -jar demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar
```

下記はJVMパラメータでデフォルトロケールを変更する場合の例です。

```bat
> java -Duser.language=en -Duser.country=US -jar demo-java12-javalin3-1.0.0-SNAPSHOT-jar-with-dependencies.jar
```

以上で『最小構成でHello World』のアプリケーションの実装は終了です。


# Javalinの特徴について

ここからはJavalinで簡単なWebアプリケーションを実装したときのメモになります。

## HTTPハンドラの実装の仕方

上記で書いたGETリクエストハンドラ(下記、パターン1)は、パターン2、パターン3のようにも実装することができます。

**パターン1**

```java
app.get("/", ctx -> ctx.result("Hello World"));
```

**パターン2**

※便宜的にAppクラスにメソッドを追加していますが、通常はハンドラクラスを別途実装することになると思います。

```java
public class App {

  void index2(@NotNull Context ctx) {
    ctx.result("Hello World2");
  }

  public static void main(String ... args) {
    Javalin app = Javalin.create().start(7000);
    App handler = new App();
    app.get("/2", handler::index2);
  }

}
```

**パターン3**

オフィシャルサイトのチュートリアルではstaticフィールドで記述する例もあります。

```java
public class App {

  static Handler index3 = ctx -> {
    ctx.result("Hello World3");
  };

  public static void main(String ... args) {
    Javalin app = Javalin.create().start(7000);
    app.get("/3", App.index3);
  }

}
```

### HTTPハンドラ

GETの他にPOST、PUT、PATCH、DELETE、HEAD、OPTIONSがあります。

|HTTPメソッド|シグネチャ|
|:----------|:---------|
|GET        |public Javalin get(@NotNull String path, @NotNull Handler handler) |
|POST       |public Javalin post(@NotNull String path, @NotNull Handler handler)|
|PUT        |public Javalin put(@NotNull String path, @NotNull Handler handler) |
|PATCH      |public Javalin patch(@NotNull String path, @NotNull Handler handler)|
|DELTE      |public Javalin delete(@NotNull String path, @NotNull Handler handler)|
|HEAD       |public Javalin head(@NotNull String path, @NotNull Handler handler)|
|OPTIONS    |public Javalin options(@NotNull String path, @NotNull Handler handler)|

### before / afterエンドポイント

HTTPハンドラの前後で実行するbefore / afterというエンドポイントを設定することができます。

|エンドポイント|シグネチャ    |
|:------------|:------------|
|before       |public Javalin before(@NotNull String path, @NotNull Handler handler)|
|before       |public Javalin before(@NotNull Handler handler)|
|after        |public Javalin after(@NotNull String path, @NotNull Handler handler)|
|after        |public Javalin after(@NotNull Handler handler)|

### HTTPハンドラのグループ化

|エンドポイント    |HTTPメソッド|機能                |
|:----------------|:----------|:-------------------|
|/api/v1/users    |GET        |ユーザ一覧を取得      |
|/api/v1/user/:id |GET        |ID指定でユーザを取得  |
|/api/v1/user     |POST       |ユーザを登録 |
|/api/v1/user/:id |DELETE     |ID指定でユーザを削除  |
|/api/v2/users    |GET        |ユーザ一覧を取得      |
|/api/v2/user/:id |GET        |ID指定でユーザを取得  |
|/api/v2/user     |POST       |ユーザを登録 |
|/api/v2/user/:id |DELETE     |ID指定でユーザを削除  |

このようなv1とv2の2系統のエンドポイントがあった場合、以下のようにHTTPハンドラをグループ化して定義できます。

```java
UserController v1Controller = /* version 1 の実装 */
UserController v2Controller = /* version 2 の実装 */

app.routes(() -> {

  // version 1
  ApiBuilder.path("/api/v1", () -> {
    // GET /api/v1/users
    ApiBuilder.get("/users", v1Controller::findAll);
    ApiBuilder.path("/user", () -> {
      // GET /api/v1/user/:id
      ApiBuilder.get(":id", v1Controller::findById);
      // POST /api/v1/user
      ApiBuilder.post(v1Controller::store);
      // DELETE /api/v1/user/:id
      ApiBuilder.delete(":id", v1Controller::remove);
    });
  });

  // version 2
  ApiBuilder.path("/api/v2", () -> {
    // GET /api/v2/users
    ApiBuilder.get("/users", v2Controller::findAll);
    ApiBuilder.path("/user", () -> {
      // GET /api/v2/user/:id
      ApiBuilder.get(":id", v2Controller::findById);
      // POST /api/v2/user
      ApiBuilder.post(v2Controller::store);
      // DELETE /api/v2/user/:id
      ApiBuilder.delete(":id", v2Controller::remove);
    });
  });

});
```

## io.javalin.http.Contextについて

HTTPハンドラメソッドのパラメータ`io.javalin.http.Context`クラスは、HTTPリクエストおよびレスポンスに必要な機能とデータを提供します。
以下オフィシャルサイトのドキュメントページからの引用です。

>  https://javalin.io/documentation#context
> 
> The Context object provides you with everything you need to handle a http-request. It contains the underlying servlet-request and servlet-response, and a bunch of getters and setters. The getters operate mostly on the request-object, while the setters operate exclusively on the response object.

ContextにはHttpServletRequest、HttpServletResponseのインスタンスを保持するreq、resフィールドもあります。

```java
javax.servlet.http.HttpServletRequest request = ctx.req;
javax.servlet.http.HttpServletResponse response = ctx.res;
```

### リクエストパラメータの受け取り方

### クエリパラメータ

クエリストリングで指定されたパラメータを受け取るメソッド

|シグネチャ|
|:--------|
|String queryParam(String key)|
|String queryParam(String key, String default)|
|List\<String\> queryParams(String key)|
|Map\<String, List\<String\>\> queryParamMap()|
|Validator\<T\> queryParam(String key, Class\<T\> clazz)|

### フォームパラメータ

フォームもクエリパラメータと同様のメソッドがあります。残念ならがらフォーム全体のフィールドを任意のオブジェクトにバインドすることはできないようです。

|シグネチャ|
|:--------|
|String formParam(String key)|
|String formParam(String key, String default)|
|List\<String\> formParams(String key)|
|Map\<String, List\<String\>\> formParamMap()|
|Validator\<T\> formParam(String key, Class\<T\> clazz)|

### パスパラメータ

`/api/user/1`のようなパスからその一部をパラメータとして受け取るメソッド

|シグネチャ|
|:--------|
|String pathParam(String key)|
|Map\<String, String\> pathParamMap()|
|Validator\<T\> pathParam(String key, Class\<T\> clazz)|

リクエストハンドラのパスにコロン(:)を付けた部分がパスパラメータとして識別されます。

```java
app.get("/api/user/:id", ctx -> {
  Long userId = ctx.pathParam("id", Long.class).get();
});
```

## Javalinアプリケーションのコンフィグレーション

* 参考 : [Configuration] (https://javalin.io/documentation#configuration)

Javalin.createメソッドにJavalinConfigクラスのインスタンスを渡すことでカスタマイズができます。
以下のようにJavalinConfigのインスタンスを渡す方法の他に、lambda式で記述する方法もあります。

**インスタンス**

```java:configuration
JavalinConfig config = new JavalinConfig();
// configuration
Javalin
    .create(config)
// ... 省略 ...
```

**lambda式**

```java:configuration
Javalin
    .create(config -> {
        // configuration
    })
// ... 省略 ...
```

### 主なコンフィグレーション

コンテキストパスの設定 (デフォルトは"/")

```java
config.contextPath = "/app";
```

デフォルトのコンテンツタイプの設定 (デフォルトは"text/plain")

```java
config.defaultContentType = "application/json";
```

ETagの設定 (デフォルトはfalse)

```java
config.autogenerateEtags = true;
```

リクエストログの出力はJavalinConfig.requestLoggerで設定できます。
第二引数(execTime)はリクエストに掛かった時間(ms)です。

```java:configuration
config.requestLogger((ctx, execTime) -> {
  LOG.debug("[{}] {} - {} ms.", ctx.fullUrl(), ctx.userAgent(), execTime);
});
```

### 開発用コンフィグレーション

開発時に有効にすると便利な機能もあります。

#### enableDevLogging

```java:configuration
config.enableDevLogging();
```

情報量が多いので常時有効にしておくというよりは、問題が起きたときの調査時に有効化するといった使い方になると思います。

```
[qtp2092769598-18] INFO io.javalin.Javalin - JAVALIN REQUEST DEBUG LOG:
Request: GET [/overview]
    Matching endpoint-handlers: [GET=/overview]
    Headers: {Cookie=Idea-c29ae3c1=53d6d99f-1169-42ed-8d69-a63cfe80a87a, Accept=text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3, Upgrade-Insecure-Requests=1, Connection=keep-alive, User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36, Sec-Fetch-Site=none, Host=localhost:7000, Sec-Fetch-User=?1, DNT=1, Accept-Encoding=gzip, deflate, br, Accept-Language=ja,en-US;q=0.9,en;q=0.8,en-GB;q=0.7, Sec-Fetch-Mode=navigate}
    Cookies: {Idea-c29ae3c1=53d6d99f-1169-42ed-8d69-a63cfe80a87a}
    Body: 
    QueryString: null
    QueryParams: {}
    FormParams: {}
Response: [200], execution took 26.37 ms
    Headers: {Server=Javalin, Content-Encoding=gzip, Date=Wed, 21 Aug 2019 13:44:46 GMT, Content-Type=text/html}
    Body is gzipped (4464 bytes, not logged)
----------------------------------------------------------------------------------
```

#### RouteOverviewPlugin

```java:configuration
config.registerPlugin(new RouteOverviewPlugin("/overview"));
```

このプラグインを登録して `http://localhost:7000/overview` にアクセスするとJavalinアプリケーションに追加されているリクエストハンドラの一覧を確認することができます。
![overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22772/f781849d-555d-911b-326a-92810c01f595.png)

## エラーレスポンスのカスタマイズ

JavalinのHttpResponseExceptionを継承したHTTPステータスに対応する例外クラスが用意されています。

|Status|例外クラス              |メッセージ|
|:-----|:----------------------|:---------|
|302   |RedirectResponse       |Redirected|
|400   |BadRequestResponse     |Bad request|
|401   |UnauthorizedResponse   |Unauthorized|
|403   |ForbiddenResponse      |Forbidden|
|404   |NotFoundResponse       |Not found|
|405   |MethodNotAllowedResponse|Method not allowed|
|409   |ConflictResponse       |Conflict|
|410   |GoneResponse           |Gone|
|500   |InternalServerErrorResponse|Internal server error|
|502   |BadGatewayResponse     |Bad gateway|
|503   |ServiceUnavailableResponse|Service unavailable|
|504   |GatewayTimeoutResponse |Gateway timeout|

### 固有の例外をハンドリングする

アプリケーション固有の例外をハンドリングして任意のエラーレスポンスを返すことができます。
下記のAppServiceExceptionというアプリケーション固有の例外がスローされた場合、

```java:AppServiceException
public class AppServiceException extends RuntimeException {
  public AppServiceException() {
  }
  public AppServiceException(String message) {
    super(message);
  }
  public AppServiceException(String message, Throwable cause) {
    super(message, cause);
  }
}
```

この例外をハンドリングしてHTTPステータス返すにはJavalinアプリケーションに例外をマッピングします。
この例では500ステータスだけを返します。

```java
app.exception(AppServiceException.class, (e, ctx) -> {
  ctx.status(500);
});
```

任意のエラーレスポンスメッセージとHTTPステータスを返すには以下のように実装します。
resultメソッドではなくjsonメソッドを使用すればjsonフォーマットのメッセージを返すこともできます。

```java
app.exception(AppServiceException.class, (e, ctx) -> {
  ctx.result("Application Error : " + e.getMessage()).status(500);
});
```

## リクエスト / レスポンスでjsonを扱う

リクエスト / レスポンスでjsonを扱えるように依存関係にjacksonを追加します。
2番目の`jackson-datatype-jsr310`はJava8 Date and Time APIを扱う場合に必要です。

```xml:pom.xml
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.0.pr1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.10.0.pr1</version>
</dependency>
```

jsonをレスポンスするGETリクエストハンドラを実装します。jsonをレスポンスするにはjsonメソッドを使用します。

```java
public static void main(String ... args) {
  // ... 省略

  // (1)
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.findAndRegisterModules();
  objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"));
  // (2)
  JavalinJackson.configure(objectMapper);

  // (3)
  app.get("/json", ctx -> {
    Map<String, Object> model = new HashMap<>();
    model.put("message", "Hello World");
    model.put("now", LocalDateTime.now());
    //jsonメソッドを使用します。
    ctx.json(model);
  });

  // (4)
  app.post("/json", ctx -> {
    Map<String, Object> model = ctx.bodyAsClass(Map.class);
    // HTTP Status 200を返します。
    ctx.status(200);
  });

}
```

* (1) : ObjectMapperのインスタンスをカスタマイズします。
* (2) : カスタマイズしたインスタンスをJavalinJacksonクラスを使って適用します。
* (3) : jsonを返すGETリクエストハンドラを追加します。
* (4) : jsonを受け取るPOSTリクエストハンドラを追加します。

## 静的ファイルを配信する

クラスパス上に配置したファイルを配信するにはJavalinConfigで設定を行います。

```text
[project_root]
 |
 `--- /src
        |
        `--- /main
               |
               `--- /resources
                      |
                      `--- /public
                             |
                             `--- index.html
```

上記の場所に配置した`index.html`を配信するにはJavalinConfig.addStaticFilesでパスを指定します。

```java:configuration
config.addStaticFiles("/public");
```

『最小構成でHello World』のアプリケーションで追加した`/`に対するGETリクエストハンドラとパスが被るので、以下のコードをコメントアウトします。

```java
// app.get("/", ctx -> ctx.result("Hello World"));
```

この状態でJavalinアプリケーションを起動し、`http://localhost:7000/index.html` または `http://localhost:7000`にアクセスするとpublic/index.htmlが返されます。

また、クラスパス外にあるディレクトリを扱うこともできます。
例えば以下のような`C:\var\static`ディレクトリ以下のファイルを扱いたい場合は、

```text
C:\var
    |
    `--- \static
           |
           `--- \images
                  |
                  `--- sample_1.jpg
```

JavalinConfig.addStaticFilesで絶対パスを指定します。なお、ここで指定したパスにアクセスできないとアプリケーション起動時にエラーが発生します。

```java:configuration
config.addStaticFiles("C:\\var\\static", Location.EXTERNAL);
```

htmlファイルで下記のようにパスを指定します。

```html
<img src="/images/sample_1.jpg" />
```

## WebJarsを利用する

pom.xmlの依存関係に利用したいライブラリを追加します。
この例ではjQueryを利用しました。

```xml:pom.xml
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>
```

これだけではjQueryは有効になりません。Webjarsをアプリケーションで利用できるようにするためにJavalinConfig.enableWebjarsで有効化します。

```java:configuration
config.enableWebjars();
```

htmlファイルで下記のようにパスを指定します。

```html
<script src="/webjars/jquery/3.4.1/jquery.min.js"></script>
```

## ロギングにlogbackを利用する

ロギングにslf4jとlogbackを利用します。`slf4j-api`はすでにJavalinが依存しているので追加するのは`logback-classic`だけになります。
また最初に追加していた`slf4j-simple`はコメントアウトします。

```xml:pom.xml
<!--
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.26</version>
</dependency>
-->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
    <scope>runtime</scope>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>
```

logbackの設定ファイルlogback.xmlを以下の場所に作成します。

```text
[project_root]
 |
 `--- /src
        |
        `--- /main
               |
               `--- /resources
                      |
                      `--- logback.xml
```

logback.xml

```xml:logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="INFO">
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Pattern>
        </encoder>
    </appender>

    <logger name="com.example.demo" level="debug" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>

    <logger name="org.jooq" level="info" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>
    <logger name="org.thymeleaf" level="info" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>
    <root level="info">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>
```

### アプリケーションログの出力

ログを出力したいクラスに以下のようにロガーを定義します。

```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class App {
  static final Logger LOG = LoggerFactory.getLogger(App.class);

  // ...省略...

}
```

## テンプレートエンジンにThymeleafを利用する

JavalinではVelocity、Freemarker、Thymeleafなどのテンプレートエンジンを簡単に利用できるようになっています。
この例ではThymeleafを利用します。

```xml:pom.xml
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf</artifactId>
    <version>3.0.9.RELEASE</version>
</dependency>
```

以下の場所に`hello_world.html`というテンプレートファイルを配置します。同じファイル名のプロパティファイルはメッセージリソースです。

```text
[project_root]
 |
 `--- /src
        |
        `--- /main
               |
               `--- /resources
                      |
                      `--- /WEB-INF
                             |
                             `--- /templates
                                   |
                                   `--- hello_world.html
                                   `--- hello_world.properties
                                   `--- hello_world_ja.properties
                                   `--- hello_world_en.properties
```

hello_world.html

```html
<!doctype html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script th:src="@{/webjars/jquery/3.4.1/jquery.min.js}"></script>
    <title>Hello World</title>
</head>
<body>
<div>
    <p th:text="${message}">message</p>
    <p th:text="${now}">now</p>
    <p th:text="#{msg.hello}">message</p>
</div>
</body>
</html>
```

メッセージリソースの定義

```properties:hello_world.properties
msg.hello = Hello World (default)
```

```properties:hello_world_en.properties
msg.hello = Hello World
```

```properties:hello_world_ja.properties
msg.hello = ハローワールド
```

テンプレートエンジンを利用してレスポンスするにはrenderメソッドを使用します。
なお、どのテンプレートエンジンが利用されるかはテンプレートファイルの拡張子で決まります。Thymeleafの場合は`.html`、`.tl`、`.thyme`、`.thymeleaf`が該当します。

```java
app.get("/hello", ctx -> {
  Map<String, Object> model = new HashMap<>();
  model.put("message", "Hello World");
  model.put("now", LocalDateTime.now());
  ctx.render("/WEB-INF/templates/hello_world.html", model);
});
```

`localhost:7000/hello`にアクセスするとテンプレートエンジンで描画されたhtmlページが表示されます。

### ブラウザのAccept-Languageでメッセージを切り替える

デフォルトでは、アプリケーション起動時のデフォルトロケールでメッセージリソースが決まります。
ある言語のメッセージリソースを利用するには、Javalinアプリケーション起動時のJVMオプションにロケールを指定します。
例えばenのメッセージリソースを利用するには以下のオプションを指定します。

```bat
> java -Duser.language=en -Duser.country=US -jar <jarファイル>
```

Spring BootのようにブラウザのAccept-Languageに対応するメッセージリソースを利用するには、FileRendererを実装したクラスを作ります。

```java:CustomThymeleafRenderer
import io.javalin.http.Context;
import io.javalin.plugin.rendering.FileRenderer;
import org.jetbrains.annotations.NotNull;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;

import java.util.Map;
import java.util.concurrent.TimeUnit;

public class CustomThymeleafRenderer implements FileRenderer {

  private final TemplateEngine templateEngine;

  public CustomThymeleafRenderer() {
    templateEngine = templateEngine();
  }

  @Override
  public String render(@NotNull String filePath, @NotNull Map<String, Object> model, @NotNull Context ctx) {
    WebContext context = new WebContext(ctx.req, ctx.res, ctx.req.getServletContext(), ctx.req.getLocale());
    context.setVariables(model);
    return templateEngine.process(filePath, context);
  }

  private TemplateEngine templateEngine() {
    TemplateEngine templateEngine = new TemplateEngine();
    templateEngine.setTemplateResolver(templateResolver());
    templateEngine.addDialect(new Java8TimeDialect());
    return templateEngine;
  }

  private ITemplateResolver templateResolver() {
    ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
    templateResolver.setPrefix("/WEB-INF/templates/");
    templateResolver.setSuffix(".html");
    templateResolver.setCharacterEncoding("UTF-8");
    templateResolver.setCacheable(true);
    templateResolver.setCacheTTLMs(TimeUnit.MINUTES.toMillis(30));
    templateResolver.setTemplateMode(TemplateMode.HTML);
    return templateResolver;
  }

}
```

このCustomThymeleafRendererをJavalinアプリケーションに登録します。

```java
JavalinRenderer.register(new CustomThymeleafRenderer(), ".html");
```

このカスタマイズを行うと、テンプレートファイルのパスを指定する必要は無くなるのでハンドラの実装は以下のようになります。

```java
// ctx.render("/WEB-INF/templates/hello_world.html", model);
ctx.render("hello_world.html", model);
```


ちなみにデフォルトで登録されているrendererは以下のとおりです。

```kotlin:JavalinRenderer
object JavalinRenderer {

  init {
    register(JavalinVelocity, ".vm", ".vtl")
    register(JavalinFreemarker, ".ftl")
    register(JavalinMustache, ".mustache")
    register(JavalinJtwig, ".jtwig", ".twig", ".html.twig")
    register(JavalinPebble, ".peb", ".pebble")
    register(JavalinThymeleaf, ".html", ".tl", ".thyme", ".thymeleaf")
    register(JavalinCommonmark, ".md", ".markdown")
  }

  // ...省略...

}
```

## データベースを利用する

データベースにMySQL 8.0、ORMにjOOQ、コネクションプールにHikariCPを利用しました。

※2019年8月現在、JavalinにはjOOQに関するプラグインやユーティリティ、コンフィグレーションクラスはないようです。なのでjOOQによるデータベースアクセス周りの実装は自前で行う必要があります。

### データベースの準備

Javalinアプリケーションで使用する`sample_db`データベースと`test_user`ユーザーを管理者権限を持つユーザーで作成します。

```sql
CREATE DATABASE IF NOT EXISTS sample_db
  CHARACTER SET = utf8mb4
  COLLATE = utf8mb4_general_ci
;

CREATE USER IF NOT EXISTS 'test_user'@'localhost'
  IDENTIFIED BY 'test_user'
  PASSWORD EXPIRE NEVER
;

GRANT ALL ON sample_db.* TO 'test_user'@'localhost';
```

次に`test_user`ユーザーで`user`テーブルを作成します。

```sql
DROP TABLE IF EXISTS user;
CREATE TABLE user (
  id BIGINT AUTO_INCREMENT                    COMMENT 'ユーザーID',
  nick_name VARCHAR(60) NOT NULL              COMMENT 'ニックネーム',
  sex CHAR(1) NOT NULL                        COMMENT '性別 M:男性 F:女性',
  prefecture_id TINYINT(1) NOT NULL DEFAULT 0 COMMENT '都道府県 0:不明、1:北海道 - 8:九州・沖縄',
  email VARCHAR(120)                          COMMENT 'メールアドレス',
  memo TEXT                                   COMMENT '備考欄',
  create_at DATETIME NOT NULL DEFAULT NOW(),
  update_at DATETIME NOT NULL DEFAULT NOW(),
  PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = UTF8MB4
COMMENT = 'ユーザーテーブル';

CREATE INDEX idx_sex on user (sex) USING BTREE;
CREATE INDEX idx_pref on user (prefecture_id) USING BTREE;
```

### 依存関係を追加

pom.xmlを編集して以下の依存関係を追加します。

* [jOOQ] (https://www.jooq.org/)
* [HikariCP] (https://github.com/brettwooldridge/HikariCP)
* [MySQL Connector/J] (https://dev.mysql.com/doc/connector-j/8.0/en/)

```xml:pom.xml
<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq</artifactId>
    <version>3.11.11</version>
</dependency>
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>3.3.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.17</version>
    <scope>runtime</scope>
</dependency>
```

### コードの自動生成

jOOQには`jooq-codegen-maven`プラグインというデータベーススキーマからモデル（エンティティ）のコードを自動生成する機能があります。

* [Running the code generator with Maven] (https://www.jooq.org/doc/3.11/manual/code-generation/codegen-maven/)
* [Configuration and setup of the generator] (https://www.jooq.org/doc/3.11/manual/code-generation/codegen-configuration/)
* [jooq-codegen-3.11.0.xsd] (https://www.jooq.org/xsd/jooq-codegen-3.11.0.xsd)

```xml
<plugin>
    <groupId>org.jooq</groupId>
    <artifactId>jooq-codegen-maven</artifactId>
    <version>3.11.11</version>
    <executions>
        <execution>
            <id>jooq-codegen</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
        </dependency>
    </dependencies>
    <configuration>
        <jdbc>
            <url>jdbc:mysql://localhost:3306</url>
            <user>test_user</user>
            <password>test_user</password>
        </jdbc>
        <generator>
            <database>
                <name>org.jooq.meta.mysql.MySQLDatabase</name>
                <includes>.*</includes>
                <inputSchema>sample_db</inputSchema>
                <!-- Configure type overrides for generated fields, attributes, sequences, parameters. -->
                <forcedTypes>
                    <forcedType>
                        <!--Specify the Java type of your custom type. This corresponds to the Converter's <U> type. -->
                        <userType>com.example.demo.converter.Prefecture</userType>
                        <!-- A converter implementation for the {@link #getUserType(). -->
                        <converter>com.example.demo.converter.PrefectureConverter</converter>
                        <!-- regex to match the column name -->
                        <expression>PREFECTURE_ID</expression>
                        <types>.*</types>
                    </forcedType>
                    <forcedType>
                        <userType>com.example.demo.converter.Sex</userType>
                        <converter>com.example.demo.converter.SexConverter</converter>
                        <expression>SEX</expression>
                        <types>.*</types>
                    </forcedType>
                </forcedTypes>
            </database>
            <target>
                <packageName>com.example.demo.model</packageName>
                <directory>src/main/java</directory>
            </target>
            <generate>
                <!-- A flag indicating whether Java 8's java.time types should be used by the source code generator, rather than JDBC's java.sql types. -->
                <javaTimeTypes>true</javaTimeTypes>
                <!-- Generate index information -->
                <indexes>true</indexes>
                <!-- Primary key / foreign key relations should be generated and used. This is a prerequisite for various advanced features -->
                <relations>true</relations>
                <!-- Generate deprecated code for backwards compatibility -->
                <deprecated>false</deprecated>
                <!-- Generate the {@link javax.annotation.Generated} annotation to indicate jOOQ version used for source code -->
                <generatedAnnotation>false</generatedAnnotation>
                <!-- Generate Sequence classes -->
                <sequences>true</sequences>
                <!-- Generate Key classes -->
                <keys>true</keys>
                <!-- Generate Table classes -->
                <tables>true</tables>
                <!-- Generate TableRecord classes -->
                <records>true</records>
                <!-- Generate POJOs -->
                <pojos>true</pojos>
                <!-- Generate basic equals() and hashCode() methods in POJOs -->
                <pojosEqualsAndHashCode>true</pojosEqualsAndHashCode>
                <!-- Generate basic toString() methods in POJOs -->
                <pojosToString>true</pojosToString>
                <!-- Generate serializable POJOs -->
                <serializablePojos>true</serializablePojos>
                <!-- Generated interfaces to be implemented by records and/or POJOs -->
                <interfaces>false</interfaces>
                <!-- Turn off generation of all global object references -->
                <globalObjectReferences>true</globalObjectReferences>
                <!-- Turn off generation of global catalog references -->
                <globalCatalogReferences>true</globalCatalogReferences>
                <!-- Turn off generation of global schema references -->
                <globalSchemaReferences>true</globalSchemaReferences>
                <!-- Turn off generation of global table references -->
                <globalTableReferences>true</globalTableReferences>
                <!-- Turn off generation of global sequence references -->
                <globalSequenceReferences>true</globalSequenceReferences>
                <!-- Turn off generation of global UDT references -->
                <globalUDTReferences>true</globalUDTReferences>
                <!-- Turn off generation of global routine references -->
                <globalRoutineReferences>true</globalRoutineReferences>
                <!-- Turn off generation of global queue references -->
                <globalQueueReferences>true</globalQueueReferences>
                <!-- Turn off generation of global database link references -->
                <globalLinkReferences>true</globalLinkReferences>
                <!-- Turn off generation of global key references -->
                <globalKeyReferences>true</globalKeyReferences>
                <!-- Generate fluent setters in records, POJOs, interfaces -->
                <fluentSetters>true</fluentSetters>
            </generate>
        </generator>
    </configuration>
</plugin>
```

以下のmvnコマンドでコード生成を行います。(mvn packageでも生成されます)

```bat
> mvn jooq-codegen:generate
```

コードは`com.example.demo.model`パッケージに出力されます。今回の例では以下のコードが生成されました。

```text
[project_root]
 |
 `--- /src
        |
        `--- /main
               |
               `--- /java
                     |
                     `--- com.example.demo.model
                     |      |
                     |      `--- DefaultCatalog.java
                     |      `--- Indexes.java
                     |      `--- Keys.java
                     |      `--- SampleDb.java
                     |      `--- Tables.java
                     |
                     `--- com.example.demo.model.tables
                     |      |
                     |      `--- User.java                          // (1)
                     |
                     `--- com.example.demo.model.tables.records
                     |      |
                     |      `--- UserRecord.java                    // (2)
                     |
                     `--- com.example.demo.model.tables.pojos
                            |
                            `--- User.java                          // (3)
```

* (1) : userテーブルに対するエンティティクラスです。
    * jOOQで操作するためのメソッドが備わっています。
* (2) : userテーブルのカラムに対するエンティティレコードクラスです。
    * jOOQで操作するためのメソッドが備わっています。
* (3) : userテーブルに対するPojoクラス（オプション）です。この記事ではjOOQで検索したエンティティはPojoへ変換して呼び出し元（コントローラ）へ返すように実装しました。

### Javalinアプリケーション側の実装

#### 接続プロパティファイル

接続情報はプロパティファイルで管理します。

```properties:datasource.properties
datasource.jdbcUrl = jdbc:mysql://localhost:3306/sample_db
datasource.userName = test_user
datasource.password = test_user
```

```java:DbPropertyLoader
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Properties;

public class DbPropertyLoader {
  private final Properties prop;

  private static final String JDBC_URL = "datasource.jdbcUrl";
  private static final String USER_NAME = "datasource.userName";
  private static final String PASSWORD = "datasource.password";

  public DbPropertyLoader(String fileName) {
    prop = loadProperties(fileName);
  }

  public String getJdbcUrl() {
    return prop.getProperty(JDBC_URL);
  }

  public String getUserName() {
    return prop.getProperty(USER_NAME);
  }

  public String getPassword() {
    return prop.getProperty(PASSWORD);
  }

  private Properties loadProperties(String fileName) {
    try (InputStream stream = getClass().getClassLoader().getResourceAsStream(fileName)) {
      if (stream == null) {
        throw new IOException("Not Found : " + fileName);
      }
      Properties prop = new Properties();
      prop.load(stream);
      return prop;
    } catch (IOException e) {
      throw new UncheckedIOException(e);
    }
  }

}
```

#### DSLContextの作成

jOOQでデータベースアクセスするにはjOOQのDSLContextというクラスのインスタンスが必要です。
以下のようにDSLContextのインスタンスを保持するシングルトンのホルダークラスを実装しました。
なお、DSLContextはスレッドセーフです。

> [DSLContext is thread safety?] (https://github.com/jOOQ/jOOQ/issues/7410)

```java:DslConfigure
import org.jooq.Configuration;
import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.TransactionProvider;
import org.jooq.conf.Settings;
import org.jooq.impl.DefaultConfiguration;

import javax.sql.DataSource;
import java.util.Optional;

public interface DslConfigure {
  DSLContext getDslContext();

  DataSource getDataSource();
  SQLDialect getSqlDialect();
  Settings getSettings();

  // optional settings
  Optional<ConnectionProvider> getConnectionProvider();
  Optional<TransactionProvider> getTransactionProvider();

  default Configuration configuration() {
    Configuration config = new DefaultConfiguration();
    config
        .set(getSqlDialect())
        .set(getSettings());
    getConnectionProvider().ifPresentOrElse(cp -> {
      config.set(cp);
      getTransactionProvider().ifPresent(tp -> {
        config.set(tp);
      });
    }, () -> {
      config.set(getDataSource());
    });
    return config;
  }
}
```

```java:DslContextHolder
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.TransactionProvider;
import org.jooq.conf.Settings;
import org.jooq.conf.StatementType;
import org.jooq.impl.DSL;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultTransactionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.io.Serializable;
import java.util.Optional;

public class DslContextHolder implements DslConfigure, Serializable {
  private static final long serialVersionUID = -8276859195238889686L;
  private static final Logger LOG = LoggerFactory.getLogger(DslContextHolder.class);

  private static DslContextHolder instance;

  private final DSLContext dslContext;
  private final DataSource dataSource;
  private final Settings settings;
  private final ConnectionProvider connectionProvider;
  private final TransactionProvider transactionProvider;

  private DslContextHolder() {
    dataSource = createDataSource();
    settings = createSettings();
    connectionProvider = new DataSourceConnectionProvider(dataSource);
    transactionProvider = new DefaultTransactionProvider(connectionProvider, true);
    // transactionProvider = new ThreadLocalTransactionProvider(connectionProvider, true);
    dslContext = DSL.using(configuration());
  }

  public static DslContextHolder getInstance() {
    if (instance == null) {
      synchronized (DslContextHolder.class) {
        if (instance == null) {
          instance = new DslContextHolder();
          LOG.debug("DSL Context create : {}", instance.getDslContext().toString());
        }
      }
    }
    return instance;
  }

  public static void destroy() {
    if (instance != null) {
      synchronized (DslContextHolder.class) {
        if (instance != null) {
          LOG.debug("DSL Context destroy : {}", instance.getDslContext().toString());
          instance.dslContext.close();
          instance = null;
        }
      }
    }
  }

  DataSource createDataSource() {
    DbPropertyLoader prop = new DbPropertyLoader("datasource.properties");
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl(prop.getJdbcUrl());
    config.setUsername(prop.getUserName());
    config.setPassword(prop.getPassword());
    config.setPoolName("hikari-cp");
    config.setAutoCommit(false);
    config.setConnectionTestQuery("select 1");
    config.setMaximumPoolSize(10);
    DataSource dataSource = new HikariDataSource(config);
    return dataSource;
  }

  Settings createSettings() {
    return new Settings()
        .withStatementType(StatementType.STATIC_STATEMENT)
        .withQueryTimeout(10)
        .withMaxRows(1000)
        .withFetchSize(20)
        .withExecuteLogging(true);
  }

  @Override
  public DSLContext getDslContext() {
    return this.dslContext;
  }

  @Override
  public DataSource getDataSource() {
    return this.dataSource;
  }

  @Override
  public SQLDialect getSqlDialect() {
    return SQLDialect.MYSQL_8_0;
  }

  @Override
  public Settings getSettings() {
    return this.settings;
  }

  @Override
  public Optional<ConnectionProvider> getConnectionProvider() {
    //
    return Optional.ofNullable(this.connectionProvider);
  }

  @Override
  public Optional<TransactionProvider> getTransactionProvider() {
    //
    return Optional.ofNullable(this.transactionProvider);
  }

}
```

#### サービスの実装例

jOOQのDSLContextと自動生成したコード（エンティティクラス）を利用してデータベースアクセスを行うサービスクラスの実装例です。
※通常はビジネスロジックを記述することろですが、サンプルコードなのでリポジトリのような実装になってしまっています。

```java:UserService
import com.example.demo.model.tables.pojos.User;

import java.util.List;
import java.util.Optional;

public interface UserService {
  Optional<User> findById(Long id);
  List<User> findByNickName(String nickName);
  List<User> findAll();
  User save(User user);
  void remove(Long id);
}
```

```java:UserServiceImpl
import com.example.demo.AppServiceException;
import com.example.demo.model.tables.pojos.User;
import com.example.demo.model.tables.records.UserRecord;
import com.example.demo.service.UserService;
import org.jooq.DSLContext;
import org.jooq.Result;
import org.jooq.impl.DSL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static com.example.demo.model.Tables.USER;

public class UserServiceImpl implements UserService {
  private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);
  private final DSLContext dsl;

  public UserServiceImpl(DSLContext dsl) {
    this.dsl = dsl;
  }

  @Override
  public Optional<User> findById(Long id) {
    LOG.debug("find by Id : {}", id);
    UserRecord record = dsl
        .selectFrom(USER)
        .where(USER.ID.eq(id))
        .fetchOne();
    if (record != null) {
      return Optional.of(record.into(User.class));
    }
    return Optional.empty();
  }

  @Override
  public List<User> findByNickName(String nickName) {
    LOG.debug("find by nickName : {}", nickName);
    Result<UserRecord> result = dsl
        .selectFrom(USER)
        .where(USER.NICK_NAME.contains(nickName))
        .orderBy(USER.NICK_NAME.asc())
        .fetch();
    return result.into(User.class);
  }

  @Override
  public List<User> findAll() {
    LOG.debug("findAll");
    Result<UserRecord> result = dsl
        .selectFrom(USER)
        .orderBy(USER.ID.desc())
        .fetch();
    return result.into(User.class);
  }

  @Override
  public User save(User user) {
    LOG.debug("save : {}", user);
    UserRecord result = dsl.transactionResult(conf -> {
      if (user.getId() == null || user.getId().equals(0L)) {
        return DSL.using(conf)
            .insertInto(USER,
                USER.NICK_NAME,
                USER.SEX,
                USER.PREFECTURE_ID,
                USER.EMAIL,
                USER.MEMO)
            .values(
                user.getNickName(),
                user.getSex(),
                user.getPrefectureId(),
                user.getEmail(),
                user.getMemo())
            .returning()
            .fetchOne();
      } else {
        return DSL.using(conf)
            .update(USER)
            .set(USER.NICK_NAME, user.getNickName())
            .set(USER.SEX, user.getSex())
            .set(USER.PREFECTURE_ID, user.getPrefectureId())
            .set(USER.EMAIL, user.getEmail())
            .set(USER.MEMO, user.getMemo())
            .set(USER.UPDATE_AT, LocalDateTime.now())
            .where(USER.ID.eq(user.getId()))
            .returning()
            .fetchOne();
      }
    });
    LOG.debug("save result : {}", result);
    if (result == null) {
      throw new AppServiceException("[save] Not Found saved user record.");
    }
    return result.into(User.class);
  }

  @Override
  public void remove(Long id) {
    LOG.debug("remove by id : {}", id);
    int result = dsl.transactionResult(conf -> {
      int count = DSL.using(conf)
          .selectCount()
          .from(USER)
          .where(USER.ID.eq(id))
          .execute();
      if (count != 1) {
        return count;
      }
      return DSL.using(conf)
          .deleteFrom(USER)
          .where(USER.ID.eq(id))
          .execute();
    });
    LOG.debug("remove result : {}", result);
    if (result == 0) {
      throw new AppServiceException("[remove] Not Found delete user record.");
    }
  }

}
```

#### コントローラの実装例

上記のサービスを利用するコントローラの実装例です。

```java:UserController
import com.example.demo.model.tables.pojos.User;
import com.example.demo.service.UserService;
import io.javalin.Javalin;
import io.javalin.apibuilder.ApiBuilder;
import io.javalin.http.Context;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

public class UserController {
  private static final Logger LOG = LoggerFactory.getLogger(UserController.class);
  private final UserService userService;

  public UserController(UserService userService) {
    this.userService = userService;
  }

  void findById(@NotNull Context ctx) {
    Long id = ctx.pathParam("id", Long.class).get();
    userService.findById(id).ifPresentOrElse(user -> {
      ctx.json(user);
    },() -> {
      ctx.status(404);
    });
  }

  void findAll(@NotNull Context ctx) {
    List<User> users = userService.findAll();
    ctx.json(users);
  }

  void store(@NotNull Context ctx) {
    User user = ctx.bodyAsClass(User.class);
    User storedUser = userService.save(user);
    ctx.json(storedUser);
  }

  void remove(@NotNull Context ctx) {
    Long id = ctx.pathParam("id", Long.class).get();
    userService.remove(id);
    ctx.status(200);
  }

  public void bindEndpoints(@NotNull Javalin app) {
    app.routes(() -> {
      ApiBuilder.path("api/user", () -> {
        // GET /api/user
        ApiBuilder.get(this::findAll);
        // GET /api/user/:id
        ApiBuilder.get(":id", this::findById);
        // POST /api/user
        ApiBuilder.post(this::store);
        // DELETE /api/user/:id
        ApiBuilder.delete(":id", this::remove);
      });
    });
  }

}
```

ハンドラのバインドはJavalinアプリケーションの起動ポイントで行いました。

```java
public static void main(String... args) {
  Javalin app = Javalin
    .create(config -> {
      // configuration
    })
    .start(7000);

  // ... 省略 ...

  // DSLContextの生成
  final DSLContext dsl = DslContextHolder.getInstance().getDslContext();

  // アプリケーションにハンドラをバインドする
  new UserController(new UserServiceImpl(dsl)).bindEndpoints(app);

  // ... 省略 ...
}
```

## テストコード

テスティングフレームワークにJUnit 5、アサーションにAssertJを利用しました。

### 依存関係を追加

pom.xmlを編集して以下の依存関係を追加します。

* [JUnit 5] (https://junit.org/junit5/)
* [AssertJ] (https://joel-costigliola.github.io/assertj/)

```xml:pom.xml
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.5.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.13.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <!--<version>1.4.199</version>-->
    <version>1.4.193</version>
    <scope>test</scope>
</dependency>
```

### サービスクラスのテスト

サービスクラスのテストは単体テストとして実装します。サービスが依存する外部リソースのデータベースにはH2を使用します。
テスト開始前にスキーマを作成、テストメソッド毎にテストデータをロード、テスト終了後にスキーマを削除するという方法でテスト環境を準備します。

#### テスト用の接続プロパティファイル

```properties:test-datasource.properties
#datasource.driver = org.h2.Driver
datasource.jdbcUrl = jdbc:h2:mem:sample_db;MODE=MySQL;DB_CLOSE_DELAY=-1;
#datasource.jdbcUrl = jdbc:h2:tcp://localhost:9092/./sample_db;MODE=MySQL;DATABASE_TO_UPPER=false
datasource.userName = sa
datasource.password =
```

#### テスト用DSLContextの作成

H2用のDSLContextを作成するコードを実装します。

```java:TestDslContextHolder
import com.example.demo.config.DbPropertyLoader;
import com.example.demo.config.DslConfigure;
import org.h2.jdbcx.JdbcDataSource;
import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.TransactionProvider;
import org.jooq.conf.Settings;
import org.jooq.conf.StatementType;
import org.jooq.impl.DSL;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultTransactionProvider;

import javax.sql.DataSource;
import java.io.Serializable;
import java.util.Optional;

public class TestDslContextHolder implements DslConfigure, Serializable {
  private static final long serialVersionUID = -5728144007357037196L;

  private static TestDslContextHolder instance;

  private final DSLContext dslContext;
  private final DataSource dataSource;
  private final Settings settings;
  private final ConnectionProvider connectionProvider;
  private final TransactionProvider transactionProvider;

  public TestDslContextHolder() {
    dataSource = createDataSource();
    settings = createSettings();
    connectionProvider = new DataSourceConnectionProvider(dataSource);
    transactionProvider = new DefaultTransactionProvider(connectionProvider, true);
    // transactionProvider = new ThreadLocalTransactionProvider(connectionProvider, true);
    dslContext = DSL.using(configuration());
  }

  public static TestDslContextHolder getInstance() {
    if (instance == null) {
      synchronized (TestDslContextHolder.class) {
        if (instance == null) {
          instance = new TestDslContextHolder();
        }
      }
    }
    return instance;
  }

  public static void destroy() {
    if (instance != null) {
      synchronized (TestDslContextHolder.class) {
        if (instance != null) {
          instance.dslContext.close();
          instance = null;
        }
      }
    }
  }

  DataSource createDataSource() {
    DbPropertyLoader prop = new DbPropertyLoader("test-datasource.properties");
    JdbcDataSource dataSource = new JdbcDataSource();
    dataSource.setURL(prop.getJdbcUrl());
    dataSource.setUser(prop.getUserName());
    dataSource.setPassword(prop.getPassword());
    return dataSource;
  }

  Settings createSettings() {
    return new Settings()
        .withStatementType(StatementType.STATIC_STATEMENT)
        .withQueryTimeout(10)
        .withMaxRows(1000)
        .withFetchSize(20)
        .withExecuteLogging(true)
        .withDebugInfoOnStackTrace(true)
        .withRenderFormatted(true);
  }

  @Override
  public DSLContext getDslContext() {
    return this.dslContext;
  }

  @Override
  public DataSource getDataSource() {
    return this.dataSource;
  }

  @Override
  public SQLDialect getSqlDialect() {
    return SQLDialect.H2;
  }

  @Override
  public Settings getSettings() {
    return this.settings;
  }

  @Override
  public Optional<ConnectionProvider> getConnectionProvider() {
    //
    return Optional.ofNullable(this.connectionProvider);
  }

  @Override
  public Optional<TransactionProvider> getTransactionProvider() {
    //
    return Optional.ofNullable(this.transactionProvider);
  }

}
```

サービスのテストクラス
このコードはインメモリのH2を使用するパターンです。（サーバーモードのH2を利用するパターンは後述します。）

```java:TestService
import org.jooq.DSLContext;
import org.jooq.Queries;
import org.jooq.Query;
import org.jooq.Schema;
import org.jooq.Table;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;

import java.util.List;

public interface TestService {

  DSLContext getDSLContext();
  void setDSLContext(DSLContext dsl);

  default void createSchema(Schema schema) {
    Queries queries = getDSLContext().ddl(schema);
    for (Query query : queries.queries()) {
      getDSLContext().execute(query);
    }
  }

  default void dropSchema(Schema schema) {
    List<Table<?>> tables = schema.getTables();
    for (Table<?> table : tables) {
      getDSLContext().dropTableIfExists(table).execute();
    }
    getDSLContext().dropSchemaIfExists(schema).execute();
  }

  default void cleanUp(List<Table<?>> tables) {
    getDSLContext().transaction(conf -> {
      for (Table table : tables) {
        DSL.using(conf).deleteFrom(table).execute();
      }
    });
  }

  default void testDataRollback() {
    throw new DataAccessException("rollback");
  }

  default void isFailed(RuntimeException e) {
    if (!e.getMessage().equals("rollback")) {
      System.err.println(e);
      throw e;
    }
  }

}
```

```java:UserServiceImplTests
import com.example.demo.TestDslContextHolder;
import com.example.demo.converter.Prefecture;
import com.example.demo.converter.Sex;
import com.example.demo.model.tables.pojos.User;
import com.example.demo.model.tables.records.UserRecord;
import com.example.demo.service.TestService;
import com.example.demo.service.UserService;
import org.assertj.core.groups.Tuple;
import org.jooq.DSLContext;
import org.jooq.exception.DataAccessException;
import org.jooq.impl.DSL;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static com.example.demo.model.SampleDb.SAMPLE_DB;
import static com.example.demo.model.Tables.*;

@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
public class UserServiceImplTests implements TestService {

  private DSLContext dsl;

  @Override
  public DSLContext getDSLContext() {
    return dsl;
  }

  @Override
  public void setDSLContext(DSLContext dsl) {
    this.dsl = dsl;
  }

  @BeforeAll
  public void setupSchema() {
    System.out.println("setup class");
    setDSLContext(TestDslContextHolder.getInstance().getDslContext());
    createSchema(SAMPLE_DB);
  }

  @AfterAll
  public void tearDownSchema() {
    System.out.println("teardown class");
    dropSchema(SAMPLE_DB);
    TestDslContextHolder.destroy();
  }

  @BeforeEach
  public void setup() {
    System.out.println("setup");
  }

  @AfterEach
  public void tearDown() {
    System.out.println("teardown");
    cleanUp(List.of(USER));
  }

  @Test
  public void findById() {
    System.out.println("findById");
    try {
      dsl.transaction(conf -> {
        // setup test data
        DSL.using(conf)
            .insertInto(USER,
                USER.ID,
                USER.NICK_NAME,
                USER.SEX,
                USER.PREFECTURE_ID,
                USER.EMAIL,
                USER.MEMO)
            .values(1L, "nick name AAA", Sex.FEMALE, Prefecture.HOKKAIDO, "test.user_a@example.com", "memo aaa")
            .values(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "test.user_b@example.com", null)
            .values(3L, "nick name CCC", Sex.FEMALE, Prefecture.KANTOU, "test.user_c@example.com", "memo ccc")
            .execute();

        // exercise
        UserService sut = new UserServiceImpl(DSL.using(conf));
        Optional<User> actual = sut.findById(1L);

        // verify
        assertThat(actual.isPresent()).isTrue();
        actual.ifPresent(user -> {
          assertThat(user)
              .extracting(
                  "id",
                  "nickName",
                  "sex",
                  "prefectureId",
                  "email",
                  "memo")
              .contains(
                  1L,
                  "nick name AAA",
                  Sex.FEMALE,
                  Prefecture.HOKKAIDO,
                  "test.user_a@example.com",
                  "memo aaa");
        });

        testDataRollback();
      });
    } catch (DataAccessException e) {
      isFailed(e);
    }
  }

  @Test
  public void findByNickName() {
    System.out.println("findByNickName");
    try {
      dsl.transaction(conf -> {
        // setup test data
        DSL.using(conf)
            .insertInto(USER,
                USER.ID,
                USER.NICK_NAME,
                USER.SEX,
                USER.PREFECTURE_ID,
                USER.EMAIL,
                USER.MEMO)
            .values(1L, "nick name AAA", Sex.FEMALE, Prefecture.HOKKAIDO, "test.user_a@example.com", "memo aaa")
            .values(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "test.user_b@example.com", null)
            .values(3L, "nick name CCC", Sex.FEMALE, Prefecture.KANTOU, "test.user_c@example.com", "memo ccc")
            .execute();

        // exercise
        UserService sut = new UserServiceImpl(DSL.using(conf));
        List<User> actual = sut.findByNickName("BBB");

        // verify
        assertThat(actual).hasSize(1);
        assertThat(actual)
            .extracting(
                "id",
                "nickName",
                "sex",
                "prefectureId",
                "email",
                "memo")
            .contains(Tuple.tuple(2L, "nick name BBB", Sex.MALE, Prefecture.TOHOKU, "test.user_b@example.com", null));

        testDataRollback();
      });
    } catch (DataAccessException e) {
      isFailed(e);
    }
  }

  @Test
  public void save() {
    try {
      dsl.transaction(conf -> {
        // exercise
        User testData = new User(null, "nick name DDD", Sex.MALE, Prefecture.KANTOU, "test.user_d@example.com", null, null, null);
        UserService sut = new UserServiceImpl(DSL.using(conf));
        User expected = sut.save(testData);

        // verify
        Optional<User> actual = sut.findById(expected.getId());
        assertThat(actual.isPresent()).isTrue();
        actual.ifPresent(user -> {
          assertThat(user)
              .extracting(
                  "id",
                  "nickName",
                  "sex",
                  "prefectureId",
                  "email",
                  "memo")
              .contains(
                  expected.getId(),
                  expected.getNickName(),
                  expected.getSex(),
                  expected.getPrefectureId(),
                  expected.getEmail(),
                  expected.getMemo());
        });

        testDataRollback();
      });
    } catch (DataAccessException e) {
      isFailed(e);
    }
  }

  @Test
  public void remove() {
    try {
      dsl.transaction(conf -> {
        // setup
        Long userId = 1L;
        DSL.using(conf)
            .insertInto(USER,
                USER.ID,
                USER.NICK_NAME,
                USER.SEX,
                USER.PREFECTURE_ID,
                USER.EMAIL,
                USER.MEMO)
            .values(
                userId,
                "nick name EEE",
                Sex.MALE,
                Prefecture.CHUBU,
                "test.user_e@example.com",
                "memo eee")
            .execute();

        // exercise
        UserService sut = new UserServiceImpl(DSL.using(conf));
        sut.remove(userId);

        // verify
        UserRecord actual = DSL.using(conf)
            .selectFrom(USER)
            .where(USER.ID.eq(userId))
            .fetchOne();
        assertThat(actual).isNull();

        testDataRollback();
      });
    } catch (DataAccessException e) {
      isFailed(e);
    }
  }

}
```

#### H2をサーバモードで使用する

サーバーモードではスキーマやテーブル、テストデータは以下の場所に配置したSQLスクリプトを実行して作成します。
テストを実行すると、H2データベースのデータベースファイルが`sample_db.mv.db`ファイルとして作成されます。

```text
[project_root]
 |
 `--- /h2
        |
        `--- /sql
        |      |
        |      `--- 01_schema.sql     //スキーマを作成する
        |      `--- 02_table.sql      //テーブルを作成する
        |      `--- 03_data.sql       //データを挿入する
        |      `--- 04_table.sql      //テーブルを削除する
        |      `--- 05_schema.sql     //スキーマを削除する
        |
        `--- sample_db.mv.db
```

```java:TestH2Server
import com.example.demo.config.DbPropertyLoader;
import org.h2.tools.RunScript;
import org.h2.tools.Server;

import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

public class TestH2Server implements AutoCloseable {

  private final Server server;

  private final String jdbcUrl;
  private final String userName;
  private final String password;

  public TestH2Server() {
    DbPropertyLoader prop = new DbPropertyLoader("test-datasource.properties");
    jdbcUrl = prop.getJdbcUrl();
    userName = prop.getUserName();
    password = prop.getPassword();
    String[] params = new String[] {"-tcp", "-tcpPort", "9092", "-baseDir", "./h2"};
    try {
      server = Server.createTcpServer(params);
    } catch (SQLException e) {
      System.err.println(e.getMessage());
      throw new RuntimeException(e);
    }
  }

  public void start() {
    System.out.println("server start");
    try {
      server.start();
      status();
      Runtime.getRuntime().addShutdownHook(new Thread(() -> shutdown()));
    } catch (SQLException e) {
      throw new RuntimeException(e);
    }
  }

  public void runScript(List<String> scripts) {
    try {
      for (String script : scripts) {
        System.out.println("run script : " + script);
        RunScript.execute(jdbcUrl, userName, password, script, StandardCharsets.UTF_8, false);
      }
    } catch (SQLException e) {
      System.err.println("run script : " + e.getMessage());
      throw new RuntimeException(e);
    }
  }

  public void shutdown() {
    if (server.isRunning(true)) {
      System.out.println("server shutdown");
      server.shutdown();
    }
  }

  public void status() {
    System.out.println(server.getStatus());
  }

  // for test
  public static void main(String ... args) {
    try (TestH2Server h2 = new TestH2Server()) {
      h2.start();
      h2.status();
      h2.runScript(List.of("./h2/sql/01_schema.sql", "./h2/sql/02_table.sql", "./h2/sql/03_data.sql"));
      h2.test();
      h2.runScript(List.of("./h2/sql/04_table.sql", "./h2/sql/05_schema.sql"));
    }
    System.exit(0);
  }

  public void test() {
    System.out.println("test");
    try (Connection conn = DriverManager.getConnection(jdbcUrl, userName, password)) {
      PreparedStatement ps = conn.prepareStatement("select * from sample_db.user");
      ResultSet rs = ps.executeQuery();
      while (rs.next()) {
        System.out.println("id : " + rs.getLong("id"));
        System.out.println("nick_name : " + rs.getString("nick_name"));
        System.out.println("sex : " + rs.getString("sex"));
        System.out.println("prefecture_id : " + rs.getInt("prefecture_id"));
        System.out.println("email : " + rs.getString("email"));
        System.out.println("memo : " + rs.getString("memo"));
        System.out.println("createAt : " + rs.getTimestamp("create_at"));
        System.out.println("updateAt : " + rs.getTimestamp("update_at"));
      }
    } catch (SQLException e) {
      System.err.println("test error : " + e.getMessage());
      throw new RuntimeException(e);
    }
  }

  @Override
  public void close() {
    shutdown();
  }

}
```

サービスのテストクラスをH2サーバーを利用するように修正します。修正する箇所はsetupSchema、tearDownSchemaメソッドです。

```java
@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
public class UserServiceImplTests implements TestService {

  private DSLContext dsl;
  private TestH2Server h2;

  private List<String> initScripts = List.of(
      "./h2/sql/01_schema.sql",
      "./h2/sql/02_table.sql"
  );

  private List<String> clearScripts = List.of(
      "./h2/sql/04_table.sql",
      "./h2/sql/05_schema.sql"
  );

  @Override
  public DSLContext getDSLContext() {
    return dsl;
  }

  @Override
  public void setDSLContext(DSLContext dsl) {
    this.dsl = dsl;
  }

  @BeforeAll
  public void setupSchema() {
    System.out.println("setup class");
    h2 = new TestH2Server();
    h2.start();
    h2.runScript(initScripts);
    setDSLContext(TestDslContextHolder.getInstance().getDslContext());
  }

  @AfterAll
  public void tearDownSchema() {
    System.out.println("teardown class");
    TestDslContextHolder.destroy();
    h2.runScript(clearScripts);
    h2.shutdown();
  }

  //... 省略...

}
```

### コントローラクラスのテスト

コントローラクラスのテストは結合テストとして実装します。データベースは実環境と同じMySQLを使用するため、テスト実行時はMySQLサーバにアクセスできる必要があります。

コントローラのテストクラス

```java
import com.example.demo.config.DslContextHolder;
import com.example.demo.config.JacksonConfig;
import com.example.demo.converter.Prefecture;
import com.example.demo.converter.Sex;
import com.example.demo.model.tables.pojos.User;
import com.example.demo.service.impl.UserServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.javalin.Javalin;
import io.javalin.plugin.json.JavalinJackson;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static org.assertj.core.api.Assertions.assertThat;

@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
public class UserControllerTests {

  Javalin app;
  int port;
  ObjectMapper objectMapper;

  @BeforeAll
  public void setup() {
    port = 7000;
    app = Javalin.create().start(port);
    objectMapper = JacksonConfig.getObjectMapper();
    JavalinJackson.configure(objectMapper);
  }

  @AfterAll
  public void tearDown() {
    app.stop();
  }

  @Test
  public void findById() throws Exception {
    // setup
    new UserController(new UserServiceImpl(DslContextHolder.getInstance().getDslContext()))
        .bindEndpoints(app);

    // exercise
    Long userId = 1L;
    String testTargetUrl = String.format("http://localhost:%d/api/user/%d", port, userId);

    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(testTargetUrl))
        .header("Accept", "application/json")
        .GET()
        .build();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

    // verify
    assertThat(response.statusCode()).isEqualTo(200);
    User actual = objectMapper.readValue(response.body(), User.class);
    assertThat(actual)
        .extracting(
            "id",
            "nickName",
            "sex",
            "prefectureId",
            "email",
            "memo")
        .contains(
            1L,
            "test user 1",
            Sex.MALE,
            Prefecture.HOKKAIDO,
            "test.user1@example.com",
            "memo 1"
        );
  }

}
```