├── README.md
├── blog-api.md
├── blog-ui.md
├── create-domain.md
├── extra.md
├── install.md
├── memory-calculator.md
├── notification.md
└── prep.md
/README.md:
--------------------------------------------------------------------------------
1 | # ブログシステム作成ハンズオン
2 |
3 | Spring Boot 2を使って下図のような自分専用ブログシステムを構築するハンズオンです。
4 |
5 | **ハッシュタグ**: [#bloghol](https://twitter.com/search?f=tweets&q=%23bloghol&src=typd)
6 |
7 | Blog APIとBlog UIという二つのWebアプリケーションを実装(穴埋め)して、[Pivotal Web Services](https://run.pivotal.io/)にデプロイします。
8 | ブログの記事はGitHubで管理し、Webhookを使ってデータベースの更新を行います。
9 |
10 | 
11 |
12 | ## 目次
13 |
14 | 1. [必要なソフトウェアのインストール](install.md)
15 | 1. [(**必須**) 事前準備](prep.md)
16 | 1. [Blog APIの実装およびデプロイ](blog-api.md)
17 | 1. [Blog UIの実装およびデプロイ](blog-ui.md)
18 | 1. [Java Memory Calculatorでメモリの調節](memory-calculator.md)
19 | 1. [HTML5のServer-Sent EventsとNotifications APIを使ってブログ記事の更新通知](notification.md)
20 | 1. [独自ドメインの利用](create-domain.md)
21 |
22 |
23 | スケールアウト、ロギング、Blue/GreenデプロイなどCloud Foundryの基本的な使い方は[Cloud Foundryワークショップ資料](https://github.com/pivotal-japan/cf-workshop)を参照してください。
24 | ConcourseによるCI/CDを実践したい場合は、[Concourseワークショップ資料](https://github.com/pivotal-japan/concourse-workshop)を参照してください。
25 |
26 | ## 利用規約
27 |
28 | 無断で本ドキュメントの一部または全部を改変したり、本ドキュメントを用いた二次的著作物を作成することを禁止します。ただし、ドキュメント修正のためのPull Requestは大歓迎です。
29 |
30 | ---
31 |
32 | © 2018 Toshiaki Maki
33 |
--------------------------------------------------------------------------------
/blog-api.md:
--------------------------------------------------------------------------------
1 | ## Blog APIの実装およびデプロイ
2 |
3 | まずはブログシステムのAPI部分から実装します。
4 |
5 | 大方の部分は既に実装済みです。足りない部分をテストドリブンで埋めていきます。
6 |
7 | ### プロジェクトの作成
8 |
9 |
10 | https://github.com/making/demo-blog-api
11 |
12 | をフォークして、クローンしてください。
13 |
14 | 
15 |
16 | ```
17 | GIT_USER=
18 | git clone git@github.com:${GIT_USER}/demo-blog-api.git
19 | ```
20 |
21 | クローンしたプロジェクトをIDEにインポートしてください。
22 |
23 |
24 | ### テストの実行
25 |
26 | インポートしたプロジェクトのテストディレクトリを右クリックしてテストを実行してください。(下図はIntelliJ IDEの例)
27 |
28 | 
29 |
30 |
31 | `EntryControllerTest`と`WebhookControllerTest`がエラーになるでしょう。
32 |
33 | 
34 |
35 |
36 | ### ✏️ 演習1: `EntryController`の実装
37 |
38 |
39 | [`demo-blog-api/src/main/java/com/example/blog/entry/EntryController.java`](https://github.com/making/demo-blog-api/blob/master/src/main/java/com/example/blog/entry/EntryController.java)
40 |
41 | を開き、`TODO`の部分を確認し、実装してください。
42 |
43 |
44 | > **ヒント**:
45 | >
46 | > `EntryRepository`のメソッドを呼び出して下さい。404エラーを返したい場合は`new ResponseStatusException(HttpStatus.NOT_FOUND, "error message")"`をスローすれば良いです。
47 |
48 |
49 | 実装できたら、`EntryControllerTest`をテストしてください。全てグリーンになれば正解です。
50 |
51 | 
52 |
53 |
54 | ### ✏️ 演習2: `WebHookController`の実装
55 |
56 | [`demo-blog-api/src/main/java/com/example/blog/webhook/WebhookController.java`](https://github.com/making/demo-blog-api/blob/master/src/main/java/com/example/blog/webhook/WebhookController.java)
57 |
58 | を開き、`TODO`の部分を確認し、実装してください。
59 |
60 |
61 | > **ヒント**:
62 | >
63 | > `Flux added`, `Flux modified`, `Flux removed`それぞれに`doOnNext`メソッドを追加して、それぞれ`EntryCreateEvent`、`EntryUpdateEvent`、`EntryDeleteEvent`を作成して、`ApplicationEventPublisher`から`publish`してください。
64 |
65 | 実装できたら、`WebHookControllerTest`をテストしてください。全てグリーンになれば正解です。
66 |
67 | 
68 |
69 |
70 | ### アプリケーションの実行
71 |
72 |
73 | `com.example.blog.DemoBlogApiApplication`の起動時オプションとして、プログラム引数に
74 |
75 | `blog.github.webhook-secret`(Webhook用の秘密キー。任意の値で良いですが、次のテストデータを試すには`foofoo`を入力してください。)と`blog.github.access-token`(GitHubのアクセストークン)を設定して実行してください。
76 |
77 | 
78 |
79 |
80 | #### 記事全件取得APIの動作確認
81 |
82 | [http://localhost:8080/v1/entries](http://localhost:8080/v1/entries)にアクセスしてください。
83 |
84 | 
85 |
86 |
87 | #### 記事1件取得APIの動作確認
88 |
89 | [http://localhost:8080/v1/entries/100](http://localhost:8080/v1/entries/100)にアクセスしてください。
90 |
91 | 
92 |
93 |
94 | #### Webhookの動作確認
95 |
96 | `src/test`以下にWebhookのテスト用ペイロードとリクエストを送るシェルスクリプトが用意されています。
97 |
98 |
99 | 記事作成のWebhookは次のコマンドで試せます。
100 |
101 | ```
102 | cd demo-blog-api
103 | ./src/test/resources/sample-create-request.sh
104 | ```
105 |
106 | `[{"added":497}]`というレスポンスが返れば、記事1件取得API([http://localhost:8080/v1/entries/497](http://localhost:8080/v1/entries/497))で内容を確認してください。
107 |
108 |
109 | `{"timestamp":"2018-01-21T09:55:49.085+0000","status":403,"error":"Forbidden","message":"Could not verify signature: 'sha1=6ff50ec0e2f69d5831d8a5a88570be819b18515a'","path":"/webhook"}`というレスポンスが返る場合は、
110 | `blog.github.webhook-secret`が正しくないです。`foofoo`を設定してください。
111 |
112 | 記事更新のWebhookは次のコマンドで試せます。
113 |
114 | ```
115 | ./src/test/resources/sample-update-request.sh
116 | ```
117 |
118 | `[{"modified":497}]`というレスポンスが返れば正しいです。
119 |
120 | ### Cloud Foundryへのデプロイ
121 |
122 | まずはビルドして実行可能jarを作成してください。
123 |
124 | ```
125 | ./mvnw clean package
126 | ```
127 |
128 | Pivotal Web Servicesにログインをしていない場合は、次のコマンドでログインしてください。
129 |
130 | ```
131 | cf login -a api.run.pivotal.io
132 | ```
133 |
134 | 次の`manifest.yml`を作成してください。``の値は重複しないように自分のアカウント等を含めてください。
135 |
136 | ``` yaml
137 | applications:
138 | - name: blog-api
139 | path: target/demo-blog-api-0.0.1-SNAPSHOT.jar
140 | routes:
141 | - route: blog-api-.cfapps.io
142 | env:
143 | BLOG_GITHUB_ACCESS_TOKEN:
144 | BLOG_GITHUB_WEBHOOK_SECRET: <任意の値>
145 | ```
146 |
147 | 次のコマンドでデプロイできます。
148 |
149 |
150 | ```
151 | cf push
152 | ```
153 |
154 | 次のようなログが出力されます。
155 |
156 | ```
157 | $ cf push
158 | Using manifest file /Users/maki/git/demo-blog-api/manifest.yml
159 |
160 | Creating app blog-api in org APJ / space development as tmaki@pivotal.io...
161 | OK
162 |
163 | Creating route blog-api-tmaki.cfapps.io...
164 | OK
165 |
166 | Binding blog-api-tmaki.cfapps.io to blog-api...
167 | OK
168 |
169 | Uploading blog-api...
170 | Uploading app files from: /var/folders/9n/34xf4kbd1nl_8__3ghkl8_kc0000gn/T/unzipped-app744606369
171 | Uploading 729.8K, 137 files
172 | Done uploading
173 | OK
174 |
175 | Starting app blog-api in org APJ / space development as tmaki@pivotal.io...
176 | Downloading binary_buildpack...
177 | Downloading staticfile_buildpack...
178 | Downloading java_buildpack...
179 | Downloading ruby_buildpack...
180 | Downloading nodejs_buildpack...
181 | Downloaded nodejs_buildpack
182 | Downloading go_buildpack...
183 | Downloaded staticfile_buildpack
184 | Downloading python_buildpack...
185 | Downloaded go_buildpack
186 | Downloading php_buildpack...
187 | Downloaded java_buildpack
188 | Downloading dotnet_core_buildpack...
189 | Downloaded ruby_buildpack
190 | Downloading dotnet_core_buildpack_beta...
191 | Downloaded binary_buildpack
192 | Downloaded python_buildpack
193 | Downloaded php_buildpack
194 | Downloaded dotnet_core_buildpack
195 | Downloaded dotnet_core_buildpack_beta
196 | Creating container
197 | Successfully created container
198 | Downloading app package...
199 | Downloaded app package (27.3M)
200 | -----> Java Buildpack v4.5 (offline) | https://github.com/cloudfoundry/java-buildpack.git#ffeefb9
201 | -----> Downloading Jvmkill Agent 1.10.0_RELEASE from https://java-buildpack.cloudfoundry.org/jvmkill/trusty/x86_64/jvmkill-1.10.0_RELEASE.so (found in cache)
202 | -----> Downloading Open Jdk JRE 1.8.0_141 from https://java-buildpack.cloudfoundry.org/openjdk/trusty/x86_64/openjdk-1.8.0_141.tar.gz (found in cache)
203 | Expanding Open Jdk JRE to .java-buildpack/open_jdk_jre (1.4s)
204 | -----> Downloading Open JDK Like Memory Calculator 3.9.0_RELEASE from https://java-buildpack.cloudfoundry.org/memory-calculator/trusty/x86_64/memory-calculator-3.9.0_RELEASE.tar.gz (found in cache)
205 | Loaded Classes: 16816, Threads: 300
206 | -----> Downloading Client Certificate Mapper 1.2.0_RELEASE from https://java-buildpack.cloudfoundry.org/client-certificate-mapper/client-certificate-mapper-1.2.0_RELEASE.jar (found in cache)
207 | -----> Downloading Container Security Provider 1.8.0_RELEASE from https://java-buildpack.cloudfoundry.org/container-security-provider/container-security-provider-1.8.0_RELEASE.jar (found in cache)
208 | -----> Downloading Spring Auto Reconfiguration 1.12.0_RELEASE from https://java-buildpack.cloudfoundry.org/auto-reconfiguration/auto-reconfiguration-1.12.0_RELEASE.jar (found in cache)
209 | Exit status 0
210 | Uploading droplet, build artifacts cache...
211 | Uploading build artifacts cache...
212 | Uploading droplet...
213 | Uploaded build artifacts cache (131B)
214 | Uploaded droplet (73.6M)
215 | Uploading complete
216 | Stopping instance 37ce9828-a30c-479a-910f-3441b97f18e0
217 | Destroying container
218 | Successfully destroyed container
219 |
220 | 0 of 1 instances running, 1 starting
221 | 0 of 1 instances running, 1 starting
222 | 0 of 1 instances running, 1 starting
223 | 1 of 1 instances running
224 |
225 | App started
226 |
227 |
228 | OK
229 |
230 | App blog-api was started using this command `JAVA_OPTS="-agentpath:$PWD/.java-buildpack/open_jdk_jre/bin/jvmkill-1.10.0_RELEASE=printHeapHistogram=1 -Djava.io.tmpdir=$TMPDIR -Djava.ext.dirs=$PWD/.java-buildpack/container_security_provider:$PWD/.java-buildpack/open_jdk_jre/lib/ext -Djava.security.properties=$PWD/.java-buildpack/security_providers/java.security $JAVA_OPTS" && CALCULATED_MEMORY=$($PWD/.java-buildpack/open_jdk_jre/bin/java-buildpack-memory-calculator-3.9.0_RELEASE -totMemory=$MEMORY_LIMIT -stackThreads=300 -loadedClasses=17525 -poolType=metaspace -vmOptions="$JAVA_OPTS") && echo JVM Memory Configuration: $CALCULATED_MEMORY && JAVA_OPTS="$JAVA_OPTS $CALCULATED_MEMORY" && SERVER_PORT=$PORT eval exec $PWD/.java-buildpack/open_jdk_jre/bin/java $JAVA_OPTS -cp $PWD/. org.springframework.boot.loader.JarLauncher`
231 |
232 | Showing health and status for app blog-api in org APJ / space development as tmaki@pivotal.io...
233 | OK
234 |
235 | requested state: started
236 | instances: 1/1
237 | usage: 1G x 1 instances
238 | urls: blog-api-tmaki.cfapps.io
239 | last uploaded: Sun Jan 21 15:54:31 UTC 2018
240 | stack: cflinuxfs2
241 | buildpack: client-certificate-mapper=1.2.0_RELEASE container-security-provider=1.8.0_RELEASE java-buildpack=v4.5-offline-https://github.com/cloudfoundry/java-buildpack.git#ffeefb9 java-main java-opts jvmkill-agent=1.10.0_RELEASE open-jdk-like-jre=1.8.0_1...
242 |
243 | state since cpu memory disk details
244 | #0 running 2018-01-22 12:55:47 AM 174.4% 290.5M of 1G 154.5M of 1G
245 | ```
246 |
247 | Cloud Foundry上では`cloud`プロファイルが有効になるため、`DemoInserter`は動作しません。`entry`テーブルは空です。
248 |
249 | [https://blog-api-your-account.cfapps.io/v1/entries](https://blog-api-your-account.cfapps.io/v1/entries)にアクセスすると空のリストが返ります。
250 |
251 | > `manifest.yml`に機密情報を含めたくない場合は`cf set-env`コマンドでアプリケーションに環境変数を埋め込めます。
252 | >
253 | > ```
254 | > cf set-env blog-api BLOG_GITHUB_ACCESS_TOKEN asdfghujiko
255 | > cf set-env blog-api BLOG_GITHUB_WEBHOOK_SECRET foofoo
256 | > cf restart blog-api
257 | > ```
258 |
259 | #### MySQLのバインド
260 |
261 | バックエンドのデータベースをMySQLに切り替えます。
262 |
263 | MySQLのサービスブローカーである`cleardb`の`spark`プラン(free)で`blog-db`という名前のサービスインスタンスを作成します。
264 |
265 | ```
266 | cf create-service cleardb spark blog-db
267 | ```
268 |
269 | 次のように`manifest.yml`に`services`を追加してください。
270 |
271 | ``` yaml
272 | applications:
273 | - name: blog-api
274 | path: target/demo-blog-api-0.0.1-SNAPSHOT.jar
275 | routes:
276 | - route: blog-api-.cfapps.io
277 | env:
278 | BLOG_GITHUB_ACCESS_TOKEN:
279 | BLOG_GITHUB_WEBHOOK_SECRET: <任意の値>
280 | services:
281 | - blog-db
282 | ```
283 |
284 | 再度`cf push`してください。Maria DBのJDBCドライバーが自動で含まれます。
285 |
286 |
287 | ```
288 | $ cf push
289 | Using manifest file /Users/maki/git/demo-blog-api/manifest.yml
290 |
291 |
292 | Updating app blog-api in org APJ / space development as tmaki@pivotal.io...
293 | OK
294 |
295 | Using route blog-api-tmaki.cfapps.io
296 | Uploading blog-api...
297 | Uploading app files from: /var/folders/9n/34xf4kbd1nl_8__3ghkl8_kc0000gn/T/unzipped-app531234602
298 | Uploading 729.8K, 137 files
299 | Done uploading
300 | OK
301 | Binding service blog-db to app blog-api in org APJ / space development as tmaki@pivotal.io...
302 | OK
303 |
304 | Stopping app blog-api in org APJ / space development as tmaki@pivotal.io...
305 | OK
306 |
307 | Starting app blog-api in org APJ / space development as tmaki@pivotal.io...
308 | Downloading binary_buildpack...
309 | Downloading staticfile_buildpack...
310 | Downloading java_buildpack...
311 | Downloading ruby_buildpack...
312 | Downloading nodejs_buildpack...
313 | Downloaded ruby_buildpack
314 | Downloaded java_buildpack
315 | Downloading python_buildpack...
316 | Downloading go_buildpack...
317 | Downloaded binary_buildpack
318 | Downloading php_buildpack...
319 | Downloaded nodejs_buildpack
320 | Downloading dotnet_core_buildpack...
321 | Downloaded staticfile_buildpack
322 | Downloading dotnet_core_buildpack_beta...
323 | Downloaded php_buildpack
324 | Downloaded python_buildpack
325 | Downloaded go_buildpack
326 | Downloaded dotnet_core_buildpack
327 | Downloaded dotnet_core_buildpack_beta
328 | Creating container
329 | Successfully created container
330 | Downloading build artifacts cache...
331 | Downloading app package...
332 | Downloaded build artifacts cache (131B)
333 | Downloaded app package (27.3M)
334 | -----> Java Buildpack v4.5 (offline) | https://github.com/cloudfoundry/java-buildpack.git#ffeefb9
335 | -----> Downloading Jvmkill Agent 1.10.0_RELEASE from https://java-buildpack.cloudfoundry.org/jvmkill/trusty/x86_64/jvmkill-1.10.0_RELEASE.so (found in cache)
336 | -----> Downloading Open Jdk JRE 1.8.0_141 from https://java-buildpack.cloudfoundry.org/openjdk/trusty/x86_64/openjdk-1.8.0_141.tar.gz (found in cache)
337 | Expanding Open Jdk JRE to .java-buildpack/open_jdk_jre (1.4s)
338 | -----> Downloading Open JDK Like Memory Calculator 3.9.0_RELEASE from https://java-buildpack.cloudfoundry.org/memory-calculator/trusty/x86_64/memory-calculator-3.9.0_RELEASE.tar.gz (found in cache)
339 | Loaded Classes: 16816, Threads: 300
340 | -----> Downloading Client Certificate Mapper 1.2.0_RELEASE from https://java-buildpack.cloudfoundry.org/client-certificate-mapper/client-certificate-mapper-1.2.0_RELEASE.jar (found in cache)
341 | -----> Downloading Container Security Provider 1.8.0_RELEASE from https://java-buildpack.cloudfoundry.org/container-security-provider/container-security-provider-1.8.0_RELEASE.jar (found in cache)
342 | -----> Downloading Maria Db JDBC 2.1.0 from https://java-buildpack.cloudfoundry.org/mariadb-jdbc/mariadb-jdbc-2.1.0.jar (found in cache)
343 | -----> Downloading Spring Auto Reconfiguration 1.12.0_RELEASE from https://java-buildpack.cloudfoundry.org/auto-reconfiguration/auto-reconfiguration-1.12.0_RELEASE.jar (found in cache)
344 | Exit status 0
345 | Uploading droplet, build artifacts cache...
346 | Uploading build artifacts cache...
347 | Uploading droplet...
348 | Uploaded build artifacts cache (131B)
349 | Uploaded droplet (74.1M)
350 | Uploading complete
351 | Stopping instance e37ef977-7989-41e2-b2be-e18b8df03d9d
352 | Destroying container
353 | Successfully destroyed container
354 |
355 | 0 of 1 instances running, 1 starting
356 | 0 of 1 instances running, 1 starting
357 | 0 of 1 instances running, 1 starting
358 | 0 of 1 instances running, 1 starting
359 | 0 of 1 instances running, 1 starting
360 | 1 of 1 instances running
361 |
362 | App started
363 |
364 |
365 | OK
366 |
367 | App blog-api was started using this command `JAVA_OPTS="-agentpath:$PWD/.java-buildpack/open_jdk_jre/bin/jvmkill-1.10.0_RELEASE=printHeapHistogram=1 -Djava.io.tmpdir=$TMPDIR -Djava.ext.dirs=$PWD/.java-buildpack/container_security_provider:$PWD/.java-buildpack/open_jdk_jre/lib/ext -Djava.security.properties=$PWD/.java-buildpack/security_providers/java.security $JAVA_OPTS" && CALCULATED_MEMORY=$($PWD/.java-buildpack/open_jdk_jre/bin/java-buildpack-memory-calculator-3.9.0_RELEASE -totMemory=$MEMORY_LIMIT -stackThreads=300 -loadedClasses=17672 -poolType=metaspace -vmOptions="$JAVA_OPTS") && echo JVM Memory Configuration: $CALCULATED_MEMORY && JAVA_OPTS="$JAVA_OPTS $CALCULATED_MEMORY" && SERVER_PORT=$PORT eval exec $PWD/.java-buildpack/open_jdk_jre/bin/java $JAVA_OPTS -cp $PWD/. org.springframework.boot.loader.JarLauncher`
368 |
369 | Showing health and status for app blog-api in org APJ / space development as tmaki@pivotal.io...
370 | OK
371 |
372 | requested state: started
373 | instances: 1/1
374 | usage: 1G x 1 instances
375 | urls: blog-api-tmaki.cfapps.io
376 | last uploaded: Sun Jan 21 16:15:42 UTC 2018
377 | stack: cflinuxfs2
378 | buildpack: client-certificate-mapper=1.2.0_RELEASE container-security-provider=1.8.0_RELEASE java-buildpack=v4.5-offline-https://github.com/cloudfoundry/java-buildpack.git#ffeefb9 java-main java-opts jvmkill-agent=1.10.0_RELEASE maria-db-jdbc=2.1.0 open-...
379 |
380 | state since cpu memory disk details
381 | #0 running 2018-01-22 01:17:07 AM 143.0% 353.1M of 1G 155M of 1G
382 | ```
383 |
384 | ### ブログ記事の作成
385 |
386 | いよいよブログ記事を作成します。GitHubで新規レポジトリを作成してください。
387 |
388 | 
389 |
390 | レポジトリ名は任意の値で構いません。ここでは`my-blog`として説明します。"Initialize this repository with a README"にチェックを入れて下さい。
391 |
392 | 
393 |
394 |
395 | "Settings"をクリックして、"Webhooks"をクリックし、"Add webhook"をクリックして下さい。
396 |
397 | 
398 |
399 | * "Payload URL"には`https://blog-api-.cfapps.io/webhook`
400 | * "Content type"には`application/json`
401 | * "Secret"には`BLOG_GITHUB_WEBHOOK_SECRET`で設定した値
402 |
403 | を設定し、"Add webhook"ボタンをクリックして下さい。
404 |
405 | 
406 |
407 |
408 | Topに戻って、"Create new file"をクリックして下さい。
409 |
410 | 
411 |
412 | ファイル名に`content/00001.md`を入力して下さい。
413 |
414 | 
415 |
416 | ファイルコンテンツに次の内容を入力して下さい。
417 |
418 | ``` markdown
419 | ---
420 | title: First article
421 | tags: ["Demo"]
422 | categories: ["Demo", "Hello"]
423 | ---
424 |
425 | This is my first blog post!
426 | ```
427 |
428 | 
429 |
430 | "Commit new file"ボタンをクリックして下さい。
431 |
432 | Webhook画面に戻って、最新のWebhookが✅になっていることを確認して下さい。
433 |
434 | 
435 |
436 | これでデータベースが更新されているので、
437 | [https://blog-api-your-account.cfapps.io/v1/entries](https://blog-api-your-account.cfapps.io/v1/entries)にアクセスすると作成した記事が返ります。
438 |
439 | 
440 |
441 | Github上で記事を更新すると、[https://blog-api-your-account.cfapps.io/v1/entries](https://blog-api-your-account.cfapps.io/v1/entries)の結果も反映されることを確認して下さい。
442 |
443 | ### [補足] メモリを節約する
444 |
445 | `manifest.yml`を次のように変更し、コンテナメモリサイズを512MBに減らせます。
446 |
447 | ``` yaml
448 | applications:
449 | - name: blog-api
450 | memory: 512m
451 | path: target/demo-blog-api-0.0.1-SNAPSHOT.jar
452 | routes:
453 | - route: blog-api-.cfapps.io
454 | env:
455 | BLOG_GITHUB_ACCESS_TOKEN:
456 | BLOG_GITHUB_WEBHOOK_SECRET: <任意の値>
457 | SERVER_TOMCAT_MAX_THREADS: 4
458 | JAVA_OPTS: '-XX:ReservedCodeCacheSize=32M -Xss512k -XX:+PrintCodeCache'
459 | JBP_CONFIG_OPEN_JDK_JRE: '[memory_calculator: {stack_threads: 24}]' # 4 (tomcat) + 20 (etc)
460 | services:
461 | - blog-db
462 | ```
463 |
464 | 詳細は[Java Memory Calculatorでメモリの調節](memory-calculator.md)を参照してください。
465 |
466 | ### [補足] 外部のMySQLを使用する
467 |
468 | cleardbのsparkプランは貧弱(ディスクサイズ5MB,最大接続数4)なので、他のMySQLサービスを使いたいことが多いです。外部のMySQLサービスを使う場合は、次の2通りあります。
469 |
470 | いずれにせよ、まずは`blog-api`から`blog-db`をアンバインドして、`blog-db`サービスインスタンスを削除して下さい。
471 |
472 | ```
473 | cf unbind-service blog-api blog-db
474 | cf delete-service blog-db
475 | ```
476 |
477 | > 外部のMySQLサービスとしては[Cloud SQL](https://cloud.google.com/sql)の`db-f1-micro`プランがリーズナブルです。
478 |
479 | #### BuildpackのSpring Auto Reconfigurationを使う場合
480 |
481 | BuildpackのSpring Auto Reconfigurationを使う場合はJDBCドライバの設定は不要で、DIコンテナ中の`Datasource`インスタンスも自動で挿し変わります。
482 | 設定が不要で便利な一方、`DataSource`の設定を自由に行うことができません。
483 |
484 | こちらを使用する場合、User Provided Serviceを次の形式で作成して下さい。
485 |
486 | ```
487 | cf create-user-provided-service blog-db -p '{"uri":"mysql://username:password@hostname:port/db"}'
488 | ```
489 |
490 | このあと、再度`cf push`して下さい。
491 |
492 | #### BuildpackのSpring Auto Reconfigurationを使わない場合
493 |
494 | BuildpackのSpring Auto Reconfigurationを使わない場合はサービスインスタンスは作成せず、環境変数でDBの情報を設定します。
495 | また、JDBCドライバを含めてアプリケーションをビルドし直す必要があります。
496 |
497 | `pom.xml`に次の`dependency`を追加して下さい。
498 |
499 | ``` xml
500 |
501 | mysql
502 | mysql-connector-java
503 | runtime
504 |
505 | ```
506 |
507 | アプリケーションを再度ビルドして下さい。
508 |
509 | ```
510 | ./mvnw clean package
511 | ```
512 |
513 | `manifest.yml`の`env`にMySQLの接続情報を記述します。
514 |
515 | ``` yaml
516 | applications:
517 | - name: blog-api
518 | path: target/demo-blog-api-0.0.1-SNAPSHOT.jar
519 | routes:
520 | - route: blog-api-.cfapps.io
521 | env:
522 | BLOG_GITHUB_ACCESS_TOKEN:
523 | BLOG_GITHUB_WEBHOOK_SECRET: <任意の値>
524 | SPRING_DATASOURCE_DRIVER_CLASS_NAME: com.mysql.jdbc.Driver
525 | SPRING_DATASOURCE_URL: jdbc:mysql://hostname:port/db
526 | SPRING_DATASOURCE_USERNAME: user
527 | SPRING_DATASOURCE_PASSWORD: password
528 | ```
529 |
530 | このあと、再度`cf push`して下さい。こちらの方法のメリットはCloud Foundry(Buildpack)に依存しないPureな12 Factor Appになる点です。
531 |
532 | > `manifest.yml`に`SPRING_DATASOURCE_PASSWORD`を含めたくない場合は、別途`cf set-env`を実行して下さい。
533 | >
534 | > ```
535 | > cf set-env blog-api SPRING_DATASOURCE_PASSWORD password
536 | > cf restart blog-api
537 | > ```
538 |
539 | MySQLをサービスブローカーで作成しつつ、Spring Auto Reconfigurationを使わずに環境変数でDB接続情報を設定したい場合は、`cf create-service-key`でサービスインスタンスから接続情報だけ作成して表示してください。
540 |
541 | ```
542 | $ cf create-service cleardb spark blog-db # 未作成の場合
543 | $ cf create-service-key blog-db blog-api-key
544 | $ cf service-key blog-db blog-api-key
545 |
546 | {
547 | "hostname": "us-cdbr-iron-east-04.cleardb.net",
548 | "jdbcUrl": "jdbc:mysql://us-cdbr-iron-east-04.cleardb.net/ad_bc365753fe5cd77?user=b746d44bae5f3a\u0026password=b5e1d9d7",
549 | "name": "ad_bc365753fe5cd77",
550 | "password": "b5e1d9d7",
551 | "port": "3306",
552 | "uri": "mysql://b746d44bae5f3a:b5e1d9d7@us-cdbr-iron-east-05.cleardb.net:3306/ad_bc365753fe5cd77?reconnect=true",
553 | "username": "b746d44bae5f3a"
554 | }
555 | ```
556 |
557 | 得られた`uri`、`username`、`password`をそれぞれ環境変数`SPRING_DATASOURCE_URL`、`SPRING_DATASOURCE_USERNAME`、`SPRING_DATASOURCE_PASSWORD`に設定すれば良いです。`cf bind-service`は**行わないでください**。
558 |
559 |
560 | > Pivotal Web ServicesとRDSまたはCloud SQLをTLS通信する場合は次の記事を参照してください
561 | > https://blog.ik.am/entries/492
562 |
563 | ### [補足] DB更新処理を行うスレッドを指定する
564 |
565 |
566 | 今回のケースでは問題にはなりませんが、次のコードには改善すべき点があります。(`modified`、`deleted`も同じ)
567 |
568 | ```java
569 | Flux added = this.paths(commit.get("added"))
570 | .flatMap(path -> this.entryFetcher.fetch(owner, repo, path)) // (A)
571 | .doOnNext(e -> this.publisher.publishEvent(new EntryCreateEvent(e))) // (B)
572 | .map(Entry::entryId);
573 | ```
574 |
575 | `(A)`では`WebClient`を使用しているため、Reactor Nettyのイベントループスレッドプールが使用されます。
576 | このコードではそのまま`(B)`の処理が行われるため、`(B)`も`(A)`と同じスレッド上で実行されます。
577 | `(B)`ではデータベースアクセスを伴うブロッキングIO処理が行われますが、Nettyのイベントループスレッドは
578 | ノンブロッキングIO処理を想定しており、スレッドプール数はCPU数しかありません。
579 | もしも`(B)`の処理が同時に多数呼ばれるような状況では、この処理がスレッドプールを専有してしまい、
580 | 本来ノンブロッキングである`(A)`の`WebClient`の処理が妨げられます。
581 |
582 | 今回のケースでは`(B)`はWebHook経由でしか呼ばれないため、実質的に問題ありません。
583 | ただし、このようにブロッキング処理がNettyのイベントループスレッドプールで実行されることを避けるには、
584 | 明示的に`(B)`を実行するスレッドプールを指定する必要があります。
585 |
586 | Blog APIの実装では`com.example.blog.DemoBlogApiApplication`にデータベースアクセス用のスレッドプール`ThreadPoolTaskExecutor`が定義されています。
587 | このスレッドプール数はコネクションプール数と同じであるべきです。適切な値を設定してください。
588 |
589 | この`TaskExecutor`を`WebhookController`にインジェクションします。
590 |
591 | ```java
592 | public class WebhookController {
593 | private final EntryFetcher entryFetcher;
594 | private final TaskExecutor taskExecutor; // **追加**
595 | private final ApplicationEventPublisher publisher;
596 | private final WebhookVerifier webhookVerifier;
597 | private final ObjectMapper objectMapper;
598 |
599 | public WebhookController(BlogProperties props, EntryFetcher entryFetcher,
600 | TaskExecutor taskExecutor /** 追加 **/, ApplicationEventPublisher publisher,
601 | ObjectMapper objectMapper)
602 | throws NoSuchAlgorithmException, InvalidKeyException {
603 | this.entryFetcher = entryFetcher;
604 | this.taskExecutor = taskExecutor; // **追加**
605 | this.publisher = publisher;
606 | this.objectMapper = objectMapper;
607 | this.webhookVerifier = new WebhookVerifier(props.getGithub().getWebhookSecret());
608 | }
609 | /* ... */
610 | }
611 | ```
612 |
613 | そして、データベースアクセス処理の前に`publishOn`メソッドでこの`TaskExecutor`を使った`reactor.core.scheduler.Scheduler`を指定する必要があります。
614 |
615 | ```java
616 | Flux added = this.paths(commit.get("added"))
617 | .flatMap(path -> this.entryFetcher.fetch(owner, repo, path)) // (A)
618 | .publishOn(Schedulers.fromExecutor(this.taskExecutor)) // (*)
619 | .doOnNext(e -> this.publisher.publishEvent(new EntryCreateEvent(e))) // (B)
620 | .map(Entry::entryId);
621 | ```
622 |
623 | このコードの`(*)`より後のオペレーションは`publishOn`で指定した`Scheduler`で生成されるスレッド上で実行されます。
624 |
625 | > `(*)`より前のオペレーションを実行する`Scheduler`を指定したい場合は`subscribeOn`を使用してください。
626 |
627 | コード変更前はWebHookで次のログが出力されます。
628 |
629 | ```
630 | 2018-01-30 01:40:03.735 DEBUG 82098 --- [ctor-http-nio-6] r.ipc.netty.http.client.HttpClient : [id: 0x5be4e902, L:/192.168.11.6:54511 - R:api.github.com/192.30.255.116:443] READ COMPLETE
631 | 2018-01-30 01:40:03.766 INFO 82098 --- [ctor-http-nio-4] c.e.b.e.event.EntryCreateEventListener : Create 497
632 | 2018-01-30 01:40:03.766 DEBUG 82098 --- [ctor-http-nio-4] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [com.example.blog.entry.EntryRepository.create]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
633 | 2018-01-30 01:40:03.767 DEBUG 82098 --- [ctor-http-nio-4] o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@1208355775 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] for JDBC transaction
634 | 2018-01-30 01:40:03.767 DEBUG 82098 --- [ctor-http-nio-4] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@1208355775 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] to manual commit
635 | 2018-01-30 01:40:03.767 DEBUG 82098 --- [ctor-http-nio-4] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
636 | 2018-01-30 01:40:03.767 DEBUG 82098 --- [ctor-http-nio-4] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO entry(entry_id, title, content, created_by, created_date, last_modified_by, last_modified_date) VALUES(?, ?, ?, ?, ?, ?, ?)]
637 | ```
638 |
639 | HTTP処理もデータベースアクセス処理も`reactor-http-nio-N`という名前のスレッド上で実行されており、Reactor Nettyのイベントループスレッド上であることがわかります。
640 |
641 | コード変更後はWebHookで次のログが出力されます
642 |
643 | ```
644 | 2018-01-30 01:42:55.583 DEBUG 82649 --- [ctor-http-nio-4] r.ipc.netty.http.client.HttpClient : [id: 0x7c2d0c17, L:/192.168.11.6:54537 - R:api.github.com/192.30.255.117:443] READ COMPLETE
645 | 2018-01-30 01:42:55.583 INFO 82649 --- [lTaskExecutor-4] c.e.b.e.event.EntryCreateEventListener : Create 497
646 | 2018-01-30 01:42:55.583 DEBUG 82649 --- [lTaskExecutor-4] o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [com.example.blog.entry.EntryRepository.create]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ''
647 | 2018-01-30 01:42:55.584 DEBUG 82649 --- [lTaskExecutor-4] o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@1785931203 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] for JDBC transaction
648 | 2018-01-30 01:42:55.584 DEBUG 82649 --- [lTaskExecutor-4] o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [HikariProxyConnection@1785931203 wrapping conn0: url=jdbc:h2:mem:testdb user=SA] to manual commit
649 | 2018-01-30 01:42:55.584 DEBUG 82649 --- [lTaskExecutor-4] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update
650 | 2018-01-30 01:42:55.584 DEBUG 82649 --- [lTaskExecutor-4] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [INSERT INTO entry(entry_id, title, content, created_by, created_date, last_modified_by, last_modified_date) VALUES(?, ?, ?, ?, ?, ?, ?)]
651 | ```
652 |
653 | 今度はHTTP処理は`reactor-http-nio-N`スレッド上ですが、データベースアクセス処理は`threadPoolTaskExecutor-N`スレッド上で実行されています。
654 | これでデータベースアクセス処理にNettyのスレッドプールを使用されることを防げました。当然ですが、その分生成するスレッド数は増えるのでメモリ使用量は増えます。
655 |
--------------------------------------------------------------------------------
/blog-ui.md:
--------------------------------------------------------------------------------
1 | ## Blog UIの実装およびデプロイ
2 |
3 | 次はブログシステムのUI部分を実装します。
4 |
5 | 大方の部分は既に実装済みです。足りない部分をテストドリブンで埋めていきます。
6 |
7 | ### プロジェクトの作成
8 |
9 |
10 | https://github.com/making/demo-blog-ui
11 |
12 | をフォークして、クローンしてください。
13 |
14 | 
15 |
16 | ```
17 | GIT_USER=
18 | git clone git@github.com:${GIT_USER}/demo-blog-ui.git
19 | ```
20 |
21 | クローンしたプロジェクトをIDEにインポートしてください。
22 |
23 | ### テストの実行
24 |
25 | インポートしたプロジェクトのテストディレクトリを右クリックしてテストを実行してください。(下図はIntelliJ IDEの例)
26 |
27 | `UiControllerTest`がエラーになるでしょう。
28 |
29 | 
30 |
31 | ### ✏️ 演習3: `UiController`の実装
32 |
33 | [demo-blog-ui/src/main/java/com/example/blog/UiController.java](https://github.com/making/demo-blog-ui/blob/master/src/main/java/com/example/blog/UiController.java)
34 |
35 |
36 | を開き、TODOの部分を確認し、実装してください。
37 |
38 | > ヒント:
39 | >
40 | > `WebClient`を使ってblog-apiの"記事全件取得API"と"記事1件取得API"をそれぞれ呼び出して下さい。
41 | > コンストラクタを見ればわかるように、`WebClient`には`baseUrl`としてblog-apiのurlが設定されています。このプロパティは`application.properties`に設定されています。
42 | > `WebClient`の使い方がわからない場合は、[公式ドキュメント](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-client)を参照して下さい。
43 |
44 |
45 | 実装できたら、`UiControllerTest`をテストしてください。全てグリーンになれば正解です。
46 |
47 | 
48 |
49 | ### アプリケーションの実行
50 |
51 |
52 | blog-apiが起動した状態で、`com.example.blog.DemoBlogUiApplication`を実行して下さい。
53 |
54 | #### 一覧画面の動作確認
55 |
56 | [http://localhost:8082](http://localhost:8082)にアクセスしてください。
57 |
58 | 
59 |
60 |
61 | #### 記事画面の動作確認
62 |
63 | [http://localhost:8082/entries/100](http://localhost:8082/entries/100)にアクセスしてください。
64 |
65 |
66 | 
67 |
68 |
69 | ### ✏️ 演習4: HTMLのカスタマイズ
70 |
71 | * 一覧画面: [demo-blog-ui/src/main/resources/templates/index.html](https://github.com/making/demo-blog-ui/blob/master/src/main/resources/templates/index.html)
72 | * 記事画面: [demo-blog-ui/src/main/resources/templates/entry.html](https://github.com/making/demo-blog-ui/blob/master/src/main/resources/templates/entry.html)
73 |
74 | を開き、HTMLを編集して好みのデザインにしてください。
75 |
76 | ### Cloud Foundryへのデプロイ
77 |
78 | まずはビルドして実行可能jarを作成してください。
79 |
80 | ```
81 | ./mvnw clean package
82 | ```
83 |
84 | Pivotal Web Servicesにログインをしていない場合は、次のコマンドでログインしてください。
85 |
86 | ```
87 | cf login -a api.run.pivotal.io
88 | ```
89 |
90 | 次の`manifest.yml`を作成してください。``の値は重複しないように自分のアカウント等を含めてください。
91 |
92 | ``` yaml
93 | applications:
94 | - name: blog-ui
95 | path: target/demo-blog-ui-0.0.1-SNAPSHOT.jar
96 | routes:
97 | - route: blog-ui-.cfapps.io
98 | env:
99 | BLOG_API_URI: https://blog-api-.cfapps.io
100 | ```
101 |
102 | 次のコマンドでデプロイできます。
103 |
104 | ```
105 | cf push
106 | ```
107 |
108 | 次のようなログが出力されます。
109 |
110 | ```
111 | $ cf push
112 | Using manifest file /Users/maki/git/demo-blog-ui/manifest.yml
113 |
114 | Creating app blog-ui in org APJ / space development as tmaki@pivotal.io...
115 | OK
116 |
117 | Creating route blog-ui-tmaki.cfapps.io...
118 | OK
119 |
120 | Binding blog-ui-tmaki.cfapps.io to blog-ui...
121 | OK
122 |
123 | Uploading blog-ui...
124 | Uploading app files from: /var/folders/9n/34xf4kbd1nl_8__3ghkl8_kc0000gn/T/unzipped-app156799962
125 | Uploading 1.1M, 122 files
126 | Done uploading
127 | OK
128 |
129 | Starting app blog-ui in org APJ / space development as tmaki@pivotal.io...
130 | Downloading binary_buildpack...
131 | Downloading nodejs_buildpack...
132 | Downloading staticfile_buildpack...
133 | Downloading java_buildpack...
134 | Downloading ruby_buildpack...
135 | Downloaded binary_buildpack
136 | Downloading php_buildpack...
137 | Downloaded staticfile_buildpack
138 | Downloading go_buildpack...
139 | Downloaded ruby_buildpack
140 | Downloading python_buildpack...
141 | Downloaded go_buildpack
142 | Downloading dotnet_core_buildpack...
143 | Downloaded dotnet_core_buildpack
144 | Downloading dotnet_core_buildpack_beta...
145 | Downloaded dotnet_core_buildpack_beta
146 | Downloaded python_buildpack
147 | Downloaded php_buildpack
148 | Downloaded java_buildpack
149 | Downloaded nodejs_buildpack
150 | Creating container
151 | Successfully created container
152 | Downloading app package...
153 | Downloaded app package (22M)
154 | -----> Java Buildpack v4.5 (offline) | https://github.com/cloudfoundry/java-buildpack.git#ffeefb9
155 | -----> Downloading Jvmkill Agent 1.10.0_RELEASE from https://java-buildpack.cloudfoundry.org/jvmkill/trusty/x86_64/jvmkill-1.10.0_RELEASE.so (found in cache)
156 | -----> Downloading Open Jdk JRE 1.8.0_141 from https://java-buildpack.cloudfoundry.org/openjdk/trusty/x86_64/openjdk-1.8.0_141.tar.gz (found in cache)
157 | Expanding Open Jdk JRE to .java-buildpack/open_jdk_jre (1.5s)
158 | -----> Downloading Open JDK Like Memory Calculator 3.9.0_RELEASE from https://java-buildpack.cloudfoundry.org/memory-calculator/trusty/x86_64/memory-calculator-3.9.0_RELEASE.tar.gz (found in cache)
159 | Loaded Classes: 15787, Threads: 300
160 | -----> Downloading Client Certificate Mapper 1.2.0_RELEASE from https://java-buildpack.cloudfoundry.org/client-certificate-mapper/client-certificate-mapper-1.2.0_RELEASE.jar (found in cache)
161 | -----> Downloading Container Security Provider 1.8.0_RELEASE from https://java-buildpack.cloudfoundry.org/container-security-provider/container-security-provider-1.8.0_RELEASE.jar (found in cache)
162 | -----> Downloading Spring Auto Reconfiguration 1.12.0_RELEASE from https://java-buildpack.cloudfoundry.org/auto-reconfiguration/auto-reconfiguration-1.12.0_RELEASE.jar (found in cache)
163 | Exit status 0
164 | Uploading droplet, build artifacts cache...
165 | Uploading droplet...
166 | Uploading build artifacts cache...
167 | Uploaded build artifacts cache (131B)
168 | Uploaded droplet (68.4M)
169 | Uploading complete
170 | Stopping instance 3231cd37-33a7-498b-85ff-be2b65eb97cd
171 | Destroying container
172 | Successfully destroyed container
173 |
174 | 0 of 1 instances running, 1 starting
175 | 0 of 1 instances running, 1 starting
176 | 1 of 1 instances running
177 |
178 | App started
179 |
180 |
181 | OK
182 |
183 | App blog-ui was started using this command `JAVA_OPTS="-agentpath:$PWD/.java-buildpack/open_jdk_jre/bin/jvmkill-1.10.0_RELEASE=printHeapHistogram=1 -Djava.io.tmpdir=$TMPDIR -Djava.ext.dirs=$PWD/.java-buildpack/container_security_provider:$PWD/.java-buildpack/open_jdk_jre/lib/ext -Djava.security.properties=$PWD/.java-buildpack/security_providers/java.security $JAVA_OPTS" && CALCULATED_MEMORY=$($PWD/.java-buildpack/open_jdk_jre/bin/java-buildpack-memory-calculator-3.9.0_RELEASE -totMemory=$MEMORY_LIMIT -stackThreads=300 -loadedClasses=16497 -poolType=metaspace -vmOptions="$JAVA_OPTS") && echo JVM Memory Configuration: $CALCULATED_MEMORY && JAVA_OPTS="$JAVA_OPTS $CALCULATED_MEMORY" && SERVER_PORT=$PORT eval exec $PWD/.java-buildpack/open_jdk_jre/bin/java $JAVA_OPTS -cp $PWD/. org.springframework.boot.loader.JarLauncher`
184 |
185 | Showing health and status for app blog-ui in org APJ / space development as tmaki@pivotal.io...
186 | OK
187 |
188 | requested state: started
189 | instances: 1/1
190 | usage: 1G x 1 instances
191 | urls: blog-ui-tmaki.cfapps.io
192 | last uploaded: Wed Jan 24 07:16:09 UTC 2018
193 | stack: cflinuxfs2
194 | buildpack: client-certificate-mapper=1.2.0_RELEASE container-security-provider=1.8.0_RELEASE java-buildpack=v4.5-offline-https://github.com/cloudfoundry/java-buildpack.git#ffeefb9 java-main java-opts jvmkill-agent=1.10.0_RELEASE open-jdk-like-jre=1.8.0_1...
195 |
196 | state since cpu memory disk details
197 | #0 running 2018-01-24 04:17:33 PM 180.5% 300.5M of 1G 148.8M of 1G
198 | ```
199 |
200 |
201 | [https://blog-ui-your-account.cfapps.io](https://blog-ui-your-account.cfapps.io)にアクセスすると一覧画面が表示されます。
202 |
203 | 
204 |
205 | 記事画面にもアクセスしてください。
206 |
207 | 
208 |
209 | ### [補足] メモリを節約する
210 |
211 | `manifest.yml`を次のように変更し、コンテナメモリサイズを256MBに減らせます。
212 |
213 | ``` yaml
214 | applications:
215 | - name: blog-ui
216 | memory: 256m
217 | path: target/demo-blog-ui-0.0.1-SNAPSHOT.jar
218 | routes:
219 | - route: blog-ui-.cfapps.io
220 | env:
221 | BLOG_API_URI: https://blog-api-.cfapps.io
222 | JAVA_OPTS: '-XX:ReservedCodeCacheSize=32M -Xss512k -XX:+PrintCodeCache'
223 | JBP_CONFIG_OPEN_JDK_JRE: '[memory_calculator: {stack_threads: 24}]' # 4 (core) + 20 (etc)
224 | ```
225 |
226 | 詳細は[Java Memory Calculatorでメモリの調節](memory-calculator.md)を参照してください。
227 |
228 |
--------------------------------------------------------------------------------
/create-domain.md:
--------------------------------------------------------------------------------
1 | ## 独自ドメインの利用
2 |
3 | せっかく自分専用のブログができたので、ブログに独自ドメインを設定しましょう。
4 |
5 | Cloud FoundryはLoad Balancerで全てのHTTP(s)リクエストを受け付けてGoRouterというコンポーネントに転送します。
6 | GoRouterはHTTPリクエストヘッダの`Host`をみて、該当の`route`をもつアプリケーションのコンテナにリクエストをルーティングします。
7 |
8 | 
9 |
10 | アプリケーションには複数の`route`を設定可能であり、独自ドメインも利用可能です。アプリケーションに独自ドメインの`route`を設定し、
11 | そのドメインがDNSのCNAMEなどでLoad Balancerに向くようになっていれば、`Host`ヘッダはそのまま独自ドメインのものが使用されるため、
12 | そのドメインで対象のアプリケーションにアクセス可能になります。
13 |
14 | 
15 |
16 |
17 | ### Pivotal Web Servicesにドメインを追加
18 |
19 | 独自ドメインでPivotal Web Servicesにアクセスした場合に、対象のアプリケーションにルーティングさせるには、まずは自分のOrganizationにドメインを追加する必要があります。
20 |
21 |
22 | ```
23 | cf create-domain
24 | ```
25 |
26 | `cf domains`を実行して、独自ドメインが`owned`(`所有`)状態で追加されていることを確認してください。
27 |
28 |
29 | ```
30 | $ cf domains
31 | name status type
32 | cfapps.io shared
33 | cf-tcpapps.io shared tcp
34 | mydomain.test owned
35 | ```
36 |
37 |
38 | Blog UIの`manifest.yml`に、独自ドメインの`route`を追加します。
39 |
40 | ``` yaml
41 | applications:
42 | - name: blog-ui
43 | path: target/demo-blog-ui-0.0.1-SNAPSHOT.jar
44 | routes:
45 | - route: blog-ui-.cfapps.io
46 | - route: blog.
47 | env:
48 | BLOG_API_URI: https://blog-api-.cfapps.io
49 | ```
50 |
51 | `cf push`してください。
52 |
53 | 次のコマンドで`Host`ヘッダを変更しても、Blog UIにアクセスできることを確認してください。
54 |
55 | ```
56 | curl -k https://blog-ui-.cfapps.io -H "Host: blog."
57 | ```
58 |
59 | これで、Pivotal Web Servicesに独自ドメインでアクセス(`Host`ヘッダの値が独自ドメイン)した場合に、Blog UIにアクセスできるようになりました。
60 |
61 | 次に実際に独自ドメインがPivotal Web Servicesを向くようにDNSの設定を変更する必要があります。
62 |
63 | ### CloudFlareで独自ドメインを管理し、Pivotal Web Servicesに転送
64 |
65 | [CloudFlare](https://www.cloudflare.com/)を使うとドメインを管理するだけでなく、HTTPSプロキシサーバーように使うことができます。
66 | ブログの独自ドメインのCNAMEにPivotal Web ServicesのLBに向くホスト名(`api.run.pivotal.io`など)を設定すると、
67 | `Host`ヘッダを独自ドメインのままリクエストをPivotal Web Servicesに向けることができます。また、TLS TerminationもCloudFlareで行えます。
68 |
69 | 
70 |
71 | [https://www.cloudflare.com/](https://www.cloudflare.com/)でアカウントを作成し、自分のドメインを登録してください。
72 | ドメインを購入したサイト上で対象の独自ドメインのName ServerにCloudFlareのName Serverを登録してください。DNSのページの下部に
73 | (アカウント毎に異なるName Serverが用意されています)
74 |
75 | 
76 |
77 | ブログのホスト名を`blog.<独自ドメイン>`としたい場合は、
78 | DNSでCNAMEのNameに`blog`、Domain Nameに`api.run.pivotal.io`を設定してください。
79 |
80 | 
81 |
82 |
83 | CloudFlare <-> Pivotal Web Services間もTLS通信にしたい場合は、Cryptoは"Full"または"Full(Strict)"を選択してください(推奨)。
84 |
85 | 
86 |
87 |
88 | これで[https://blog.mydomain.test](https://blog.mydomain.test)でBlog UIにアクセスできます。
89 |
90 | > Cloud FlareのFreeプランではTLS証明書は多くのドメインでSAN(Subject Alternative Names)を共有する形になっています。
91 | > $10/month払うことで、専有のTLS証明書を使用することができます。
92 |
93 | ### Amazon CloudFrontからPivotal Web Servicesに転送
94 |
95 | Amazon Route53でドメインを管理している場合は、Amazon CloudFrontでリクエストをPivotal Web Servicesに転送可能です。
96 | この場合は、必ず`Host`ヘッダーを転送するようにしてください。
97 |
--------------------------------------------------------------------------------
/extra.md:
--------------------------------------------------------------------------------
1 | ### 追加機能の実装
2 |
3 | ハンズオンを通して、blog-api, blog-uiはブログアプリケーションとして最低限の機能しか実装されていないことがわかったと思います。
4 | ここからの実装は自由に行ってください。
5 |
6 | 追加すべき機能としては
7 |
8 | * ページング機能
9 | * タグの検索
10 | * カテゴリの検索
11 | * 記事の検索
12 | * エラーハンドリング
13 |
14 | などが挙げられます。Spring Boot 2を機能を試す題材として色々実装してみてください。
15 | Cloud Foundryを使うことでアプリケーション開発に集中できることを感じてもらえれば幸いです。
16 |
--------------------------------------------------------------------------------
/install.md:
--------------------------------------------------------------------------------
1 | ## 必要なソフトウェアのインストール
2 |
3 |
4 | * JDK 8 (JDK 9は対応しません)
5 | * git
6 | * Githubアカウント
7 | * `curl`
8 | * [Cloud Foundry CLI](https://github.com/cloudfoundry/cli#downloads)
9 | * [Pivotal Web Services](https://run.pivotal.io)のアカウント
10 |
11 |
12 |
13 | ```
14 | cf login -a api.run.pivotal.io
15 | ```
16 |
17 | でPivotal Web Servicesにログインできることを必ず確認してください。
18 |
--------------------------------------------------------------------------------
/memory-calculator.md:
--------------------------------------------------------------------------------
1 | ## Java Memory Calculatorでメモリの調節
2 |
3 | Cloud Foundryの[Java Buildpack](https://github.com/cloudfoundry/java-buildpack)では[Java Memory Calculator](https://github.com/cloudfoundry/java-buildpack-memory-calculator)によって自動でJVMに割り当てるメモリサイズが計算されます。
4 | デフォルトでは次の式で計算されます。
5 |
6 | * Heap Size = Container's `memory` Size - Native Size
7 | * Native Size = `-XX:MaxMetaSpaceSize` + `-XX:ReservedCodeCacheSize` + `-XX:MaxDirectMemorySize` + (`-Xss` * Number of threads)
8 |
9 | 各種JVMパラメーターはデフォルトで次の値が設定されます。
10 |
11 | * `-XX:MaxMetaSpaceSize` ... クラスファイル数から**自動で算出**
12 | * ~`-XX:CompressedClassSpaceSize` ... クラスファイル数から**自動で算出**~ <- `CompressedClassSpaceSize`は`MaxMetaSpaceSize`に内容される
13 | * `-XX:ReservervedCodeCacheSize` ... 240MB
14 | * `-XX:DirectMemorySize` ... 10MB
15 | * `-Xss` ... 1MB
16 | * Number of threads ... 250 (Tomcatのデフォルト最大スレッド数200 + その他50)
17 |
18 |
19 | ヒープサイズを意識することは多いですが、ネイティブサイズを細かく考えられる人は多くないです。
20 | 特に、コンテナ環境ではネイティブサイズは無視されがちで、予期せぬOut of Memory Errorを招きます。
21 |
22 | Java Buildpackはこのような設定項目を意識しなくても、コンテナのメモリサイズを指定さえすれば、
23 | アップロードされたファイルのクラス数から適切なメモリサイズを設定してくれるため、プラットフォームに設定を**お任せ**することができます。
24 |
25 | 一方で、ヒープサイズと`MaxMetaSpaceSize`、`CompressedClassSpaceSize`を抜いても
26 |
27 | ```
28 | 240M + 10M + 1M * 250 = 500M
29 | ```
30 |
31 | のメモリが必要です。通常は700MB〜1GBを設定しないとOut of Memory Errorでアプリケーションが起動しなくなります。
32 | コンテナのメモリが1GBの場合、ヒープサイズに割かれるのはおよそ350MBくらいです。
33 |
34 | 実際に設定されている値は起動時のログから確認可能です。
35 |
36 | クラス数とスレッド数はステージングのログに出力されます。
37 |
38 | 
39 |
40 | > スクリーンキャプチャ上は`300`と出力されていますが、デフォルトが`300`から`250`に変わりました。
41 |
42 | JVMへのパラメータはアプリケーションログの先頭に出力されます。
43 |
44 | 
45 |
46 |
47 | プロダクションでは通常1GB以上を設定することが推奨されますが、
48 | Pivotal Web Servicesの無償利用期間に2GB制限があるのと、メモリ従量課金であるため、
49 | 不要であればメモリサイズ小さい方が運用費を低く抑えられるのでできるだけ小さく設定したいことがあります。
50 | Pivotal Web Servicesではデフォルトのコンテナメモリサイズは1GBです。
51 |
52 | 大きく減らすことができるのはスレッドに必要なメモリ数と`ReservervedCodeCacheSize`です。
53 | スレッド数の多くはリクエストを処理するために使用されます。デフォルトが250で想定されているため、
54 | 同時にアクセスされる数が少ないと判断できる場合はこの値を大きく減らせます。
55 |
56 | `ReservervedCodeCacheSize`はJITコンパイルの結果をキャッシュするメモリサイズです。
57 | この値を小さくするとJITコンパイルが増えCPU使用率が高騰する可能性があります。
58 | パフォーマンスに影響しますが、コストを優先する場合は減らしても良いでしょう。
59 |
60 | > JVMオプションで`-XX:+PrintCodeCache`をつけるとJVM終了時にCodeCacheの使用量が出力されます。
61 | >
62 | > 
63 |
64 |
65 | Spring MVC (Tomcat)の場合とSpring WebFlux (Netty)の場合でそれぞれメモリの調節方法を説明します。
66 |
67 | ### Spring MVC (Tomcat)の場合
68 |
69 | Spring MVC (Tomcat)の場合は、スレッド数を減らす場合に`server.tomcat.max-threads`を指定して、同時に受け付けられる処理数を制限する必要があります。
70 | 指定しないとキャパシティを超えて処理してしまう可能性があります。その場合はOut of Memory Errorでクラッシュするでしょう。
71 |
72 | ここではmemoryを500MBに抑えるために、Tomcat側では
73 |
74 |
75 | `manifest.yml`の`env`を次のように変更してください。
76 |
77 | ``` yaml
78 | env:
79 | SERVER_TOMCAT_MAX_THREADS: 4
80 | JAVA_OPTS: '-XX:ReservedCodeCacheSize=32M -Xss512k -XX:+PrintCodeCache'
81 | JBP_CONFIG_OPEN_JDK_JRE: '[memory_calculator: {stack_threads: 24}]' # 4 (tomcat) + 20 (etc)
82 | ```
83 |
84 | `stack_threads`はJava Memory Calculatorが計算に使用するスレッド数(デフォルト:250)です。
85 |
86 | 上記の設定により、`ReservedCodeCacheSize` + スレッドメモリサイズ (デフォルト: 240M + 1M * 250 = 490M)は
87 |
88 | ```
89 | 32M + 0.5M * 24 = 44M
90 | ```
91 |
92 | まで減らせます。これにヒープサイズを16M減らすとコンテナのメモリサイズを合計462M (490 - 44 + 16)減らすことができます。
93 |
94 |
95 | これで次のように`manifest.yml`を設定可能です。
96 |
97 | ``` yaml
98 | memory: 462m
99 | env:
100 | SERVER_TOMCAT_MAX_THREADS: 4
101 | JAVA_OPTS: '-XX:ReservedCodeCacheSize=32M -Xss512k -XX:+PrintCodeCache'
102 | JBP_CONFIG_OPEN_JDK_JRE: '[memory_calculator: {stack_threads: 24}]' # 4 (tomcat) + 20 (etc)
103 | ```
104 |
105 | この設定で`cf push`するとステージング時に出力されるスレッド数が24に変更され、
106 |
107 | 
108 |
109 | 起動時のメモリオプションのログは算出された値のみになります。
110 |
111 | 
112 |
113 |
114 | ヒープサイズに余裕がある場合は、ヒープサイズを減らしてスレッドメモリサイズを増やしても構いません。
115 |
116 | > Pivotal Web Services上でJConsoleを使う方法は次のリンクを参照してください。
117 | >
118 | > https://discuss.pivotal.io/hc/en-us/articles/221330108-How-to-remotely-monitor-Java-applications-deployed-on-PCF-via-JMX
119 |
120 | ### Spring WebFlux (Netty)の場合
121 |
122 | Spring WebFlux (Netty)の場合はノンブロッキングアーキテクチャであるため、元々少ないスレッド数(CPUコア数)で沢山のリクエストを同時に処理できます。
123 | したがって、デフォルトのスレッド数250は大きすぎでありリソースの無駄です。スレッド数は24-32で十分でしょう。
124 | またヒープサイズもSpring MVCに比べて減らすことができるので、次の設定を使用します。
125 |
126 | ``` yaml
127 | memory: 256m
128 | env:
129 | JAVA_OPTS: '-XX:ReservedCodeCacheSize=32M -Xss512k -XX:+PrintCodeCache'
130 | JBP_CONFIG_OPEN_JDK_JRE: '[memory_calculator: {stack_threads: 24}]' # 4 (core) + 20 (etc)
131 | ```
132 |
133 | 同じ4スレッドでもスループットはSpring WebFluxの方が高くなるはずです。
134 |
135 | > Spring WebFluxの[Router Function](https://docs.spring.io/spring-framework/docs/5.0.x/spring-framework-reference/web-reactive.html#webflux-fn)フレームワークを使えば、更に低フットプリントなSpring WebFluxアプリを作成可能です。
136 | > 次のMaven Archetypeを使うとDIコンテナを使わずにWebアプリケーションを作成可能です。
137 | >
138 | > https://github.com/making/vanilla-spring-webflux-fn-blank
139 |
--------------------------------------------------------------------------------
/notification.md:
--------------------------------------------------------------------------------
1 | # HTML5のServer-Sent EventsとNotifications APIを使ってブログ記事の更新通知
2 |
3 | Blog APIの`@EventListener`に処理を追加して、記事の更新イベントをServer Pushで通知できるようにします。
4 | ここではBlog API側からはServer-Sent EventでEventを通知します。
5 | Blog UI側では`WebClient`でEventを購読して、再度HTMLにServer-Sent Eventでイベントを通知します。
6 |
7 | ```
8 | [Blog API] --(SSE)--> <--(WebClient)-- [Blog UI] --(SSE)--> <--(EventSource)--[HTML]
9 | ```
10 |
11 | ### Blog APIの修正
12 |
13 | https://github.com/making/demo-blog-api/commit/68883c41fc406f581bf0f4a42498a9bec4b4a820
14 |
15 | ### Blog UIの修正
16 |
17 | https://github.com/making/demo-blog-ui/commit/ac45538116eab7361ecf90e589b2a226867a8fe9
18 |
19 | ### 動作確認
20 |
21 | Blog API, Blog UIを再起動して、`sample-create-request.sh`、`sample-update-request.json`を実行してください。
22 |
23 | 
24 |
25 | ### Cloud Foundryへデプロイ
26 |
27 |
28 | ```
29 | cd ../demo-blog-api
30 | ./mvn clean package
31 | cf push
32 |
33 | cd ../demo-blog-ui
34 | ./mvn clean package
35 | cf push
36 | ```
37 |
38 | 記事の作成、更新でイベントが通知されることを確認してください。
39 |
40 | ### 📖 宿題: スケールアウト対策
41 |
42 | 今の実装ではBlog APIをスケールアウトした際に、WebHookがロードバランスしてしまうため、
43 | Server Pushをするインスタンスがラウンドロビンになってしまい、通知を受けられる場合と受けられない場合が出てしまいます。
44 |
45 | これを防ぐには、`EventNotifyer#notify`でRabbitMQやKafkaのようなMessage Brokerにメッセージを送信し、
46 | そのメッセージリスナーで`this.processor.onNext(event);`を実行する必要があります。
47 |
48 |
49 | Spring Cloud Streamを使う場合は次のような実装になるでしょう。
50 |
51 | ``` java
52 | @Component
53 | public class EventNotifyer {
54 | final UnicastProcessor processor = UnicastProcessor.create();
55 | final Flux flux;
56 | final Source source;
57 |
58 | public EventNotifyer(Source source) {
59 | this.flux = this.processor.publish().autoConnect().log("event").share();
60 | this.source = source;
61 | }
62 |
63 | public void notify(EntryEvent event) {
64 | source.output().send(MessageBuilder.withPayload(event).build());
65 | }
66 |
67 | @StreamListener(Sink.INPUT)
68 | public void onMessage(EntryEvent event) {
69 | this.processor.onNext(event);
70 | }
71 |
72 | public Publisher publisher() {
73 | return this.flux;
74 | }
75 |
76 | }
77 | ```
78 |
79 |
80 | [Spring Cloud Stream Tutorial](https://github.com/Pivotal-Japan/spring-cloud-stream-tutorial)を実践し、Message Broker版を実装してみてください。
81 |
--------------------------------------------------------------------------------
/prep.md:
--------------------------------------------------------------------------------
1 | ## 事前準備
2 |
3 | ハンズオンで作成するブログシステムの記事のフォーマットと、そのドメインクラスである`Entry`クラスについて説明します。
4 |
5 | **演習が3つありますので、ハンズオン前に必ず実施してください。**
6 |
7 | ### ブログ記事のフォーマット
8 |
9 | ブログ記事のフォーマットは次のようになります。
10 |
11 |
12 | ``` markdown
13 | ---
14 | title: Hello Spring Boot
15 | tags: ["Spring", "Spring Boot", "Java"]
16 | categories: ["Programming", "Java", "org", "springframework", "boot"]
17 | date: 2015-11-15T23:59:32+09:00
18 | updated: 2015-11-15T23:59:32+09:00
19 | ---
20 |
21 | Content(markdown)
22 | Here
23 |
24 | `date` and `updated` are optional.
25 | If `date` is not specified, first commit date is used.
26 | If `updated` is not specified, last commit date is used.
27 | ```
28 |
29 | `---`と`---`の間に記事のメタ情報(Front Matter)を書きます。Front Matter以降は記事本文(`content`)であり、[Markdown](https://guides.github.com/features/mastering-markdown/)形式です。サポートするMarkdown方言はレンダリング側(ブログのフロントエンド)で規定します。
30 |
31 | [Jekyll](https://jekyllrb.com/docs/frontmatter/)や[Hugo](https://gohugo.io/content-management/front-matter/)のFront Matterに似ていますが、同じではありません。
32 |
33 | Front Matterの仕様は
34 |
35 | * `title` ... 記事のタイトル / 文字列 / **必須**
36 | * `tags` ... 記事のタグ / 文字列リスト / 任意
37 | * `categories` ... 記事の階層カテゴリ / 文字列リスト / 任意
38 | * `date` ... 記事の作成時刻 / ISO8601のyyyy-MM-ddTHH:mm:ss.SSSZ / 任意
39 | * `updated` ... 記事の更新時刻 / ISO8601のyyyy-MM-ddTHH:mm:ss.SSSZ / 任意
40 |
41 | `date`や`update`は記事の時刻を固定したい場合にのみ使用し、基本的に記事の作成・更新時刻ははgitのコミット履歴を基に設定されることを想定しています。
42 |
43 | Front Matter以外の記事情報は次の通りです。
44 |
45 | * `entryId` ... エントリID / Long / **必須**
46 | * `created.name` ... 作成者名 / Long / **必須**
47 | * `created.date` ... 作成時刻 / ISO8601のyyyy-MM-ddTHH:mm:ss.SSSZ / **必須**
48 | * `updated.name` ... 更新者名 / Long / **必須**
49 | * `updated.date` ... 更新時刻 / ISO8601のyyyy-MM-ddTHH:mm:ss.SSSZ / **必須**
50 |
51 | `entryId`はファイル名から取得されます。ファイル名が`00100.md`であれば、`100`が`entryId`になります。
52 | `created`と`updated`はgitのコミット情報から得られる想定です。
53 | Front Matterの`date`、`updated`と意味が重複しますが、明示的に時刻を上書きしたい場合にFront Matterの`date`、`updated`を使用できます。
54 |
55 |
56 | サンプルプロジェクトは[https://github.com/making/demo-blog-posts](https://github.com/making/demo-blog-posts)です。
57 |
58 | ### ブログ記事のドメインオブジェクト
59 |
60 | ブログ記事のドメインオブジェクトをJavaで表現するライブラリが`am.ik.blog:blog-domain`で使えます。
61 |
62 | ``` xml
63 |
64 | am.ik.blog
65 | blog-domain
66 | 4.3.4
67 |
68 | ```
69 |
70 | 記事は`Entry`クラスで表現され、`Entry`オブジェクトはImmutableです。
71 |
72 | ``` java
73 | Entry entry = new Entry(new EntryId(100L), new Content("記事本文"),
74 | new Author(new Name("作成者名"), new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00"))),
75 | new Author(new Name("更新者名"), new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00"))),
76 | new FrontMatter(new Title("タイトル"),
77 | new Categories(new Category("カテゴリ1"), new Category("カテゴリ2")),
78 | new Tags(new Tag("タグ1"), new Tag("タグ2"))));
79 | ```
80 |
81 |
82 | `EntryBuilder`経由で作成すると便利です。
83 |
84 | ``` java
85 | Entry entry = Entry.builder()
86 | .entryId(new EntryId(100L))
87 | .content(new Content("記事本文"))
88 | .frontMatter(new FrontMatter(new Title("タイトル"),
89 | new Categories(new Category("カテゴリ1"), new Category("カテゴリ2")),
90 | new Tags(new Tag("タグ1"), new Tag("タグ2")))),
91 | .created(new Author(new Name("作成者名"), new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00"))))
92 | .updated(new Author(new Name("更新者名"), new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00"))))
93 | .build();
94 | ```
95 |
96 | Front Matterに`date`や`updated`を設定する場合は、次のように作成できます。
97 |
98 | ``` java
99 | Entry entry = Entry.builder()
100 | .entryId(new EntryId(100L))
101 | .content(new Content("記事本文"))
102 | .frontMatter(new FrontMatter(new Title("タイトル"),
103 | new Categories(new Category("カテゴリ1"), new Category("カテゴリ2")),
104 | new Tags(new Tag("タグ1"), new Tag("タグ2")),
105 | new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00" /* date */)),
106 | new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00" /* updated */)))),
107 | .created(new Author(new Name("作成者名"), new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00"))))
108 | .updated(new Author(new Name("更新者名"), new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00"))))
109 | .build()
110 | .useFrontMatterDate();
111 | ```
112 |
113 | Markdownファイルから`Entry`オブジェクトを作成する場合は、次のように`EntryFactory`から作成可能です。返り値は`Optional`になります。
114 | ファイルの内容からは`entryId`やgitコミット履歴の`created`、`updated`は判断できないため、`EntryBuilder`を返し、残りのフィールドは個別に設定する必要があります。
115 |
116 | ``` java
117 | EntryFactory factory = new EntryFactory();
118 | Resource file = new ClassPathResource("foo/bar/00100.md"); // クラスパス上のMarkdownファイルから作成
119 | // Resource file = nnew FileSystemResource("/tmp/foo/bar/00100.md"); // ファイルシステム上のMarkdownファイルから作成
120 | Optional entryBuilder = factory.createFromYamlFile(file);
121 | Optional entry = entryBuilder.map(
122 | builder -> builder.entryId(EntryId.fromFileName(file.getFilename()))
123 | .created(new Author(new Name("作成者名"), new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00"))))
124 | .updated(new Author(new Name("更新者名"), new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00"))))
125 | .build());
126 | ```
127 |
128 | 次のように、Markdownのファイル名とファイル内容(文字列)から`Entry`オブジェクトを作成することもできます。
129 |
130 | ``` java
131 | EntryFactory factory = new EntryFactory();
132 | Optional entryBuilder = factory.parseBody(new EntryId(100L), "00100.mdのファイルの中身");
133 | Optional entry = entryBuilder.map(builder -> builder
134 | .created(new Author(new Name("作成者名"), new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00"))))
135 | .updated(new Author(new Name("更新者名"), new EventTime(OffsetDateTime.parse("2018-01-12T18:31:25.899+09:00"))))
136 | .build());
137 | ```
138 |
139 | ### ✏️演習1: `Entry`オブジェクトを作成してみる
140 |
141 | ここまでの内容を試すために、[https://github.com/making/blog-handson-prep](https://github.com/making/blog-handson-prep)を`git clone`して、
142 | [`EntryCreator`](https://github.com/making/blog-handson-prep/blob/master/src/main/java/com/example/blog/EntryCreator.java)を実装して、
143 | [`EntryCreatorTest`](https://github.com/making/blog-handson-prep/blob/master/src/test/java/com/example/blog/EntryCreatorTest.java)がグリーンになるようにしてください。テストコードは修正する必要はありません。
144 |
145 | (ヒント:上記のサンプルコードほぼそのままでグリーンになるはずです)
146 |
147 | ### GitHub APIを使って、`Entry`オブジェクトを作成
148 |
149 | `Entry`オブジェクトをGitHubレポジトリ上のMarkdownコンテンツから[Content API](https://developer.github.com/v3/repos/contents/)と[Commits API](https://developer.github.com/v3/repos/commits/)を使用して作成しましょう。
150 |
151 | ここではGitHub APIをSpring 5の`WebClient`を使ってアクセスするラッパーライブラリを使用します。
152 |
153 | ``` xml
154 |
155 | am.ik.github
156 | reactive-github-client
157 | 0.0.4
158 |
159 | ```
160 |
161 |
162 |
163 | ``` java
164 | WebClient.Builder builder = WebClient.builder();
165 | GitHubClient client = new GitHubClient(builder,
166 | new AccessToken(""));
167 | ```
168 |
169 | Access Tokenは[Personal access tokens](https://github.com/settings/tokens)ページから"Generate new token"ボタンをクリックして生成してください。`Select scopes`では`public_repo`を選択してください。
170 |
171 | #### Content APIで`EntryBuilder`オブジェクトの作成
172 |
173 | Content APIを使ってファイルを取得するには次のように書けば良いです。
174 |
175 | ``` java
176 | Mono file = client.file("making", "demo-blog-posts", "content/00001.md").get();
177 | file.map(f -> f.decode())
178 | .doOnNext(System.out::println)
179 | .block(); // 非同期処理中にプログラムが終わらないようにblockする
180 | ```
181 |
182 | `client.file("owner名", "repository名", "Markdownファイルまでのパス").get()`で`Mono`型のオブジェクトを取得できます。
183 | GitHubのAPIで返るファイルの中身はBase64でエンコードされているため、ファイル内容をデコードする`decode()`メソッドを呼ぶことで記事本文を取得できます。
184 |
185 | ちなみに、`File`は`am.ik.github.repositories.contents.ContentsResponse.File`であり、`java.io.File`ではありません。
186 |
187 | この記事本文を`EntryFactory`に渡します。
188 |
189 | ``` java
190 | EntryId entryId = new EntryId(1L);
191 | Mono file = client.file("making", "demo-blog-posts", String.format("content/%05d.md", entryId.getValue())).get();
192 | Mono builder = file
193 | .map(f -> new EntryFactory().parseBody(entryId, f.decode()))
194 | .flatMap(b -> Mono.justOrEmpty(b));
195 | ```
196 |
197 | すでに見てきたように`EntryFactory.parseBody`の返り値は`Optional`なので、`Optional`から`Mono`に変換するために、`Mono.justOrEmpty`を使用します。
198 | ラムダ式の返り値が`Mono`(`Publisher`)なので、`subscribe`を伝播させるために`map`ではなく`flatMap`を使用します。その結果`Mono`を得られます。
199 |
200 |
201 | #### Commits APIから`Author`オブジェクトの作成
202 |
203 | 次にCommits APIから、作成者名・作成時刻および更新者名・更新時刻を取得します。
204 |
205 | ``` java
206 | Flux commits = client.commits("making", "demo-blog-posts")
207 | .get(params -> params
208 | .path(String.format("content/%05d.md", entryId.getValue())));
209 | ```
210 |
211 | 返り値は`Flux`です。
212 |
213 | この`Flux`の先頭が最終更新コミット、最後が作成コミットとみなし、`collectList()`メソッドを使い、一旦`List`にまとめ、先頭と最後の`Commit`を取得します。
214 | これを後述の`toAuthor`メソッドに渡し、`Author`オブジェクトを作成します。
215 |
216 | ``` java
217 | commits.collectList()
218 | .doOnNext(list -> {
219 | Author updated = toAuthor(list.get(0));
220 | Author created = toAuthor(list.get(list.size() - 1));
221 | });
222 | ```
223 |
224 | `toAuthor`メソッドは次の通りです。
225 |
226 | ``` java
227 | private static Author toAuthor(Commit commit) {
228 | Committer committer = commit.getCommit().getAuthor();
229 | return new Author(new Name(committer.getName()), new EventTime(committer.getDate()));
230 | }
231 | ```
232 |
233 | 2つの`Author`をまとめて返せるように、`Tuple`を使います。
234 |
235 | ``` java
236 | Mono> authors = commits.collectList()
237 | .map(list -> {
238 | Author updated = toAuthor(list.get(0));
239 | Author created = toAuthor(list.get(list.size() - 1));
240 | return Tuples.of(created, updated);
241 | });
242 | ```
243 |
244 | #### `EntryBuilder`と`Author`の合成
245 |
246 | `EntryBuilder`と`Author`をそれぞれ非同期で取得できましたが、`Entry`を作成するためには、これらを合成する必要があります。
247 | まず、
248 |
249 | `Mono`と`Mono>`の二つの`Mono`を`Mono.zip`で合成します。さらに`map`で`EntryBuilder`と`created`、`updated`を使い`Entry`オブジェクトを完成させます。
250 |
251 |
252 | ``` java
253 | Mono entry = Mono.zip(builder, authors)
254 | .map(t -> {
255 | EntryBuilder entryBuilder = t.getT1();
256 | Author created = t.getT2().getT1();
257 | Author updated = t.getT2().getT2();
258 | return entryBuilder
259 | .created(created)
260 | .updated(updated)
261 | .build()
262 | .useFrontMatterDate();
263 | });
264 | ```
265 |
266 | これでノンブロッキングでGitHub APIから`Entry`オブジェクトを作ることができました。
267 |
268 |
269 | ### ✏️演習2: Mock GitHub APIにアクセスして`Entry`オブジェクトを作成してみる
270 |
271 | ここまでの内容を試すために、[`EntryFetcher`](https://github.com/making/blog-handson-prep/blob/master/src/main/java/com/example/blog/webhook/EntryFetcher.java)を実装して、
272 | [`EntryFetcherTest`](https://github.com/making/blog-handson-prep/blob/master/src/test/java/com/example/blog/webhook/EntryFetcherTest.java)がグリーンになるようにしてください。テストコードは修正する必要はありません。
273 |
274 | (ヒント:上記のサンプルコードほぼそのままでグリーンになるはずです)
275 |
276 | ### ✏️演習3: アクセストークンを使って実際のGitHub APIにアクセス
277 |
278 | [https://github.com/making/demo-blog-posts](https://github.com/making/demo-blog-posts)をフォークし、
279 | [`MyGithubAccessTest`](https://github.com/making/blog-handson-prep/blob/master/src/test/java/com/example/blog/MyGithubAccessTest.java)に自分のGithubアカウントとアクセストークンを設定し、テスト結果がグリーンになるようにしてください。
280 |
281 | 演習3は`EntryFetcher`実装の再確認とアクセストークンの疎通チェックを目的としています。
282 |
283 |
--------------------------------------------------------------------------------