SpringBoot事始め

struts2からSpringBootへの移行検討

struts2からspring bootへの移行手順について調べてみた時の備忘録メモ
試した環境は以下になります。
- java8
- Eclipse Neon

ビルドツール

ビルドツールにGradleを使いたいので、Gradleプロジェクトとしてプロジェクトを作成する。
MavenではなくGradleを使う理由として、よう々な環境用にデプロイを行う場合などの管理がしやすくなるという点が挙げられます。Mavenが設定ファイルなのに対してGradleがスクリプトなので柔軟な対応が行えるっぽいです。

事前準備

プロジェクト作成

以下の手順でプロジェクトを作成します。
New → Other → GradleProjectと進みます。プロジェクト名は任意のものを選び次のGradleDistributionではGradleWrapperを選択します。GradleWrapperは開発環境にGradleがインストールされていなくてもGradleのコマンドを実行することができるようになったり複数の環境で開発を行う場合はこっちの方がオススメです。それからFinishクリックでプロジェクトを作成します。

*SpringBootのGradleプロジェクト作成であれば、プロジェクト作成→SpringStarterProjectのSPringBootプロジェクト作成画面でTypeにMavenではなくGradle(STS)やGradle(BuildShip)を行うことでも可能でこっちの方が簡単かもしれないです。

依存モジュールの追加

まずSpringBoot用の依存モジュールを追加してみたいと思います。依存モジュールの追加はbuild.gradleから行いまして、プロジェクト作成直後は以下のようになっていると思います。

apply plugin: 'java'
repositories {
    jcenter()
}
dependencies {
    compile 'org.slf4j:slf4j-api:1.7.21'
    testCompile 'junit:junit:4.12'
}

Eclipseのbuildshipでプロジェクトを新規作成するとリポジトリがMavenCentralではなくJCenterになっています。リポジトリにはMevenCentralや自前で公開しているリポジトリを指定することもできますが今回はデフォルトのJCenterをリポジトリに指定します。

SpringBoot(Web)のモジュールを追加する場合はBuild.gradleを以下のように修正します。

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    jcenter()
}

buildscript {
    ext {
        springBootVersion = '1.5.2.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-web-services')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

でBuild.gradleを更新しただけではElipse側が参照モジュールの反映処理を行ってくれないと思います。そういったときはプロジェクトを右クリックしてGradle → Reflesh Gradle Projectをクリックしてください。これで参照モジュールが追加できたと思います。

SpringBootプロジェクト起動用Mainクラスの追加

SpringBootを起動させるためのクラスを追加してみます。以下のようなクラスを追加すれば大丈夫です。

package jp.co.sample.boot;

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

@SpringBootApplication
public class BootMain {

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

それからプロジェクトを右クリックして"Run → Run As Spring Boot Project"を実行してみてください。コンソールに"Tomcat started on port(s): 8080 (http)“といった出力が行われSpringBoot内に組み込まれたtomcatで起動していることが確認できたと思います。

最初のコントローラを追加してみる

ブラウザよりリクエストを受け取れるようにするため以下のクラスを追加してみたいと思います。

package jp.co.sample.boot.controller;

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

@RestController
@RequestMapping("/")
public class SampleController {

    @RequestMapping(value = "OK", method = RequestMethod.GET)
    public SampleResponse index() {
        return SampleResponse.OK();
    }
}


class SampleResponse{
    String message = "";
    private SampleResponse(String message) {
        this.message = message;
    };
    static SampleResponse OK(){
        return new SampleResponse("OK");
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

それからSpringBootを再起動して"http://localhost:8080/OK"にアクセスするとレスポンスが帰ってくるのが確認できます。ここではクラスにRestControllerを指定していまして、こうすることでRestFulなレスポンスを返せるようになりまして、indexメソッドではレスポンスに指定しているSmapleResponseクラスをjson形式に変換して返してくれます。

Thymeleafで画面を描画する

spring bootではjspを使用することもできますが、ここではThymeleafを使用してみたいと思います。Thymeleafはhtml風に記述をすることができるためエンジニア以外の人であっても扱いやすいはずです。Thymeleafを使用する場合はbuild.gradleのdependenciesにthymeleafを追加する必要があります。

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.boot:spring-boot-starter-web-services')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

build.gradleを修正したらReflesh Gradle Projectを実行します。
次にsrc/main/resorucesに以下のapplication.propertiesを追加します。ちなみに設定ファイルはymlでも大丈夫です。

spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html
spring.thymeleaf.cache=false

それから画面表示用コントローラとして以下のクラスを追加します。

ppackage jp.co.sample.boot.controller;

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

@Controller
@RequestMapping("/")
public class SampleThymeleaf {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String index(Model model) {
        return "hello";
    }
}

最後にsrc/main/resoruces/templatesに以下のhello.htmlを追加後spring bootを再起動して"http://localhost:8080/hello"にアクセスすると作成した画面が表示されたかと思います。

<!DOCTYPE html>
  <html>
  <head>
    <meta charset="utf-8" />
    <style>
      article, aside, dialog, figure, footer, header,
      hgroup, menu, nav, section { display: block; }
    </style>
  </head>
  <body>
    <p>Hello Thymeleaf</p>
  </body>
</html>

Thymeleafに任意の値を渡してみる

java側で画面に表示する内容を指定する場合は、Modelクラスに値をセットすることで可能です。詳しくは公式を見るのが早いですが簡単なサンプルを試してみたいと思います。
ます先ほど作成したコントローラを以下のように修正します。

package jp.co.sample.boot.controller;

import java.util.ArrayList;

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

@Controller
@RequestMapping("/")
public class SampleThymeleaf {

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("valForJs", "abc");
        model.addAttribute("valForFlg", true);
        model.addAttribute("valForText", "Sample Thymeleaf");
        model.addAttribute("valForList", new ArrayList<String>(){{add("AA"); add("BB"); add("CC");}});
        return "hello";
    }
}

それからhtmlを以下のように編集することでjava側で設定した値が反映されていることが確認できます。

<!DOCTYPE html>
  <html>
  <head>
    <meta charset="utf-8" />
    <style>
      article, aside, dialog, figure, footer, header,
      hgroup, menu, nav, section { display: block; }
    </style>
    <script th:inline="javascript">
      /*<![CDATA[*/
      var JsVal = /*[[${valForJs}]]*/ 'default val';
      console.log(JsVal);
      /*]]>*/
    </script>
  </head>
  <body>

    <div th:if="${valForFlg} == true">
      <p>flag is on</p>
    </div>
    <div th:if="${valForFlg} == false">
      <p>flag is off</p>
    </div>

    <span th:text="${valForText}" />

    <table>
      <tr th:each="val : ${valForList}">
        <td th:text="${val}"></td>
      </tr>
    </table>
  </body>
</html>

プロパティファイルを読み込んでみる

アプリケーションの設定ファイルを読み込んでみたいと思います。試しにsrc/main/resourcesに以下のsystem.propertiesを追加してみてください。

app.version=0.01

それから設定ファイルを読み込む以下のクラスを追加してください。

package jp.co.sample.boot.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "app")
@PropertySource(value = "system.properties")
public class SampleConfig {

    private String version;

    public String getVersion() {
        return version;
    }
    public void setVersion(String version) {
        this.version = version;
    }
}

ここではプレフィックスにappを指定していますが必須ではありません。ここではプレフィックスにappを指定していますが必須ではありません。上記のサンプルではプレフィックス指定で変数名をプロパティと一致させることで値をセットしていますが、以下のようにセットする値を直接指定することも可能です。

@Value("${app.version}")
private String version;

また@ComponentとしておりDIの対象としております。これを利用する場合は以下のように@Autowiredすることで可能です。@Component意外にも@Servieや@RepositoryもDIの対象とすることができまして、アノテーションによって動きが違うのかと思ったけど特にそんなことはなさそうな感じでした。

package jp.co.sample.boot.controller;

import java.util.ArrayList;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import jp.co.sample.boot.config.SampleConfig;

@Controller
@RequestMapping("/")
public class SampleThymeleaf {

    @Autowired
    private SampleConfig sampleConfig;

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("valForJs", "abc");
        model.addAttribute("valForFlg", true);
        model.addAttribute("valForText", "Sample Thymeleaf");
        model.addAttribute("valForList", new ArrayList<String>(){{add("AA"); add("BB"); add("CC");}});
        model.addAttribute("configVal", sampleConfig.getVersion());
        return "hello";
    }
}

これでhtml側に表示用の処理を追加することで値が取得できていることが確認できると思います。

HttpSessionを使ってみる

springではredisなどにsessionを永続化することも可能ですが、strutsからの移行案として検討するためまずHttpSessionを試してみたいと思います。HttpSessionもDIの対象となっておりまして使う場合は以下のようにすることで可能です。

package jp.co.sample.boot.controller;

import java.util.ArrayList;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import jp.co.sample.boot.config.SampleConfig;

@Controller
@RequestMapping("/")
public class SampleThymeleaf {

    @Autowired
    private HttpSession session;

    @Autowired
    private SampleConfig sampleConfig;

    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String index(Model model) {
        model.addAttribute("valForJs", "abc");
        model.addAttribute("valForFlg", true);
        model.addAttribute("valForText", "Sample Thymeleaf");
        model.addAttribute("valForList", new ArrayList<String>() {
            {
                add("AA");
                add("BB");
                add("CC");
            }
        });
        model.addAttribute("configVal", sampleConfig.getVersion());
        model.addAttribute("count", count());
        return "hello";
    }

    private int count() {
        int count = session.getAttribute("count") == null ? 1 : (int) session.getAttribute("count") + 1;
        session.setAttribute("count", count);
        return count;
    }
}

画面からのリクエストを受け取ってみる

HttpServletRequestもHttpSessionと同ようにDIの対象になっています。動作確認ように以下のクラスを作成してみます。

package jp.co.sample.boot.controller;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("test")
public class TestController {

    @Autowired
    private HttpSession session;

    @Autowired
    private HttpServletRequest request;

    @RequestMapping(value = "", method = RequestMethod.GET)
    public String get(Model model) {
        setModelFromSession(model);
        return "test";
    }

    @RequestMapping(value = "", method = RequestMethod.POST)
    public  String post(Model model) {
        setRquest(request.getParameter("input_text"));
        setModelFromSession(model);
        return "test";
    }

    private void setRquest(String inputText){
        List<String> textList = session.getAttribute("textList") == null ? new ArrayList<String>() : (List<String>)session.getAttribute("textList");
        textList.add(inputText);
        session.setAttribute("textList", textList);
    }

    private void setModelFromSession(Model model){
        List<String> textList = session.getAttribute("textList") == null ? new ArrayList<String>() : (List<String>)session.getAttribute("textList");
        model.addAttribute("textList", textList);
    }
}

それから、画面表示用のhtmlを追加します。

<!DOCTYPE html>
  <html>
  <head>
    <meta charset="utf-8" />
    <style>
      article, aside, dialog, figure, footer, header,
      hgroup, menu, nav, section { display: block; }
    </style>
    <script th:inline="javascript">
    </script>
  </head>
  <body>

    <form method="post" action="test">
      <input type="text" name="input_text" ></input><input type="submit" name="butto" value="送信" />
    </form>

    <ul>
      <li th:each="val : ${textList}">
        <span th:text="${val}" />
      </li>
    </ul>
  </body>
</html>

これでリクエストに受け取ったパラメータをセッションに保存させて表示できていることが確認できると思います。

リクエストパラメータをFormにセットする

もちろんリクエストパラメータをFormにセットすることも可能でその場合は以下のようにまず値をセットするクラスを作成します。

package jp.co.sample.boot.form;

public class SampleForm {

    private String input_text;

    public String getInput_text() {
        return input_text;
    }
    public void setInput_text(String input_text) {
        this.input_text = input_text;
    }
}

それから先ほどのpostメソッドを以下のように@ModelAttributeのアノテーションを指定した引数を追加することでFormに値をセットして使用することができます。

@RequestMapping(value = "", method = RequestMethod.POST)
public  String post(@ModelAttribute SampleForm sampleForm, Model model) {
  setRquest(sampleForm.getInput_text());
  setModelFromSession(model);
  return "test";
}

Mybatisを使ってみる

JPAなどのO/Rマッピングツールもありますが、既存のSQL資源を活かすためにMyBatisを試してみたいと思います。まずbuild.gradleに以下のdependeicyを追加します。

compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.0')
runtime('com.h2database:h2')

今回は動作確認するだけなのでDBにはH2を使用します。
それからDBの接続先を設定ファイルに記述します。今回はymlに設定したいと思いますのでsrc/mail/resourcesに以下のapplication.ymlを追加します。

spring:
  datasource:
    driverClassName: org.h2.Driver
    url: jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE
    username: sa
    password:

  h2:
    console:
      enabled: true

SpringBoot起動後に'http://localhost:8080/h2-console'にアクセスするとh2のコンソールにアクセスできるので、必要なクラスが作成できた後に以下のSQLを実行してテーブルを作成します。

create sequence text_id_seq;
create table request_text (
  id bigint default text_id_seq.nextval primary key,
  input_text char(256)
);

とりあえずinsertとselectを行う以下のDaoを作成します。@Mapperとかibatisのモジュールのようで流用できるようです。SQLxmlアノテーションのどちらかに記述することができ、insertとかならアノテーションで大丈夫そうですがselectになってくるとxmlファイルじゃないと見づらくなりそうだったのでファイルを分けています。

package jp.co.sample.boot.dao;

import java.util.List;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import jp.co.sample.boot.someContext.vo.RequestText;

@Mapper
public interface RequestTextDao {

    @Insert("INSERT INTO request_text (input_text) VALUES (#{input_text})")
    @Options(useGeneratedKeys = true)
    void insert(RequestText todo);

    List<RequestText> find(@Param("SEARCH_TEXT") String SEARCH_TEXT);
}

select用のxmlファイルは以下になります。

<?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="jp.co.sample.boot.dao.RequestTextDao">
    <select id="find" resultType="jp.co.sample.boot.someContext.vo.RequestText">

    SELECT
        id,
        input_text
    FROM
        request_text
    <if test="SEARCH_TEXT != null">
        WHERE
            input_text like #{SEARCH_TEXT}
    </if>
    ORDER BY ID

    </select>
</mapper>

それからSQLの実行結果をセットするクラスを作成します。

package jp.co.sample.boot.someContext.vo;


public class RequestText {

    private Long id;
    private String input_text;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getInput_text() {
        return input_text;
    }
    public void setInput_text(String input_text) {
        this.input_text = input_text;
    }
}

それからDaoを利用するcontrollerを作成します。

package jp.co.sample.boot.controller;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import jp.co.sample.boot.dao.RequestTextDao;
import jp.co.sample.boot.form.SampleForm;
import jp.co.sample.boot.someContext.vo.RequestText;

@Controller
@RequestMapping("test")
public class TestController {

    @Autowired
    private HttpSession session;

    @Autowired
    private RequestTextDao requestTextDao;

    @RequestMapping(value = "", method = RequestMethod.GET)
    public String get(Model model) {
        setModelFromDb(model);
        return "test";
    }

    @RequestMapping(value = "", method = RequestMethod.POST)
    public  String post(@ModelAttribute SampleForm sampleForm, Model model) {
        setRquestToDb(sampleForm.getInput_text());
        setModelFromDb(model);
        return "test";
    }

    private void setRquestToDb(String inputText){
        RequestText insertData = new RequestText();
        insertData.setInput_text(inputText);
        requestTextDao.insert(insertData);
    }
    private void setModelFromDb(Model model){
        List<RequestText> textList = requestTextDao.find(null);
        model.addAttribute("textList", textList);
    }
}

そして結果を表示する以下のhtmlを準備したら動作確認が行えます。

<!DOCTYPE html>
  <html>
  <head>
    <meta charset="utf-8" />
    <style>
      article, aside, dialog, figure, footer, header,
      hgroup, menu, nav, section { display: block; }
    </style>
    <script th:inline="javascript">
    </script>
  </head>
  <body>

    <form method="post" action="test">
      <input type="text" name="input_text" ></input><input type="submit" name="butto" value="送信" />
    </form>

    <ul>
      <li th:each="val : ${textList}">
        <span th:text="${val.input_text}" />
      </li>
    </ul>
  </body>
</html>

トランザクション処理を行う

対象のコンポーネントに"@Transactional"のアノテーションを付与するだけでトランザクション対象にすることができる。"@Transactional"はクラスまたはメソッド単位で設定することができる。

静的コンテンツにアクセスする

spring bootで静的コンテンツにアクセスする場合は"src/main/resources/static"の配下にファイルを追加します。"src/main/resources/static/asset.html"を追加した場合はブラウザから"http://localhost:8080/asset.html"と指定することでアクセスすることができます。

フィルターを追加する

例えば特定のURLにアクセスされた場合にリダイレクトするフィルターのクラスは以下のように作成します。

package jp.co.sample.boot.filter;

import java.io.IOException;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SampleFilter implements Filter {
    Pattern urlPatter = Pattern.compile("dummy*");

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("Before");
        if(this.urlCheck(request, response)){
            ((HttpServletResponse)response).sendRedirect("/asset.html");
        }
        chain.doFilter(request, response);
        System.out.println("After");
    }

    @Override
    public void destroy() {
    }

    private boolean urlCheck(ServletRequest request, ServletResponse response) throws IOException{
        String reuqestURI = ((HttpServletRequest)request).getRequestURI();
        return urlPatter.matcher(reuqestURI).find();

    }
}

それからSpring Bootを起動するmainクラス内に以下のようにbeanを登録することでfilterとして動作するようになります。

package jp.co.sample.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import jp.co.sample.boot.filter.SampleFilter;

@SpringBootApplication
public class BootMain {

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

    @Bean
    SampleFilter sampleFilter() {
        return new SampleFilter();
    }
}

それからspring bootを起動して"http://localhost:8080/dummy"にアクセスするとフィルターでリダイレクトされるようになりますが事前に対象のURLをコントローラに登録しておかなければフィルターに通る前にページが存在しないということでエラーになります。

エラーをハンドリングする

まずエラー発生時に返すjsonを表すクラスを作成します。

public class ErrorResponse {
    String error_code;
    String message;

    public String getError_code() {
        return error_code;
    }

    public void setError_code(String error_code) {
        this.error_code = error_code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public static ErrorResponse defaultError() {
        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setError_code("001");
        errorResponse.setMessage("error occurred!");
        return errorResponse;
    }
}

それからエラーをハンドリングするクラスを作成します。@RestControllerAdviceを付与するだけで全てのコントローラで発生するエラーをハンドリングできるようになるので簡単です。

package jp.co.sample.boot.errorHandling;

import java.io.PrintWriter;
import java.io.StringWriter;

import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class SampleErrorHandling {
    public void initBinder(WebDataBinder binder) {
        System.out.println("controller advice: init binder");
    }

    @ExceptionHandler(Exception.class)
    public ErrorResponse exception(Exception e) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        e.printStackTrace(pw);
        pw.flush();
        System.out.println(sw.toString());
        return ErrorResponse.defaultError();
    }

    @ModelAttribute
    public void modelAttribute() {
        System.out.println("controller advice:model Attribute");
    }
}

コントローラーごとで発生するエラーをハンドリングできるようにする。

コントローラごとでエラーハンドリングする場合は@ExceptionHandlerアノテーションを付与したメソッドを定義するだけです

package jp.co.sample.boot.controller;

import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import jp.co.sample.boot.errorHandling.ErrorResponse;

@RestController
@RequestMapping("/error")
public class ErrorSampleController {

    @ExceptionHandler
    public ErrorResponse notFound(NullPointerException ex) {
        return ErrorResponse.defaultError2();
    }

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public String index(Model model) {
        String a = null;
        a.trim();
        return "error_test";
    }
}

logbackでログを出力する

logbackでログ出力を行いたいと思います。まず以下のlogback.xmlをsrc/main/resourcesに作成しエラーログを出力できるようにします。ここでは日別のエラーログを2世代分残すように設定しています。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <timestamp key="LOG_DATE" datePattern="yyyyMMdd"/>
    <property name="LOG_FILE" value="logs/error.log" />

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${LOG_FILE}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
          <fileNamePattern>${LOG_FILE}.%d{yyyyMMdd}</fileNamePattern>
        <maxHistory>2</maxHistory>
    </rollingPolicy>

    </appender>

    <root level="ERROR">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>

</configuration>

それからjava側でエラーログを残すように設定しておきます。

package jp.co.sample.boot.errorHandling;

import java.io.PrintWriter;
import java.io.StringWriter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestControllerAdvice;


@RestControllerAdvice
public class SampleErrorHandling {
    private final static Logger logger = LoggerFactory.getLogger(SampleErrorHandling.class);

    public void initBinder(WebDataBinder binder) {
        System.out.println("controller advice: init binder");
    }

    @ExceptionHandler(Exception.class)
    public ErrorResponse exception(Exception e) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        e.printStackTrace(pw);
        pw.flush();
        logger.error(sw.toString());
        return ErrorResponse.defaultError();
    }

    @ModelAttribute
    public void modelAttribute() {
        System.out.println("controller advice:model Attribute");
    }
}

アノテーションからAPIドキュメントを自動生成できるようにする

SpringFoxがSwaggerで使用するjsonの自動生成及びswagger-uiの機能を提供します。
まずbuild.gradleに以下のdependencyを追加します。

compile('io.springfox:springfox-swagger2:2.6.1')
compile('io.springfox:springfox-swagger-ui:2.6.1')

それから、以下のSwagger設定用のクラスを作成します。公式を確認してみるとAPI KEYやセキュリティなど他にも設定を行える箇所があるので、導入を検討する場合はここを確認した方が良さそです。

package jp.co.sample.boot.tool;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket documentation() {
        return new Docket(DocumentationType.SWAGGER_2).select().apis(
                RequestHandlerSelectors.any()).build().pathMapping("/").apiInfo(metadata());
    }

    private ApiInfo metadata() {
        return new ApiInfoBuilder().title("Sample Swagger-ui title").version("1.0").build();
    }
}

次にコントローラーに対してAPIドキュメント生成用のアノテーションを追加します。試しに以下のように修正します。

package jp.co.sample.boot.controller;

import javax.websocket.server.PathParam;

import org.springframework.ui.Model;
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.RestController;

import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;

@RestController
@RequestMapping("dummy")
public class Dummy {

    @RequestMapping(value = "", method = RequestMethod.GET)
    public String get(Model model) {
        return "dummy";
    }


    @RequestMapping(value = "/{id}", method = RequestMethod.POST)
    @ApiOperation(value = "Returns dummy response", notes = "response description", response = DummyResponse.class, produces = "application/json")
    public DummyResponse post(
            @PathVariable("id") @ApiParam(name = "dummyPathParam", value = "dummy description", required = true) String id,
            @PathParam("dummyParam") @ApiParam(name = "dummyRequestParam", value = "dummy description", required = false)  String dummyParam
            ) {
        return new DummyResponse();
    }
}

ここでは、@ApiOperationでレスポンスの情報を記述しています。それからapiに渡すパラメータは@ApiParamで指定しています。コントローラーに対してアノテーションの付与が完了したらspring boot起動後"http://localhost:8080/swagger-ui.html"にアクセスしてみたらAPIドキュメントが確認できるかと思います。

ただし、実際に使う場合は開発環境のみ利用できるなどセキュリティ面は気をつけた方が良さそうです。例えばリリース環境では@EnableSwagger2アノテーションを付与したクラスを含めないようにするやSpring-Securityや公式の説明を参考に一般の利用者からはアクセスできないようにする工夫には気をつけた方が良さそうです。