Edited at

Spring Bootで簡単な検索アプリケーションを開発する


概要

Spring Bootを使用して、簡単な検索ができるWebアプリケーションを開発します。

完成図

開発するアプリケーションは「俳優」の情報を扱い、データの一覧表示、登録、削除を行います。

環境


  • Windows7 (64bit)

  • Java 1.8.0_45

  • Spring Boot 1.2.4


    • thymeleaf 2.1.4

    • logback 1.1.3



  • MySQL 5.6

  • Eclipse 4.4

  • Maven 3.3.3

参考

下記のサイトを参考にさせていただきました。

Spring

Thymeleaf

Logback

Qiita

github

ソースコードはrubytomato/actor-search-exampleにあります。

事前準備

Java,Eclipse,Maven,MySQLのインストール、設定方法については省略します。

サンプルデータは下記の通り準備します。


  • データベース: sample_db

  • ユーザー: test_user

  • テーブル: actor, prefecture

データベース


sample_db

create database if not exists sample_db;


ユーザー


create_user

create user 'test_user'@'localhost' identified by 'test_user';



grant

grant all on sample_db.* to 'test_user'@'localhost';


actorテーブル


actor

create table if not exists actor (

id int not null auto_increment,
name varchar(30) not null,
height smallint,
blood varchar(2),
birthday date,
birthplace_id smallint,
update_at timestamp(6) not null default current_timestamp(6) on update current_timestamp(6),
primary key (id)
) engine = INNODB;

初期化


init

insert into actor (name, height, blood, birthday, birthplace_id) values

('丹波哲郎', 175, 'O', '1922-07-17', 13),
('森田健作', 175, 'O', '1949-12-16', 13),
('加藤剛', 173, null, '1938-02-04', 22),
('島田陽子', 171, 'O', '1953-05-17', 43),
('山口果林', null, null,'1947-05-10', 13),
('佐分利信', null, null, '1909-02-12', 1),
('緒形拳', 173, 'B', '1937-07-20', 13),
('松山政路', 167, 'A', '1947-05-21', 13),
('加藤嘉', null, null, '1913-01-12', 13),
('菅井きん', 155, 'B', '1926-02-28', 13),
('笠智衆', null, null, '1904-05-13', 43),
('殿山泰司', null, null, '1915-10-17', 28),
('渥美清', 173, 'B', '1928-03-10', 13);

prefectureテーブル


prefecture

create table if not exists prefecture (

id smallint not null,
name varchar(6) not null,
primary key (id)
) engine = INNODB;

初期化


init

insert into prefecture (id, name) values

(1,'北海道'),(2,'青森県'),(3,'岩手県'),(4,'宮城県'),(5,'秋田県'),(6,'山形県'),(7,'福島県'),
(8,'茨城県'),(9,'栃木県'),(10,'群馬県'),(11,'埼玉県'),(12,'千葉県'),(13,'東京都'),(14,'神奈川県'),
(15,'新潟県'),(16,'富山県'),(17,'石川県'),(18,'福井県'),(19,'山梨県'),(20,'長野県'),(21,'岐阜県'),
(22,'静岡県'),(23,'愛知県'),(24,'三重県'),(25,'滋賀県'),
(26,'京都府'),(27,'大阪府'),(28,'兵庫県'),(29,'奈良県'),(30,'和歌山県'),
(31,'鳥取県'),(32,'島根県'),(33,'岡山県'),(34,'広島県'),(35,'山口県'),
(36,'徳島県'),(37,'香川県'),(38,'愛媛県'),(39,'高知県'),
(40,'福岡県'),(41,'佐賀県'),(42,'長崎県'),(43,'熊本県'),(44,'大分県'),(45,'宮崎県'),(46,'鹿児島県'),(47,'沖縄県');


アプリケーションの作成


プロジェクトの雛形を生成

アプリケーション名: actor

mavenでアプリケーションの雛形を作成

> mvn archetype:generate -DgroupId=com.example.actor -DartifactId=actor -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

> cd actor

> mvn eclipse:eclipse

eclipseにインポート

メニューバーの"File" -> "Import..." -> "Maven" -> "Existing Maven Projects"を選択します。

プロジェクトのディレクトリを選択し、"Finish"ボタンをクリックします。

pom.xmlの編集


pom.xml

<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.actor</groupId>
<artifactId>actor</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>actor</name>
<url>http://maven.apache.org</url>

+ <properties>
+ <java.version>1.8</java.version>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>

+ <parent>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-parent</artifactId>
+ <version>1.2.4.RELEASE</version>
+ </parent>

<dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-data-jpa</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-thymeleaf</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-actuator</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-java</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>3.4</version>
+ </dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
- <version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>

+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <version>1.2.5.RELEASE</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <verbose>true</verbose>
+ <source>${java.version}</source>
+ <target>${java.version}</target>
+ <encoding>${project.build.sourceEncoding}</encoding>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>springloaded</artifactId>
+ <version>1.2.3.RELEASE</version>
+ </dependency>
+ </dependencies>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>versions-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>

</project>


resourcesフォルダの作成

src/main/resourcesフォルダを作成します。


  • Build Path -> Configure Build Path -> Java Buld Path -> Sourceタブを選択する。

  • Add Folderボタンをクリック -> 作成したresourcesフォルダにチェックを入れる。

application.ymlの作成

src/main/resourcesフォルダ内にapplication.ymlを作成します。


application.yml

# EMBEDDED SERVER CONFIGURATION (ServerProperties)

server:
port: 9000

spring:
# THYMELEAF (ThymeleafAutoConfiguration)
thymeleaf:
enabled: true
cache: false
# DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
datasource:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost/sample_db
username: test_user
password: test_user
# JPA (JpaBaseConfiguration, HibernateJpaAutoConfiguration)
jpa:
hibernate:
show-sql: true
ddl-auto: update
database-platform: org.hibernate.dialect.MySQLDialect

# INTERNATIONALIZATION (MessageSourceAutoConfiguration)
messages:
basename: messages
cache-seconds: -1
encoding: UTF-8

# ENDPOINTS (AbstractEndpoint subclasses)
endpoints:
enabled: true


logback.xmlの作成

src/main/resourcesフォルダ内にlogback.xmlを作成します。

ログの出力先フォルダを"D:/logs"に指定しました。


logback.xml

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<property name="LOG_DIR" value="D:/logs" />

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MMM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{35} - %msg %n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${LOG_DIR}/spring-boot-sample-logger-example.log</file>
<encoder>
<charset>UTF-8</charset>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] - %msg %n</pattern>
</encoder>
</appender>

<logger name="com.example.actor" level="DEBUG" />

<logger name="org.hibernate" level="ERROR"/>
<logger name="org.springframework" level="INFO"/>
<logger name="org.thymeleaf" level="INFO"/>
<root>
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>


ビルド

この時点で動作検証を兼ねてビルドします。


package

> mvn package


ビルドが成功したら生成したjarファイルを実行します。

コマンドプロンプトに"Hello World!"と表示されれば成功です。

> cd target

> java -jar actor-1.0-SNAPSHOT.jar
Hello World!


アプリケーションの開発

最終的には下記のディレクトリ/ファイル構成になります。


tree

Actor

├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─example
│ │ │ └─actor
│ │ │ │ App.java
│ │ │ │
│ │ │ ├─repository
│ │ │ │ Actor.java
│ │ │ │ ActorRepository.java
│ │ │ │ Prefecture.java
│ │ │ │ PrefectureRepository.java
│ │ │ │
│ │ │ └─web
│ │ │ ActorController.java
│ │ │ ActorForm.java
│ │ │
│ │ └─resources
│ │ │ application.yml
│ │ │ logback.xml
│ │ │ messages_ja.properties
│ │ │
│ │ └─templates
│ │ │ error.html
│ │ │ _temp.html
│ │ │
│ │ └─Actor
│ │ create.html
│ │ detail.html
│ │ index.html
│ │ save.html
│ └─test
└─static
└─vendor


App

エンドポイントとなるクラスを作成します。

すでにサンプルのApp.javaがありますので、このファイルを下記のように変更します。


App

package com.example.actor;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

public static void main(String[] args) {
SpringApplication.run(App.class, args);
}

}



Repository

パッケージ: com.example.actor.repository

Prefecture

Prefectureテーブルに対応するリポジトリークラスです。


Prefecture

package com.example.actor.repository;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

@Entity
@Table(name = "prefecture")
public class Prefecture {

@Id
@Column(name="id")
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;
@Column(name="name", nullable=false)
private String name;

public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
}
}



PrefectureRepository

package com.example.actor.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PrefectureRepository extends JpaRepository<Prefecture, Integer> {

}


Actor

Actorテーブルに対応するリポジトリークラスです。


Actor

package com.example.actor.repository;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

@Entity
@Table(name = "actor")
public class Actor {

@Id
@Column(name="id")
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
@Column(name="name", nullable=false)
private String name;
@Column(name="height")
private Integer height;
@Column(name="blood")
private String blood;
@Temporal(TemporalType.DATE)
@Column(name="birthday")
private Date birthday;
@Column(name="birthplace_id")
private Integer birthplaceId;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="update_at")
private Date updateAt;

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name="birthplace_id", insertable = false, updatable = false )
private Prefecture pref;

public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getHeight() {
return height;
}
public void setHeight(Integer height) {
this.height = height;
}
public String getBlood() {
return blood;
}
public void setBlood(String blood) {
this.blood = blood;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public Integer getBirthplaceId() {
return birthplaceId;
}
public void setBirthplaceId(Integer birthplaceId) {
this.birthplaceId = birthplaceId;
}
public Date getUpdateAt() {
return updateAt;
}
public void setUpdateAt(Date updateAt) {
this.updateAt = updateAt;
}

public Prefecture getPref() {
return pref;
}
public void setPref(Prefecture pref) {
this.pref = pref;
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
}

}



ActorRepository

package com.example.actor.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface ActorRepository extends JpaRepository<Actor, Integer> {

@Query("select a from Actor a where a.name like %:keyword% order by a.id asc")
List<Actor> findActors(@Param("keyword") String keyword);

}


Prefectureテーブルとの結合

JoinColumnアノテーションでPrefectureテーブルとの関係を定義します。


@JoinColumn

@ManyToOne(fetch = FetchType.EAGER)

@JoinColumn(name="birthplace_id", insertable = false, updatable = false )
private Prefecture pref;


Controller

パッケージ: com.example.actor.web

ActorForm

ActorFormは俳優の登録フォームのパラメータがバインドされるクラスです。

フィールドに@NotNullなどのアノテーションを付けてバリデーションルールを指定することができます。


ActorForm

package com.example.actor.web;

import java.io.Serializable;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

public class ActorForm implements Serializable {

private static final long serialVersionUID = 1330043957072942381L;

@NotNull
@Size(min=1, max=30)
private String name;
@Min(1)
@Max(200)
private String height;
@Pattern(regexp = "A|B|AB|O")
private String blood;
@Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}")
private String birthday;
@Min(1)
@Max(47)
private String birthplaceId;

public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getHeight() {
return height;
}
public void setHeight(String height) {
this.height = height;
}
public String getBlood() {
return blood;
}
public void setBlood(String blood) {
this.blood = blood;
}
public String getBirthday() {
return birthday;
}
public void setBirthday(String birthday) {
this.birthday = birthday;
}
public String getBirthplaceId() {
return birthplaceId;
}
public void setBirthplaceId(String birthplaceId) {
this.birthplaceId = birthplaceId;
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
}

}


ActorController


ActorController

package com.example.actor.web;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.example.actor.repository.Actor;
import com.example.actor.repository.ActorRepository;
import com.example.actor.repository.Prefecture;
import com.example.actor.repository.PrefectureRepository;

@Controller
public class ActorController {
final static Logger logger = LoggerFactory.getLogger(ActorController.class);

@Autowired
ActorRepository actorRepository;

@Autowired
PrefectureRepository prefectureRepository;

@Autowired
MessageSource msg;

@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
}

@RequestMapping(value = "/actor", method = RequestMethod.GET)
public String index(Model model) {
logger.debug("Actor + index");
List<Actor> list = actorRepository.findAll();
if (CollectionUtils.isEmpty(list)) {
String message = msg.getMessage("actor.list.empty", null, Locale.JAPAN);
model.addAttribute("emptyMessage", message);
}
model.addAttribute("list", list);
modelDump(model, "index");
return "Actor/index";
}

@RequestMapping(value = "/actor/{id}", method = RequestMethod.GET)
public ModelAndView detail(@PathVariable Integer id) {
logger.debug("Actor + detail");
ModelAndView mv = new ModelAndView();
mv.setViewName("Actor/detail");
Actor actor = actorRepository.findOne(id);
mv.addObject("actor", actor);
return mv;
}

@RequestMapping(value = "/actor/search", method = RequestMethod.GET)
public ModelAndView search(@RequestParam String keyword) {
logger.debug("Actor + search");
ModelAndView mv = new ModelAndView();
mv.setViewName("Actor/index");
if (StringUtils.isNotEmpty(keyword)) {
List<Actor> list = actorRepository.findActors(keyword);
if (CollectionUtils.isEmpty(list)) {
String message = msg.getMessage("actor.list.empty", null, Locale.JAPAN);
mv.addObject("emptyMessage", message);
}
mv.addObject("list", list);
}
return mv;
}

@RequestMapping(value = "/actor/create", method = RequestMethod.GET)
public String create(ActorForm form, Model model) {
logger.debug("Actor + create");
List<Prefecture> pref = prefectureRepository.findAll();
model.addAttribute("pref", pref);
modelDump(model, "create");
return "Actor/create";
}

@RequestMapping(value = "/actor/save", method = RequestMethod.POST)
public String save(@Validated @ModelAttribute ActorForm form, BindingResult result, Model model) {
logger.debug("Actor + save");
if (result.hasErrors()) {
String message = msg.getMessage("actor.validation.error", null, Locale.JAPAN);
model.addAttribute("errorMessage", message);
return create(form, model);
}
Actor actor = convert(form);
logger.debug("actor:{}", actor.toString());
actor = actorRepository.saveAndFlush(actor);
modelDump(model, "save");
return "redirect:/actor/" + actor.getId().toString();
}

@RequestMapping(value = "/actor/delete/{id}", method = RequestMethod.GET)
public String delete(@PathVariable Integer id, RedirectAttributes attributes, Model model) {
logger.debug("Actor + delete");
actorRepository.delete(id);
attributes.addFlashAttribute("deleteMessage", "delete ID:" + id);
return "redirect:/actor";
}

/**
* convert form to model.
*/

private Actor convert(ActorForm form) {
Actor actor = new Actor();
actor.setName(form.getName());
if (StringUtils.isNotEmpty(form.getHeight())) {
actor.setHeight(Integer.valueOf(form.getHeight()));
}
if (StringUtils.isNotEmpty(form.getBlood())) {
actor.setBlood(form.getBlood());
}
if (StringUtils.isNotEmpty(form.getBirthday())) {
DateTimeFormatter withoutZone = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime parsed = LocalDateTime.parse(form.getBirthday() + " 00:00:00", withoutZone);
Instant instant = parsed.toInstant(ZoneOffset.ofHours(9));
actor.setBirthday(Date.from(instant));
}
if (StringUtils.isNotEmpty(form.getBirthplaceId())) {
actor.setBirthplaceId(Integer.valueOf(form.getBirthplaceId()));
}
actor.setUpdateAt(new Date());
return actor;
}

/**
* for debug.
*/

private void modelDump(Model model, String m) {
logger.debug(" ");
logger.debug("Model:{}", m);
Map<String, Object> mm = model.asMap();
for (Entry<String, Object> entry : mm.entrySet()) {
logger.debug("key:{}", entry.getKey());
}
}

}



initBinder

initBinderで行っている下記の設定を行うと、リクエストパラメータが空文字の場合にそのパラメータの値をnullへ変換します。

これにより、不要なバリデーションが行われなくなります。


initBinder

binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));


このほかにも、任意の日付フォーマットの文字列パラメータをDateクラスへ変換するといった設定も行うことができます。

SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");

sdf.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(sdf, true));


テンプレート

テンプレートファイルはtemplatesフォルダ以下に配置するのでsrc/main/resources/templatesフォルダを作成します。

また、ActorControllerが使用するビュー名を"Actor/***"としたので、対応するテンプレートファイルを配置するsrc/main/resources/templates/Actorフォルダも作成します。


tree

resources


└─templates
│ error.html
│ _temp.html

└─Actor
create.html
detail.html
index.html
save.html

_temp.html

_temp.htmlは各テンプレートファイルで共通する部分(ヘッダーやフッター、メニューなど)を管理するテンプレートです。

部品化する要素にth:fragment="..."という属性で名前を付けます。

呼び出し側のテンプレートではth:include="..."th:replace="..."という属性で部品を呼び出します。

(このように1つのファイルに複数の部品を定義することができ、呼び出し側は部品単位で利用することができます。)

テンプレートファイル: resources/templates/_temp.html


_temp.html

<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head th:fragment="header (title)">
<title th:text="${title}">title</title>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="/vendor/bootstrap-3.3.5/css/bootstrap.min.css" th:href="@{/vendor/bootstrap-3.3.5/css/bootstrap.min.css}" rel="stylesheet" />
</head>
<body>

<div class="container">

<div th:fragment="nav" class="row">
<div class="col-md-12">
<ul class="nav nav-pills">
<li role="presentation"><a href="/actor" th:href="@{/actor}" th:utext="#{actor.nav.index}">index</a></li>
<li role="presentation"><a href="/actor/create" th:href="@{/actor/create}" th:utext="#{actor.nav.create}">create</a></li>
</ul>
</div>
</div>

<div th:fragment="footer" class="page-header">
<div th:utext="#{footer.text}">original footer</div>
</div>

</div>

<div th:fragment="script">
<script src="/vendor/jquery/jquery-1.11.3.js" th:src="@{/vendor/jquery/jquery-1.11.3.js}"></script>
<script src="/vendor/bootstrap-3.3.5/js/bootstrap.js" th:src="@{/vendor/bootstrap-3.3.5/js/bootstrap.js}"></script>
</div>
</body>
</html>


index.html

テンプレートファイル: resources/templates/Actor/index.html

俳優一覧を表示するテンプレートファイルです。


index.html

<!DOCTYPE HTML>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_temp :: header ('ACTOR')">
</head>
<body>

<div class="container">
<div class="page-header">
<h1 th:utext="#{actor.index.title}">actor.index.title</h1>
<p th:if="${emptyMessage}" th:text="${emptyMessage}">empty message</p>
<p th:if="${errorMessage}" th:text="${errorMessage}">error message</p>
<p th:if="${deleteMessage}" th:text="${deleteMessage}">delete message</p>
</div>

<div th:replace="_temp :: nav"></div>

<div class="row">
<div class="col-md-6">
<form action="/actor/search" th:action="@{/actor/search}" method="get">
<div class="input-group">
<input type="text" name="keyword" class="form-control" placeholder="Search for..." />
<span class="input-group-btn">
<input class="btn btn-default" type="submit" value="Search!" />
</span>
</div>
</form>
</div>
</div>

<div class="row">
<div class="col-md-12">

<table class="table">
<thead>
<tr>
<th th:utext="#{actor.id}">id</th>
<th th:utext="#{actor.name}">name</th>
<th th:utext="#{actor.height}">height</th>
<th th:utext="#{actor.blood}">blood</th>
<th th:utext="#{actor.birthday}">birthday</th>
<th th:utext="#{actor.birthplace}">birthplace</th>
<th th:utext="#{actor.update_at}">update_at</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr th:each="item,iterStat : ${list}">
<td>
<a class="btn btn-default" href="/actor/${item.id}" th:href="@{/actor/{id}(id=${item.id})}" th:text="${item.id}">1</a>
</td>
<td th:text="${item.name}">1</td>
<td th:text="${item.height != null}? ${item.height} : '-'">1</td>
<td th:text="${item.blood != null}? ${item.blood} : '-'">1</td>
<td th:text="${item.birthday != null}? ${#dates.format(item.birthday,'yyyy-MM-dd')} : '-'">1</td>
<td th:text="(${item.birthplaceId != null}? ${item.birthplaceId} + ':' : '') + (${item.pref != null }? ${item.pref.name} : '(unknown)')"></td>
<td th:text="${item.updateAt}">1</td>
<td>
<a class="btn btn-warning" href="/actor/delete/${item.id}" th:href="@{/actor/delete/{id}(id=${item.id})}">delete</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>

<div th:replace="_temp :: footer"></div>
</div>

<div th:include="_temp :: script"></div>
</body>
</html>


detail.html

テンプレートファイル: resources/templates/Actor/detail.html

一人の俳優の情報を表示するテンプレートファイルです。


detail.html

<!DOCTYPE HTML>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_temp :: header ('ACTOR DETAIL')">
</head>
<body>

<div class="container">
<div class="page-header">
<h1 th:utext="#{actor.detail.title}">actor.detail.title</h1>
</div>

<div th:replace="_temp :: nav"></div>

<div class="row">
<div class="col-md-12">
<table class="table" th:object="${actor}">
<tr>
<th th:utext="#{actor.id}">id</th>
<td th:text="*{id}"></td>
</tr>
<tr>
<th th:utext="#{actor.name}">name</th>
<td th:text="*{name}"></td>
</tr>
<tr>
<th th:utext="#{actor.height}">height</th>
<td th:text="*{height}"></td>
</tr>
<tr>
<th th:utext="#{actor.blood}">blood</th>
<td th:text="*{blood}"></td>
</tr>
<tr>
<th th:utext="#{actor.birthday}">birthday</th>
<td th:text="*{birthday != null}? *{#dates.format(birthday,'yyyy-MM-dd')} : '-'"></td>
</tr>
<tr>
<th th:utext="#{actor.birthplace}">birthplace</th>
<td th:text="*{birthplaceId} + ':' + (*{pref != null}? *{pref.name} : '(unknown)')"></td>
</tr>
<tr>
<th th:utext="#{actor.update_at}">update_at</th>
<td th:text="*{updateAt}"></td>
</tr>
</table>
</div>
</div>

<div th:replace="_temp :: footer"></div>
</div>

<div th:include="_temp :: script"></div>
</body>
</html>


create.html

テンプレートファイル: resources/templates/Actor/create.html

俳優の情報を登録するフォームを表示するテンプレートファイルです。


create.html

<!DOCTYPE HTML>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_temp :: header ('ACTOR CREATE')">
</head>
<body>

<div class="container">
<div class="page-header">
<h1 th:utext="#{actor.create.title}">actor.create.title</h1>
</div>

<div th:replace="_temp :: nav"></div>

<div class="row">
<div class="col-md-12">

<form class="form-horizontal" role="form" action="/actor/save" th:action="@{/actor/save}" th:object="${actorForm}" method="post">
<!-- name -->
<div class="form-group">
<label for="id_name" class="col-sm-2 control-label" th:utext="#{actor.name}">name</label>
<div class="col-sm-10">
<input id="id_name" class="form-control" type="text" name="name" th:field="*{name}" />
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="help-block">error!</span>
</div>
</div>
<!-- height -->
<div class="form-group">
<label for="id_height" class="col-sm-2 control-label" th:utext="#{actor.height}">height</label>
<div class="col-sm-10">
<input id="id_height" class="form-control" type="text" name="height" th:field="*{height}" />
<span th:if="${#fields.hasErrors('height')}" th:errors="*{height}" class="help-block">error!</span>
</div>
</div>
<!-- blood -->
<div class="form-group">
<label for="id_blood" class="col-sm-2 control-label" th:utext="#{actor.blood}">blood</label>
<div class="col-sm-10">
<input id="id_blood" class="form-control" type="text" name="blood" th:field="*{blood}" />
<span th:if="${#fields.hasErrors('blood')}" th:errors="*{blood}" class="help-block">error!</span>
</div>
</div>
<!-- birthday -->
<div class="form-group">
<label for="id_birthday" class="col-sm-2 control-label" th:utext="#{actor.birthday}">birthday</label>
<div class="col-sm-10">
<input id="id_birthday" class="form-control" type="text" name="birthday" th:field="*{birthday}" />
<span th:if="${#fields.hasErrors('birthday')}" th:errors="*{birthday}" class="help-block">error!</span>
</div>
</div>
<!-- birthplaceId -->
<div class="form-group">
<label for="id_birthplaceId" class="col-sm-2 control-label" th:utext="#{actor.birthplace}">birthplace</label>
<div class="col-sm-10">
<select id="id_birthplaceId" class="form-control" name="birthplaceId">
<option value="">---</option>
<option th:each="item : ${pref}" th:value="${item.id}" th:text="${item.name}" th:selected="${item.id} == *{birthplaceId}">pulldown</option>
</select>
</div>
</div>

<div class="form-group">
<input class="btn btn-default" type="submit" value="save" />
</div>
</form>

</div>
</div>

<div th:replace="_temp :: footer"></div>
</div>

<div th:include="_temp :: script"></div>
</body>
</html>


save.html

テンプレートファイル: resources/templates/Actor/save.html

登録結果を表示するテンプレートファイルです。


save.html

<!DOCTYPE HTML>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_temp :: header ('ACTOR SAVE')">
</head>
<body>

<div class="container">
<div class="page-header">
<h1 th:utext="#{actor.save.title}">actor.save.title</h1>
</div>

<div th:replace="_temp :: nav"></div>

<div class="row">
<div class="col-md-12">
<table class="table" th:object="${actor}">
<tr>
<th th:utext="#{actor.id}">id</th>
<td th:text="*{id}"></td>
</tr>
<tr>
<th th:utext="#{actor.name}">name</th>
<td th:text="*{name}"></td>
</tr>
<tr>
<th th:utext="#{actor.height}">height</th>
<td th:text="*{height}"></td>
</tr>
<tr>
<th th:utext="#{actor.blood}">blood</th>
<td th:text="*{blood}"></td>
</tr>
<tr>
<th th:utext="#{actor.birthday}">birthday</th>
<td th:text="*{birthday != null}? *{#dates.format(birthday,'yyyy-MM-dd')} : '-'"></td>
</tr>
<tr>
<th th:utext="#{actor.birthplace}">birthplace</th>
<td th:text="*{birthplaceId} + ':' + (*{pref != null}? *{pref.name} : '(unknown)')"></td>
</tr>
<tr>
<th th:utext="#{actor.update_at}">update_at</th>
<td th:text="*{updateAt}"></td>
</tr>
</table>
</div>
</div>

<div th:replace="_temp :: footer"></div>
</div>

<div th:include="_temp :: script"></div>
</body>
</html>



エラーページ

デフォルトではThymeleafのエラーページが表示されます。

下図は存在しないURLにアクセスしたときに表示されるエラーページです。

error.html

任意のエラーページを表示したい場合は、templatesフォルダ直下にerror.htmlという名前のテンプレートファイルでエラーページを用意します。

今回は下記のような簡単なエラーページを用意しました。

存在しないURLにアクセスするとこのerror.htmlが表示されます。


error.html

<!DOCTYPE HTML>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_temp :: header ('ERROR')">
</head>
<body>

<div class="container">
<div class="page-header">
<h1>default error page</h1>
</div>
<div th:replace="_temp :: footer"></div>
</div>

<div th:include="_temp :: script"></div>
</body>
</html>


HTTPステータスコード別にエラーページを用意したい場合

今回のアプリケーションでは使用していませんが、下記のようなコントローラーを作成してhttpステータスコード毎に任意のエラーページを表示することができます。

この例ではhttpステータスが404と500のときに指定するエラーページを表示します。


ErrorController.java

package com.example.actor.web;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.ErrorPage;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ErrorController {
final static Logger logger = LoggerFactory.getLogger(ErrorController.class);

@Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return new EmbeddedServletContainerCustomizer() {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
ErrorPage error404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error/404");
ErrorPage error500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500");
container.addErrorPages(error404, error500);
}
};
}

@RequestMapping("/error/404")
public String error404() {
return "Error/404";
}
@RequestMapping("/error/500")
public String error500() {
return "Error/500";
}
}


エラーページのテンプレートファイルを作成します。

(エラーページを配置するフォルダやファイル名は任意です。)


  • resources/templates/Error/404.html

  • resources/templates/Error/500.html

エラーページの内容を確認する場合は、"http://localhost:9000/error/404"にアクセスすることで表示内容を確認することができます。


メッセージリソース

メッセージリソースはresourcesフォルダ以下に配置します。

ファイル名のベースはapplication.ymlで指定できます。

今回はベース名をmesssagesとしていますので、日本語のメッセージリソースファイルはmessages_ja.propertiesとなります。

src/main/resources/messages_ja.propertiesを作成します。


messages_ja.properties

# page title

actor.index.title = \u4FF3\u512A
actor.detail.title = \u8A73\u7D30
actor.create.title = \u65B0\u898F
actor.save.title = \u767B\u9332

# field label
actor.id = id
actor.name = \u540D\u524D
actor.height = \u8EAB\u9577
actor.blood = \u8840\u6DB2\u578B
actor.birthday = \u8A95\u751F\u65E5
actor.birthplace = \u51FA\u8EAB\u5730
actor.update_at = \u66F4\u65B0\u65E5\u6642

# navi menu label
actor.nav.index = \u4E00\u89A7
actor.nav.create = \u65B0\u898F

# footer message
footer.text = spring boot sample application

# action message
actor.list.empty = list empty
actor.validation.error = validation error



静的リソース

静的リソース(css,js,img等)はプロジェクトフォルダ(/actor)の直下のstaticフォルダ以下に配置します。

このアプリケーションではbootstrapとjQueryを使用しますので、それらのファイルをstatic以下にコピーします。


  • bootstrapは、"static/vendor/bootstrap-3.3.5"に配置しました。

  • jQueryは、"static/vendor/jquery"に配置しました。


tree

Actor

├─src
├─static
│ └─vendor
│ ├─bootstrap-3.3.5
│ │ ├─css
│ │ ├─fonts
│ │ └─js
│ └─jquery
└─target


実行する

> mvn spring-boot:run

アプリケーションが起動したら下記のURLにアクセスします。

俳優の一覧が表示されれば成功です。

http://localhost:9000/actor


Actuator

Actuatorでspring bootの状態をブラウザから確認することができます。

機能を有効にするにはapplication.ymlでendpoints.enabledをtrueに設定します。

ID
endpoint

autoconfig
http://localhost:9000/autoconfig

beans
http://localhost:9000/beans

configprops
http://localhost:9000/configprops

dump
http://localhost:9000/dump

env
http://localhost:9000/env

health
http://localhost:9000/health

metrics
http://localhost:9000/metrics

mappings
http://localhost:9000/mappings

trace
http://localhost:9000/trace

Part V. Spring Boot Actuator: Production-ready features


メモ


Spring loaded

reloadが効かない点が幾つかあったので列挙します。(もしかすると設定が足りないなどの原因があるかもしれません。)


  • Eclipse内からmavenコマンドでアプリケーションを実行した場合、terminated(赤い四角)ボタンを押しもjavaプロセスが終了せずに残留する。この為コマンドプロンプトからmvnコマンドで実行し、終了はCtrl+Cキーを押す。

  • Messageリソースファイルの変更が反映されない。


  • @ModelAttributeアノテーションのvalueの変更が反映されない。


Thymeleaf


コレクションを扱う方法

コントローラー側で下記のコレクションをセットした場合

Iterable<Category> result = categoryService.findAll();

model.addAttribute("result", result);

<table class="table" th:if="${result}">

<caption th:text="${result.size()}">result</caption>
<thead>
...省略...
</thead>
<tbody>
<tr th:each="category,status : ${result}">
...省略...
</tr>
</tbody>
</table>


  • コレクションが持つメソッドを呼び出すことができます。この例ではsize()メソッドでコレクションの件数を取得しています。