227
278

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-07-25

概要

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

完成図

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

環境

  • 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] (https://github.com/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にアクセスしたときに表示されるエラーページです。

defalut_error_page.png

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にアクセスします。
俳優の一覧が表示されれば成功です。

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] (http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready)

メモ

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()メソッドでコレクションの件数を取得しています。
227
278
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
227
278

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?