Spring Bootで簡単なWebアプリケーションを書いてみる

JavaでWebアプリケーションを開発する際のフレームワークとして、近年Apache Strutsに代わりSpring Frameworkが広く使われている。 ここでは、Springが提供するBootstrapフレームワークSpring Bootを用いて、簡単なWebアプリケーションを書いてみる。

環境

Windows 10 Pro、Java SE 8、Spring Framework 4.3.7.RELEASE(Spring Boot 1.5.2.RELEASE)

>systeminfo
OS 名:                  Microsoft Windows 10 Pro
OS バージョン:          10.0.14393 N/A ビルド 14393
OS ビルドの種類:        Multiprocessor Free
システムの種類:         x64-based PC
プロセッサ:             1 プロセッサインストール済みです。
                        [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz

>java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

JDK 8のインストール

まず、Java開発環境であるJDK 8をインストールする。 JDKをインストールすると、実行環境であるJREも同時にインストールされる。

Spring Tool Suite (STS) のダウンロード

次に、EclipseベースのIDEであるSpring Tool Suite (STS) をダウンロードする。

spring-tool-suite-3.8.4.RELEASE-e4.6.3-win32-x86_64.zipを展開し、STS.exeを実行する。 初回起動時にWorkspace(プロジェクトの保存先)を聞かれるので、適当なパスを指定しOKを選択すると、STSが起動する。

Spring FrameworkとSpring Boot

Spring Frameworkは、Dependency Injection(DI)とAspect-oriented programming(AOP)と呼ばれる設計手法を活用したJavaフレームワークである。 これらの手法により、従来のフレームワークに対して、比較的簡潔にプログラムを実装することができる。

Spring Bootは、Springの各種プロジェクトを使って簡単にWebアプリケーションを書くことができるBootstrapフレームワークである。 Spring BootにはApache Tomcatが付属しており、従来のWARファイルを作成してアプリケーションサーバにデプロイする方法の他に、実行可能JARを作成して単独でTomcatサーバを起動する方法を選ぶことができる。

Spring Bootで簡単なWebアプリケーションを書いてみる

ここでは、Webアプリケーションとして簡単な掲示板を作ることにする。 また、構築を簡単にするために、データベースとしてMySQL等の代わりにJava製インメモリDBであるH2を利用する。

まず、STSでプロジェクトを作成する。

  • メニューバーから「File」→「New」→「Spring Starter Project」を選択
  • Nameを適当に設定し(ここではdemoのままとする)、PackagingにWarを選択してNext
  • DependenciesとしてWeb、Thymeleaf、JPA、H2を選択してNext、Finish

Spring Bootのテンプレートが展開されるので、順に必要なクラスファイルを作成していく。 クラスファイルを新規作成するには、例えばsrc/main/java/com.example/MessageController.javaの場合次のようにする。

  • 「src/main/java/com.example」を右クリックして「New」→「Class」を選択
  • NameにMessageControllerを入力してFinish

以降に述べるすべてのファイルを作成した後の、Package Explorerスクリーンショットを次に示す。

f:id:inaz2:20170405195657p:plain

Web (Spring MVC)

Spring MVCは、MVCフレームワークでWebアプリケーション開発を行うためのライブラリであり、Spring Frameworkの中核を担うものである。 RubyにおけるRailsPythonにおけるDjangoに対応。

HTTPリクエストを処理するControllerは次のようになる。

  • src/main/java/com.example/MessageController.java
package com.example;

import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;

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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class MessageController {

    @Autowired
    private MessageService service;

    @GetMapping("/messages")
    public String messages(Model model) {
        model.addAttribute("messageForm", new MessageForm());

        List<Message> messages = service.getRecentMessages(100);
        model.addAttribute("messages", messages);

        return "messages";
    }

    @PostMapping("/messages")
    public String messagesPost(Model model, @Valid MessageForm messageForm, BindingResult bindingResult, HttpServletRequest request) {
        if (bindingResult.hasErrors()) {
            List<Message> messages = service.getRecentMessages(100);
            model.addAttribute("messages", messages);
            return "messages";
        }

        service.save(new Message(messageForm.getName(), messageForm.getText(), request.getRemoteAddr()));
        return "redirect:/messages";
    }

}

@GetMappingおよび@PostMappingはそれぞれHTTPのエンドポイントに対応しており、アノテーションが付けられた関数がリクエストに応じて実行される。 returnで返される文字列はViewのテンプレートファイル名を表しており、redirect:がついている場合はそのエンドポイントにHTTPリダイレクトが行われる。 また、@AutowiredDependency Injectionを意味しており、MessageServiceのインスタンスが実行時に代入される。

フォームから送信されるパラメータを定義するクラスは次のようになる。

  • src/main/java/com.example/MessageForm.java
package com.example;

import javax.validation.constraints.Size;

public class MessageForm {

    @Size(max=80)
    private String name;

    @Size(min=1, max=140)
    private String text;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

}

@Sizeアノテーションにより、各パラメータの制約が指定されている。 この制約はControllerの@Validアノテーションによりチェックされ、エラーがある場合はエラーメッセージがViewに渡される。

Thymeleaf

Thymeleafはテンプレートエンジンであり、従来のJSPに代わるものである。 RubyにおけるERB、PythonにおけるJinja2に対応。

テンプレートファイルは次のようになる。

  • src/main/resources/templates/messages.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Hello, Spring Boot!</title>
</head>
<body>
<h1>Hello, Spring Boot!</h1>

<form action="#" th:action="@{/messages}" th:object="${messageForm}" method="post">
    <p>Name (optional): <input type="text" th:field="*{name}" />
       <em th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Name Error</em></p>
    <p><textarea cols="40" rows="4" placeholder="Type anything" th:field="*{text}"></textarea>
       <em th:if="${#fields.hasErrors('text')}" th:errors="*{text}">Text Error</em></p>
    <p><input type="submit" value="Submit" /></p>
</form>

<h2>Recent messages</h2>

<dl>
    <th:block th:each="message : ${messages}">
        <dt>
            <span class="name" th:text="${message.name}" th:attr="title=${message.remoteAddr}">John Doe</span>
            <small th:text="${#dates.format(message.createdAt, '(yyyy-MM-dd HH:mm:ss)')}">(1970-01-01 00:00:00)</small>
        </dt>
        <dd th:text="${message.text}">Lorem ipsum dolor sit amet</dd>
    </th:block>
</dl>

</body>
</html>

Thymeleafではth名前空間を用いて構造を記述する。 要素テキストの出力にth:textを用いることで、HTMLエスケープが自動で行われる。 このとき、テンプレート中の要素テキストは無視されるため、例示テキストを記述しておく。

変数は${messages}のようにして参照する。 また、th:objectでオブジェクトを指定し、その下位要素で*{name}のように記述することで指定したオブジェクトのプロパティを参照できる。

HTML要素に対応しないブロック構造はth:blockで表すことができる。

JPA

JPAJavaのオブジェクトとDBのリレーションを結び付けるO/Rマッパーである。 JPAを用いることで、DBに依存したSQL文を直接記述することなくデータの取得や保存ができる。 RubyにおけるActive Record、PythonにおけるSQLAlchemyに対応。

一般に、JPAではEntity、Repository、Serviceの三つが実装される。 テーブル定義に対応するEntityクラスは次のようになる。

  • src/main/java/com.example/Message.java
package com.example;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PrePersist;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
public class Message {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    
    @Column(nullable = false)
    private String name;
    
    @Column(nullable = false)
    private String text;
    
    @Column(nullable = false)
    private String remoteAddr;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(updatable = false)
    private Date createdAt;

    // JPA requirement
    protected Message() {}

    public Message(String name, String text, String remoteAddr) {
        this.name = name;
        this.text = text;
        this.remoteAddr = remoteAddr;
    }

    @PrePersist
    public void prePersist() {
        this.createdAt = new Date();
    }

    @Override
    public String toString() {
        return String.format("Message[id=%d, name='%s', text='%s']", id, name, text);
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getRemoteAddr() {
        return remoteAddr;
    }

    public void setRemoteAddr(String remoteAddr) {
        this.remoteAddr = remoteAddr;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

}

JPAの仕様に従い空のコンストラクタをprotectedで実装する必要があること、Date型には@Temporal(TemporalType.TIMESTAMP)をつける必要があることに注意。 nullが入ることを期待しないフィールドには@Column(nullable = false)をつけておくのが無難である。

Insert時、Update時の処理は、@PrePersist@PreUpdateアノテーションをつけたメソッドで定義できる。 ここでは、@PrePersistでcreatedAtメンバに作成日時をセットし、@Column(updatable = false)かつsetter未定義とすることで更新できないようにしている。 また、idメンバも@GeneratedValue(strategy = GenerationType.AUTO)により自動生成するため、setter未定義としている。

なお、getter/setterメソッドは右クリックから「Source」→「Generate Getter and Setters...」を選択して自動生成すると楽である。

DB操作に対応するRepositoryインタフェースは次のようになる。

  • src/main/java/com.example/MessageRepository.java
package com.example;

import java.util.List;

import org.springframework.stereotype.Repository;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.CrudRepository;

@Repository
public interface MessageRepository extends CrudRepository<Message, Long> {

    List<Message> findByOrderByIdDesc(Pageable pageable);

}

CrudRepositoryインタフェースを継承することで、findAll()save()delete()等のメソッドが暗黙に定義される。 また、findByName(String name)のようなメソッドを定義すると、SELECT * FROM messages WHERE name = ?に相当する操作を行うメソッドとなる。 上のコードにおけるfindByOrderByIdDesc()はByの後のカラム名を抜いたもので、SELECT * FROM messages ORDER BY id DESCに相当する。 なお、インタフェースの実装は実行時に自動で提供される。

Controllerに対して公開するServiceクラスは次のようになる。

  • src/main/java/com.example/MessageService.java
package com.example;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class MessageService {

    @Autowired
    private MessageRepository repository;

    public List<Message> getRecentMessages(Integer n) {
        return repository.findByOrderByIdDesc(new PageRequest(0, n));
    }

    @Transactional
    public void save(Message message) {
        repository.save(message);
    }

}

save()のようなDBに変更を加えるメソッドは、@Transactionalをつけて例外発生時にロールバックされるようにする。 Controllerと同様に、上のコードでも@AutowiredによるDependency Injectionが定義されており、MessageRepositoryのインスタンスが実行時に代入される。

付属のTomcatサーバで動かしてみる

付属のTomcatサーバでWebアプリケーションを動かし、ブラウザからアクセスしてみる。

  • 「demo [boot]」を右クリックして「Run As」→「Spring Boot App」を選択
  • Tomcatが起動したら、ブラウザから http://localhost:8080/messages にアクセス

ブラウザで表示した後のスクリーンショットを次に示す。

f:id:inaz2:20170405195712p:plain

実行を中止しTomcatサーバを停止するには、Stopボタンを押せばよい。

Pivotal tc Serverで動かしてみる

STSでは、デプロイ用サーバとしてPivotal tc Serverが用意されている。 このサーバの上でWebアプリケーションを動かすには、次のようにする。

  • 「demo [boot]」を右クリックして「Run As」→「1 Run on Server」を選択
  • サーバとしてlocalhostのPivotal tc Serverを選択してNext
  • 右側のConfiguredに作成したプロジェクト(ここではdemo)が入っていることを確認してFinish
  • サーバが起動した後、STSの中央ペインでWebブラウザが開くのでそのまま http://localhost:8080/demo/messages にアクセス

サーバを停止させるには、付属のTomcatサーバの場合と同様にStopボタンを押せばよい。

WARファイルを作成してみる

他のアプリケーションサーバにデプロイするためのWARファイルを作成するには次のようにする。

  • 「demo [boot]」を右クリックして「Export...」を選択
  • 「Web」→「WAR file」を選択してNext
  • Destinationに保存先ディレクトリを指定してFinish

関連リンク