grails 3.0先取り!? spring boot入門ハンズオン #jggug_boot

157
Grails 3.0 先取り !? Spring Boot 入門ハンズオン 2014-08-01 日本 Grails/Groovy ユーザーグループ G* ワークショップ "Z" 14 俊明 (@making)

Upload: makingx

Post on 08-Sep-2014

19 views

Category:

Technology


1 download

DESCRIPTION

Spring Bootのハンズオン資料です。 ---- Grailsの次期バージョン3.0でベースになることが予定されている、Spring界隈の新しいトレンド"Spring Boot"のハンズオンを通じて、Spring Bootのイメージを掴んでもらいたいと思います。内容は以下の通りです。 Spring Boot概要説明 Spring Bootを用いて簡単なアプリケーションを実際に作ってみる (合計で約二時間弱)

TRANSCRIPT

Grails 3.0先取り!? Spring Boot入門ハンズオン

2014-08-01 日本Grails/Groovyユーザーグループ G*ワークショップ"Z" 第14弾

槙 俊明(@making)

最初にことわっておきますが

Groovy/Grails/Gradle

使いません(キリ)

Java/Mavenで許して😜

※ GroovyやGradleを使っても勧められますが、 自己責任でお願いします。

自己紹介• @making

• http://blog.ik.am

• 日本Javaユーザーグループ(JJUG)幹事

• Groovy書けません

• Spring Boot本書いています

http://amzn.to/hajiboo

今日のハッシュタグ

#jggug_boot

今日のコンテンツ

• Spring Boot超概要(5分)

• Spring Bootハンズオン(100分)

•まとめ(5分)

Spring Boot超概要

Spring Bootとは?

•簡単にいうと、「Spring Boot」とは「Spring Framework」でアプリケーションを簡単に作るた めの仕組み

Grails3のベースになるらしい

Spring Boot以前のSpring Framework

Spring Boot以前のSpring Framework

色々ありすぎて、 組み合わせがわからない! 初期セットアップが大変!

Spring Boot•あらかじめオススメの組み合わせが決まっている

•依存ライブラリを同梱するだけで自動で設定がきまる

•組み込みサーバーを同梱し、アプリを即実行可能

Spring Bootで何が変わる?

•アプリの設定が変わる

•アプリのデプロイが変わる

Spring Bootで何が変わる?

•アプリの設定が変わる

•アプリのデプロイが変わる

ほとんど設定不要!

Spring Bootで何が変わる?

•アプリの設定が変わる

•アプリのデプロイが変わる

ほとんど設定不要!

jarを実行するだけ!

Spring Bootに関する詳しい話はまた今度!

• 2014-08-14に日本Springユーザー会 勉強会でSpring Bootについて話します。

• https://atnd.org/events/53770 (今から登録は厳しいかも・・・)

体験してみましょう

Spring Bootハンズオン

ハンズオンの流れ

1.Hello WorldアプリでSpring Bootことはじめ

2.Spring BootでREST APIを作ろう

3.Spring Bootで画面のあるアプリを作ろう

4.Spring Securityで認証認可を追加しよう

ハンズオンの流れ

1.Hello WorldアプリでSpring Bootことはじめ

2.Spring BootでREST APIを作ろう

3.Spring Bootで画面のあるアプリを作ろう

4.Spring Securityで認証認可を追加しよう

100分だと多分ここまで

ハンズオンの流れ

1.Hello WorldアプリでSpring Bootことはじめ

2.Spring BootでREST APIを作ろう

3.Spring Bootで画面のあるアプリを作ろう

4.Spring Securityで認証認可を追加しよう

100分だと多分ここまで 土日にやろう

質問があれば

•#jggug_boot でつぶいやいてくればいつか回答します

JDK 8のインストール• http://www.oracle.com/technetwork/java/javase/

downloads/jdk8-downloads-2133151.html

• JAVA_HOMEを設定してね!

Spring Tool Suite(STS) のインストール

• http://spring.io/tools

Mavenのインストール• http://maven.apache.org/

• ダウンロードして、PATHに追加

curlのインストール• http://curl.haxx.se/

• Windowsの人はダウンロードして、PATHに追加

本ハンズオン扱う技術

Webブラウザ

curl

TomcatSpring Boot

Spring FrameworkSpring Security

ThymeLeaf

Spring MVC

Jackson

Spring Data JPA

Hibernate

H2 Database画面のあるアプリ

REST API

題材のアプリ•簡易ブックマークシステム

1. Hello WorldアプリでSpring Bootことはじめ

Mavenアーキタイプでプロジェクト雛形生成

$ mvn -B archetype:generate -DgroupId=com.example -DartifactId=jggug-helloworld -Dversion=1.0.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-quickstart

Spring Bootに関係のない汎用的な手順

http://bit.ly/jggug-01-00

pom.xmlを編集

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.1.4.RELEASE</version></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies><build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins></build><properties> <java.version>1.8</java.version></properties>

この設定を追加

http://bit.ly/jggug-01-01

Mavenプロジェクトをインポート

インポート後

いろいろな依存関係が追加されている

package com.example;!import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;!@RestController@EnableAutoConfigurationpublic class App {! @RequestMapping("/") String home() { return "Hello World!"; }! public static void main(String[] args) { SpringApplication.run(App.class, args); }}

App.javaの編集http://bit.ly/jggug-01-02

package com.example;!import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;!@RestController@EnableAutoConfigurationpublic class App {! @RequestMapping("/") String home() { return "Hello World!"; }! public static void main(String[] args) { SpringApplication.run(App.class, args); }}

App.javaの編集http://bit.ly/jggug-01-02

魔法のアノテーション

まずは実行•実行方法は2通り

または

$ mvn spring-boot:run

ログ

組込Tomcatが起動した

http://localhost:8080 にアクセス

実行可能jarを作成

$ mvn package

jarを実行

$ java -jar target/jggug-helloworld-1.0.0-SNAPSHOT.jar

プロパティを変更して実行

$ java -jar target/jggug-helloworld-1.0.0-SNAPSHOT.jar --server.port=8888

--(プロパティ名)=(プロパティ値)

予め用意されている沢山のプロパティを変更可能

• http://docs.spring.io/spring-boot/docs/1.1.4.RELEASE/reference/html/common-application-properties.html

一度作ったjarはそのまま本番環境で使用可能。配布も可能。

• -Drun.arguments="--(プロパティ名)=(プロパティ値)"で指定。

[補足] mavenプラグインの場合

$ mvn spring-boot:run -Drun.arguments="--server.port=8888"

http://bit.ly/jggug-01-03

[参考] Spring LoadedでHot Reload<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> <version>1.2.0.RELEASE</version> </dependency> </dependencies></plugin>

maven pluginに追加

http://bit.ly/jggug-01-04

[参考] Spring LoadedでHot Reload

$ mvn spring-boot:run

[INFO] Attaching agents: [/Users/****/.m2/repository/org/springframework/springloaded/1.2.0.RELEASE/springloaded-1.2.0.RELEASE.jar]objc[11505]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_11.jdk/Contents/Home/jre/bin/java and /Library/Java/JavaVirtualMachines/jdk1.8.0_11.jdk/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be used. Which one is undefined.! . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.1.4.RELEASE)

Hot Reload用のagentが自動的にアタッチされる

アプリ実行中にソースを変更してコンパイル

@RequestMapping("/")String home() { return "Hello World!";}

@RequestMapping("/")String home() { return "Hello Spring!";} 再起動することなく

アプリが更新された

注意•まだまだSpring Loadedは未熟で、リロードされないケースや副作用によるエラーが発生することもある。

•うまく更新できたらラッキー。

•開発を暖かく見守りましょう。

Integration Test@RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(classes = App.class)@WebAppConfiguration@IntegrationTest("server.port:0")public class AppTest { @Value("${local.server.port}") int port;! RestTemplate restTemplate = new TestRestTemplate();! @Test public void testHome() { ResponseEntity<String> response = restTemplate.getForEntity( "http://localhost:" + port, String.class); assertThat(response.getStatusCode(), is(HttpStatus.OK)); assertThat(response.getBody(), is("Hello World!")); }}

空いているポートを使用

実際に使ったポート番号

テスト用HTTPクライアント

エントリポイントのクラス指定

http://bit.ly/jggug-01-05

Integration Test

組込Tomcatを起動して、HTTPリクエストを送り、

HTTPレスポンスをチェック

他のJVM言語の例• https://github.com/making/spring-boot-demo-jvm-

languages

• Java/Groovy/Scala/KotlinそれぞれのGradleプロジェクトサンプルがあるのでお好みの言語でSpring Bootアプリを作りましょう

2. Spring BootでREST APIを作ろう

Webブラウザ

curl

TomcatSpring Boot

Spring FrameworkSpring Security

ThymeLeaf

Spring MVC

Jackson

Spring Data JPA

Hibernate

H2 Database画面のあるアプリ

REST API

REST APIでBookmark管理• 「REST」はクライアントとサーバ間でデータをやりとりするためのソフトウェアアーキテクチャスタイルの一つ。

• RESTでは、「リソース」に対するCRUD操作をHTTPメソッド(POST/GET/PUT/DELETEなど)を使ってWeb APIとしてクライアントに公開する。

• 今回は「ブックマーク」が「リソース」

実装するAPI

API HTTP メソッド リソースパス 正常時レスポンス

ステータス

ブックマーク全件取得 GET /api/bookmarks 200 OK

ブックマーク新規登録

POST! /api/bookmarks 201 CREATED

ブックマーク一件削除 DELETE /api/bookmarks/{id} 204 NO CONTENT

Mavenアーキタイプでプロジェクト雛形生成

$ mvn -B archetype:generate -DgroupId=com.example -DartifactId=bookmark -Dversion=1.0.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-quickstart$ mkdir bookmark/src/main/resources

artifactId以外helloworldのときと同じ

src/main/resourcesを作成しておく

http://bit.ly/jggug-02-00

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.1.4.RELEASE</version></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies><build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> <version>1.2.0.RELEASE</version> </dependency> </dependencies> </plugin> </plugins></build><properties> <java.version>1.8</java.version></properties>

pom修正JPAを使用したい場合は、

spring-boot-starter-data-jpaを追加するだけ。

JDBCドライバも必要

http://bit.ly/jggug-02-01

必要な依存関係が追加される

Mavenプロジェクトのインポート

出来上がりイメージ

レイヤー化アプリケーション

DI Container

Controller Service Repositoryuse use

inject inject

レイヤー化アプリケーション

DI Container

Controller Service Repositoryuse use

inject inject

この順で実装する

ドメインオブジェクト作成

• ブックマークサービスに必要な情報

• ID

• ブックマーク名

• URL

ドメインオブジェクト作成• com.example.domain.Bookmark

@Entitypublic class Bookmark { @Id @GeneratedValue private Long id; @NotNull @Size(min = 1, max = 255) private String name; @NotNull @Size(min = 1, max = 255) @URL private String url; // omitted setter & getter}

JPAのエンティティとして アノテーションをつける

デフォルトでは インメモリ組み込みDBが使用され、DDLも自動生成&実行

http://bit.ly/jggug-02-02

リポジトリ作成

• 「リポジトリ」は、 ドメインオブジェクトの保存、取得、検索といった操作をカプセル化し、”コレクションオブジェクト”のように振る舞う役割をもつ。

• 「リポジトリ」にロジックを含めない。

Spring Data JPAでリポジトリ作成• com.example.repository.BookmarkRepository

package com.example.repository;!import com.example.domain.Bookmark;import org.springframework.data.jpa.repository.JpaRepository;!public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {!} エンティティクラス、主キークラス

たったこれだけでJPAのEntityManagerを使用した 基本的なDBのCRUD操作を利用できる。(SQL不要)

http://bit.ly/jggug-02-03

サービス作成• 全件取得、新規作成、一件削除用のメソッドを作成する

• POJOに@Serviceアノテーションを付けるとDIコンテナに自動登録される(コンポーネントスキャン)

サービス作成• com.example.service.BookmarkService

@Service@Transactionalpublic class BookmarkService { @Autowired BookmarkRepository bookmarkRepository;! public List<Bookmark> findAll() { return bookmarkRepository.findAll(new Sort(Sort.Direction.ASC, "id")); }! public Bookmark save(Bookmark bookmark) { return bookmarkRepository.save(bookmark); }! public void delete(Long id) { bookmarkRepository.delete(id); }}

リポジトリをインジェクション

宣言的トランザクション管理

IDで昇順に検索

http://bit.ly/jggug-02-04

コントローラー作成•基本的にSpring MVCを使ったプログラミングを行う

• POJOに@Controllerを付けるとHTTPのリクエストを受けられる

•@RestControllerを付けると、Controllerのメソッドの返り値が、シリアライズされ、そのままHTTPレスポンスのボディになる

コントローラーの リクエストマッピング

• HTTPリクエストとコントローラーのメソッドのマッピング表

APIHTTP メソッド

リソースパス メソッド 返り値の型

ブックマーク全件取得 GET /api/bookmarks getBookmarks List<Bookmark>

ブックマーク新規登録

POST! /api/bookmarks postBookmarks Bookmark

ブックマーク一件削除

DELETE

/api/bookmarks/{id} deleteBookmark void

コントローラー作成• com.example.api.BookmarkRestController

@RestController@RequestMapping("api/bookmarks")public class BookmarkRestController { @Autowired BookmarkService bookmarkService;! @RequestMapping(method = RequestMethod.GET) List<Bookmark> getBookmarks() { return bookmarkService.findAll(); } @RequestMapping(method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) Bookmark postBookmarks(@RequestBody Bookmark bookmark) { return bookmarkService.save(bookmark); } @RequestMapping(value = "{id}", method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) void deleteBookmarks(@PathVariable("id") Long id) { bookmarkService.delete(id); }}

サービスをインジェクション

パスやHTTPメソッド等の組み合わせとコントローラーのメソッ

ドを結びつける

リクエストボディをJavaBeanにマッピング

プレースホルダの値を取得

http://bit.ly/jggug-02-05

入力チェックを実施@RestController@RequestMapping("api/bookmarks")public class BookmarkRestController { @Autowired BookmarkService bookmarkService;! @RequestMapping(method = RequestMethod.GET) List<Bookmark> getBookmarks() { return bookmarkService.findAll(); } @RequestMapping(method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) Bookmark postBookmarks(@Validated @RequestBody Bookmark bookmark) { return bookmarkService.save(bookmark); } @RequestMapping(value = "{id}", method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) void deleteBookmarks(@PathVariable("id") Long id) { bookmarkService.delete(id); }}

詳細は割愛・・・参照URLを後述

http://bit.ly/jggug-02-06

アプリケーションのエントリポイント作成

package com.example;!import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.context.annotation.ComponentScan;!@EnableAutoConfiguration@ComponentScanpublic class App {! public static void main(String[] args) { SpringApplication.run(App.class, args); }}

http://bit.ly/jggug-02-07

アプリケーション実行• リクエストマッピングのログが出力されることを確認s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/

bookmarks],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto java.util.List<com.example.domain.Bookmark> com.example.api.BookmarkRestController.getBookmarks()s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/bookmarks],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto com.example.domain.Bookmark com.example.api.BookmarkRestController.postBookmarks(com.example.domain.Bookmark)s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/bookmarks/{id}],methods=[DELETE],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto void com.example.api.BookmarkRestController.deleteBookmarks(java.lang.Long)

APIチェック• ブックマーク新規作成

$ curl http://localhost:8080/api/bookmarks -v -X POST -H 'Content-Type:application/json' -d '{"name":"Google", "url":"http://google.com"}'

http://bit.ly/jggug-02-08Windowsのコマンドプロンプトだとシングルクオートが効か

ないのでダブルクオートと\エスケープしてください

APIチェック• ブックマーク新規作成

> POST /api/bookmarks HTTP/1.1> User-Agent: curl/7.30.0> Host: localhost:8080> Accept: */*> Content-Type:application/json> Content-Length: 44>< HTTP/1.1 201 Created< Server: Apache-Coyote/1.1< Content-Type: application/json;charset=UTF-8< Transfer-Encoding: chunked< Date: Sat, 26 Jul 2014 17:44:11 GMT<{"id":1,"name":"Google","url":"http://google.com"}

APIチェック• ブックマーク全件取得

$ curl http://localhost:8080/api/bookmarks -v -X GET

http://bit.ly/jggug-02-09

APIチェック• ブックマーク全件取得

> GET /api/bookmarks HTTP/1.1> User-Agent: curl/7.30.0> Host: localhost:8080> Accept: */*>< HTTP/1.1 200 OK< Server: Apache-Coyote/1.1< Content-Type: application/json;charset=UTF-8< Transfer-Encoding: chunked< Date: Sat, 26 Jul 2014 17:55:48 GMT<[{"id":1,"name":"Google","url":"http://google.com"}]

APIチェック• ブックマーク1件削除

$ curl http://localhost:8080/api/bookmarks/1 -v -X DELETE

http://bit.ly/jggug-02-10

APIチェック• ブックマーク1件削除

> DELETE /api/bookmarks/1 HTTP/1.1> User-Agent: curl/7.30.0> Host: localhost:8080> Accept: */*>< HTTP/1.1 204 No Content< Server: Apache-Coyote/1.1< Date: Sat, 26 Jul 2014 17:58:02 GMT<

課題1• ブックマーク1件取得APIを実装してみよう

APIHTTP メソッド

リソースパス メソッド 返り値の型

ブックマーク一件取得 GET /api/

bookmarks/{id} getBookmark Bookmark

アプリケーションをカスタマイズしよう

• 設定ファイルの設定方法

• JavaConfigでBean定義を行う方法

• ログの設定変更方法

これらを試していきます

JDBCドライバの設定値を変更• インメモリH2からファイルベースH2へ

• 設定ファイルはクラスパス直下のapplication.ymlまたはapplication.properties

YAMLが便利

application.yml作成

spring: datasource: driverClassName: org.h2.Driver url: jdbc:h2:file:/tmp/bookmark username: sa password: jpa: hibernate: ddl-auto: update

インメモリDB使用時はcreate-dropが指定されており、毎回破棄・生成が行われていた。今回はupdateを指定し、差分があれば適用する方式に。

DBの実体のファイルパスを指定する。なかったら作成される。

src/main/resources/application.yml

http://bit.ly/jggug-02-11

[補足] 設定値一覧(再掲) • http://docs.spring.io/spring-boot/docs/

1.1.4.RELEASE/reference/html/common-application-properties.html

Log4JDBCでSQLログを出力しよう

• pom.xmlに以下を追加

<dependency> <groupId>org.lazyluke</groupId> <artifactId>log4jdbc-remix</artifactId> <version>0.2.7</version></dependency>

http://bit.ly/jggug-02-12

• Bean定義を行うJavaConfigクラスを作成

Log4JDBCでSQLログを出力しよう

package com.example;!import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;!@Configurationpublic class AppConfig {! @Bean SomeBean someBean() { returen new SomeBean(); }}

JavaConfigの記法

JavaConfig宣言

Bean定義宣言

※この部分は書かなくて良い

com.example.AppConfig

Log4JDBCでSQLログを出力しよう@Configurationpublic class AppConfig { @Autowired DataSourceProperties properties; DataSource dataSource;! @ConfigurationProperties(prefix = DataSourceAutoConfiguration.CONFIGURATION_PREFIX) @Bean(destroyMethod = "close") DataSource realDataSource() { DataSourceBuilder factory = DataSourceBuilder .create(this.properties.getClassLoader()) .url(this.properties.getUrl()) .username(this.properties.getUsername()) .password(this.properties.getPassword()); this.dataSource = factory.build(); return this.dataSource; } @Bean DataSource dataSource() { return new Log4jdbcProxyDataSource(this.dataSource); }}

Spring Bootが内部で行っている、DataSourceの作成方法。 難しい場合は気にしなくて良い。

作成しDataSourceにログ出力処理をラップする。こちらを使う。

http://bit.ly/jggug-02-13

このメソッド名(=Bean名)が重要

• src/main/resources/logback.xmlを作成

<?xml version="1.0" encoding="UTF-8"?><configuration> <include resource="org/springframework/boot/logging/logback/base.xml" /> <logger name="jdbc" level="OFF" /> <logger name="jdbc.sqltiming" level="DEBUG" /></configuration>

Log4JDBCでSQLログを出力しよう

http://bit.ly/jggug-02-14

• アプリケーションを再起動して各APIを実行

Log4JDBCでSQLログを出力しよう

jdbc.sqltiming : org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:187)3. insert into bookmark (id, name, url) values (null, 'Google', 'http://google.com') {executed in 3 msec}jdbc.sqltiming : org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:80)4. select bookmark0_.id as id1_0_, bookmark0_.name as name2_0_, bookmark0_.url as url3_0_ from bookmark bookmark0_ order by bookmark0_.id asc {executed in 0 msec}jdbc.sqltiming : org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:80)5. select bookmark0_.id as id1_0_0_, bookmark0_.name as name2_0_0_, bookmark0_.url as url3_0_0_ from bookmark bookmark0_ where bookmark0_.id=1 {executed in 0 msec}jdbc.sqltiming : org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:187)5. delete from bookmark where id=1 {executed in 2 msec}

課題2• bookmarkアプリケーションのjarを作成し、実行時にspring.datasource.*プロパティを変更して、接続先DBを変更しよう(MySQLやPostgreSQLで試してみると面白い)。

REST APIを任意のクライアントからアクセスできるようにする

• Angular.jsで作成したSingle Page Applicationからアクセスしてみよう

• http://jsfiddle.net/Ca2g2/

作っておきました

REST APIを任意のクライアントからアクセスできるようにする

REST APIを任意のクライアントからアクセスできるようにする

XMLHttpRequest cannot load http://localhost:8080/api/bookmarks. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://fiddle.jshell.net' is therefore not allowed access.

Same Origin Policy制限!

REST APIを任意のクライアントからアクセスできるようにする

• Cross-Origin Resource Sharing (CORS) の設定を行うServletFilterを作成

• http://spring.io/guides/gs/rest-service-cors/

• Spring BootではServlet FilterはDIコンテナに登録しておけば自動的に有効になる。

REST APIを任意のクライアントからアクセスできるようにする

• AppConfigに以下のBean定義を追加@BeanFilter corsFilter() { return new Filter() { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; String method = request.getMethod(); response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE"); response.setHeader("Access-Control-Max-Age", Long.toString(60 * 60)); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader( "Access-Control-Allow-Headers", "Origin,Accept,X-Requested-With," + "Content-Type,Access-Control-Request-Method," + "Access-Control-Request-Headers,Authorization"); if ("OPTIONS".equals(method)) { response.setStatus(HttpStatus.OK.value()); } else { chain.doFilter(req, res); } } public void init(FilterConfig filterConfig) { } public void destroy() { } };}

別クラスにして@Componentを付ければ定義は不要

FilterRegistrationBeanを使えばurl-pattern等の指定もできる。

http://bit.ly/jggug-02-15

• 再起動後に、再アクセス

REST APIを任意のクライアントからアクセスできるようにする

REST APIのIntegrationTest

@RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(classes = App.class)@WebAppConfiguration@IntegrationTest({ "server.port:0", "spring.datasource.url:jdbc:h2:mem:bookmark;DB_CLOSE_ON_EXIT=FALSE" })public class BookmarkRestControllerIntegrationTest { // write test code}

test用にインメモリDBを使用

http://bit.ly/jggug-02-16

テストの初期化@AutowiredBookmarkRepository bookmarkRepository;@Value("${local.server.port}")int port;String apiEndpoint;RestTemplate restTemplate = new TestRestTemplate();Bookmark springIO;Bookmark springBoot;!@Beforepublic void setUp() { bookmarkRepository.deleteAll(); springIO = new Bookmark(); springIO.setName("Spring IO"); springIO.setUrl("http://spring.io"); springBoot = new Bookmark(); springBoot.setName("Spring Boot"); springBoot.setUrl("http://projects.spring.io/spring-boot");! bookmarkRepository.save(Arrays.asList(springIO, springBoot)); apiEndpoint = "http://localhost:" + port + "/api/bookmarks";}// write test case

リポジトリを使って、データ削除&登録。 テストの順番は不定なので、毎回初期化すべき。

http://bit.ly/jggug-02-17

全件取得APIのテスト@Testpublic void testGetBookmarks() throws Exception { ResponseEntity<List<Bookmark>> response = restTemplate.exchange( apiEndpoint, HttpMethod.GET, null /* body,header */, new ParameterizedTypeReference<List<Bookmark>>() { }); assertThat(response.getStatusCode(), is(HttpStatus.OK)); assertThat(response.getBody().size(), is(2));! Bookmark bookmark1 = response.getBody().get(0); assertThat(bookmark1.getId(), is(springIO.getId())); assertThat(bookmark1.getName(), is(springIO.getName())); assertThat(bookmark1.getUrl(), is(springIO.getUrl()));! Bookmark bookmark2 = response.getBody().get(1); assertThat(bookmark2.getId(), is(springBoot.getId())); assertThat(bookmark2.getName(), is(springBoot.getName())); assertThat(bookmark2.getUrl(), is(springBoot.getUrl()));}

ちょっと面倒くさい・・・

http://bit.ly/jggug-02-18

新規作成APIのテスト@Testpublic void testPostBookmarks() throws Exception { Bookmark google = new Bookmark(); google.setName("Google"); google.setUrl("http://google.com");! ResponseEntity<Bookmark> response = restTemplate.exchange(apiEndpoint, HttpMethod.POST, new HttpEntity<>(google), Bookmark.class); assertThat(response.getStatusCode(), is(HttpStatus.CREATED)); Bookmark bookmark = response.getBody(); assertThat(bookmark.getId(), is(notNullValue())); assertThat(bookmark.getName(), is(google.getName())); assertThat(bookmark.getUrl(), is(google.getUrl()));! assertThat(restTemplate.exchange(apiEndpoint,HttpMethod.GET,null, new ParameterizedTypeReference<List<Bookmark>>() { }).getBody().size(), is(3));}

http://bit.ly/jggug-02-19

1件削除APIのテスト

@Testpublic void testDeleteBookmarks() throws Exception { ResponseEntity<Void> response = restTemplate.exchange(apiEndpoint + "/{id}", HttpMethod.DELETE, null /* body,header */, Void.class, Collections.singletonMap("id", springIO.getId())); assertThat(response.getStatusCode(), is(HttpStatus.NO_CONTENT));! assertThat(restTemplate.exchange(apiEndpoint, HttpMethod.GET, null, new ParameterizedTypeReference<List<Bookmark>>() { }).getBody().size(), is(1));}

http://bit.ly/jggug-02-20

REST編修了• お疲れ様でした・・・

• 本当は説明したかったけれども省略した内容

• 入力チェック

• 例外ハンドリング

• ページネーション

http://terasolunaorg.github.io/guideline/1.0.x/ja/ArchitectureInDetail/REST.html

ここが詳しい。 URL変わる可能性があるので注意。

3. Spring Bootで画面のあるアプリを作ろう

Webブラウザ

curl

TomcatSpring Boot

Spring FrameworkSpring Security

ThymeLeaf

Spring MVC

Jackson

Spring Data JPA

Hibernate

H2 Database画面のあるアプリ

REST API

DI Container

Controller Service Repositoryuse use

inject inject

ここだけ追加 REST編と同じ

出来上がりイメージ

追加するファイル

画面遷移

リダイレクト

入力エラー

画面遷移

API HTTP メソッド パス コントローラー

のメソッドVIEW

ブックマーク一覧表示 GET /bookmark list bookmark/list

ブックマーク新規登録

POST!

/bookmark/create create redirect:/

bookmark/list

ブックマーク一件削除 POST /bookmark/

delete?id={id} delete redirect:/bookmark/list

普通の画面遷移アプリであればREST風にする必要はない。

コントローラー作成• com.example.web.BookmarkController@Controller@RequestMapping("bookmark")public class BookmarkController { @Autowired BookmarkService bookmarkService;! @ModelAttribute Bookmark setUp() { Bookmark bookmark = new Bookmark(); return bookmark; } // 続く}

フォームオブジェクトの初期化。ここでは簡単のため

Bookmarkクラスを使用する。

※ 本当はドメインオブジェクトをフォームとして使わない方がよい。画面にドメインが汚染されないように。(BookmarkFormクラスを作ってコピー推奨)

普通の画面遷移には@Controllerアノテーションを使用。

http://bit.ly/jggug-02-21

ブックマーク一覧表示

@RequestMapping(value = "list", method = RequestMethod.GET)String list(Model model) { List<Bookmark> bookmarks = bookmarkService.findAll(); model.addAttribute("bookmarks", bookmarks); return "bookmark/list";}

Modelオブジェクトに追加することで画面(view)からアクセスできる。

View名を返す。Spring Bootではデフォルトで、クラスパス下の templates/bookmark/list.htmlがViewとして使用される。

bookmark/listをGETでアクセスすると呼ばれるメソッドhttp://bit.ly/jggug-02-22

ブックマーク新規登録

@RequestMapping(value = "create", method = RequestMethod.POST)String create(@Validated Bookmark bookmark, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { return list(model); } bookmarkService.save(bookmark); return "redirect:/bookmark/list";}

bookmark/createをPOSTでアクセスすると呼ばれるメソッド

フォームの入力チェック

入力エラーがある場合は、 一覧表示へ。

PRG(POST-Redirect-GET)パターンを用いる。 /bookmakr/listへリダイレクト。

http://bit.ly/jggug-02-23

ブックマーク1件削除

@RequestMapping(value = "delete", method = RequestMethod.POST)String delete(@RequestParam("id") Long id) { bookmarkService.delete(id); return "redirect:/bookmark/list";}

bookmark/deleteをPOSTでアクセスすると呼ばれるメソッド

クエリパラメータからidを取得する。

http://bit.ly/jggug-02-24

文字コード設定フィルターを定義• AppConfigにCharacterEncodingFilterの定義を追加。コレがないとPOSTで日本語が文字化けする。

@Bean@Order(Ordered.HIGHEST_PRECEDENCE)CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("UTF-8"); return filter;}

フィルターの先頭にくるように優先順位を設定

http://bit.ly/jggug-02-25

ThymeLeafで画面作成• ThymeLeafは素のHTMLにth:***属性(またはdata-th-***属性)をつけることで動的な画面を作れるテンプレートエンジン。

• http://www.thymeleaf.org/

• テンプレートをブラウザやオーサリングツールでそのまま見れるため、デザイナーフレンドリー。

依存関係追加<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>

http://bit.ly/jggug-02-26

HTML作成• src/main/resources/templates/bookmark/list.htmlを作成

[補足] ThymeLeafのキャッシュを無効化

• 開発中は変更を即反映してほしいため、spring.thymeleaf.cache: falseのプロパティを追加

spring: thymeleaf: cache: false

application.ymlhttp://bit.ly/jggug-02-27

list.html<!DOCTYPE html><html xmlns:th="http:///www.thymeleaf.org"><head><meta charset="UTF-8" /><title>Bookmarks</title></head><body> <div> <h1>Bookmarks</h1> <div> <!-- 新規作成フォームを書く --> </div> <div> <!-- 一覧表示テーブルを書く --> </div> </div></body></html>

ThymeLeafの名前空間

デフォルトでは、XHTMLでないとエラーになる。

タグの閉じ忘れに注意。

http://bit.ly/jggug-02-28

一覧表示画面

<ul> <li th:each="bookmark : ${bookmarks}"><a th:href="${bookmark.url}" th:text="${bookmark.name}">dummy</a> <form style="display: inline" method="post" th:action="@{/bookmark/delete?id=${bookmark.id}}"> <input type="submit" value="Remove" /> </form></li></ul>

繰り返し要素にth:each属性を設定。

そのままブラウザでみるとdummyが表示されるが、サーバー経由だとth:text属性に指定した値で置換される(HTMLエスケープ有)

URLを表示する際は@{}を使うことで、 コンテキストルート相対パスを指定できる。

Modelに設定した属性値に${…}でアクセス。

http://bit.ly/jggug-02-29

ブラウザでテンプレートを表示

サーバーで表示

REST APIで登録したデータが表示されているはず

新規作成フォーム<form th:action="@{/bookmark/create}" th:object="${bookmark}" method="post"> <dl> <dt><label for="name">Name</label></dt> <dd><input type="text" id="name" name="name" th:field="*{name}" th:errorclass="error-input" /> <span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="error-messages">error!</span></dd> </dl> <dl> <dt><label for="url">URL</label></dt> <dd><input type="url" id="url" name="url" th:field="*{url}" th:errorclass="error-input" /> <span th:if="${#fields.hasErrors('url')}" th:errors="*{url}" class="error-messages">error!</span></dd> </dl> <input type="submit" value="Add" /></form>

th:object属性にフォームオブジェクトを指定

th:field="{*フィールド名}"でバインドするフィールドを指定

th:errors="{*フィールド名}"でエラーメッセージを表示

http://bit.ly/jggug-02-30

静的リソースの配置• JavaScript、CSS、画像などの静的リソースはクラスパス直下のstaticフォルダに置くことで、コンテキストルートからアクセスできる。

CSS作成• src/main/resources/static/css/style.css

.error-input { border-color: #b94a48; margin-left: 5px;}!.error-messages { color: #b94a48;}

http://bit.ly/jggug-02-31

CSSの読み込み

<link rel="stylesheet" type="text/css" href="../../static/css/style.css" th:href="@{/css/style.css}" />

サーバーで実行した場合に th:href属性でhref属性を置換する

ブラウザで見るときはこちらの設定が有効になる

http://bit.ly/jggug-02-32

ブラウザでテンプレートを表示

サーバーで実行

サーバーで実行

[補足] WebJarを使ってみよう• TODO

課題3•新規作成画面と一覧画面を別のページにしてみよう。

画面のあるアプリ編修了•本当は説明したかったけれども省略した内容

•入力チェック

•http://terasolunaorg.github.io/guideline/1.0.x/ja/ArchitectureInDetail/Validation.html

•例外ハンドリング

•http://terasolunaorg.github.io/guideline/public_review/ArchitectureInDetail/ExceptionHandling.html

•ページネーション

•http://terasolunaorg.github.io/guideline/public_review/ArchitectureInDetail/Pagination.html

• ThymeLeafでレイアウトの使い方

• https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples/spring-boot-sample-web-ui/src/main/

URL変わる可能性があるので注意。

4. Spring Securityで認証・認可を追加しよう

Webブラウザ

curl

TomcatSpring Boot

Spring FrameworkSpring Security

ThymeLeaf

Spring MVC

Jackson

Spring Data JPA

Hibernate

H2 Database画面のあるアプリ

REST API

出来上がりイメージ

追加するファイル

依存関係追加<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>

http://bit.ly/jggug-04-00

依存性を追加しただけで• サーバーを再起動すると、Basic認証が有効になる

デフォルトのBasic認証• ユーザー名はuser

• パスワードは起動時にランダム値が生成され、ログに出力される

2014-07-27 23:01:33.306 INFO 15121 --- [ost-startStop-1] b.a.s.AuthenticationManagerConfiguration : !Using default security password: 8aa11dda-d9ba-4f98-8a53-d1ec58e67584

• security.basic.enabled: falseのプロパティを追加

デフォルトのBasic認証は無効にする

security: basic: enabled: false

application.yml

http://bit.ly/jggug-04-01

ログイン画面のある認証・認可の設定を行う

• com.example.SecurityConfigにSpring Securityの設定を行う

@Configuration@EnableWebMvcSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { // override configuration} WebSecurityConfigurerAdapterのメソッドをオーバーライ

ドすることでデフォルト設定を上書きできる。

@EnableWebSecurityと間違えないように。 (間違えるとCSRFトークンが設定されない・・)

http://bit.ly/jggug-04-02

認証設定

@Override@SuppressWarnings({ "rawtypes", "unchecked" })protected void configure(AuthenticationManagerBuilder auth) throws Exception { UserDetailsManagerConfigurer configurer = new InMemoryUserDetailsManagerConfigurer(); configurer.withUser("hoge").password("hoge").roles("USER"); configurer.withUser("admin").password("demo").roles("ADMIN"); configurer.configure(auth); UserDetailsService userDetailsService = configurer .getUserDetailsService();! auth.userDetailsService(userDetailsService);}

AuthenticationManagerBuilderを引数にもつconfigureメソッド

認証ユーザーを取得するメソッドを持つUserDetailsServiceインタフェースを設定し、ユーザーの取得方式を決める。

今回はメモリ上にユーザー情報を持つUserDetailsServiceを

使用する。

http://bit.ly/jggug-04-03

認可設定 (1/2)

@Overridepublic void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/image/**", "/api/**");}

WebSecurityを引数にもつconfigureメソッド

静的リソースは認可制御の対象外にする

今回はREST APIも認可制御の対象外にする

http://bit.ly/jggug-04-04

認可設定 (2/2)

@Overrideprotected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/loginForm").permitAll() .anyRequest().authenticated(); http.formLogin().loginProcessingUrl("/login").loginPage("/loginForm") .failureUrl("/loginForm?error").defaultSuccessUrl("/book/list") .usernameParameter("username").passwordParameter("password") .permitAll(); http.logout().logoutUrl("/logout").permitAll();}

HttpSecurityを引数にもつconfigureメソッド ログイン画面は常にアクセス許可、

それ以外のページは要認証

ログイン画面のURLやログイン処理のURL、 パラメータ名等を設定

ログアウト設定

http://bit.ly/jggug-04-05

ログイン画面作成• com.example.web.LoginController

package com.example.web;!import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;!@Controllerpublic class LoginController { @RequestMapping("/loginForm") String loginForm() { return "login/loginForm"; }}

http://bit.ly/jggug-04-06

ログイン画面作成• src/main/resources/templates/login/loginForm.html

<!DOCTYPE html><html xmlns:th="http:///www.thymeleaf.org"><head> <meta charset="UTF-8"/> <title>Login Page</title> <link rel="stylesheet" type="text/css" href="../../static/css/style.css" th:href="@{/css/style.css}"/></head><body><div th:if="${param.error}"> <span class="error-messages">Invalid username and password.</span></div><form th:action="@{/login}" method="post"> <dl> <dt><label for="username">Username</label></dt> <dd><input type="text" id="username" name="username"/></dd> </dl> <dl> <dt><label for="password">Password</label></dt> <dd><input type="password" id="password" name="password"/></dd> </dl> <input type="submit" value="Login"/></form></body></html> http://bit.ly/jggug-04-07

アプリケーション実行

CSRFトークンが自動で埋め込まれている

セキュアなHTTPレスポンスヘッダが埋め込まれている

認証・認可編修了• お疲れ様でした!

• 本当は説明したかったけれども省略した内容

• UserDetailsServiceを実装してDBから認証ユーザー取得

• ログインユーザーの表示(@AuthenticationPrincipal)

• パスワードハッシュ • 認可制御でパスごとにアクセス制限 • 認可制御で画面表示切り替え • Spring Security OAuthでREST APIにOAuth2導入

まとめ

まとめ• Spring Bootで

• REST APIを作成した

• 画面のあるアプリを作成した • 認証・認可処理を追加した

Spring Bootによるアプリケーション開発の基礎が掴めたはず!

是非職場で Spring Bootを この資料を

広めてください!

宣伝• Spring Bootの入門本(今日の内容みたいな話+いろいろ)を執筆中。

•出版されたら是非買ってください!

中級者編やりたい•Bookmarkエンティティに所有者Userをひも付けし、User毎にブックマーク管理

•Userエンティティを使って認証・認可 •OAuth2でREST APIに認可処理を追加 •FlywayでDBマイグレーション •Spring Boot Actuatorでメトリクス取得 •CodaHale Metricsでメトリクスをさらに取得

会場提供してくれる方、募集中!

お疲れ様でした!