Spring Boot 使い方メモ

  • 633
    いいね
  • 0
    コメント

Spring Boot とは

  • Spring プロジェクトが提供する様々なライブラリやフレームワークを、手っ取り早く使えるようにしたフレームワーク。
  • Dropwizard の Spring 版みたいなの。
  • ビルドすると単独の jar ファイルができあがる。
    • Web アプリの場合は、組み込みの Tomcat が起動する(Jetty や Undertow に切り替え可)。
    • Web アプリでなく、普通の Java プログラムとしても動かせる。
  • Maven や Gradle などのビルドツールを利用する(Ant でもできなくはない)。
    • 使用したいコンポーネントを依存関係に追加するだけで、結合に必要な設定などが自動で行われる。

環境

Java

  • 1.8.0_45

Gradle

  • 2.3

Spring Boot

  • 1.2.3

Hello World

実装

build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE'
    }
}

apply plugin: 'java'
apply plugin: 'spring-boot' // 新しいバージョンのプラグインを使うと、 'spring-boot' ではなく 'org.springframework.boot' を使ってと警告が出るので注意

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter'
}

jar.baseName = 'spring-boot-sample'
Main.java
package sample.springboot;

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

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    public void hello() {
        System.out.println("Hello Spring Boot!!");
    }
}

Gradle から実行

$ gradle bootRun
:compileJava
:processResources UP-TO-DATE
:classes
:findMainClass
:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.3.RELEASE)

(略)

Hello Spring Boot!!

(略)

2015-04-29 12:45:26.542  INFO 5792 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown

BUILD SUCCESSFUL

Total time: 4.639 secs

ビルドして実行

$ gradle build

$ java -jar build/libs/spring-boot-sample.jar

(略)

Hello Spring Boot!!

(略)

説明

build.gradle の設定

Spring Boot 用の Gradle プラグインが用意されているので、最初にそれを読み込む。

build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE'
    }
}

apply plugin: 'spring-boot'

そして、通常の Java プログラムを作るだけなら、 spring-boot-starter を依存関係に追加する。

build.gradle
repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter'
}

Spring Boot の起動

Spring Boot の起動には、 SpringApplication クラスを使う。

最も単純な方法が、 SpringApplication#run(Object, String...) を使う方法。

Main.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    public void hello() {
        System.out.println("Hello Spring Boot!!");
    }
}
  • run() メソッドの第一引数には、 @EnableAutoConfiguration でアノテートしたクラスの Class オブジェクトを渡す。
    • Main クラスは @SpringBootApplication でアノテートされているが、これは @Configuration, @EnableAutoConfiguration, @ComponentScan の3つでクラスをアノテートしたのと同じ扱いになる。
    • @Configuration は、 Spring の色々な設定を Java コード上で行えるようにするためのアノテーション。
      • 昔の Spring は XML で設定を書いていたが、今は Java コード上で設定を行うのが主流になっているっぽい。
    • @EnableAutoConfiguration は、 Spring の設定を自動化するためのアノテーション。
      • これがあることで、依存関係を追加するだけで Spring MVC などのライブラリを設定記述なしで使えるようになる。
    • @ComponentScan は、 DI コンテナが管理する Bean を自動登録するためのアノテーション。
      • これでアノテートされたクラスを起点として、配下のパッケージを再帰的にスキャンして、 @Component でアノテートされたクラスを Bean としてコンテナに登録する。
    • この3つはだいたい一緒に使うことが多いので、 @SpringBootApplication を使うと少し楽になる。
  • 第二引数には、コマンドラインの引数を渡す。

Gradle での実行・ビルド

  • アプリケーションの起動は、 spring-boot-gradle-plugin が提供する bootRun タスクを使用する。
  • jar の作成は、普通に build タスクで OK。
  • 作成した jar は、普通に jar -jar <jarファイル> で実行できる。

Java コード上で Bean を定義する

CDI で言うところの Provider 的なやつ。

基本

Hoge.java
package sample.springboot;

public class Hoge {

    private String name;

    public Hoge(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Hoge [name=" + name + "]";
    }
}
Main.java
package sample.springboot;

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

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Hoge h = ctx.getBean(Hoge.class);
            System.out.println(h);
        }
    }

    @Bean
    public Hoge getHoge() {
        System.out.println("Main#getHoge()");
        return new Hoge("hoge");
    }
}
実行結果
Main#getHoge()
Hoge [name=hoge]
  • @Bean でメソッドをアノテートすると、そのメソッドを通じて Bean のインスタンスを生成できるようになる。
  • このような Bean を定義するメソッドは、 @Configuration でアノテートしたクラスに宣言できる。
    • @SpringBootApplication@Configuration でアノテートしたのと同じ効果がある。

@Configuration でアノテートしたクラスを別途作成する

HogeProvider.java
package sample.springboot;

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

@Configuration
public class HogeProvider {

    @Bean
    public Hoge getHoge() {
        System.out.println("HogeProvider#getHoge()");
        return new Hoge("hoge provider");
    }
}
Main.java
package sample.springboot;

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

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Hoge h = ctx.getBean(Hoge.class);
            System.out.println(h);
        }
    }
}
実行結果
HogeProvider#getHoge()
Hoge [name=hoge provider]
  • @Configuration でクラスをアノテートし、 @Bean でメソッドをアノテートすれば、任意のクラスで Bean を生成するメソッドを定義できる。

Web アプリを作る

Hello World

実装

build.gradle
dependencies {
-   compile 'org.springframework.boot:spring-boot-starter'
+   compile 'org.springframework.boot:spring-boot-starter-web'
}
Main.java
package sample.springboot;

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

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
HelloController.java
package sample.springboot.web;

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

@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    public String hello() {
        return "Hello Spring MVC";
    }
}

動作確認

起動
$ gradle bootRun
(略)
2015-04-29 18:29:29.317  INFO 5772 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-04-29 18:29:29.318  INFO 5772 --- [           main] sample.springboot.Main                   : Started Main in 2.244 seconds (JVM running for 2.531)
> Building 80% > :bootRun
curlでテスト
$ curl http://localhost:8080/hello
Hello Spring MVC

説明

Web アプリ用の依存関係

build.gradle
dependencies {
-   compile 'org.springframework.boot:spring-boot-starter'
+   compile 'org.springframework.boot:spring-boot-starter-web'
}
  • Web アプリを作る場合は、 spring-boot-starter-web モジュールを使用する。
  • デフォルトでは、 Spring MVC を使って Web アプリを作ることになる。

起動方法の変更

Main.java
    public static void main(String[] args) {
-       try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
-           ....
-       }
+       SpringApplication.run(Main.class, args);
  • サーバー起動後にコンテナがシャットダウンしてしまうので、try-with-resources 文は使わないように変更する。

Spring MVC のコントローラクラス

HelloController.java
@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    public String hello() {
        return "Hello Spring MVC";
    }
}
  • Web API のエントリポイントとなるクラスを作る場合は、 @RestController でクラスをアノテートする。
    • Web API ではなく、 MVC の C となるコントローラにしたい場合は @Controller でアノテートする(詳細後述)。
  • @RequestMapping で、パスや HTTP メソッドのマッピングをする(だいたい JAX-RS と同じノリ)。

サーバーのポート番号を変更する

application.properties
server.port=1598
動作確認
$ gradle bootRun
(略)

2015-05-02 00:09:11.201  INFO 5968 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 1598 (http)

(略)
  • server.port でポート番号を指定できる。
  • application.properties についての説明は こちら
  • 他にも以下のような変更ができる。
    • server.address :リスアンドレス(localhost にすれば、ローカルからしかアクセスできなくなる)。
    • server.sessionTimeout :セッションタイムアウト時間。

リクエストとレスポンスのマッピング

Hoge.java
package sample.springboot.web;

public class Hoge {

    public int id;
    public String value;

    @Override
    public String toString() {
        return "Hoge [id=" + id + ", value=" + value + "]";
    }
}
HelloController.java
package sample.springboot.web;

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

@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.POST)
    public Hoge hello(@RequestBody Hoge param) {
        System.out.println(param);

        Hoge hoge = new Hoge();
        hoge.id = 20;
        hoge.value = "Response";

        return hoge;
    }
}
動作確認
$ curl -H "Content-type: application/json" -X POST -d '{"id": 10, "value": "Request"}' http://localhost:8080/hello
{"id":20,"value":"Response"}
サーバーコンソール出力
Hoge [id=10, value=Request]
  • デフォルトでは、リクエスト・レスポンスともに JSON によるマッピングが有効になっている。
  • マッピングは Jackson がやっている(なので、マッピングの調整は Jackson のアノテーションでできる)。

Spring MVC の簡単な使い方メモ

URL のマッピング

HelloController.java
package sample.springboot.web;

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

@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    public String getMethod() {
        return "get";
    }

    @RequestMapping(method=RequestMethod.POST)
    public String postMethod1() {
        return "post";
    }

    @RequestMapping(value="/hey", method=RequestMethod.POST)
    public String postMethod2() {
        return "hey post";
    }
}
動作確認
$ curl http://localhost:8080/hello
get

$ curl http://localhost:8080/hello -X POST
post

$ curl http://localhost:8080/hello/hey -X POST
hey post
  • @RequestMapping でメソッド(クラス)とパスをマッピングする。
  • value 属性にパスを指定する。
  • method 属性に、 HTTP メソッドを指定する。

パスパラメータの取得

HelloController.java
package sample.springboot.web;

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;

@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(value="/{id}/{name}", method=RequestMethod.GET)
    public void getMethod(@PathVariable int id, @PathVariable String name) {
        System.out.println("id=" + id + ", name=" + name);
    }
}
動作確認
$ curl http://localhost:8080/hello/100/hoge
サーバーコンソール出力
id=100, name=hoge
  • パスの定義に波括弧({})で括ったパラメータを定義し、メソッドのパラメータに同名の引数を定義して @PathVariable でアノテートする。

クエリパラメータの取得

HelloController.java
package sample.springboot.web;

import java.util.Map;

import org.springframework.util.MultiValueMap;
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.bind.annotation.RestController;

@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    public void getMethod(
            @RequestParam String id,
            @RequestParam Map<String, String> queryParameters,
            @RequestParam MultiValueMap<String, String> multiMap) {

        System.out.println("id=" + id);
        System.out.println(queryParameters);
        System.out.println(multiMap);
    }
}
動作確認
$ curl "http://localhost:8080/hello?id=100&name=hoge&name=fuga"
サーバーコンソール出力
id=100
{id=100, name=hoge}
{id=[100], name=[hoge, fuga]}
  • @RequestParam でメソッドの引数をアノテートすることで、クエリパラメータを取得できる。
  • 引数の型が Map の場合は、クエリパラメータの情報を Map 形式で取得できる。
  • 1つのパラメータに複数の値が設定されている場合は、 Spring が提供する MultiValueMap で受け取ることができる。

リクエストヘッダーを取得する

HelloController.java
package sample.springboot.web;

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

@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    public void getMethod(@RequestHeader("Test-Header") String value) {
        System.out.println("Test-Header=" + value);
    }
}
動作確認
$ curl -H "Test-Header: hoge" http://localhost:8080/hello
サーバーコンソール出力
Test-Header=hoge
  • @RequestHeader でヘッダー情報を取得できる。

リクエストボディの値を取得する

HelloController.java
package sample.springboot.web;

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

@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.POST)
    public void getMethod(@RequestBody String body) {
        System.out.println("body=" + body);
    }
}
動作確認
$ curl http://localhost:8080/hello -X POST -d "Request Body"
サーバーコンソール出力
body=Request+Body=
  • @RequestBody でリクエストボディを取得できる。

レスポンスのステータスコードを指定する

package sample.springboot.web;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public void getMethod() {
    }
}
動作確認
$ curl http://localhost:8080/hello -v
(略)

< HTTP/1.1 400 Bad Request
* Server Apache-Coyote/1.1 is not blacklisted
< Server: Apache-Coyote/1.1
< Content-Length: 0
< Date: Wed, 29 Apr 2015 11:50:08 GMT
< Connection: close

(略)
  • メソッドを @ResponseStatus でアノテートし、 value にステータスコードを指定すると、そのレスポンスのステータスコードを指定できる。
  • 何も指定しない場合は 200 OK が返される。

レスポンスの返し方色々

Java - Spring MVCのコントローラでの戻り値いろいろ - Qiita

@tag1216 さんの上記ページに、分かりやすくまとめられていました。

例外ハンドリング

デフォルトだと、以下のように例外がハンドリングされる。

  • REST のクライアントの場合
    • スローされた例外の情報や、 HTTP のステータスコードを保持した JSON 文字列。
{"timestamp":1430484452755,"status":500,"error":"Internal Server Error","exception":"sample.springboot.web.MyException","message":"No message available","path":"/hello"}
  • ブラウザの場合
    • デフォルトのエラーページ(Whilelabel Error Page)

spring-boot.JPG

特定の例外がスローされたときのステータスコードを指定する

MyException.java
package sample.springboot.web;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class MyException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public MyException(String msg) {
        super(msg);
    }
}
WebApiController.java
package sample.springboot.web;

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

@RestController
@RequestMapping("/api")
public class WebApiController {

    @RequestMapping(method=RequestMethod.GET)
    public void method1() {
        throw new MyException("test exception");
    }
}
動作確認
$ curl http://localhost:8080/api
{"timestamp":1430489386562,"status":400,"error":"Bad Request","exception":"sample.springboot.web.MyException","message":"test exception","path":"/api"}
  • 自作の例外クラスを @ResponseStatus でアノテートすることで、その例外がスローされたときのステータスコードを指定できる。
  • ブラウザからアクセスした場合は、デフォルトのエラーページが表示される。

spring-boot.JPG

ほぼ全ての例外をハンドリングする

MyExceptionResolver.java
package sample.springboot.web;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

@Component
public class MyExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println(ex.getClass() + " : " + ex.getMessage());

        ModelAndView mv = new ModelAndView();
        mv.setViewName("my-error");

        return mv;
    }
}
src/main/resources/templates/my-error.html
<h1>My Error Page</h1>
WebApiController.java
package sample.springboot.web;

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

@RestController
@RequestMapping("/api")
public class WebApiController {

    @RequestMapping(method=RequestMethod.GET)
    public void method1() {
        throw new MyException("test exception");
    }

    @RequestMapping(value="/null", method=RequestMethod.GET)
    public void method2() {
        throw new NullPointerException("test exception");
    }
}
動作確認
$ curl http://localhost:8080/api
{"timestamp":1430490625809,"status":400,"error":"Bad Request","exception":"sample.springboot.web.MyException","message":"test exception","path":"/api"}

$ curl http://localhost:8080/api/null
<h1>My Error Page</h1>
サーバーコンソール出力
class java.lang.NullPointerException : test exception
  • HandlerExceptionResolver を実装したクラスを作成し、 @Component でコンテナに登録する。
  • すると、コントローラで例外が発生すると登録したクラスの resolveException() メソッドが呼ばれるようになる。
    • ただし、 @ResponseStatus でアノテートされたクラスがスローされた場合は呼ばれない。
  • resolveException() メソッドは ModelAndView を返すようになっているので、任意のページを表示させることができる。

Web API のアクセスの場合は json で返したい

MyExceptionResolver.java
package sample.springboot.web;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

@Component
public class MyExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        if (isRestController(handler)) {
            return null;
        }

        ModelAndView mv = new ModelAndView();
        mv.setViewName("my-error");

        return mv;
    }

    private boolean isRestController(Object handler) {
        if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod)handler;

            return method.getMethod().getDeclaringClass().isAnnotationPresent(RestController.class);
        }

        return false;
    }
}
動作確認
$ curl http://localhost:8080/api
{"timestamp":1430490748092,"status":400,"error":"Bad Request","exception":"sample.springboot.web.MyException","message":"test exception","path":"/api"}

$ curl http://localhost:8080/api/null
{"timestamp":1430490749586,"status":500,"error":"Internal Server Error","exception":"java.lang.NullPointerException","message":"test exception","path":"/api/null"}
  • コントローラが @RestController でアノテートされている場合は、 resolveException() で null を返すようにする。
  • すると、レスポンスがデフォルトのハンドリング方法で処理されるようになる(クライアントが curl のような非ブラウザなら json になる)。
    • ブラウザでアクセスした場合は、デフォルトのエラーページ(Whitelabel Error Page)が表示される。
    • ブラウザから画面遷移でアクセスする場合は、 @Controller でアノテートされたコントローラクラスにアクセスするようにし、 resolveException() で適切なエラーページに飛ばしてあげるようにする。

コントローラ単位で例外ハンドリングを定義する

WebApiController.java
package sample.springboot.web;

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;

@RestController
@RequestMapping("/api")
public class WebApiController {

    @RequestMapping(method=RequestMethod.GET)
    public void method1() {
        throw new MyException("test exception");
    }

    @RequestMapping(value="/null", method=RequestMethod.GET)
    public void method2() {
        throw new NullPointerException("test exception");
    }

    @ExceptionHandler(NullPointerException.class)
    public String handling(NullPointerException e) {
        return "{\"message\":\"" + e.getMessage() + "\"}";
    }
}
動作確認
$ curl http://localhost:8080/api/null
{"message":"test exception"}
  • @ExceptionHandler でアノテートしたメソッドを定義すると、そのコントローラ内でだけ有効な例外ハンドリングができる。
  • @ExceptionHandlervalue には、ハンドリングしたい例外の Class オブジェクトを渡す。

静的ファイルを配置する

フォルダ構成
|-build.gradle
`-src/main/resources/
  |-static/
  | `-static.html
  |-public/
  | `-public.html
  |-resources/
  | `-resources.html
  `-META-INF/resources/
    `-m-resourceshtml

各 HTML ファイルの中身は、ファイル名が書かれただけのプレーンテキスト。

この状態で Spring Boot を起動して、以下のようにアクセスする。

$ curl http://localhost:8080/static.html
static.html

$ curl http://localhost:8080/public.html
public.html

$ curl http://localhost:8080/resources.html
resources.html

$ curl http://localhost:8080/m-resources.html
m-resources.html
  • クラスパス以下の、次のフォルダにファイルを配置すると、静的ファイルとしてアクセスできる。
    • static
    • public
    • resources
    • META-INF/resources

WebJars を利用する

WebJars とは

jQuery とか Bootstrap のようなクライアントサイドのライブラリを jar に固めて、 Java のライブラリと同じ要領で Maven や Gradle で依存管理できるようにしたサービス。

WebJars - Web Libraries in Jars

jQuery UI を入れてみる

ここ で、利用できるライブラリを調べられる。

build.gradle
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
+   compile 'org.webjars:jquery-ui:1.11.4'
}
src/main/resources/static/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>jQuery UI by WebJars</title>

    <link rel="stylesheet" href="/webjars/jquery-ui/1.11.4/jquery-ui.min.css" />

    <script src="/webjars/jquery/1.11.1/jquery.min.js"></script>
    <script src="/webjars/jquery-ui/1.11.4/jquery-ui.min.js"></script>
    <script>
    $(function() {
        $('button')
          .button()
          .on('click', function() {
              alert('Hello WebJars!!');
          });
    });
    </script>
  </head>
  <body>
    <button>Hello</button>
  </body>
</html>

サーバーを起動して、ブラウザで http://localhost:8080/ にアクセスする。

spring-boot.JPG

spring-boot.JPG

  • WebJars で追加したライブラリは、 webjars/ 以下のパスからアクセスできる。
  • フォルダ構成は、 jar の中を見るか前述のページの右端にある Files のリンクをクリックすれば分かる。

テンプレートエンジンを利用する

Spring Boot 的には JSP はおすすめしないらしい。

はじめてのSpring Boot でも紹介されてる Thymeleaf を使ってみる。

Hello World

実装

build.gradle
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
+   compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
    compile 'org.webjars:jquery-ui:1.11.4'
}
HelloController.java
package sample.springboot.web;

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

@Controller
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    public String hello() {
        return "hello";
    }
}
src/main/resources/templates/hello.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello Thymeleaf</title>
  </head>
  <body>
    <h1>Hello Thymeleaf</h1>
  </body>
</html>

動作確認

ブラウザで http://localhost:8080/hello にアクセスする。

spring-boot.JPG

説明

依存関係の追加
build.gradle
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
+   compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
    compile 'org.webjars:jquery-ui:1.11.4'
}
  • Thymeleaf を使えるように依存関係を追加する。
コントローラの実装
@Controller
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    public String hello() {
        return "hello";
    }
}
  • テンプレートを返す場合は、 @RestController ではなく @Controller でクラスをアノテートする。
  • メソッドの戻り値に、表示するテンプレートのパスを指定する。
    • テンプレートファイルは、クラスパス上の templates パッケージの下に配置する。
    • コントローラのメソッドが返した文字列は、この templates パッケージからの相対パスになる(拡張子は省略可)。

画面に値を埋め込む

Hoge.java
package sample.springboot.web;

public class Hoge {

    public int id;
    public String value;

    @Override
    public String toString() {
        return "Hoge [id=" + id + ", value=" + value + "]";
    }
}
HelloController.java
package sample.springboot.web;

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("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    public String hello(Model model) {
        Hoge hoge = new Hoge();
        hoge.id = 10;
        hoge.value = "hoge";

        model.addAttribute("myData", hoge);

        return "hello";
    }
}
hello.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>Hello Thymeleaf</title>
  </head>
  <body>
    <dl>
      <dt>id</dt>
      <dd th:text="${myData.id}"></dd>

      <dt>value</dt>
      <dd th:text="${myData.value}"></dd>
    </dl>
  </body>
</html>

ブラウザで http://localhost:8080/hello にアクセスする。

spring-boot.JPG

  • コントローラのメソッドで Model を引数に受け取るようにする。
  • この ModeladdAttribute() メソッドを使って、画面で出力したい情報を設定する。
  • 画面側では、まず Thymeleaf 用の名前空間を定義する(xmlns:th
    • th:text 属性で、指定した値をテキストとして出力する。
    • th:text の値には、 ${...} のように EL 式っぽく出力する値を指定する。

繰り返し出力

HelloController.java
package sample.springboot.web;

import java.util.Arrays;
import java.util.List;

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("/hello")
public class HelloController {

    @RequestMapping(method=RequestMethod.GET)
    public String hello(Model model) {
        List<Hoge> list = Arrays.asList(
                            new Hoge() {{
                                id = 10;
                                value = "hoge";
                            }},
                            new Hoge() {{
                                id = 20;
                                value = "fuga";
                            }},
                            new Hoge() {{
                                id = 30;
                                value = "piyo";
                            }});

        model.addAttribute("hogeList", list);

        return "hello";
    }
}
hello.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>Hello Thymeleaf</title>
  </head>
  <body>
    <dl th:each="hoge : ${hogeList}">
      <dt>id</dt>
      <dd th:text="${hoge.id}"></dd>

      <dt>value</dt>
      <dd th:text="${hoge.value}"></dd>
    </dl>
  </body>
</html>

spring-boot.JPG

  • th:each で、指定したコレクションを繰り返し処理できる。

その他の使い方

ここに書いてたらキリがないので、 公式ドキュメント を参照。

気が向いたら別途まとめる。

ホットデプロイ

build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE'
+       classpath 'org.springframework:springloaded:1.2.1.RELEASE'
    }
}

apply plugin: 'java'
apply plugin: 'spring-boot'

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
}

jar.baseName = 'spring-boot-sample'

buildscript の依存関係に org.springframework:springloaded:1.2.1.RELEASE を追加する。
あとは、普通に gradle bootRun すれば、ホットデプロイが有効になる。

テンプレートエンジンに Thymeleaf を利用している場合は、キャッシュ機能をオフにしておく必要がある。

application.properties
spring.thymeleaf.cache=false

Thymeleaf 以外のテンプレートエンジンを利用している場合は、 このページ を参照。

IntelliJ IDEA を使用している場合

build.gradle に以下を追記する必要がある。

build.gradle
apply plugin: 'idea'

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

デフォルトだと、 IlleliJ がコンパイル結果を出力する先が Gradle の出力先と異なるためファイルの監視がうまくいかないらしく、そのへんを変更しているらしい。

参考:80. Hot swapping

データベースアクセス

Hello World

build.gradle
dependencies {
    compile 'org.hsqldb:hsqldb'
    compile 'org.springframework.boot:spring-boot-starter-jdbc'
}
Main.java
package sample.springboot;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private JdbcTemplate jdbc;

    public void method() {
        this.jdbc.execute("CREATE TABLE TEST_TABLE (ID INTEGER NOT NULL IDENTITY, VALUE VARCHAR(256))");

        this.jdbc.update("INSERT INTO TEST_TABLE (VALUE) VALUES (?)", "hoge");
        this.jdbc.update("INSERT INTO TEST_TABLE (VALUE) VALUES (?)", "fuga");
        this.jdbc.update("INSERT INTO TEST_TABLE (VALUE) VALUES (?)", "piyo");

        List<Map<String, Object>> list = this.jdbc.queryForList("SELECT * FROM TEST_TABLE");
        list.forEach(System.out::println);
    }
}
動作確認
{ID=0, VALUE=hoge}
{ID=1, VALUE=fuga}
{ID=2, VALUE=piyo}
  • 依存関係に spring-boot-starter-jdbc と、使用する DB (org.hsqldb:hsqldb)を追加する。
  • すると、指定した DB をオンメモリで利用できるようになる。
  • オンメモリなので、 JVM が停止するとデータは失われる。
  • HSQLDB の他に H2 と Derby を同じく組み込みで利用できる。

データをファイルに永続化する

application.properties
spring.datasource.url=jdbc:hsqldb:file:./db/testdb;shutdown=true
  • プロパティファイルspring.datasource.url を定義することで、 JDBC 接続するときの URL を指定できる。
  • HSQLDB の場合は、 URL でデータをファイルに保存するかどうかを指定できるので、上記のように設定すればデータをファイルに永続化できる。

外部のデータベースを利用する

ローカルの MySQL を利用する。

MySQL のテーブル

spring-boot.JPG

実装

build.gradle
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-jdbc'
    compile 'mysql:mysql-connector-java:5.1.35'
}
application.properties
spring.datasource.url=jdbc:mysql://localhost/test
spring.datasource.username=test
spring.datasource.password=test
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
Main.java
package sample.springboot;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private JdbcTemplate jdbc;

    public void method() {
        List<Map<String, Object>> list = this.jdbc.queryForList("SELECT * FROM TEST_TABLE");
        list.forEach(System.out::println);
    }
}
実行結果
{id=1, value=hoge}
{id=2, value=fuga}
{id=3, value=piyo}
  • application.properties に接続設定を記述することで、外部の DB に接続できる。

JPA を利用する

基本

build.gradle
dependencies {
-   compile 'org.springframework.boot:spring-boot-starter-jdbc'
+   compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile 'org.hsqldb:hsqldb'
}
application.properties
spring.datasource.url=jdbc:hsqldb:file:./db/testdb;shutdown=true
spring.jpa.hibernate.ddl-auto=update
MyEntity.java
package sample.springboot.jpa;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class MyEntity {

    @Id @GeneratedValue
    private Long id;
    private String value;

    public MyEntity(String value) {
        this.value = value;
    }

    private MyEntity() {}

    @Override
    public String toString() {
        return "MyEntity [id=" + id + ", value=" + value + "]";
    }
}
MyEntityRepository.java
package sample.springboot.jpa;

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

public interface MyEntityRepository extends JpaRepository<MyEntity, Long> {
}
Main.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.MyEntity;
import sample.springboot.jpa.MyEntityRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private MyEntityRepository repository;

    public void method() {
        this.repository.save(new MyEntity("test"));

        this.repository.findAll().forEach(System.out::println);
    }
}
動作確認
$ gradle bootRun
MyEntity [id=1, value=test]

$ gradle bootRun
MyEntity [id=1, value=test]
MyEntity [id=2, value=test]

$ gradle bootRun
MyEntity [id=1, value=test]
MyEntity [id=2, value=test]
MyEntity [id=3, value=test]
  • JPA を使う場合は、 org.springframework.boot:spring-boot-starter-data-jpa を依存関係に追加する。
  • JPA の実装には Hibernate が利用される。
    • デフォルトだとテーブルが毎回作りなおされるので、 spring.jpa.hibernate.ddl-auto=update を設定している。
  • JpaRepository を継承したインターフェースを定義すると、 Spring が良しなにデータアクセスの実装を作ってくれる。

メソッド名からのクエリ自動生成

データベース

spring-boot.JPG

エンティティ

Hoge.java
package sample.springboot.jpa;

import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Hoge {

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    private int number;
    private String string;
    @Embedded
    private Fuga fuga;

    @Override
    public String toString() {
        return "Hoge [id=" + id + ", number=" + number + ", string=" + string + ", fuga=" + fuga + "]";
    }
}
Fuga.java
package sample.springboot.jpa;

import javax.persistence.Embeddable;

@Embeddable
public class Fuga {

    private String value;

    @Override
    public String toString() {
        return "Fuga [value=" + value + "]";
    }
}

リポジトリインターフェース

HogeRepository.java
package sample.springboot.jpa;

import java.util.List;

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

public interface HogeRepository extends JpaRepository<Hoge, Long> {

    List<Hoge> findByNumber(int number);

    List<Hoge> findByNumberOrderByIdDesc(int number);

    List<Hoge> findByStringLike(String string);

    List<Hoge> findByNumberLessThan(int number);

    List<Hoge> findByStringIgnoreCase(String string);

    List<Hoge> findByFugaValue(String string);

    long countByStringLike(String string);

    List<Hoge> findByNumberAndStringLike(int number, String string);

    List<Hoge> findByNumberOrString(int number, String string);
}

動作確認

Main.java
package sample.springboot;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.Hoge;
import sample.springboot.jpa.HogeRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private HogeRepository repository;

    public void method() {
        print("findByNumber",              repository.findByNumber(1));
        print("findByNumberAndStringLike", repository.findByNumberAndStringLike(1, "%e"));
        print("findByNumberOrString",      repository.findByNumberOrString(2, "seven"));
        print("findByNumberOrderByIdDesc", repository.findByNumberOrderByIdDesc(2));
        print("findByStringLike",          repository.findByStringLike("t%"));
        print("findByNumberLessThan",      repository.findByNumberLessThan(3));
        print("findByStringIgnoreCase",    repository.findByStringIgnoreCase("FIVE"));
        print("findByFugaValue",           repository.findByFugaValue("hoge"));
        print("countByStringLike",         repository.countByStringLike("%o%"));
    }

    private void print(String methodName, List<Hoge> list) {
        System.out.println("<<" + methodName + ">>");
        list.forEach(System.out::println);
        System.out.println();
    }

    private void print(String methodName, long number) {
        System.out.println("<<" + methodName + ">>");
        System.out.println(number);
        System.out.println();
    }
}
実行結果
<<findByNumber>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

<<findByNumberOrderByIdDesc>>
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]

<<findByStringLike>>
Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

<<findByNumberLessThan>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]

<<findByStringIgnoreCase>>
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]

<<findByFugaValue>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=7, number=3, string=seven, fuga=Fuga [value=hoge]]

<<countByStringLike>>
3

<<findByNumberAndStringLike>>
Hoge [id=1, number=1, string=one, fuga=Fuga [value=hoge]]
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]

<<findByNumberOrString>>
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=5, number=2, string=five, fuga=Fuga [value=fuga]]
Hoge [id=7, number=3, string=seven, fuga=Fuga [value=hoge]]

spring-boot.JPG

  • Repository を継承したインターフェースに find~~ のようなメソッドを定義すると、 Spring が良しなに解釈してクエリを自動生成してくれる。
  • 基本は、 findBy<条件とするプロパティの名前> で定義する。
  • AndOr で連結できる。
  • OrderBy<プロパティ名><Asc | Desc> で、ソートを指定できる。
  • Like をつければ文字列のあいまい検索ができる。
  • LessThan, GreaterThan, Between なども使える。
  • IgnoreCase をつければ、大文字小文字の区別なしで比較できる。
  • count~~ とすると、検索結果のエンティティ数を取得できる。
  • 組み込み可能クラスのプロパティを条件にする場合は、 findBy<組み可能クラス><組み込み可能クラスのプロパティ> と繋げる。

JPQL を使用する

HogeRepository.java
package sample.springboot.jpa;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface HogeRepository extends JpaRepository<Hoge, Long> {

    @Query("SELECT h FROM Hoge h WHERE (h.id % 2) = 0")
    List<Hoge> findEvenIdEntities();
}
Main.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.HogeRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private HogeRepository repository;

    public void method() {
        this.repository.findEvenIdEntities().forEach(System.out::println);
    }
}
実行結果
Hoge [id=2, number=1, string=two, fuga=Fuga [value=fuga]]
Hoge [id=4, number=2, string=four, fuga=Fuga [value=hoge]]
Hoge [id=6, number=3, string=six, fuga=Fuga [value=piyo]]

spring-boot.JPG

  • @Query でメソッドをアノテートすることで、 JPQL を指定することができる。
  • JPQL は、 @Queryvalue に設定する。

EntityManager を取得する

Main.java
package sample.springboot;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.Hoge;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private EntityManager em;

    public void method() {
        TypedQuery<Hoge> query = this.em.createQuery("SELECT h FROM Hoge h WHERE h.id=:id", Hoge.class);
        query.setParameter("id", 3L);

        Hoge hoge = query.getSingleResult();

        System.out.println(hoge);
    }
}
実行結果
Hoge [id=3, number=1, string=three, fuga=Fuga [value=piyo]]
  • @Autowired を使って普通に EntityManager をインジェクションできる。

宣言的トランザクションを使用する

MyService.java
package sample.springboot.jpa;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class MyService {

    @Autowired
    private HogeRepository repository;

    public void save(String value) {
        Hoge hoge = new Hoge(value);
        this.repository.save(hoge);
    }

    public void saveAndThrowRuntimeException(String value) {
        this.save(value);
        throw new RuntimeException("test");
    }

    @Transactional
    public void saveAndThrowRuntimeExceptionWithTransactional(String value) {
        this.saveAndThrowRuntimeException(value);
    }

    @Transactional
    public void saveAndThrowExceptionWithTransactional(String value) throws Exception {
        this.save(value);
        throw new Exception("test");
    }

    public void show() {
        this.repository.findAll().forEach(System.out::println);
    }
}
Main.java
package sample.springboot;

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

import sample.springboot.jpa.MyService;

@SpringBootApplication
public class Main {

    public static void main(String[] args) throws Exception {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyService s = ctx.getBean(MyService.class);

            s.save("normal");

            try {
                s.saveAndThrowRuntimeException("runtime exception without @Transactional");
            } catch (Exception e) {}

            try {
                s.saveAndThrowRuntimeExceptionWithTransactional("runtime exception with @Transactional");
            } catch (Exception e) {}

            try {
                s.saveAndThrowExceptionWithTransactional("exception with @Transactional");
            } catch (Exception e) {}

            s.show();
        }
    }
}
実行結果
Hoge [id=1, value=normal]
Hoge [id=2, value=runtime exception without @Transactional]
Hoge [id=4, value=exception with @Transactional]
  • @Transactional でメソッドをアノテートすると、そのメソッドの前後がトランザクション境界になる。
  • トランザクション境界内で RuntimeException およびそのサブクラスがスローされると、トランザクションはロールバックされる。
  • @Transactional でアノテートされていなかったり、 Exception およびそのサブクラスがスローされた場合は、ロールバックされない。
  • Exception がスローされた場合もロールバックして欲しい場合は、 @Transactional(rollbackFor=Exception.class) のように設定する。

Flyway でマイグレーションする

build.gradle
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-data-jpa'
    compile 'org.hsqldb:hsqldb'
+   compile 'org.flywaydb:flyway-core'
}
application.properties
spring.jpa.hibernate.ddl-auto=none
src/main/resources/db/migration/V1__create_database.sql
CREATE TABLE HOGE (
    ID INTEGER NOT NULL IDENTITY,
    VALUE VARCHAR(256)
);

INSERT INTO HOGE (VALUE) VALUES ('HOGE');
INSERT INTO HOGE (VALUE) VALUES ('FUGA');
INSERT INTO HOGE (VALUE) VALUES ('PIYO');
Main.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import sample.springboot.jpa.HogeRepository;

@SpringBootApplication
public class Main {

    public static void main(String[] args) throws Exception {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.method();
        }
    }

    @Autowired
    private HogeRepository repository;

    public void method() {
        this.repository.findAll().forEach(System.out::println);
    }
}
実行結果
Hoge [id=0, value=HOGE]
Hoge [id=1, value=FUGA]
Hoge [id=2, value=PIYO]
  • Flyway を依存関係に追加するだけで、サーバー起動時にマイグレーションを実行してくれるようになる。
  • JPA を使う場合は、 JPA が DB を自動生成しないようにしないといけないので、 spring.jpa.hibernate.ddl-auto=none を指定する。
  • Flyway 自体の使い方については こちら を参照。

複数のデータソースを使用する

基本

実装

PrimaryDataSourceConfiguration.java
package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class PrimaryDataSourceConfiguration {

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }
}
SecondaryDataSourceConfiguration.java
package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class SecondaryDataSourceConfiguration {

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }
}
MySecondary.java
package sample.springboot;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.beans.factory.annotation.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE})
public @interface MySecondary {
}
MyDatabaseAccess.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

@Component
public class MyDatabaseAccess {

    private static final String CREATE_TABLE_SQL = "CREATE TABLE TEST_TABLE (VALUE VARCHAR(256))";
    private static final String INSERT_SQL = "INSERT INTO TEST_TABLE VALUES (?)";
    private static final String SELECT_SQL = "SELECT * FROM TEST_TABLE";

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;

    public void initialize() {
        this.primary.execute(CREATE_TABLE_SQL);
        this.secondary.execute(CREATE_TABLE_SQL);
    }

    public void insertPrimary(String value) {
        this.primary.update(INSERT_SQL, value);
    }

    public void insertSecondary(String value) {
        this.secondary.update(INSERT_SQL, value);
    }

    public void showRecords() {
        System.out.println("Primary >>>>");
        this.primary.queryForList(SELECT_SQL).forEach(System.out::println);

        System.out.println("Secondary >>>>");
        this.secondary.queryForList(SELECT_SQL).forEach(System.out::println);
    }
}
Main.java
package sample.springboot;

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

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyDatabaseAccess db = ctx.getBean(MyDatabaseAccess.class);

            db.initialize();

            db.insertPrimary("primary!!");
            db.insertSecondary("secondary!!");

            db.showRecords();
        }
    }
}

動作確認

コンソール出力
Primary >>>>
{VALUE=primary!!}

Secondary >>>>
{VALUE=secondary!!}

説明

PrimaryDataSourceConfiguration.java
    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }
  • @Bean を使って、 DataSource のビーンを定義している(createPrimaryDataSource())。
  • 作成した DataSource を引数に受け取りつつ、さらに JdbcTemplate のビーンを定義している(createPrimaryJdbcTemplate())。
  • DataSource を複数定義するときは、一方の定義を @Primary でアノテートする。
    • @Primary は、デフォルトでインジェクションされるビーンを指定するためのアノテーション。
    • ビーンの候補が複数存在する状態で限定子を指定しないと、 @Primary でアノテートされたビーンがインジェクションされる。
  • DataSource のインスタンスは、 DataSourceBuilder を使って作成できる。
SecondaryDataSourceConfiguration.java
    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }
  • 2つ目の DataSource の定義には、自作の限定子を付与している。
    • 限定子については こちら を参照。
MyDatabaseAccess.java
    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;
  • インジェクションするときに、 @Autowired だけなら @Primary でアノテートした方のビーンが、
    自作限定子でアノテートすれば、対応するビーンがインジェクションされる。
  • あとは、だいたい今まで通りにデータベースアクセスができる。

宣言的トランザクション

複数の DataSource を定義した場合、そのままだと @Primary じゃない方のデータソースについて、宣言的トランザクションが使用できない。

@Primary でない方のデータソースでも宣言的トランザクションを使用する場合は、以下のように実装する。

実装

PrimaryDataSourceConfiguration.java
package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
+ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+ import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class PrimaryDataSourceConfiguration {

    @Bean @Primary
    public DataSource createPrimaryDataSource() {
        return DataSourceBuilder
            .create()
            .driverClassName("org.hsqldb.jdbcDriver")
            .url("jdbc:hsqldb:mem:primary")
            .username("SA")
            .password("")
            .build();
    }

    @Bean @Primary
    public JdbcTemplate createPrimaryJdbcTemplate(DataSource ds) {
        return new JdbcTemplate(ds);
    }

+   @Bean @Primary
+   public PlatformTransactionManager createTransactionManager(DataSource ds) {
+       return new DataSourceTransactionManager(ds);
+   }
}
SecondaryDataSourceConfiguration.java
package sample.springboot;

import javax.sql.DataSource;

import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
+ import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+ import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class SecondaryDataSourceConfiguration {

+   public static final String TRANSACTION_MANAGER_NAME = "secondary-tx-manager";

    @Bean @MySecondary
    public DataSource createSecondaryDataSource() {
        return DataSourceBuilder
                .create()
                .driverClassName("org.hsqldb.jdbcDriver")
                .url("jdbc:hsqldb:mem:secondary")
                .username("SA")
                .password("")
                .build();
    }

    @Bean @MySecondary
    public JdbcTemplate createSecondaryJdbcTemplate(@MySecondary DataSource ds) {
        return new JdbcTemplate(ds);
    }

+   @Bean(name=SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
+   public PlatformTransactionManager createTransactionManager(@MySecondary DataSource ds) {
+       return new DataSourceTransactionManager(ds);
+   }
}
MyDatabaseAccess.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
+ import org.springframework.transaction.annotation.Transactional;

@Component
public class MyDatabaseAccess {

    private static final String CREATE_TABLE_SQL = "CREATE TABLE TEST_TABLE (VALUE VARCHAR(256))";
    private static final String INSERT_SQL = "INSERT INTO TEST_TABLE VALUES (?)";
    private static final String SELECT_SQL = "SELECT * FROM TEST_TABLE";

    @Autowired
    private JdbcTemplate primary;

    @Autowired @MySecondary
    private JdbcTemplate secondary;

    public void initialize() {
        this.primary.execute(CREATE_TABLE_SQL);
        this.secondary.execute(CREATE_TABLE_SQL);
    }

-   public void insertPrimary(String value) {
-       this.primary.update(INSERT_SQL, value);
-   }
-   
-   public void insertSecondary(String value) {
-       this.secondary.update(INSERT_SQL, value);
-   }

+   @Transactional
+   public void insertPrimary(String value, boolean rollback) {
+       this.primary.update(INSERT_SQL, value);
+       if (rollback) throw new RuntimeException("test exception");
+   }
+   
+   @Transactional(SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
+   public void insertSecondary(String value, boolean rollback) {
+       this.secondary.update(INSERT_SQL, value);
+       if (rollback) throw new RuntimeException("test exception");
+   }

    public void showRecords() {
        System.out.println("Primary >>>>");
        this.primary.queryForList(SELECT_SQL).forEach(System.out::println);

        System.out.println("Secondary >>>>");
        this.secondary.queryForList(SELECT_SQL).forEach(System.out::println);
    }

}
Main.java
package sample.springboot;

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

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyDatabaseAccess db = ctx.getBean(MyDatabaseAccess.class);

            db.initialize();

            db.insertPrimary("primary commit!!", false);
            db.insertSecondary("secondary commit!!", false);

            try {
                db.insertPrimary("primary rollback!!", true);
            } catch (Exception e) {}

            try {
                db.insertSecondary("secondary rollback!!", true);
            } catch (Exception e) {}

            db.showRecords();
        }
    }
}

動作確認

コンソール出力
Primary >>>>
{VALUE=primary commit!!}

Secondary >>>>
{VALUE=secondary commit!!}

説明

PrimaryDataSourceConfiguration.java
    @Bean @Primary
    public PlatformTransactionManager createTransactionManager(DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
SecondaryDataSourceConfiguration.java
    public static final String TRANSACTION_MANAGER_NAME = "secondary-tx-manager";

    ...

    @Bean(name=SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
    public PlatformTransactionManager createTransactionManager(@MySecondary DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
  • 複数データソースを定義したうえで宣言的トランザクションを使用する場合は、 PlatformTransactionManager のビーンを定義する。
  • @Primary の方は @Primary でアノテートするだけでいいが、そうでない方はビーン名を指定しておく。
MyDatabaseAccess.java
    @Transactional
    public void insertPrimary(String value, boolean rollback) {
        this.primary.update(INSERT_SQL, value);
        if (rollback) throw new RuntimeException("test exception");
    }

    @Transactional(SecondaryDataSourceConfiguration.TRANSACTION_MANAGER_NAME)
    public void insertSecondary(String value, boolean rollback) {
        this.secondary.update(INSERT_SQL, value);
        if (rollback) throw new RuntimeException("test exception");
    }
  • @PrimaryDataSource を使用する場合は、従来通り @Transactional でアノテートすることで宣言的トランザクションが使用できる。
  • @Primary でない方の DataSource を使用する場合は、 @Transactionalvalue に、 PlatformTransactionManager のビーン名を指定しなければならない。

参考

外部設定(Externalized Configuration)を利用する

プロパティファイルを使用する

基本

フォルダ構成
|-build.gradle
`-src/main/
  |-java/sample/springboot/
  |  `-Main.java
  `-resources/
    `-application.properties
Main.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    @Value("${sample.value}")
    private String value;

    public void hello() {
        System.out.println("sample.value = " + this.value);
    }
}
実行結果
sample.value = Hello Properties File!!
  • クラスパス直下に application.properties という名前でプロパティファイルを配置する。
  • すると、 Spring Boot が自動でそのファイルを読み込んでくれる。
  • プロパティファイルの値は、 @Value アノテーションを使うことで Bean にインジェクションできる。
    • ${プロパティ名} という形式で取得したい値を指定する。

ファイルの置き場所

プロパティファイルの置き場はいくつかあり、読み込みの優先順位が存在する。

  1. 起動時に --spring.config.location で指定したファイル。
  2. カレントディレクトリ直下の config ディレクトリにあるファイル。
  3. カレントディレクトリにあるファイル。
  4. クラスパス直下の config パッケージにあるファイル。
  5. クラスパス直下にあるファイル。

数字が若い方が、優先順位が高い。
優先順位が下位の設定は、上位の設定で上書きされる。

フォルダ構成(jar内)
|-application.properties
|-config/
|  `-application.properties
`-sample/springboot/
   `-Main.class
フォルダ構成(実行時)
|-application.properties
|-other.properties
|-config/
| `-application.properties
`-build/libs/
  `-spring-boot-sample.jar
other.properties
value5=other
application.properties(カレントディレクトリのconfigディレクトリ直下)
value4=currentdir/config
value5=currentdir/config
application.properties(カレントディレクトリ直下)
value3=currentdir/
value4=currentdir/
value5=currentdir/
application.properties(クラスパス内のconfigパッケージ直下)
value2=classpath/config
value3=classpath/config
value4=classpath/config
value5=classpath/config
application.properties(クラスパス直下)
value1=classpath/
value2=classpath/
value3=classpath/
value4=classpath/
value5=classpath/
Main.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    @Value("${value1}") private String value1;
    @Value("${value2}") private String value2;
    @Value("${value3}") private String value3;
    @Value("${value4}") private String value4;
    @Value("${value5}") private String value5;

    public void hello() {
        System.out.println("value1=" + value1);
        System.out.println("value2=" + value2);
        System.out.println("value3=" + value3);
        System.out.println("value4=" + value4);
        System.out.println("value5=" + value5);
    }
}
実行
$ java -jar build/libs/spring-boot-sample.jar --spring.config.location=other.properties

value1=classpath/
value2=classpath/config
value3=currentdir/
value4=currentdir/config
value5=other

優先順位に合わせて設定が上書きされていっている。

プロファイルを指定する

フォルダ構成
|-application.properties
|-application-develop.properties
`-build/libs/
   `-spring-boot-sample.jar
application.properties
value=release module
application-develop.properties
value=develop module
実行
$ java -jar build/libs/spring-boot-sample.jar

value=release module

$ java -jar build/libs/spring-boot-sample.jar --spring.profiles.active=develop

value=develop module
  • プロパティファイルを application-<プロファイル名>.properties という形式で作成する。
  • コマンドライン引数などで、 spring.profiles.active に有効にしたいプロファイル名を指定する。
    • コマンドライン引数以外にも、システムプロパティや OS の環境変数でも指定可能。
  • すると、指定されたプロファイルに該当するプロパティファイルが読み込まれる。

同じプレフィックスを持つプロパティを Bean にマッピングする

application.properties
person.firstName=Sato
person.last-name=Taro
person.age=18
Person.java
package sample.springboot;

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

@Component
@ConfigurationProperties(prefix="person")
public class Person {

    private String firstName;
    private String lastName;
    private int age;

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void hello() {
        System.out.println(firstName + " " + lastName + " : " + age);
    }
}
Main.java
package sample.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
@EnableConfigurationProperties
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Person person = ctx.getBean(Person.class);
            person.hello();
        }
    }
}
実行結果
Sato Taro : 18
  • @ConfigurationProperties を使うことで、特定のプレフィックスを持つプロパティたちを Bean にマッピングできる。
    • Bean にはセッターメソッドが必要になる。
    • フィールドの名前は、キャメルケース以外にもハイフン区切りやスネークケースでもマッピングしてくれる。
  • この仕組を有効にするには、 @EnableConfigurationProperties アノテーションを追加する必要がある。
    • 厳密には、 @Configuration でアノテートされたクラスに追加する。

Yaml を使用する

設定ファイルを application.yaml にすれば、 Yaml が使用できる。

application.yaml
aaa:
    bbb:
        ccc: Hoge
        ddd: Fuga
    eee:
        fff: Piyo
Main.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    @Value("${aaa.bbb.ccc}") private String ccc;
    @Value("${aaa.bbb.ddd}") private String ddd;
    @Value("${aaa.eee.fff}") private String fff;

    public void hello() {
        System.out.println("ccc=" + ccc);
        System.out.println("ddd=" + ddd);
        System.out.println("fff=" + fff);
    }
}
実行結果
ccc=Hoge
ddd=Fuga
fff=Piyo

リストをマッピングする

application.yaml
myconf:
    list:
        - hoge
        - fuga
        - piyo
MyConf.java
package sample.springboot;

import java.util.List;

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

@Component
@ConfigurationProperties(prefix="myconf")
public class MyConfig {

    private List<String> list;

    public List<String> getList() {
        return list;
    }

    public void setList(List<String> list) {
        this.list = list;
    }
}
Main.java
package sample.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
@EnableConfigurationProperties
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            MyConfig conf = ctx.getBean(MyConfig.class);
            System.out.println(conf.getList());
        }
    }
}
実行結果
[hoge, fuga, piyo]
  • Bean へのマッピングを利用すれば、 List へのマッピングも可能になる。

プロパティファイル以外から設定値を渡す

Main.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    @Value("${value}") private String value;

    public void hello() {
        System.out.println("value=" + value);
    }
}

コマンドライン引数

$ java -jar build/libs/spring-boot-sample.jar --value=commandline

value=commandline
  • --<プロパティ名>=<値> で、コマンドライン引数から設定値を渡せる。

Java のシステムプロパティ

$ java -Dvalue=systemproperty -jar build/libs/spring-boot-sample.jar

value=systemproperty
  • --D<プロパティ名>=<値> で、システムプロパティから設定値を渡せる。

OS の環境変数

$ set value=osenvironment

$ java -jar build/libs/spring-boot-sample.jar

value=osenvironment
  • ※OS は Windows です。

デフォルトプロパティ

Main.java
package sample.springboot;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        Map<String, Object> properties = new HashMap<>();
        properties.put("value", "default property");

        SpringApplication app = new SpringApplication(Main.class);
        app.setDefaultProperties(properties);

        try (ConfigurableApplicationContext ctx = app.run(args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    @Value("${value}") private String value;

    public void hello() {
        System.out.println("value=" + value);
    }
}
実行結果
value=default property
  • SpringApplication#setDefaultProperties(Map<String, Object>) で、デフォルトの設定を指定できる。

優先順位

プロパティファイルの場合と同じように、設定値の渡し方には優先順位があり、優先度が上位の方法が、下位の方法で指定された設定値を上書きする。

優先順位は以下のようになっている。

  1. コマンドライン引数
  2. JNDI の java:comp/env から取得した属性
  3. システムプロパティ
  4. OS の環境変数
  5. jar の外にあるプロファイル指定されたプロパティファイル
  6. jar の中にあるプロファイル指定されたプロパティファイル
  7. jar の外にあるプロパティファイル
  8. jar の中にあるプロパティファイル
  9. @PropertySource で指定されたプロパティファイル
  10. デフォルトプロパティ

数字が小さいほうが、優先度が高い。

メール送信

Gmail を使ってメールを送信してみる。

アプリパスワードの生成

2段階認証を有効にしている場合は、先にアプリパスワードを生成しておく必要がある。

アプリパスワードの生成

依存 jar を取得する

Java MailJavaBeans Activation Framework をダウンロードしてくる。

依存関係の追加

build.gradle
dependencies {
    compile 'org.springframework.boot:spring-boot-starter'
+   compile 'org.springframework.boot:spring-boot-starter-mail'
+   compile fileTree(dir: 'libs', include: '*.jar')
}
フォルダ構成
|-build.gradle
|-libs/
|  |-activation.jar
|  `-javax.mail.jar
`-src/

実装

application.properties
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=<Gmail のアドレス>
spring.mail.password=<パスワード※>
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

※2段階認証を有効にしている場合は「アプリパスワード」を、そうでない場合は普通のログインパスワードを設定する

Main.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            ctx.getBean(Main.class).sendMail();
        }
    }

    @Autowired
    private MailSender sender;

    public void sendMail() {
        SimpleMailMessage msg = new SimpleMailMessage();

        msg.setFrom("test@mail.com");
        msg.setTo("宛先メールアドレス");
        msg.setSubject("Send mail from Spring Boot");
        msg.setText("Spring Boot からメールを送信するよ!");

        this.sender.send(msg);
    }
}

受信結果

spring-boot.JPG

ロギング

ロギングには、 Commons Logging, Log4j, Slf4j, Logback などなど色々使えるようになっているっぽい。

Main.java
package sample.springboot;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
@EnableConfigurationProperties
public class Main {

    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        try (ConfigurableApplicationContext ctx = SpringApplication.run(Main.class, args)) {
            logger.error("error log");
            logger.warn("warn log");
            logger.info("info log");
            logger.debug("debug log");
            logger.trace("trace log");
        }
    }
}
実行結果
2015-04-29 17:31:25.023 ERROR 8872 --- [           main] sample.springboot.Main                   : error log
2015-04-29 17:31:25.023  WARN 8872 --- [           main] sample.springboot.Main                   : warn log
2015-04-29 17:31:25.023  INFO 8872 --- [           main] sample.springboot.Main                   : info log
  • デフォルトでは、 INFO レベル以上だけが出力される。
  • フォーマットは、日付 エラーレベル PID --- [スレッド名] ロガー名 : ログメッセージ

ファイルに出力する

デフォルトでは標準出力にしかログが出力されないが、ログファイルを指定すればファイルにも出力されるようになる。
ログファイルは、 10MB ずつローテーションされる。

ファイル名指定

$ java -jar build/libs/spring-boot-sample.jar --logging.file=sample.log

$ dir /b *.log
sample.log
  • logging.file で、出力するファイルの名前を指定できる。
  • ファイルの出力先にディレクトリが存在しない場合は、勝手に作成される。

フォルダ指定

$ java -jar build/libs/spring-boot-sample.jar --logging.path=logs

$ dir /b logs
spring.log
  • logging.path で、ログファイルの出力先を指定できる。
  • ログファイルの名前は、 spring.log になる。
  • ディレクトリが存在しない場合は勝手に作成される。

ロガーごとにログレベルを指定する

$ java -jar build/libs/spring-boot-sample.jar --logging.level.sample.springboot.Main=TRACE

2015-04-29 18:14:17.969 ERROR 8288 --- [           main] sample.springboot.Main                   : error log
2015-04-29 18:14:17.970  WARN 8288 --- [           main] sample.springboot.Main                   : warn log
2015-04-29 18:14:17.970  INFO 8288 --- [           main] sample.springboot.Main                   : info log
2015-04-29 18:14:17.970 DEBUG 8288 --- [           main] sample.springboot.Main                   : debug log
2015-04-29 18:14:17.970 TRACE 8288 --- [           main] sample.springboot.Main                   : trace log
  • logging.level.<ロガー>=<ログレベル> で、ロガーごとのログレベルを指定できる。
  • ロガー名が FQCN になるようにしているなら、 --logging.level.sample.springboot=DEBUG のようにしてパッケージ単位での指定もできる。

エンドポイント

spring-boot-starter-actuator を依存関係に追加すると、システムの状態を Web API で取得できるようになる。

build.gradle
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.boot:spring-boot-starter-actuator'
}
Main.java
package sample.springboot;

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

@SpringBootApplication
public class Main {

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

アプリケーションを起動して、いくつかの URL にアクセスしてみる。

$ curl http://localhost:8080/health
{"status":"UP"}

$ curl http://localhost:8080/metrics
{"mem":253440,"mem.free":127785,"processors":8,"instance.uptime":51033,"uptime":53546,"systemload.average":-1.0,"heap.committed":253440,"heap.init":262144,"heap.used":125654,"heap":3717632,"threads.peak":16,"threads.daemon":14,"threads":16,"classes":5490,"classes.loaded":5490,"classes.unloaded":0,"gc.ps_scavenge.count":3,"gc.ps_scavenge.time":39,"gc.ps_marksweep.count":1,"gc.ps_marksweep.time":44,"httpsessions.max":-1,"httpsessions.active":0,"counter.status.200.health":1,"counter.status.200.metrics":1,"gauge.response.health":47.0,"gauge.response.metrics":23.0}

以下が、エンドポイントの一例。

id 説明
dump スレッドダンプ
env システムプロパティ・環境変数・プロパティファイルの設定値など
health アプリケーションの状態
metrics メモリ使用率やスレッド数、 GC 回数などなど
trace 最近のアクセス履歴
shutdown POST メソッドでアクセスすることで、アプリケーションを停止できる。デフォルトは無効。

他にも いろいろある

その他

起動時のバナーを表示させないようにする

1.3.2 で確認

Main.java
package sample.springboot;

import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Main.class);
        app.setBannerMode(Banner.Mode.OFF);

        try (ConfigurableApplicationContext ctx = app.run(args)) {
            Main m = ctx.getBean(Main.class);
            m.hello();
        }
    }

    public void hello() {
        System.out.println("Hello Spring Boot!!");
    }
}
  • SpringApplication#setBannerMode(Banner.Mode)Banner.Mode.OFF を設定すると、バナー表示がなくなる。

war で出力する

war で出力して、 Tomcat などの既存の AP サーバーにデプロイできるようにする。

実装

build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE'
    }
}

apply plugin: 'war'
apply plugin: 'spring-boot'

sourceCompatibility = '1.8'
targetCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
    providedCompile 'org.springframework.boot:spring-boot-starter-tomcat'
}

war {
    baseName = 'spring-boot-war'
}
  • war プラグインを読み込む。
  • デフォルトで組み込みサーバーとして使用している Tomcat の依存関係を providedCompile に変更。
Main.java
package sample.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

@SpringBootApplication
public class Main extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Main.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
  • main メソッドを定義していたクラスは、以下のように修正する。
    • SpringBootServletInitializer を継承する。
    • configure(SpringApplicationBuilder) をオーバーライドする。
SampleResource.java
package sample.springboot;

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

@RestController
@RequestMapping("/sample")
public class SampleResource {

    @RequestMapping(method=RequestMethod.GET)
    public String hello() {
        return "Hello Spring Boot!!";
    }
}
  • 動作確認用のリソースクラス。

動作確認

warをビルドする
$ gradle war

build/libs の下に spring-boot-war.jar が出力されるので、 Tomcat にデプロイする。

curlで動作確認
$ curl http://localhost:8080/spring-boot-war/sample
Hello Spring Boot!!

割と簡単に war 化できた。

参考

限定子

Boot というよりかは、 Spring 自体の使い方。
Spring にも CDI の限定子と同じようなものが用意されている。

名前で指定する

実装

MyInterface.java
package sample.springboot;

public interface MyInterface {
}
Hoge.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
@Qualifier("hoge")
public class Hoge implements MyInterface {
}
Fuga.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
@Qualifier("fuga")
public class Fuga implements MyInterface {
}
Main.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext context = SpringApplication.run(Main.class, args)) {
            Main m = context.getBean(Main.class);
            System.out.println("hoge = " + m.hoge.getClass());
            System.out.println("fuga = " + m.fuga.getClass());
        }
    }

    @Autowired @Qualifier("hoge")
    private MyInterface hoge;

    @Autowired @Qualifier("fuga")
    private MyInterface fuga;
}

動作確認

コンソール出力
hoge = class sample.springboot.Hoge
fuga = class sample.springboot.Fuga

説明

  • @Qualifier アノテーションを使って、ビーンの名前を指定できる。
  • @Autowired と合わせて @Qualifier で名前を指定することで、特定のビーンをインジェクションできる。
  • CDI でいうと @Named 的な使い方になる。

限定子を自作する

実装

MyQualifier.java
package sample.springboot;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.beans.factory.annotation.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface MyQualifier {
    MyType value();
}
MyType.java
package sample.springboot;

public enum MyType {
    HOGE,
    FUGA,
}
Hoge.java
package sample.springboot;

import org.springframework.stereotype.Component;

@Component
@MyQualifier(MyType.HOGE)
public class Hoge implements MyInterface {
}
Fuga.java
package sample.springboot;

import org.springframework.stereotype.Component;

@Component
@MyQualifier(MyType.FUGA)
public class Fuga implements MyInterface {
}
Main.java
package sample.springboot;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Main {

    public static void main(String[] args) {
        try (ConfigurableApplicationContext context = SpringApplication.run(Main.class, args)) {
            Main m = context.getBean(Main.class);
            System.out.println("hoge = " + m.hoge.getClass());
            System.out.println("fuga = " + m.fuga.getClass());
        }
    }

    @Autowired @MyQualifier(MyType.HOGE)
    private MyInterface hoge;

    @Autowired @MyQualifier(MyType.FUGA)
    private MyInterface fuga;
}

動作確認

コンソール出力
hoge = class sample.springboot.Hoge
fuga = class sample.springboot.Fuga

説明

  • @Qualifier で自作のアノテーションをアノテートすることで、限定子を自作できる。
  • CDI のカスタム限定子と同じ要領で使えるっぽい。

参考