Spring Bootの基本のよもやま
Initializrで選んだ依存パッケージ(Dependencies)
Spring Initializrで私がいつも選ぶのは、 分類「DEVELOPER TOOLS」から Spring Boot DevTools と Lombok、 「WEB」からSpring Web、「TEMPLATE ENGINES」からThymeleaf、 「SQL」からJDBC API、MyBatis Framework、H2 Database、「I/O」からValidationの8つです。
なおSpring Data JDBCというのはJDBC APIとは別物で、どちらかというとSpring Data JPAやMyBatisの親戚です。 ここでのサンプルはMyBatisを前提にしているので、Spring Data JDBCは除外しています。
また、後の例で出てくる Thymeleaf Layout Dialectはサードパーティの配布ライブラリなので、 この初期設定では追加できません。追って別途自分でpom.xmlに追記する必要があります。

最初に作るHello World Contoller

package local.testapp01.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/")
    String home() {
        return "Hello World Spring Boot!!";
    }
}

最初に作るThymeleafサンプル(おみくじController)
Omikuji1Controller.java

package local.controller;

import java.time.LocalDateTime;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class Omikuji1Controller {
    /** おみくじ判定ファンクション */
    private static String getOmikujiResult(String s){
        final String omikuji[] = { "大吉", "中吉", "小吉", "末吉", "凶", "おおむね吉" };

        // 名前の入力がない場合は結果もnull
        if(s == null || s.equals("")){ return null; }

        // 名前のバイトを全て足す
        byte[] arr = s.getBytes();
        int v = 0;
        for(int i=0; i<arr.length; i++){ v += arr[i]; }
        // 現在の日付を追加。同じ人は同じ日は同じ結果になる。
        LocalDateTime dt = LocalDateTime.now();
        v += dt.getDayOfMonth();
        if(v < 0){ v = -v; }
        return omikuji[v % omikuji.length];
    }

    @RequestMapping("/omikuji1")
    public String omi(@RequestParam(value="userName", required=false, defaultValue="") String userName, Model model) {
        model.addAttribute("userName", userName);
        model.addAttribute("omikujiResult", getOmikujiResult(userName));
        return "omikuji1";
    }
}

omikuji1.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>おみくじ</title><meta charset="UTF-8" /></head>
<body>
<h3>おみくじ1</h3>
<form th:action="@{/omikuji1}">
お名前を入力してください。<input type="text" name="userName" th:value="${userName}" /><br>
<input type="submit" value="引く"/>
</form>

<p th:if="${userName != ''}"
   th:text="${userName} + ' さんは ' + ${omikujiResult} + 'です。'" />
<form th:action="@{/}">
<input type="submit" value="メインメニューに戻る"/>
</form>
</body></html>

Formクラスを使ったおみくじController
Omikuji2Form.java

package local.form;
import lombok.Data;
@Data
public class Omikuji2Form {
    private String userName;
}

Omikuji2Controller.java

package local.controller;

import java.time.LocalDateTime;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import local.form.Omikuji2Form;
import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
@SessionAttributes(types=Omikuji2Form.class)
public class Omikuji2Controller {

    /** おみくじ判定ファンクション */
    private static String getOmikujiResult(String s){
        final String omikuji[] = { "大吉", "中吉", "小吉", "末吉", "凶", "おおむね吉" };

        // 名前の入力がない場合は結果もnull
        if(s == null || s.equals("")){ return null; }

        // 名前のバイトを全て足す
        byte[] arr = s.getBytes();
        int v = 0;
        for(int i=0; i<arr.length; i++){ v += arr[i]; }
        // 現在の日付を追加。同じ人は同じ日は同じ結果になる。
        LocalDateTime dt = LocalDateTime.now();
        v += dt.getDayOfMonth();
        if(v < 0){ v = -v; }
        return omikuji[v % omikuji.length];
    }

    @ModelAttribute("omikuji2Form")
    Omikuji2Form initForm() {
        return new Omikuji2Form();
    }

    @RequestMapping("/omikuji2s")
    public String init() {
        return "omikuji2";
    }

    @RequestMapping("/omikuji2")
    public String omi(@ModelAttribute("omikuji2Form") Omikuji2Form form, Model model) {
        model.addAttribute("omikujiResult", getOmikujiResult(form.getUserName()));
        return "omikuji2";
    }
}

omikuji2.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>おみくじ</title><meta charset="UTF-8" /></head>
<body>
<h3>おみくじ2</h3>
<form th:action="@{/omikuji2}">
お名前を入力してください。<input type="text" name="userName" th:value="${omikuji2Form.userName}" /><br>
<input type="submit" value="引く"/>
</form>

<p th:if="${omikujiResult != null}"
   th:text="${omikuji2Form.userName} + ' さんは ' + ${omikujiResult} + 'です。'" />

<form th:action="@{/}">
<input type="submit" value="メインメニューに戻る"/>
</form>

</body></html>

application.propertiesの基本形
データベース接続関連の記述はMyBatis連携のページでも触れます。

spring.application.name=testapp01
server.servlet.context-path=/testapp01
server.port=8090

spring.datasource.url=jdbc:h2:C:/usr/dbms/h2/shiten/stdb;IFEXISTS=TRUE
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=dbuser1
spring.datasource.password=********

mybatis.configuration.map-underscore-to-camel-case=true

Lombokを使う
Lombokの@Dataなどのアノテーションを使うと、Java Beanのsetter/getterの記述が不要になり、大幅に記述量を減らせます。
ちなみにSTS、Eclipse等のIDEからLombokのアノテーションを含むSpringBootアプリケーションを起動して動作確認する場合、 pom.xmlにlombokを書くだけでは駄目(コンパイルエラーが出ないだけでは駄目)で、 lombok.jarをダブルクリックしてIDEに「インストール」する必要があります。詳しくはマニュアル等をご覧ください。

import lombok.Data;

@Data
public class Area {
    private Integer areaCode;
    private String areaName;
}

logback-spring.xmlの基本形

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy/MM/dd HH:mm:ss} [%thread] %-5level %-50logger{40} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="APPLICATION_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>C:/temp/testapp01.log</file>
        <encoder>
            <pattern>%d{yyyy/MM/dd HH:mm:ss} [%thread] %-5level %-50logger{40} - %msg%n</pattern>
        </encoder>

        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>C:/temp/testapp01-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>1MB</maxFileSize>
            <maxHistory>10</maxHistory>
            <totalSizeCap>10MB</totalSizeCap>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="APPLICATION_LOG" />
    </root>
</configuration>

Thymeleafのコメント

<!--/* Thymeleafのコメント */-->

ThymeleafでリストをループするTABLE記述の基本形
データ登録処理で複数のエラーが起きた場合の表示画面を想定したサンプルです。

<p th:if="${result.errorLines != null}">
◆入力エラーがあります。<br>
<table class="default">
<thead>
  <tr><th>行番号</th><th>エラーコード</th><th>エラー内容</th></tr>
</thead>
<tbody>
  <tr th:each="e : ${result.errorLines}">
    <td style="text-align:right;">[[${e.lineno}]]</td><td>[[${e.errorCode}]]</td><td>[[${e.errorMessage}]]</td>
  </tr>
</tbody>
</table>
</p>

ThymeleafからCSSとJavaScript外部ファイルを読み込む

<link th:href="@{/css/default.css}" rel="stylesheet"></link>
<script th:src="@{/js/f_common.js}"></script>

CSSやJavaScriptの外部ファイルは src/main/resources/static 配下に配置し、 パスは src/main/resources/static からの相対位置を指定します。 したがって、上記のように/(スラッシュ)で書き始めるのが慣例のようです。

ThymeleafのonClickイベントの処理中で変数を参照する
なかなかにいたしい(難しい)文法ですが、こんな書き方になります。

<input type="button" value="編集" th:onClick="|fl_moveTo('__${e.shitenCode}__', 'edit')|">

th:onclickを使い、両側の|と変数を囲む__が必須の3点に注意です。

ThymeleafでForm Bean内のListプロパティ変数をループしながらtext入力欄を反復表示する
これも添字部分を先に評価させるために__で囲む必要があるので、次のような書き方になります。

<form name="RecordEditForm" method="POST" th:action="RecordEdit" th:object="${RecordEditForm}">
<table class="default">
  <thead><tr><th>伝票番号</th><th>数量</th><th>単価</th></tr></thead>
  <tbody>
  <tr th:each="e,stat : *{recordList}">
    <td th:text="${e.denno}" class="tdtest">
      <input type="hidden" th:field="*{recordList[__${stat.index}__].denno}">
    </td>
    <td>
      <input type="text" th:field="*{recordList[__${stat.index}__].quantity}"
       maxlength="8" style="width:80px;text-align:right" />
    </td>
    <td>
      <input type="text" th:field="*{recordList[__${stat.index}__].unitPrice}"
       maxlength="8" style="width:80px;text-align:right" />
    </td>
  </tr>
  </tbody>
</table>
<input type="submit" value="変更">
</form>

@Autowiredはもう使わない?
いまはAutowired変数を1つ以上使うControllerクラスの全体に @RequiredArgsConstructor というアノテーションを付ける方法が主流だそう。
ちなみに変数を必ずfinalにする必要があります。

application.propertiesの値をControllerクラスから参照する
独自クラスを作るなどの方法もありますが、簡単には、application.propertiesに

myapp.excel-template-path=C:/usr/doc/template/excel_template.xlsx
このように定義しておき、Javaクラスで
@Value("${myapp.excel-template-path}")
private String templatePath;
で自動設定されます。@RequiredArgsConstructor(旧Autowired)の変数と異なり、finalは必須ではないようです。

modelやフォームをセッションスコープにする
複数の画面を行き来した時に検索条件や検索結果データを保持しておきたい等の目的で、 変数をセッションスコープにするには幾つか方法がありますが、分かりやすいのはControllerで

@SessionAttributes(value="recordList", types=SearchForm.class)
のように属性名かクラスで指定すること。複数ある場合は
@SessionAttributes(value={"recordList","userMap"})
のような書き方になります。
セッションに格納したmodelの属性は、同じ属性名であってもControllerクラスごとに区別されます。
例えばAとBというControllerがあり、Aでmodel.setAttribute("ZZZ","value1")としたものを、 Bでmodel.setAttribute("ZZZ","value2")としても上書きされません。

セッション変数をクリアする
SessionStatus#setComplete()を使うと、 そのコントローラークラスの@SessionAttributesに記載したセッション変数を全てクリアしてくれます。
以下のように「メインメニューに戻る」ボタンが押された場合の処理に使うと良いと思います。

@Controller
@SessionAttributes(value= {"recordList","shitenList"},types= {SearchForm.class,EditForm.class})
public class BackToMainController {
    @RequestMapping("/backtomain")
    public String doAction(Model model, SessionStatus ss) {
        ss.setComplete();
        return "main";
    }
}

特定のセッション変数だけをクリアする
例えば検索画面⇒編集画面⇒変更の保存⇒検索画面に戻る、ような画面遷移の場合、 検索条件はセッションに保持したまま、検索結果だけはクリアして表示したいみたいな場合に使います。
次のようにWebRequestを引数で受け取って、WebRequest#removeAttribute()でセッションから削除し、 モデルからも削除(空データで初期化するのでもOK)する両方が必要です。

@RequestMapping("/EditSave")
public String save(@ModelAttribute("shiten") @Valid Shiten shiten, BindingResult bindingResult,
        Model model, SessionStatus ss, WebRequest req) {
    // 変更の保存処理(略)
    model.addAttribute("recordList", null);
    req.removeAttribute("recordList", WebRequest.SCOPE_SESSION);
}

STSでTomcat内蔵jar(起動可能jar)を作る(Maven)
STSからプロジェクトを右クリック⇒Run as⇒Maven Build...⇒Goalsに 「clean package」と入力して RunすればOKです。

[INFO] BUILD SUCCESS
と出たら、プロジェクトのディレクトリに target\testapp01-0.0.1-SNAPSHOT.jar というファイルが作られていることを確認します。 あれば早速実行してみましょう。コマンドプロンプトから
cd target
java -jar .\testapp01-0.0.1-SNAPSHOT.jar
終了はCTRL+Cです。

STSでTomcat内蔵jar(起動可能jar)を作る(Gradle)
過去の経緯からか、STSのMavenとGradleの使い方はかなり、というか全然違います。 Gradleでjar等をビルドする場合、簡単なのはIDEは置いといて、コマンドプロンプトでプロジェクトのルートに行き、

gradlew build
と打つ方法です。 ビルドが成功すると、build\libs\testapp01-0.0.1-SNAPSHOT.jarが作られます。 コマンドはpackageじゃないし、出力フォルダー名もtargetではありません。 なんで全部違うの!と言われてもそういう物なので仕方がない。救いは実行方法で、
cd build\libs
java -jar .\testapp01-0.0.1-SNAPSHOT.jar
とこれだけは同じです。
ちなみに、どうしてもIDEの中からGradleを呼び出してビルドしたい場合は、 STSにはbuildshipというプラグインが最初から入っているので、 Window⇒Show View...⇒Gradle Tasksを選ぶとConsoleとかErrorsとかあるタブにGradle Tasksタブが現れるので、 そこでcleanやbuildを選べばIDEの中で掃除とビルドが行われます。

設定ファイルをjarの外部に出す
例えば application.properties と logback-spring.xml を $HOME/config の下に配置して、 jar内部の物(src/main/resources)ではなくこちらから設定を読ませたい場合、こうします。

cd $HOME/config
java -jar $HOME/app/testapp01-0.0.1-SNAPSHOT.jar \
  --spring.config.location=$HOME/config/ \
  --logging.config=$HOME/config/logback-spring.xml

ネット情報によると、(1) -cp(-classpath)でクラスパスとして指定する。(2) -Dlogging.config=で指定する、 の方法もあるそうですが、私が試した環境ではこれらは効きませんでした。

CSV文字列データのダウンロード処理
画像などWebサーバ上に既にファイルとして存在する物のダウンロード処理はネット上に多数サンプルがあるので省略して、 Javaプログラム内で生成したCSVなどの文字列のダウンロード処理のサンプルです。

private static ResponseEntity<Resource> download(StringBuffer sb) throws IOException {
    final String defaultFileName = "ダウンロードファイル.txt";

    HttpHeaders header = new HttpHeaders();
    header.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""
            + new String(defaultFileName.getBytes("Windows-31J"), "8859_1") + "\"");
    header.add("Cache-Control", "no-cache, no-store, must-revalidate");
    header.add("Pragma", "no-cache");
    header.add("Expires", "0");

    ByteArrayResource resource = new ByteArrayResource(sb.toString().getBytes(StandardCharsets.UTF_8));

    return ResponseEntity.ok()
            .headers(header)
            .contentLength(resource.contentLength())
            .contentType(MediaType.parseMediaType("application/octet-stream"))
            .body(resource);
}

この例ではStringBufferを渡していますが、Stringが対象の場合は単に「sb.toString()」 の部分をString変数に置き換えればOKです。

responseの参照とExcel(POIのWorkbook)ダウンロード処理
ちなみにHttpServletResponseはControllerのメソッドの引数にしれっと追加しておけば、 自動的に渡され、参照できます。

public String download(@ModelAttribute("searchForm") @Valid SearchForm form,
    BindingResult result, Model model, HttpServletResponse response) {
    // ...
}

POIのWorkbookオブジェクトをバイナリでダウンロードさせるには、次のようにすればOKです。

response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=\""+defaultFileName+"\"");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
wb.write(response.getOutputStream());
wb.close();

独自のエラーページの作成
簡単に言うと、致命的エラーの場合はデフォルトで /error に遷移するので、 ErrorControllerを実装し、かつこの /error をハンドルするControllerクラスを作ればそちらに飛ぶようになります。

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import jakarta.servlet.http.HttpServletRequest;

@Controller
public class CustomErrorController implements ErrorController {

    @RequestMapping("/error")
    @ResponseBody
    public String handleError(HttpServletRequest request) {
        String statusCode = request.getAttribute("jakarta.servlet.error.status_code").toString();
        Exception ex = (Exception) request.getAttribute("jakarta.servlet.error.exception");
        String exstr = (ex == null) ? null : ex.toString();
        String message = (String) request.getAttribute("jakarta.servlet.error.message");
        String uri = (String) request.getAttribute("jakarta.servlet.error.request_uri");
        // 内部的にはスタックトレースを出す
        if(ex != null) {
            ex.printStackTrace();
        }
        // HTMLを生成して出力
        final String s = String.format(
          "<html lang='ja'><body><h3>アプリケーションエラー</h3>" +
          "エラーが発生しました…<br>" +
          "<div>Status Code:<b>%s</b></div>" +
          "<div>Exception:<b>%s</b></div>" +
          "<div>Message:<b>%s</b></div>" +
          "<div>Request URI:<b>%s</b></div></body></html>",
          statusCode, exstr, message, uri);
        return s;
    }
}

この例ではエラー内容を画面に出していますが、利用者にどこまで見せるかは環境によると思います。

Git管理
Spring Bootアプリケーションの開発ディレクトリをGitローカルリポジトリとして使えます。 Initializrで生成したディレクトリには最初から.gitignoreファイルが含まれているので、 それに沿って管理をすればいいと思います。つまり

  • .classpath、.project、.factorypath、.settings/ などIDEに依存するファイルやディレクトリは除外する
  • target/ および build/ サブディレクトリは除外する
  • pom.xmlやbuild.gradleは含める
  • .mvn/ は含める(JVMオプション設定を共有できるため)
  • mvnw、mvnw.cmdも含める (Maven非導入PCでもビルドできるため)
  • .gitignore自身も含める
で良いと思います。
ローカルリポジトリを作るにはgit initでもTortoiseGitの右クリック⇒「ここにリポジトリを作成」 でも可能です。後者はファイルがある場所で実行すると警告されるので、 一旦内容を別ディレクトリに退避して空にしてから実行すると安心かもしれません。

フォワードとリダイレクト
Controllerクラスからフォワードやリダイレクトをするのは簡単です。
「return "page1"」とすればテンプレートpage1(page1.html)を返すのと同様に、 「return "forward:/action1"」とすれば/action1にフォワードされます。
同様に「return "redirect:/action1"」とすれば/action1にリダイレクトされます。

Java kowaza Top

(first uploaded 2024/08/04 last updated 2026/01/27, URANO398)