Upload
makingx
View
20
Download
1
Embed Size (px)
DESCRIPTION
Spring Bootのハンズオン資料です。 ---- Grailsの次期バージョン3.0でベースになることが予定されている、Spring界隈の新しいトレンド"Spring Boot"のハンズオンを通じて、Spring Bootのイメージを掴んでもらいたいと思います。内容は以下の通りです。 Spring Boot概要説明 Spring Bootを用いて簡単なアプリケーションを実際に作ってみる (合計で約二時間弱)
Citation preview
Grails 3.0先取り!? Spring Boot入門ハンズオン
2014-08-01 日本Grails/Groovyユーザーグループ G*ワークショップ"Z" 第14弾
槙 俊明(@making)
自己紹介• @making
• http://blog.ik.am
• 日本Javaユーザーグループ(JJUG)幹事
• Groovy書けません
• Spring Boot本書いています
http://amzn.to/hajiboo
Spring Bootに関する詳しい話はまた今度!
• 2014-08-14に日本Springユーザー会 勉強会でSpring Bootについて話します。
• https://atnd.org/events/53770 (今から登録は厳しいかも・・・)
ハンズオンの流れ
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分だと多分ここまで 土日にやろう
JDK 8のインストール• http://www.oracle.com/technetwork/java/javase/
downloads/jdk8-downloads-2133151.html
• JAVA_HOMEを設定してね!
本ハンズオン扱う技術
Webブラウザ
curl
TomcatSpring Boot
Spring FrameworkSpring Security
ThymeLeaf
Spring MVC
Jackson
Spring Data JPA
Hibernate
H2 Database画面のあるアプリ
REST API
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
<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
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
魔法のアノテーション
プロパティを変更して実行
$ 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!";} 再起動することなく
アプリが更新された
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
他のJVM言語の例• https://github.com/making/spring-boot-demo-jvm-
languages
• Java/Groovy/Scala/KotlinそれぞれのGradleプロジェクトサンプルがあるのでお好みの言語でSpring Bootアプリを作りましょう
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
ドメインオブジェクト作成• 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
サービス作成• 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
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を任意のクライアントからアクセスできるようにする
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の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変わる可能性があるので注意。
Webブラウザ
curl
TomcatSpring Boot
Spring FrameworkSpring Security
ThymeLeaf
Spring MVC
Jackson
Spring Data JPA
Hibernate
H2 Database画面のあるアプリ
REST API
画面遷移
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
[補足] 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
新規作成フォーム<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
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
画面のあるアプリ編修了•本当は説明したかったけれども省略した内容
•入力チェック
•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変わる可能性があるので注意。
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認証• ユーザー名は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
認証・認可編修了• お疲れ様でした!
• 本当は説明したかったけれども省略した内容
• UserDetailsServiceを実装してDBから認証ユーザー取得
• ログインユーザーの表示(@AuthenticationPrincipal)
• パスワードハッシュ • 認可制御でパスごとにアクセス制限 • 認可制御で画面表示切り替え • Spring Security OAuthでREST APIにOAuth2導入
中級者編やりたい•Bookmarkエンティティに所有者Userをひも付けし、User毎にブックマーク管理
•Userエンティティを使って認証・認可 •OAuth2でREST APIに認可処理を追加 •FlywayでDBマイグレーション •Spring Boot Actuatorでメトリクス取得 •CodaHale Metricsでメトリクスをさらに取得
会場提供してくれる方、募集中!