Más contenido relacionado La actualidad más candente (20) Similar a Grails 3.0先取り!? Spring Boot入門ハンズオン #jggug_boot (20) Más de Toshiaki Maki (20) Grails 3.0先取り!? Spring Boot入門ハンズオン #jggug_boot31. 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
37. 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
@EnableAutoConfiguration
public 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
38. 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
@EnableAutoConfiguration
public 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
魔法のアノテーション
47. [参考] 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
48. [参考] 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が自動
的にアタッチされる
51. 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
58. 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
66. ドメインオブジェクト作成
• com.example.domain.Bookmark
@Entity
public 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
68. 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
70. サービス作成
• com.example.service.BookmarkService
@Service
@Transactional
public 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
73. コントローラー作成
• 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
74. 入力チェックを実施
@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
76. アプリケーション実行
• リクエストマッピングのログが出力される
ことを確認s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/
bookmarks],methods=[GET],params=[],headers=[],consumes=[],produces=[],cust
om=[]}" 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=[],cus
tom=[]}" onto com.example.domain.Bookmark
com.example.api.BookmarkRestController.postBookmarks(com.example.domain.Bo
okmark)
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)
77. 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-08
Windowsのコマンドプロンプトだとシングルクオートが効か
ないのでダブルクオートとエスケープしてください
78. 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"}
80. 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"}]
82. 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
<
90. Log4JDBCでSQLログを出力しよう
@Configuration
public 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名)が重要
91. • 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
92. • アプリケーションを再起動して各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}
98. REST APIを任意のクライアント
からアクセスできるようにする
• AppConfigに以下のBean定義を追加
@Bean
Filter 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を付
ければ定義は不要
FilterRegistra
tionBeanを使え
ばurl-pattern等
の指定もできる。
http://bit.ly/jggug-02-15
101. テストの初期化
@Autowired
BookmarkRepository bookmarkRepository;
@Value("${local.server.port}")
int port;
String apiEndpoint;
RestTemplate restTemplate = new TestRestTemplate();
Bookmark springIO;
Bookmark springBoot;
!
@Before
public 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
102. 全件取得APIのテスト
@Test
public 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
103. 新規作成APIのテスト
@Test
public 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
104. 1件削除APIのテスト
@Test
public 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
113. ブックマーク一覧表示
@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
114. ブックマーク新規登録
@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
115. ブックマーク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
122. 一覧表示画面
<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
125. 新規作成フォーム
<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
143. 認証設定
@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
144. 認可設定 (1/2)
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/image/**", "/api/**");
}
WebSecurityを引数にもつconfigureメソッド
静的リソースは認可制御の対象外にする
今回はREST APIも認可制御の対象外にする
http://bit.ly/jggug-04-04
145. 認可設定 (2/2)
@Override
protected 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
147. ログイン画面作成
• 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
153. まとめ
• Spring Bootで
• REST APIを作成した
• 画面のあるアプリを作成した
• 認証・認可処理を追加した
Spring Bootによるアプリケーション開発
の基礎が掴めたはず!