├── 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 | ![image](https://user-images.githubusercontent.com/106908/35030944-363f5740-fba4-11e7-88a5-b2c387eedc16.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35192495-24d1e186-fed7-11e7-8ff6-5e7b4305562f.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35192523-b092613c-fed7-11e7-8a6a-4701561d2bcd.png) 29 | 30 | 31 | `EntryControllerTest`と`WebhookControllerTest`がエラーになるでしょう。 32 | 33 | ![image](https://user-images.githubusercontent.com/106908/35192552-12233a84-fed8-11e7-8bdf-e946c6692d1f.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35192604-3f7a794c-fed9-11e7-9e1c-084abbc27b27.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35192678-b4526e5e-feda-11e7-97d3-4af4397c92f4.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35192759-52315504-fedb-11e7-8565-7de4b1136135.png) 78 | 79 | 80 | #### 記事全件取得APIの動作確認 81 | 82 | [http://localhost:8080/v1/entries](http://localhost:8080/v1/entries)にアクセスしてください。 83 | 84 | ![image](https://user-images.githubusercontent.com/106908/35192818-7984029a-fedc-11e7-8004-3b6c47c8641e.png) 85 | 86 | 87 | #### 記事1件取得APIの動作確認 88 | 89 | [http://localhost:8080/v1/entries/100](http://localhost:8080/v1/entries/100)にアクセスしてください。 90 | 91 | ![image](https://user-images.githubusercontent.com/106908/35192820-85212d08-fedc-11e7-8030-ad8251e822db.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35196207-7176d786-ff12-11e7-8934-41c03a206d2b.png) 389 | 390 | レポジトリ名は任意の値で構いません。ここでは`my-blog`として説明します。"Initialize this repository with a README"にチェックを入れて下さい。 391 | 392 | ![image](https://user-images.githubusercontent.com/106908/35196247-d8b5581e-ff12-11e7-8852-60b9c005f23c.png) 393 | 394 | 395 | "Settings"をクリックして、"Webhooks"をクリックし、"Add webhook"をクリックして下さい。 396 | 397 | ![image](https://user-images.githubusercontent.com/106908/35196316-a700979c-ff13-11e7-8fe6-2cdbe12b5ebd.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35196359-51245362-ff14-11e7-9b49-e5426b3a783b.png) 406 | 407 | 408 | Topに戻って、"Create new file"をクリックして下さい。 409 | 410 | ![image](https://user-images.githubusercontent.com/106908/35196299-5af8748c-ff13-11e7-8f4c-00cf86c4a9b1.png) 411 | 412 | ファイル名に`content/00001.md`を入力して下さい。 413 | 414 | ![image](https://user-images.githubusercontent.com/106908/35196454-8d3a5a26-ff15-11e7-80a5-250939416d24.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35196464-b08bd144-ff15-11e7-89e7-87758b67fdba.png) 429 | 430 | "Commit new file"ボタンをクリックして下さい。 431 | 432 | Webhook画面に戻って、最新のWebhookが✅になっていることを確認して下さい。 433 | 434 | ![image](https://user-images.githubusercontent.com/106908/35196515-5c40daca-ff16-11e7-9d01-c438b8a10bee.png) 435 | 436 | これでデータベースが更新されているので、 437 | [https://blog-api-your-account.cfapps.io/v1/entries](https://blog-api-your-account.cfapps.io/v1/entries)にアクセスすると作成した記事が返ります。 438 | 439 | ![image](https://user-images.githubusercontent.com/106908/35196505-4599b4c2-ff16-11e7-8b39-6a4208cb6a3a.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35192499-35e84726-fed7-11e7-851c-77e46f940dec.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35318337-30c69600-011e-11e8-8764-4b31ba12793c.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35318670-af8e56f2-011f-11e8-8aa5-3cbda470de7e.png) 48 | 49 | ### アプリケーションの実行 50 | 51 | 52 | blog-apiが起動した状態で、`com.example.blog.DemoBlogUiApplication`を実行して下さい。 53 | 54 | #### 一覧画面の動作確認 55 | 56 | [http://localhost:8082](http://localhost:8082)にアクセスしてください。 57 | 58 | ![image](https://user-images.githubusercontent.com/106908/35318772-32e434c2-0120-11e8-9760-18663b5fefe8.png) 59 | 60 | 61 | #### 記事画面の動作確認 62 | 63 | [http://localhost:8082/entries/100](http://localhost:8082/entries/100)にアクセスしてください。 64 | 65 | 66 | ![image](https://user-images.githubusercontent.com/106908/35318797-49006dc0-0120-11e8-8aad-6e08023be77c.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35319227-3d26a062-0122-11e8-95bf-b8ac2d71d165.png) 204 | 205 | 記事画面にもアクセスしてください。 206 | 207 | ![image](https://user-images.githubusercontent.com/106908/35319235-43c4395c-0122-11e8-95ac-c8544b49e8ff.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35513402-3f97e4a0-0546-11e8-86be-6003fef2ebce.png) 9 | 10 | アプリケーションには複数の`route`を設定可能であり、独自ドメインも利用可能です。アプリケーションに独自ドメインの`route`を設定し、 11 | そのドメインがDNSのCNAMEなどでLoad Balancerに向くようになっていれば、`Host`ヘッダはそのまま独自ドメインのものが使用されるため、 12 | そのドメインで対象のアプリケーションにアクセス可能になります。 13 | 14 | ![image](https://user-images.githubusercontent.com/106908/35514687-5c584338-054a-11e8-8a4b-51e28da03f41.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35514699-68683c3c-054a-11e8-875d-eda3a12d6f2b.png) 70 | 71 | [https://www.cloudflare.com/](https://www.cloudflare.com/)でアカウントを作成し、自分のドメインを登録してください。 72 | ドメインを購入したサイト上で対象の独自ドメインのName ServerにCloudFlareのName Serverを登録してください。DNSのページの下部に 73 | (アカウント毎に異なるName Serverが用意されています) 74 | 75 | ![image](https://user-images.githubusercontent.com/106908/35515442-86b94a62-054c-11e8-900c-fc9f5520627e.png) 76 | 77 | ブログのホスト名を`blog.<独自ドメイン>`としたい場合は、 78 | DNSでCNAMEのNameに`blog`、Domain Nameに`api.run.pivotal.io`を設定してください。 79 | 80 | ![image](https://user-images.githubusercontent.com/106908/35514342-3b924726-0549-11e8-8b83-f90c72ead33d.png) 81 | 82 | 83 | CloudFlare <-> Pivotal Web Services間もTLS通信にしたい場合は、Cryptoは"Full"または"Full(Strict)"を選択してください(推奨)。 84 | 85 | ![image](https://user-images.githubusercontent.com/106908/35513899-d7881d7e-0547-11e8-9847-c56b7cc3455a.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35510377-2db19610-053b-11e8-8758-4263e2e0ab85.png) 39 | 40 | > スクリーンキャプチャ上は`300`と出力されていますが、デフォルトが`300`から`250`に変わりました。 41 | 42 | JVMへのパラメータはアプリケーションログの先頭に出力されます。 43 | 44 | ![image](https://user-images.githubusercontent.com/106908/35510357-1c1d8300-053b-11e8-8123-134897d33c9e.png) 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 | > ![image](https://user-images.githubusercontent.com/106908/35508867-56ea108a-0535-11e8-835e-4751f11213fc.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35510303-eaf703fa-053a-11e8-8302-a9e8cf162283.png) 108 | 109 | 起動時のメモリオプションのログは算出された値のみになります。 110 | 111 | ![image](https://user-images.githubusercontent.com/106908/35510342-0ee80b60-053b-11e8-8b51-131f74ad55fd.png) 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 | ![image](https://user-images.githubusercontent.com/106908/35485244-1fdca908-04a0-11e8-8842-847101649c52.png) 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 | --------------------------------------------------------------------------------