Spring Frameworkを聞いたことがないWEBシステム開発エンジニアはいないと思いますが、一から作る場合、どこから着手するのかわからない方が多いかもしれません。なぜならば、参加したシステム開発は大体フレームワークが既に構築されて、業務ロジックを設計、実装すれば順調に進められます。本記事はSpring Boot2を利用して簡単なWEBアプリを作ります。詳細説明は少ないかもしれませんが、サンプル通りにアプリを動いたら、システムエンジニアにとって、短期間でも利用されているフレームワークの構造と実現方式がわかるようになると思います。
本記事では下記の知識を紹介していきます。
・ Spring Frameworkの構成
・ thymeleafテンプレート
・ bootstrap簡単設計
・ mybatisデータ持続化
・ 簡単なフォームバリデーション
1. Spring Boot とは
大規模システム開発ではフレームワークの重要性は言わずに当たり前の話です。様々な開発言語に対応する様々なフレームワークがありますが、技術発展により、これらのフレームワークは老朽化になったり、デファクトスタンダードになったりします。Javaの世界では、Spring Frameworkはほとんど採用されています。Spring Frameworkをもとに、更に使いやすいように独自なフレームワークもたくさん作らました。2002年Spring Frameworkはリリースされたときに、単純に(Dependency Injection)を実現した小さいフレームワークでした。今まで何でもできる巨大フレームワークになってきました。下図のSpring Framework製品ファミリーを見ればわかるでしょう。
图1-1. Spring Framework製品ファミリー

それでは、Spring Bootはいったい何でしょうか。簡単に言いますと、Spring BootはSpring開発を簡単化できる開発フレームワークです。わかりにくいですが、言い換えますと、Spring Bootは新しいフレームワークではなく、Spring Frameworkをベースにたくさんのフレームワークを使い方などをまとめて、サーバーも内包されますので、Springの開発をさらに簡単、スピーディにできます!
2. サンプル
今回はSpring Bootを利用して下記簡単なWEBアプリを作ります。基本的なCRUD(新規、検索、更新、削除)を実現して,Spring Webアプリの典型的な構成になります。
図2-1. 商品一覧

図2-2. 商品新規

図2-3. 商品詳細

図2-4. 商品変更

3. 開発環境
まず、開発環境を準備しましょう。本記事は2019/7/2現在の最新バージョンを利用します。なにか調べるときに、旧バージョンの情報もたくさん出ます、新旧バージョン通用しないケースも結構ありますので、掲載情報の前提に特に気をつけましょう。
3.1 JDK
2018年年9月にJava 11のリリースに伴って、OracleはJDKのサポートポリシーも調整しました。JDKメジャーバージョンアップは半年ごと、長期商用サポートなし、長期サポート(LTS)バージョンは3年毎リリースなどなど、ややこしいですので、Java有償化について別途記事をまとめていますので、下記のリンクをご参照ください。
「Java有償化」って、いったいどういうこと?
Java有償化にかかわらず、現時点個人と開発向けの場合、Oracle JDKが無料利用可能です。JDKはまだインストールされていない場合、下記のリンクからダウンロードしてください。
Oracle JDK 12: https://www.oracle.com/technetwork/java/javase/downloads/index.html
Open JDK 12: https://jdk.java.net/12/
3.2 Spring Tool Suite 4
開発効率を上げるために、開発ツールの選定も重要です。SpringはEclipseをベースにSpring Tool SuiteというIDEを提供しています。それ以外、Visual Studio Codeプラグイン、Atomプラグインもあります。ここではSTS4(Spring Tool Suite 4)を利用します。下記のリンクからOSごとのバージョンをダウンロードできます。
STS4.2.2:https://spring.io/tools
3.3 Bootstrap
BootstrapはTwitter社開発したクライアント開発のOSSです、デザイナーのセンスがなくてもプロレベルのレイアウトや画面要素を簡単に作れます。Bootstrapをプロジェクトに導入するにはCDN声明方式、または、CSS、JavaScriptをローカルにダウンロードしてimportする方式があります。ここではローカル方式を採用します。下記のリンクからbootstrap及び依頼されるjQeuryの最新バージョンをダウンロードできます。Bootstrap最新バージョンは4ですが、3より一部のタグを廃止したり、追加したりしていますので、インタネットで掲載しているソースはバージョン3のほうが多いですので、ソースをそのままコピペして、効かなければ、バージョンを確認したほうがよいと思います。
Bootstrap4.3.1: https://getbootstrap.com/
jQuery3.4.1 :https://jquery.com/
4. 実践
これから一緒にサンプルを作りましょう。このサンプルは典型的なSpring MVCを利用するWebアプリになります。H2データベースを利用しています。H2はJavaで実装されたリレーションデータベースであり、データはメモリに保存されます。WEBサーバーが起動するたびにデータがクリアされます。興味がありましたら、MySQLやPostSQLなどのデータベースを入れ替えてみてください。
システム構成は下図に示します。

最終のプロジェクト構成は下記の通りです。

実際のプロジェクトには、エンティティを整理し、属性を抽出してテーブル、画面設計を先に行うことが多いですが、ここでは設計工程を略します。直接MVCモデル順でサンプルWebアプリの構築手順を説明します。
4.1 プロジェクト作成
① Spring Tool Suite 4を起動してワークフォルダを指定します。メニュー「File → New → Spring Starter Project」を選択したら、下記の画面が表示されます。
- Type: Spring BootがMavenまたはGradleをプロジェクト管理ツールとして推薦されています、ここではデフォルトのMavenを利用します。
- Java Version:JDKバージョンを選択します。
- その他の項目はデフォルト値のままします。

② 「Next」ボタンを押下して、下図のように検索ボックスにキーワードを入力して、出てきた依存ライブラリを選択します。サンプルには使っているライブラリは下記の通りです。一つずつ選択してください。
Web、Thymeleaf、MyBatis、Validation、H2、DevTools

③ 「Finish」ボタンを押下して、アプリの雛形は自動的に生成されます。pom.xmlファイルの中身を確認して、上記各依存ライブラリが入っています。開発中必要なライブラリが必要になったら、このファイルを直接修正します。pom.xmlファイルはmavenプロジェクトのコアファイルであることを覚えてください。

4.2 モデル作成(Model)
まず、Itemというクラスを作ります(package:com.example.demo.domain)、商品の各種属性を定義したうえ、バリデーションもアノテーションの形で実装します。Spring Boot開発は一番の特徴はプロジェクト設定、DI注入、バリデーションをアノテーションで定義したら、裏の動作はSpring bootは全部やってくれます。
package com.example.demo.domain;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class Item {
private Long id;
@NotBlank(message="商品名を記入してください。")
private String name;
@Min(value=10, message="10以上の数値を入力してください。")
@Max(value=10000, message="10000以下の数値を入力してください。")
private float price;
@Size(max=50, message="ベーダー名は50文字を超えないでください。")
private String vendor;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
public String getVendor() {
return vendor;
}
public void setVendor(String vendor) {
this.vendor = vendor;
}
}
4.3 ビュー作成(View)
① 3.3でダウンロードしたBootstrap4及びjQueryを解凍して下記のように関連ファイルをresources/staticに入れます。
└static
└css
└bootstrap.min.css
└js
└bootstrap.min.js
└jquery-3.4.1.min.js
② resources/templatesの配下に4つの画面テンプレート(html)を作成します。テンプレートはThymeleafとBootstrapを利用しています。
- 商品一覧画面:index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<title>商品一覧</title>
</head>
<body>
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/items">商品管理デモ</a>
</div>
</div>
</nav>
<div class="container">
<div class="card card-primary mb-3">
<div class="card-header">
<h5 class="card-title">商品リスト<a href="/items/new" class="btn btn-success float-right">新規</a></h5>
</div>
<div class="card-body" th:if="!${items.size()}">
<p>商品がありません。</p>
</div>
<table class="table table-striped" th:if="${items.size()}">
<thead>
<tr>
<th style="width: 10%">ID</th>
<th style="width: 30%">商品名</th>
<th style="width: 10%">価格</th>
<th style="width: 20%">ベンダー</th>
<th style="width: 30%"></th>
</tr>
</thead>
<tbody>
<tr th:each="item:${items}" th:object="${item}">
<td th:text="*{id}"></td>
<td th:text="*{name}"></td>
<td th:text="*{price}"></td>
<td th:text="*{vendor}"></td>
<td class="float-right">
<form th:action="@{/items/{id}(id=*{id})}" th:method="delete">
<a class="btn btn-primary" th:href="@{/items/{id}(id=*{id})}">詳細</a>
<a class="btn btn-primary" th:href="@{/items/{id}/edit(id=*{id})}">変更</a>
<button class="btn btn-primary">削除</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<script src="/js/jquery-3.4.1.min"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>
- 商品新規画面 new.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<title>商品新規</title>
</head>
<body>
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/items">商品管理デモ</a>
</div>
</div>
</nav>
<div class="container">
<div class="card card-primary mb-3">
<div class="card-header">
<h5 class="card-title">商品新規</h5>
</div>
<div class="card-body">
<form th:method="post" th:action="@{/items}" th:object="${item}">
<div class="form-group row">
<label class="col-md-2 control-label">商品名</label>
<div class="col-md-10">
<input class="form-control" type="text" name="name" th:value="*{name}" />
<div class="text-danger" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label">価格</label>
<div class="col-md-10">
<input class="form-control" type="text" name="price" th:value="*{price}"/>
<div class="text-danger" th:if="${#fields.hasErrors('price')}" th:errors="*{price}"></div>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label">ベンダー</label>
<div class="col-md-10">
<input class="form-control" type="text" name="vendor" th:value="*{vendor}" />
<div class="text-danger" th:if="${#fields.hasErrors('vendor')}" th:errors="*{vendor}"></div>
</div>
</div>
<div class="form-group row">
<div class="offset-md-2 col-md-10">
<button class="btn btn-primary">新規</button>
</div>
</div>
</form>
</div>
</div>
</div>
<script src="/js/jquery-3.4.1.min"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>
- 商品詳細画面 show.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<title>商品詳細</title>
</head>
<body>
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/items">商品管理デモ</a>
</div>
</div>
</nav>
<div class="container">
<div class="card card-primary mb-3">
<div class="card-header">
<h5 class="card-title">商品詳細</h5>
</div>
<div class="card-body">
<div th:object="${item}">
<div class="form-group row">
<label class="col-md-2">商品名</label>
<div class="col-md-10" th:text="*{name}"></div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label">価格</label>
<div class="col-md-10 form-control-static" th:text="*{price}"></div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label">ベンダー</label>
<div class="col-md-10 form-control-static" th:text="*{vendor}"></div>
</div>
</div>
</div>
</div>
</div>
<script src="/js/jquery-3.4.1.min"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>
- 商品変更画面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/css/bootstrap.min.css" />
<title>商品情報変更</title>
</head>
<body>
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/items">商品管理デモ</a>
</div>
</div>
</nav>
<div class="container">
<div class="card card-primary mb-3">
<div class="card-header">
<h5 class="card-title">商品詳細</h5>
</div>
<div class="card-body">
<form th:action="@{/items/{id}(id=*{id})}" th:method="put" th:object="${item}">
<div class="form-group row">
<label class="col-md-2 control-label">商品名</label>
<div class="col-md-10">
<input class="form-control" type="text" th:field="*{name}" />
<div class="text-danger" th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label">価格</label>
<div class="col-md-10">
<input class="form-control" type="text" th:field="*{price}" />
<div class="text-danger" th:if="${#fields.hasErrors('price')}" th:errors="*{price}"></div>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label">ベンダー</label>
<div class="col-md-10">
<input class="form-control" type="text" th:field="*{vendor}" />
<div class="text-danger" th:if="${#fields.hasErrors('vendor')}" th:errors="*{vendor}"></div>
</div>
</div>
<div class="form-group row">
<div class="offset-md-2 col-md-9">
<button class="btn btn-primary">更新</button>
</div>
</div>
</div>
</form>
</div>
</div>
<script src="/js/jquery-3.4.1.min"></script>
<script src="/js/bootstrap.min.js"></script>
</body>
</html>
4.4 DB準備及びMyBatis Mapper作成
① resourcesフォルダ配下にDBテーブル生成ファイル、アプリが起動されると、テーブルが自動的に作成されます。テーブル構成は下記の通りです。
| No. | 項目名 | タイプ | 長さ | 説明 |
|---|---|---|---|---|
| 1 | id | bigint | 商品ID | |
| 2 | name | varchar | 255 | 商品名 |
| 3 | price | real | 価格 | |
| 4 | vendor | varchar | 255 | ベンダー |
CREATE TABLE IF NOT EXISTS item (
id bigint(20) NOT NULL AUTO_INCREMENT,
name varchar(255),
price real,
vendor varchar(255),
PRIMARY KEY (id),
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
② Spring FrameworkのDB処理はJPAフレームワークもあります、JPAは自動処理機能がありますが、実際のプロジェクト開発の複雑な検索処理を対応できないケースが多いですので、MyBatisを採用するほうが多いです。MyBatisを利用するメリットは、SQL文をそのままJavaとマッピングして、複雑なDB操作も可能です。
まず、ItemMapperというインタフェース(package:com.example.demo.mapper)を作ります, 注意:classではなく、ineterfaceです。5つのメソッドを定義します。下記の処理を行います。インタフェースを定義しましたが、実現クラスはどこに実装しますか。答えはインタフェース定義の前に@Mapperをつければ、それ以外の処理はフレームワークのほうは全部やってくれます。裏の処理は知らなくてもよいです。
| No. | メソッド名 | 説明 |
|---|---|---|
| 1 | findAll | すべての商品一覧習得 |
| 2 | findOne | 商品IDによって商品情報を取得 |
| 3 | save | 商品情報保存 |
| 4 | update | 商品情報更新 |
| 5 | delete | 商品削除 |
package com.example.demo.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.example.demo.domain.Item;
@Mapper
public interface ItemMapper {
List<Item> findAll();
Item findOne(Long id);
void save(Item item);
void update(Item item);
void delete(Long id);
}
③ com.example.demo.mapper配下に対応するSQL定義ファイルItemMapper.xmlを作ります。このファイルは上記各メソッドのSQL文と対応しますので、わかりやすいと思いますので、詳細説明を割愛します。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.ItemMapper">
<select id="findAll" resultType="com.example.demo.domain.Item">
select * from item
</select>
<select id="findOne" resultType="com.example.demo.domain.Item">
select * from item where id= #{id}
</select>
<insert id="save" useGeneratedKeys="true" keyProperty="id">
insert into item(name, price, vendor) values(#{name}, #{price}, #{vendor})
</insert>
<update id="update">
update item set name=#{name}, price=#{price}, vendor=#{vendor} where id= #{id}
</update>
<delete id="delete">
delete from item where id = #{id}
</delete>
</mapper>
④ DBアクセスクラスItemService(package:com.example.demo.service)を作ります。serviceクラスはトランザクション処理、関連機能を部品としてまとめます。下記のコードには、2点を注目してほしいです。まず、@Serviceアノテーションの定義です。このアノテーションを追加したら、このクラスが自動的にbeanとして登録されます。ほかのプログラムに直接使えます。上記の@Mapperも同じ機能があります。次は、@Autowiredです、このアノテーションを利用しますと、クラスを初期化しなくても利用できるようになります。
package com.example.demo.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.domain.Item;
import com.example.demo.mapper.ItemMapper;
@Service
public class ItemService {
@Autowired
private ItemMapper itemMapper;
@Transactional
public List<Item> findAll() {
return itemMapper.findAll();
}
@Transactional
public Item findOne(Long id) {
return itemMapper.findOne(id);
}
@Transactional
public void save(Item item) {
itemMapper.save(item);
}
@Transactional
public void update(Item item) {
itemMapper.update(item);
}
@Transactional
public void delete(Long id) {
itemMapper.delete(id);
}
}
4.5 コントロラー(Controller)作成
ItemControllerというクラス(package:com.example.demo.controller)を作成します。controller主な用途はDBからデータを取得してViewクラスに渡してレンダリングします。また、Viewクラスからデータを取得してDBに保存します。クラス定義の前に@Controllerアノテーションを付けたら、各種設定は全部自動的にやってくれます。例えば、REST APIを開発する場合、同じように@RestControllerアノテーションを付けたらOKです。このクラスには@AutoWiredアノテーションがもう一回出てきました。上記のservcieをこのクラスに導入されて、初期化処理不要でも使えるようになります。@RequestMapping、@GetMapping、@PostMapping、@PutMappingなどのアノテーションはHTTPリクエストURLやパラメータを定義します。
package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.domain.Item;
import com.example.demo.service.ItemService;
@Controller
@RequestMapping("/items")
public class ItemController {
@Autowired
private ItemService itemService;
@GetMapping
public String index(Model model) {
model.addAttribute("items", itemService.findAll());
return "index";
}
@GetMapping("{id}")
public String show(@PathVariable Long id, Model model) {
model.addAttribute("item", itemService.findOne(id));
return "show";
}
@GetMapping("new")
public String newItem(@ModelAttribute("item") Item item, Model model) {
return "new";
}
@GetMapping("{id}/edit")
public String edit(@PathVariable Long id, @ModelAttribute("item") Item item, Model model) {
model.addAttribute("item", itemService.findOne(id));
return "edit";
}
@PostMapping
public String create(@ModelAttribute("item") @Validated Item item, BindingResult result, Model model) {
if (result.hasErrors()) {
return "new";
} else {
itemService.save(item);
return "redirect:/items";
}
}
@PutMapping("{id}")
public String update(@PathVariable Long id, @ModelAttribute("item") @Validated Item item, BindingResult result, Model model) {
if (result.hasErrors()) {
model.addAttribute("item", item);
return "edit";
} else {
item.setId(id);
itemService.update(item);
return "redirect:/items";
}
}
@DeleteMapping("{id}")
public String delete(@PathVariable Long id) {
itemService.delete(id);
return "redirect:/items";
}
}
4.6 アプリ起動
① ここまで簡単WEBアプリができました。STSのPackage ExplorerからDemoApplication.javaのファイルを探します、このファイルはプロジェクトのエントリファイルです。下図のように右クリックしてコンテキストメニューから「Spring Boot App」を選択したら、サーバーは起動されます。

②任意のブラウザを開いて、「http://localhost:8080/items」にアクセスすれば、下記の画面が表示されたら、成功です。

5. 後書き
補足ですが、実際のプロジェクトには、上記のサンプルよりはるかはるかに複雑になります。例えば、認可認証処理、セッション処理、DBデータ永続化、ロードバランサーなどなど。基礎がわかれば、複雑な処理でも短期間に作れると思います。
学習中エラーが発生したら、サンプルのソースコードはGithubからダウンロードできます。ご参考ください。
URL:https://github.com/microstone-info/springbootsample
また、記述ミスや間違いがあれば、ご遠慮なくご指摘ください。


