Gradle で Multi-project を作成する ( 感想 )
記事一覧はこちらです。
Multi-project は Gradle Guides の Creating Multi-project Builds を見ながらやればそれ程難しくはない印象です。単に1つのプロジェクトに複数のサブプロジェクトを入れるだけ(スタブやテスト用のアプリケーションを置くだけで設定の共通化をしない)ならば settings.gradle に include を書くだけでよいのは簡単で便利だなと思いました。
Spring Boot ベースのプロジェクトで実行可能 Jar ではなくライブラリの jar を作成するには build.gradle に
bootJar { enabled = false } jar { enabled = true }
を記述すればよいということも今回初めて知りました。Spring Boot もバージョン 2.x から bootJar タスクで実行可能 Jar を作成するようになりましたが、バージョン 1.x の頃の記事を見直してみると jar タスクを実行してから bootRepackage タスクを実行していました。これまでこの違いをきちんと認識出来ていませんでした。。。
実行可能 Jar を起動するのに launcher subclass というものがあること、launcher subclass には JarLauncher, WarLauncher, PropertiesLauncher があること、PropertiesLauncher を使用すれば実行可能 Jar の外側に置いたライブラリ Jar を利用することができること、も今回始めて知りました。PropertiesLauncher を使用するかと聞かれると1つの実行可能 Jar にまとめてリリースした方がミスしなさそうなので自分は使用しない気がしますが、実際に本番でも利用されているものなのでしょうか?
Vue.js のプロジェクトで生成したファイルを Spring Boot の実行可能 Jar に含めようとするのは意外に大変でした。
- まさかデフォルトでは build タスクが存在しないとは。。。 Base Plugin を使えばよいのですが、Gradle は Java のプロジェクトで使われることが多く Java のプロジェクトでは Java Plugin が使われるので Base Plugin の存在になかなか気づけませんでした。
- build タスクを実行した時のサブプロジェクトの実行順がアルファベット順になるということも初めて知りました。今回 backend-app と frontend-app という名前にしたので、frontend-app → backend-app の順に build したいのにいろいろ設定しても backend-app → frontend-app の順が変わりませんでした。最終的には
copyDistToStatic.dependsOn ":frontend-app:build"
と記述することで解決しましたが、他にも解決策として書かれている記事をいくつか見つけて試したのですがなかなか変わらなかったんですよね。まだまだ Gradle を理解できていませんね。。。
今更ながら draw.io のことを知りましたが、とても便利ですね。これで個人・商用問わず無料らしいとは驚きです。EIP の Shape も結構揃っているので Spring Integration のフローを記述するのにも使えそうです。
Gradle で Multi-project を作成する ( その15 )( vuejs+springboot編、frontend-app プロジェクトを作成する )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
- frontend を Vue.js で、backend を Spring Boot でアプリケーションを実装し、各アプリケーションをサブプロジェクトとする Gradle Multi-project のサンプルを作成します。
- 今回は Vue.js のアプリケーション(frontend)を作成し、gradle build で実行可能 jar にまとめます。
参照したサイト・書籍
Vuetify
https://vuetifyjs.com/en/How to build a web app with Vue, Vuetify and Axios
https://medium.com/javascript-in-plain-english/implement-movie-app-with-vue-vuetify-axios-open-movie-database-api-d12290318cf9axios、async/awaitを使ったHTTPリクエスト(Web APIを実行)
https://qiita.com/shisama/items/61cdcc09dc69fd8d3127async関数においてtry/catchではなくawait/catchパターンを活用する
https://qiita.com/akameco/items/cc73afcdb5ac5d0774bcVue.jsでビューの変更がされないときに疑うこと+主な解決策方法
https://cloudpack.media/41984Vue Test Utils
https://vue-test-utils.vuejs.org/Vue.jsのテストでコンポーネントをいい感じにwrapする方法
https://qiita.com/ykhirao/items/8e8a9547a693c677813cVue CLI - Configuration Reference - devServer.proxy
https://cli.vuejs.org/config/#devserver-proxyGradle Docs - The Base Plugin
https://docs.gradle.org/current/userguide/base_plugin.htmlnode-gradle/gradle-node-plugin
https://github.com/node-gradle/gradle-node-pluginnode-gradle/gradle-node-plugin - Node Plugin
https://github.com/node-gradle/gradle-node-plugin/blob/master/docs/node.md
目次
- Vue.js のアプリケーション(frontend)(frontend-app)を作成する
- Gradle の build タスク実行時に
npm run test:unit
、npm run build
が実行されるようにする - Gradle の build タスク実行時に frontend-app の dist ディレクトリの下にあるファイルを backend-app の src/main/resources/static にコピーする
- settings.gradle に sample-cmdapp プロジェクトの include 文を追加する
- clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う
- 実行可能 jar から Tomcat を起動して動作確認する
- 次回は。。。
手順
Vue.js のアプリケーション(frontend)(frontend-app)を作成する
Vue CLI をインストールする
以下のコマンドを実行して Vue CLI をインストールします。
npm install -g @vue/cli
npm install -g @vue/cli-service-global
以下の警告メッセージが出ていますが、TypeScript、GraphQL は使用しないので今回は何もしません。
ts-node@8.1.0 requires a peer of typescript@>=2.0 but none is installed.
apollo-tracing@0.5.2 requires a peer of graphql@0.10.x - 14.1.x but none is installed.
インストールされた Vue CLI のバージョンを vue --version
コマンドで確認すると 3.7.0 でした。
Vue CLI で frontend-app プロジェクトを作成する
コマンドラインから vue create frontend-app
を実行して frontend-app プロジェクトを作成します。
※「Please pick a preset:」では「Manually select features」を選択します。
※「Check the features needed for your project:」はデフォルトで「Babel」「Linter / Formatter」が選択済みで、「Unit Testing」を追加で選択します。
※「Pick a linter / formatter config:」では「ESLint + Prettier」を選択します。
※「Pick additional lint features:」はデフォルトで「Lint on save」が選択済みで、そのままにします。
※「Pick a unit testing solution:」では「Jest」を選択します。
※「Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Use arrow keys)」では「In dedicated config files」を選択します。
※「Save this as a preset for future projects? (y/N)」は何も入力せずに Enter キーを押します。
インストールが実行されます。
インストール後 frontend-app ディレクトリに移動してから npm run serve
コマンドを実行して、
http://localhost:8080/ にアクセスすると画面が表示されました。
npm run build
コマンドを実行すると、警告・エラーは出ずに終了します。
IntelliJ IDEA の Project Tool Window で frontend-app のディレクトリ構成を見てみると以下のようになっていました。尚、IntelliJ IDEA で最初に表示させる時には「Indexing…」のメッセージが表示されてしばらく時間がかかります(おそらく node_modules の下の大量のファイルを index するのに時間がかかっているためでしょう)。
Vuetify をインストールする
Quick start の記述に従い vue add vuetify
コマンドを実行します。
※「Choose a preset: (Use arrow keys)」はデフォルトの「Default (recommended)」のままにします。
インストール直後の状態で npm run build
コマンドを実行すると prettier のフォーマットと異なるために eslint が警告を大量に出すので、npm run build
コマンド実行時に prettier で自動フォーマットして警告が出ないようにします。
npm run build
、npm run lint
、npm run test:unit
実行時に prettier で自動フォーマットするよう設定する
npm install --save-dev npm-run-all
コマンドを実行して npm-run-all をインストールします。
frontend-app/package.json の "scripts": { ... }
を以下のように変更します。
"scripts": { "serve": "vue-cli-service serve", "build": "run-s prettier:format vue-cli-service:build", "lint": "run-s prettier:format vue-cli-service:lint", "test:unit": "run-s prettier:format vue-cli-service:test:unit", "vue-cli-service:build": "vue-cli-service build", "vue-cli-service:lint": "vue-cli-service lint", "vue-cli-service:test:unit": "vue-cli-service test:unit", "prettier:format": "prettier --write {src,tests}/**/*.{js,vue}" },
- 以下の4行を追加します。今回初めて知りましたが、Paste した時に Paste した行や1つ上の行の末尾に "," がないと IntelliJ IDEA が自動で付けてくれますね。
"vue-cli-service:build": "vue-cli-service build"
"vue-cli-service:lint": "vue-cli-service lint"
"vue-cli-service:test:unit": "vue-cli-service test:unit"
"prettier:format": "prettier --write {src,tests}/**/*.{js,vue}"
"build": "vue-cli-service build"
→"build": "run-s prettier:format vue-cli-service:build"
に変更します。"lint": "vue-cli-service lint"
→"lint": "run-s prettier:format vue-cli-service:lint"
に変更します。"test:unit": "vue-cli-service test:unit"
→"test:unit": "run-s prettier:format vue-cli-service:test:unit"
に変更します。
以上で設定は完了です。再度 npm run build
コマンドを実行してみると2件 warning が出ていますが Build complete. が表示されました。
2件の warning はファイルサイズや lazy load に関するものでしたので、今回は解消せずに先に進みます。
また IntelliJ IDEA 上でも prettier でフォーマットできるように設定します。メインメニューから「File」-「Settings...」を選択して「Settings」ダイアログを表示させた後、画面右上に prettier
と入力して Prettier Plugin の設定画面を開いてから、画面右側の「Prettier package」に frontend-app/node_modules/prettier の絶対パスを設定します。
WebAPI を呼び出して取得したデータを画面に表示する処理を実装する
WebAPI を呼び出すのに axios を使用するので npm install --save axios
コマンドを実行してインストールします。
frontend-app/src/components/HelloWorld.vue のファイル名を CallSampleWebapi.Vue に変更します。
frontend-app/src/App.vue を以下のように変更します。
<template> <v-app> <v-content> <CallSampleWebapi /> </v-content> </v-app> </template> <script> import CallSampleWebapi from "./components/CallSampleWebapi"; export default { name: "App", components: { CallSampleWebapi: CallSampleWebapi }, data() { return { // }; } }; </script>
<v-toolbar app>...</v-toolbar>
を削除します。HelloWorld
→CallSampleWebapi
に一括置換します。
frontend-app/src/components/CallSampleWebapi.Vue を以下のように変更します。
<template> <v-container> <v-layout text-xs-center wrap> <v-flex xs12> <div>code: {{ code }}</div> <div>value: {{ value }}</div> <v-btn color="info" v-on:click="callSampleWebapi">クリック!</v-btn> </v-flex> </v-layout> </v-container> </template> <script> import axios from "axios"; export default { data: function() { return { code: "(空)", value: "(空)" }; }, methods: { callSampleWebapi: async function() { try { const response = await axios.post("/webapi/sample"); this.$set(this, "code", response.data.code); this.$set(this, "value", response.data.value); } catch (err) { alert(err); } } } }; </script> <style></style>
frontend-app/tests/unit/example.spec.js もファイル名を CallSampleWebapi.spec.js に変更した後、以下の内容に変更します。
import { mount } from "@vue/test-utils"; import Vue from "vue"; import Vuetify from "vuetify"; import CallSampleWebapi from "@/components/CallSampleWebapi.vue"; describe("CallSampleWebapi.vue test", () => { it("init render test", () => { Vue.use(Vuetify); const wrapper = mount(CallSampleWebapi, {}); expect(wrapper.html()).toContain("<div>code: (空)</div>"); }); });
vue.config.js を作成して webpack-dev-server に proxy の設定を追加する
npm run serve
コマンドで起動した webpack-dev-server に http://localhost:8080/webapi/sample でアクセスしたら backend-app の http://localhost:8081/webapi/sample へリクエストを転送させるための設定を行います。
frontend-app の下に vue.config.js を新規作成した後、以下の内容を記述します。changeOrigin: true
を記述すると backend-app 側で @RestController
アノテーションを付与したクラスに @CrossOrigin
アノテーションを付与して設定しなくてもアクセスできるようになります。
module.exports = { devServer: { proxy: { "^/webapi/sample": { target: "http://localhost:8081", changeOrigin: true } } } };
動作確認
backend-app の Tomcat を develop profile で起動してから、
npm run serve
コマンドを実行して frontend-app の webpack-dev-server を起動した後、
http://localhost:8080/ にアクセスすると以下の画面が表示されます。
「クリック!」ボタンをクリックすると backend-app の WebAPI を呼び出して取得した code, value の値が画面に表示されました。
Tomcat、webpack-dev-server を停止します。
npm run test:unit
コマンドを実行するとテストも成功します。
npm run build
コマンドも2件の warning は出たままですが、他の警告・エラーは出ずに Build complete. が表示されました。
Gradle の build タスク実行時に npm run test:unit
、npm run build
が実行されるようにする
frontend-app ディレクトリの下に build.gradle を新規作成し、以下の内容を記述します。
plugins { id "base" id "com.github.node-gradle.node" version "1.3.0" } clean.delete "dist" task npmTestUnit(type: NpmTask) { args = ["run", "test:unit", "2>&1"] execOverrides { it.standardOutput = new ByteArrayOutputStream() } } task npmBuild(type: NpmTask) { args = ["run", "build", "2>&1"] execOverrides { it.standardOutput = new ByteArrayOutputStream() } } npmBuild.dependsOn npmTestUnit build.dependsOn npmBuild
id "base"
を記述することで clean や build タスクが使えるようになります。npm run ...
コマンドの実行には gradle-node-plugin を使用します。- args に
"2>&1"
を指定して標準エラー出力を標準出力にリダイレクトし、execOverrides { it.standardOutput = new ByteArrayOutputStream() }
を記述することで標準出力、標準エラー出力に何も出力されないようにします。
ちなみに標準出力、標準エラー出力の内容をファイルに出力したい場合には、以下のように記述すれば frontend-app/build/tests/stdout.txt に出力されます。
task npmTestUnit(type: NpmTask) { args = ["run", "test:unit", "2>&1"] def testsDir = "${projectDir}/build/tests" execOverrides { // it.standardOutput = new ByteArrayOutputStream() it.standardOutput = new FileOutputStream("${testsDir}/stdout.txt") } doFirst { mkdir testsDir } }
Gradle の build タスク実行時に frontend-app の dist ディレクトリの下にあるファイルを backend-app の src/main/resources/static にコピーする
backend-app/build.gradle を以下のように変更します。
.......... dependencies { .......... } clean.delete fileTree("src/main/resources/static").include("**/*") task copyDistToStatic(type: Copy) { from project(":frontend-app").file("dist") into "src/main/resources/static" } copyDistToStatic.dependsOn ":frontend-app:build" processResources.dependsOn copyDistToStatic
clean.delete fileTree("src/main/resources/static").include("**/*")
~processResources.dependsOn copyDistToStatic
の記述を追加します。
settings.gradle に frontend-app プロジェクトの include 文を追加する
gradle-multiprj-vuejs-springboot の settings.gradle に include 'frontend-app'
を追加します。
rootProject.name = 'gradle-multiprj-vuejs-springboot' include 'backend-app' include 'frontend-app'
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。Gradle Tool Window に frontend-app が表示されます。
clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う
clean タスクを実行すると backend-app は src/main/resources/static の下がクリアされて、frontend-app は dist ディレクトリが削除されます。
build タスクを実行すると警告・エラーは出ずに BUILD SUCCESSFUL が表示されて、
frontend-app の dist ディレクトリの下のディレクトリ・ファイル一式が backend-app の src/main/resources/static の下にコピーされます。
実行可能 jar から Tomcat を起動して動作確認する
backend-app/build/libs の下に backend-app-1.0.0-RELEASE.jar が生成されていますので、
コマンドラインから java -Dspring.profiles.active=product -jar backend-app-1.0.0-RELEASE.jar
を実行します。
http://localhost:8080/ にアクセスすると以下の画面が表示されて、
「クリック!」ボタンをクリックすると WebAPI を呼び出して取得したデータが画面に表示されました。
次回は。。。
frontend-app の下に作成した build.gradle のタスクを build タスク実行時に動かすための The Base Plugin を見つけるまでが意外に時間がかかりました。書いてある記事が少ないのか、なかなか見つけられなかったんですよね。。。
これでとりあえずやりたいことは全てやったので、最後に感想を書いて終わりにします。
履歴
2019/05/08
初版発行。
Gradle で Multi-project を作成する ( その14 )( vuejs+springboot編、Multi-project のベースと backend-app プロジェクトを作成する )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
目次
- 方針
- gradle-multiprj-vuejs-springboot ディレクトリ作成+Gradle Wrapper コピー+gradlew init
- IntelliJ IDEA で gradle-multiprj-vuejs-springboot プロジェクトをオープンする
- Spring Boot ベースの Web アプリケーション(backend)(backend-app)を作成する
手順
方針
- 以下のディレクトリ構成の Multi-project を作成します。
gradle-multiprj-vuejs-springboot ├ backend-app <-- Spring Boot ベースの Web アプリケーション(backend)の Project └ frontend-app <-- Vue.js のアプリケーション(frontend)の Project
- Vue.js のアプリケーション(frontend)でボタンをクリックしたら Spring Boot ベースの Web アプリケーション(backend)の WebAPI を呼び出して、取得したデータを画面上に表示します。
- Spring Boot ベースの Web アプリケーションの Project では develop と product の2つの Profile を作成・使用します。
- Spring Boot ベースの Web アプリケーションの Project ではテスティングフレームワークは導入しません(今回はテストなし!)。
- checkstyle, spotbugs, pmd, error-prone は今回は導入しません。
- Spring Boot ベースの Web アプリケーション(backend)は develop profile では 8081番ポート、product profile では 8080番ポートを使用します。
- Vue.js のアプリケーションは Vue CLI を使用してプロジェクトを作成します。
- Vue.js のアプリケーションの画面は Vuetify を使用して作成します。
- Vue.js のアプリケーション(frontend)では npm run serve 実行時には 8080番ポートを使用します。
- Gradle の build タスクで以下の処理を行います。
- Vue.js のアプリケーション(frontend)の npm run build を実行します。
- Spring Boot ベースの Web アプリケーション(backend)の processResources タスクの前に src/main/resources/static の下に Vue.js のアプリケーション(frontend)のリリースファイル一式(/dist ディレクトリの下に出来るもの)をコピーします。
- 開発環境及び本番環境どちらも http://localhost:8080/ でアクセスします。
- 開発環境では npm run serve で起動した webpack-dev-server から http-proxy-middleware で backend-app の Tomcat へリクエストを転送します。
- こんなイメージです。
gradle-multiprj-vuejs-springboot ディレクトリ作成+Gradle Wrapper コピー+gradlew init
ksby/ksbysample-boot-miscellaneous の repository を checkout している D:\project-springboot\ksbysample-boot-miscellaneous
の下に gradle-multiprj-vuejs-springboot ディレクトリを作成します。
gradle-multiprj-doma2lib-cmdwebapp プロジェクトから Gradle Wrapper のファイルをコピーします。
- gradle ディレクトリ
- gradlew
- gradlew.bat
gradlew init
コマンドを実行します。選択肢は 1: basic
、1: groovy
を選択し、Project name は何も入力せずに Enter キーを押します。
IntelliJ IDEA で gradle-multiprj-vuejs-springboot プロジェクトをオープンする
gradle-multiprj-vuejs-springboot プロジェクトをオープンします。
Spring Boot ベースの Web アプリケーション(backend)(backend-app)を作成する
IntelliJ IDEA で backend-app プロジェクトを作成する
IntelliJ IDEA から Spring Initializr を利用して backend-app プロジェクトを作成します。
作成後 IntelliJ IDEA のウィンドウが開きますが、何もせずに閉じます。
src ディレクトリと build.gradle だけ残して、それ以外のディレクトリ・ファイルは全て削除します。
また backend-app/src/test/java/ksbysample/app/backendapp/BackendAppApplicationTests.java.java も削除し、backend-app/src/test/java/ の下はクリアします。
settings.gradle に backend-app プロジェクトの include 文を追加する
gradle-multiprj-vuejs-springboot の settings.gradle に include 'backend-app'
を追加します。
rootProject.name = 'gradle-multiprj-vuejs-springboot' include 'backend-app'
backend-app プロジェクトの build.gradle を変更する
backend-app/build.gradle を以下のように変更します。
plugins { id 'org.springframework.boot' version '2.1.4.RELEASE' id 'java' id "idea" } apply plugin: 'io.spring.dependency-management' group = 'ksby.ksbysample-boot-miscellaneous.gradle-multiprj-vuejs-springboot' version = '1.0.0-RELEASE' sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 idea { module { inheritOutputDirs = false outputDir = file("$buildDir/classes/main/") } } repositories { mavenCentral() } dependencyManagement { imports { mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) } } dependencies { def lombokVersion = "1.18.6" implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'org.springframework.boot:spring-boot-devtools' testImplementation 'org.springframework.boot:spring-boot-starter-test' // for lombok // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している annotationProcessor("org.projectlombok:lombok:${lombokVersion}") compileOnly("org.projectlombok:lombok:${lombokVersion}") }
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。Gradle Tool Window に backend-app が表示されます。
clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します。
Vue.js のアプリから呼び出す WebAPI を実装する
POST メソッドで /webapi/sample の URL にアクセスしたら以下の JSON を返す WebAPI を実装します。
{ "code": "123", "value": "sample data" }
backend-app/src/main/java/ksbysample/app/backendapp の下に SampleResponse.java を新規作成し、以下の内容を記述します。
package ksbysample.app.backendapp; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor @Builder public class SampleResponse { private String code; private String value; }
backend-app/src/main/java/ksbysample/app/backendapp の下に SampleController.java を新規作成し、以下の内容を記述します。今回は ResponseEntity を使ってみます。
package ksbysample.app.backendapp; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/webapi/sample") public class SampleController { @PostMapping public ResponseEntity<SampleResponse> index() { SampleResponse sampleResponse = SampleResponse.builder() .code("123") .value("sample data") .build(); return new ResponseEntity<>(sampleResponse, HttpStatus.OK); } }
src/main/resources の下に application-develop.properties を新規作成し、以下の内容を記述します。
server.port=8081
server.port のデフォルトが 8080 なので application-product.properties は作成しません。
動作確認
最初に clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、BUILD SUCCESSFUL が出力されることを確認します。
次に develop profile で Tomcat を起動した後、
コマンドラインから curl http://localhost:8081/webapi/sample -i -X POST
を実行すると JSON データが返ってきました。
Tomcat を停止します。
今度は product profile で実行可能 jar で Tomcat を起動した後、
コマンドラインから curl http://localhost:8080/webapi/sample -i -X POST
(ポート番号を 8081 → 8080 に変更しています) を実行するとこちらも問題なく JSON データが返ってきました。
履歴
2019/05/06
初版発行。
Gradle で Multi-project を作成する ( その13 )( doma2lib+cmdapp+webapp編、PropertiesLauncher を利用して doma2-lib の jar ファイルを外部に出す )
概要
記事一覧はこちらです。
Gradle で Multi-project を作成する ( その12 )( doma2lib+cmdapp+webapp編、sample-webapp プロジェクトを作成する ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
- doma2lib+cmdapp+webapp編のここまでの記事では実行可能 Jar ファイルを起動するのに JarLauncher を利用する方式でしたので sample-cmdapp-1.0.0-RELEASE.jar、sample-webapp-1.0.0-RELEASE.jar の中に doma2-lib-1.0.0-RELEASE.jar が入っていましたが、PropertiesLauncher を利用する方式に変更して doma2-lib-1.0.0-RELEASE.jar を外部に出すようにしてみます。
参照したサイト・書籍
E.3 Launching Executable Jars
https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#executable-jar-launchingSpring Boot 2 and external libs with the PropertiesLauncher
https://medium.com/saas-startup-factory/spring-boot-2-and-external-libs-with-the-propertieslauncher-fc49d2d93636Gradle Docs 5.4.1 - Copy
https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Copy.html
目次
- gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する
- sample-cmdapp、sample-webapp の build.gradle を変更する
- clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う
- 動作確認
- sample-webapp を IntelliJ IDEA の Run Dashboard から起動するとどうなるのか?
- 次回は。。。
手順
gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する
PropertiesLauncher を利用するのに必要な設定と、jar ファイルを gradle-multiprj-doma2lib-cmdwebapp プロジェクトのルートディレクトリ直下の libs ディレクトリ(新規に作成します)にコピーするための task を gradle-multiprj-doma2lib-cmdwebapp の build.gradle に記述します。
build.gradle を以下のように変更します。
.......... subprojects { .......... // サブプロジェクトの build/libs の下に生成された jar ファイルを // プロジェクトのルートディレクトリ直下の libs ディレクトリにコピーする task copyJarToLibsDir(type: Copy) { from "build/libs" include "*.jar" into "${rootDir}/libs" } copyJarToLibsDir.dependsOn test check.dependsOn copyJarToLibsDir } configure(subprojects.findAll { it.name ==~ /^(doma2-lib|sample-cmdapp|sample-webapp)$/ }) { .......... } configure(subprojects.findAll { it.name ==~ /^(sample-cmdapp|sample-webapp)$/ }) { bootJar { manifest { attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher' } } }
- subproject block 内に
task copyJarToLibsDir(type: Copy) { ... }
を追加します。また test タスクの後に実行するためにcopyJarToLibsDir.dependsOn test
、check.dependsOn copyJarToLibsDir
の2行を追加します。 - configure は複数記述できるようなので、sample-cmdapp、sample-webapp だけに適用する設定を記述する
configure(subprojects.findAll { it.name ==~ /^(sample-cmdapp|sample-webapp)$/ }) { ... }
を追加します。この中に PropertiesLauncher を利用するのに必要な設定を記述します。
またプロジェクトのルートディレクトリ直下に libs ディレクトリを作成しておきます。
sample-cmdapp、sample-webapp の build.gradle を変更する
sample-cmdapp、sample-webapp の build.gradle では doma2-lib への依存関係を implementation
で記述しており、このままでは jar ファイル内に doma2-lib-1.0.0-RELEASE.jar が含まれてしまうので、implementation project(":doma2-lib")
→ compileOnly project(":doma2-lib")
に変更します。
■sample-cmdapp の build.gradle
dependencies { implementation("org.springframework.boot:spring-boot-starter") compileOnly project(":doma2-lib") implementation("com.univocity:univocity-parsers:2.8.1") implementation("args4j:args4j:2.33") implementation("com.github.rozidan:modelmapper-spring-boot-starter:1.0.0") }
■sample-webapp の build.gradle
dependencies { implementation("org.springframework.boot:spring-boot-starter-web") runtimeOnly("org.springframework.boot:spring-boot-devtools") compileOnly project(":doma2-lib") }
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う
必要な設定は以上で完了です。clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、jar ファイルを生成し直して libs ディレクトリに集めます。
エラーメッセージは出ずに BUILD SUCCESSFUL が出力されています。各サブプロジェクトの test タスクの後では copyJarToLibsDir タスクが実行されて、
libs ディレクトリの下に doma2-lib-1.0.0-RELEASE.jar、sample-cmdapp-1.0.0-RELEASE.jar、sample-webapp-1.0.0-RELEASE.jar がコピーされています。
sample-cmdapp-1.0.0-RELEASE.jar を表示させてみると lib ディレクトリの下には doma2-lib-1.0.0-RELEASE.jar は入っておらず、
sample-webapp-1.0.0-RELEASE.jar の方にも同様に入っていませんでした。
動作確認
employee テーブルのデータは以下の状況です。
PropertiesLauncher を利用する方式の場合、外部に配置した jar ファイルがある場所を loader.path
で指定します。今回はコマンドラインからアプリを実行する時に libs ディレクトリに移動した後 -Dloader.path=.
を追加したコマンドを使用するようにします。
コマンドラインから java -Dspring.profiles.active=product -Dloader.path=. -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv
コマンドを実行するとエラーは出ずに終了し、
employee テーブルにもデータが登録されていました。
java -Dspring.profiles.active=product -Dloader.path=. -jar sample-webapp-1.0.0-RELEASE.jar
で sample-webapp を起動した後、
http://localhost:8080/sample にアクセスすると employee テーブルに登録されているデータが表示されました。
動作は問題なさそうです。
また libs ディレクトリから doma2-lib-1.0.0-RELEASE.jar を削除した後、sample-cmdapp を実行しようとすると java.lang.NoClassDefFoundError: ksbysample/lib/doma2lib/dao/EmployeeDao
が発生してエラーで終了しました。
sample-webapp を起動しようとすると java.lang.ClassNotFoundException: ksbysample.lib.doma2lib.entity.Employee
が発生して、こちらもエラーで終了しました。
(.....途中省略.....)
sample-webapp を IntelliJ IDEA の Run Dashboard から起動するとどうなるのか?
JarLauncher か PropertiesLauncher かは実行可能 Jar で起動する時の話ですので、Run Dashboard から起動する時には関係がありません。
Configuration でも Main class には org.springframework.boot.loader.PropertiesLauncher
ではなく ksbysample.app.samplewebapp.SampleWebappApplication
を指定しているので、
設定を変更しなくても起動します。
次回は。。。
Doma 2 の Dao インターフェース、Entity クラスを別の jar ファイルにしようとすると意外に大変ですね。今回は知らないことがいろいろありました。
Gradle で Multi-project を作成する ( その1 )( 概要 ) に記載していたパターンは一通り作成しましたが、サブプロジェクトの1つが Javascript のプロジェクト(Vue.js かな?)+1つは Spring Boot ベースのプロジェクトのパターンを作成してみたいと思ったので、もう1パターンだけ作成してみます。
履歴
2019/05/01
初版発行。
Gradle で Multi-project を作成する ( その12 )( doma2lib+cmdapp+webapp編、sample-webapp プロジェクトを作成する )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
目次
- Spring Initializr で sample-webapp プロジェクトを作成する
- sample-webapp の build.gradle を変更する
- gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'sample-webapp' を追加する
- gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-webapp を追加する
- DB のテーブルのデータを読み込んで画面に表示する機能を実装する
- テストを作成する
- 動作確認
- 次回は。。。
手順
Spring Initializr で sample-webapp プロジェクトを作成する
IntelliJ IDEA から Spring Initializr を利用して sample-webapp プロジェクトを作成します。
作成後 IntelliJ IDEA のウィンドウが開きますが、何もせずに閉じます。
src ディレクトリと build.gradle だけ残して、それ以外のディレクトリファイルは全て削除します。
また sample-webapp/src/test/java/ksbysample/app/samplewebapp/SampleWebappApplicationTests.java も削除し、sample-cmdapp/src/test/java/ の下はクリアします。
sample-webapp の build.gradle を変更する
sample-webapp の build.gradle を以下のように変更します。
dependencies { implementation("org.springframework.boot:spring-boot-starter-web") runtimeOnly("org.springframework.boot:spring-boot-devtools") implementation project(":doma2-lib") }
- doma2-lib プロジェクトへの依存関係を追加します。
gradle-multiprj-doma2lib-cmdwebapp の settings.gradle に include 'sample-webapp' を追加する
settings.gradle に include 'sample-webapp' を追加します。
rootProject.name = 'gradle-multiprj-doma2lib-cmdwebapp' include 'doma2-lib' include 'sample-cmdapp' include 'sample-webapp'
gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-webapp を追加する
gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure の適用対象に sample-webapp を追加します。
.......... configure(subprojects.findAll { it.name ==~ /^(doma2-lib|sample-cmdapp|sample-webapp)$/ }) { apply plugin: "org.springframework.boot" dependencyManagement { imports { mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) } } ext { jdbcDriver = "mysql:mysql-connector-java:8.0.15" domaVersion = "2.24.0" } dependencies { def lombokVersion = "1.18.6" testImplementation("org.springframework.boot:spring-boot-starter-test") runtimeOnly(jdbcDriver) implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.1.1") implementation("org.seasar.doma:doma:${domaVersion}") implementation("org.flywaydb:flyway-core:5.2.4") implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2") // for lombok // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している annotationProcessor("org.projectlombok:lombok:${lombokVersion}") compileOnly("org.projectlombok:lombok:${lombokVersion}") } }
/^(doma2-lib|sample-cmdapp)$/
→/^(doma2-lib|sample-cmdapp|sample-webapp)$/
に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。Gradle Tool Window に sample-webapp が表示されます。
Run Dashboard Tool Window にも SampleWebappApplication が追加されています。
clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します。
DB のテーブルのデータを読み込んで画面に表示する機能を実装する
以下の仕様で実装します。
- employee テーブルからデータを読み込んで name, age, sex のデータを画面上に表示します。
- URL は
http://localhost:8080/sample
にします。 - Thymeleaf は使用しません。@ResponseBody アノテーションを付与してテキストデータを返します。
sample-webapp/src/main/java/ksbysample/app/samplewebapp/SampleWebappApplication.java に @ComponentScan アノテーションを追加し、ksbysample.lib
、ksbysample.app
の2つの package を指定します
package ksbysample.app.samplewebapp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.TypeExcludeFilter; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; @SpringBootApplication @ComponentScan( basePackages = {"ksbysample.lib", "ksbysample.app"}, excludeFilters = { @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @ComponentScan.Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)}) public class SampleWebappApplication { public static void main(String[] args) { SpringApplication.run(SampleWebappApplication.class, args); } }
employee テーブルからデータ一覧を取得するメソッドがないので、doma2-lib/src/main/java/ksbysample/lib/doma2lib/dao/EmployeeDao.java に selectAll メソッドを追加します。
■doma2-lib/src/main/java/ksbysample/lib/doma2lib/dao/EmployeeDao.java
package ksbysample.lib.doma2lib.dao; import ksbysample.lib.doma2lib.entity.Employee; import org.seasar.doma.boot.ConfigAutowireable; import org.seasar.doma.Dao; import org.seasar.doma.Delete; import org.seasar.doma.Insert; import org.seasar.doma.Select; import org.seasar.doma.Update; import java.util.List; /** */ @Dao @ConfigAutowireable public interface EmployeeDao { /** * @param id * @return the Employee entity */ @Select Employee selectById(Integer id); @Select List<Employee> selectAll(); /** * @param entity * @return affected rows */ @Insert(excludeNull = true) int insert(Employee entity); /** * @param entity * @return affected rows */ @Update int update(Employee entity); /** * @param entity * @return affected rows */ @Delete int delete(Employee entity); }
■doma2-lib/src/main/resources/META-INF/ksbysample/lib/doma2lib/dao/EmployeeDao/selectAll.sql
select /*%expand*/* from employee order by id
DB のテーブルのデータを読み込んで画面に表示するクラスを実装します。sample-webapp/src/main/java/ksbysample/app/samplewebapp/SampleController.java を新規作成し、以下の内容を記述します。
package ksbysample.app.samplewebapp; import ksbysample.lib.doma2lib.dao.EmployeeDao; import ksbysample.lib.doma2lib.entity.Employee; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.util.List; import java.util.stream.Collectors; @Controller @RequestMapping("/sample") public class SampleController { private final EmployeeDao employeeDao; public SampleController(EmployeeDao employeeDao) { this.employeeDao = employeeDao; } @RequestMapping(produces = MediaType.TEXT_PLAIN_VALUE) @ResponseBody public String index() { List<Employee> employeeList = employeeDao.selectAll(); return employeeList.stream() .map(employee -> String.format("name = %s, age = %d, sex = %s" , employee.getName() , employee.getAge() , employee.getSex())) .collect(Collectors.joining("\n")); } }
clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します(画面キャプチャは省略します)。
テストを作成する
sample-webapp/src/main/java/ksbysample/app/samplewebapp/SampleController.java のテストを作成します。
sample-webapp/src/test/groovy/ksbysample/app/samplewebapp/SampleControllerTest.groovy が新規作成されますので、以下の内容を記述します。
package ksbysample.app.samplewebapp import groovy.sql.Sql import ksbysample.lib.doma2lib.entity.Employee import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.web.servlet.MockMvc import spock.lang.Specification import javax.sql.DataSource import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status // 通常は spring.profiles.active は IntelliJ IDEA の JUnit の Run/Debug Configuration と build.gradle に定義するが、 // 今回はテストが1つしかないので @SpringBootTest の properties 属性で指定する @SpringBootTest(properties = ["spring.profiles.active=develop"]) @AutoConfigureMockMvc class SampleControllerTest extends Specification { static final def TESTDATA = [ [id: 1, name: "鈴木 太郎", age: 42, sex: "男", update_time: "2019/05/01 00:00:00"], [id: 2, name: "渡辺 香", age: 36, sex: "女", update_time: "2019/04/30 16:51:47"], [id: 3, name: "木村 結衣", age: 23, sex: "女", update_time: "2019/04/01 09:15:02"] ] @Autowired private MockMvc mvc @Autowired private DataSource dataSource def sql List<Employee> backupData void setup() { sql = new Sql(dataSource) // employee テーブルのバックアップを取得後クリアする backupData = sql.rows("select * from employee") sql.execute("truncate table employee") } void cleanup() { // バックアップからemployee テーブルのデータをリカバリする sql.execute("truncate table employee") backupData.each { sql.execute("insert into employee values (:id, :name, :age, :sex, :update_time)", it) } sql.close() } def "employeeテーブルにデータがない場合にはcontentも空になる"() { expect: mvc.perform(get("/sample")) .andExpect(status().isOk()) .andExpect(content().string("")) } def "employeeテーブルにデータがある場合には全てのデータが出力される"() { setup: TESTDATA.each { sql.execute("insert into employee values (:id, :name, :age, :sex, :update_time)", it) } def expectedContent = TESTDATA.collect { String.format("name = ${it.name}, age = ${it.age}, sex = ${it.sex}") } .join("\n") expect: mvc.perform(get("/sample")) .andExpect(status().isOk()) .andExpect(content().string(expectedContent)) } }
テストを実行して成功することを確認します。
今回 employee テーブルをバックアップ・リカバリする仕組みを考えたので、doma2-lib/src/test/groovy/ksbysample/lib/doma2lib/dao/EmployeeDaoTest にも反映します。以下のように変更します。
package ksbysample.lib.doma2lib.dao import groovy.sql.Sql import ksbysample.lib.doma2lib.entity.Employee import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import spock.lang.Specification import spock.lang.Unroll import javax.sql.DataSource // 通常は spring.profiles.active は IntelliJ IDEA の JUnit の Run/Debug Configuration と build.gradle に定義するが、 // 今回はテストが1つしかないので @SpringBootTest の properties 属性で指定する @SpringBootTest(properties = ["spring.profiles.active=develop"]) class EmployeeDaoTest extends Specification { static final def TESTDATA = [ [id: 1, name: "鈴木 太郎", age: 42, sex: "男", update_time: "2019/05/01 00:00:00"], [id: 2, name: "渡辺 香", age: 36, sex: "女", update_time: "2019/04/30 16:51:47"], [id: 3, name: "木村 結衣", age: 23, sex: "女", update_time: "2019/04/01 09:15:02"] ] static final def TESTDATA2 = new Employee(id: null, name: "木村 太郎", age: 35, sex: "男", updateTime: null) @Autowired EmployeeDao employeeDao @Autowired private DataSource dataSource def sql List<Employee> backupData void setup() { sql = new Sql(dataSource) // employee テーブルのバックアップを取得後クリアする backupData = sql.rows("select * from employee") sql.execute("truncate table employee") } void cleanup() { // バックアップからemployee テーブルのデータをリカバリする sql.execute("truncate table employee") backupData.each { sql.execute("insert into employee values (:id, :name, :age, :sex, :update_time)", it) } sql.close() } @Unroll def "selectById メソッドのテスト(#id --> #name, #age, #sex)"() { setup: TESTDATA.each { sql.execute("insert into employee values (:id, :name, :age, :sex, :update_time)", it) } def row = employeeDao.selectById(id) expect: row.name == name row.age == age row.sex == sex where: id || name | age | sex 1 || "鈴木 太郎" | 42 | "男" 2 || "渡辺 香" | 36 | "女" } def "insert メソッドのテスト"() { setup: employeeDao.insert(TESTDATA2) expect: def row = sql.firstRow("select * from employee where name = ${TESTDATA2.name}") row.name == TESTDATA2.name row.age == TESTDATA2.age row.sex == TESTDATA2.sex } }
テストを実行して成功することを確認します。
動作確認
clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します。
Run Dashboard から sample-webapp を起動して動作確認します。
employee テーブルに以下のデータが登録されている場合、
http://localhost:8080/sample にアクセスすると employee テーブルのデータが出力されていることが確認できます。
コマンドラインから java -Dspring.profiles.active=develop -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv
を実行してデータを追加すると、
http://localhost:8080/sample の画面を更新すると追加されたデータが反映されました。
コマンドラインから sample-webapp を起動して動作確認します。java -Dspring.profiles.active=develop -jar sample-webapp-1.0.0-RELEASE.jar
コマンドを実行して起動します。
(.....途中省略.....)
http://localhost:8080/sample にアクセスすると先程と同じデータが表示されました。
最後に sample-webapp プロジェクトのディレクトリ構成を記載します。
sample-webapp-1.0.0-RELEASE.jar の中身を見てみると以下のようになっており doma2-lib-1.0.0-RELEASE.jar が lib ディレクトリの下に入っています。
次回は。。。
ここまで doma2-lib-1.0.0-RELEASE.jar を各サブプロジェクトの jar の中に入れる方式(JarLauncher による起動)で書きましたので、次は doma2-lib-1.0.0-RELEASE.jar を外に出す方式(PropertiesLauncher による起動)を書く予定です。
履歴
2019/05/01
初版発行。
Gradle で Multi-project を作成する ( その11 )( doma2lib+cmdapp+webapp編、log4jdbc-log4j2 を導入してトランザクションが有効なことを確認する )
概要
記事一覧はこちらです。
Gradle で Multi-project を作成する ( その10 )( doma2lib+cmdapp+webapp編、sample-cmdapp プロジェクトを作成する2 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
目次
- gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する
- doma2-lib の src/main/resources の下に application-develop.properties、application-product.properties を作成する
- doma2-lib の db-product.properties と同じ内容を db-develop.properties に記述する
- EmployeeDataCsvToDbLoaderTest クラスでトランザクションが有効か確認する
java.sql.SQLSyntaxErrorException: SELECT command denied to user 'sampledb_user'@'172.18.0.1' for table 'user_variables_by_thread'
を解消する- sample-cmdapp をコマンドラインから実行した時にトランザクションが有効か確認する
手順
gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する
gradle-multiprj-doma2lib-cmdwebapp の build.gradle の configure に log4jdbc-log4j2 を利用するための com.integralblue:log4jdbc-spring-boot-starter を依存関係に追加します。
.......... configure(subprojects.findAll { it.name ==~ /^(doma2-lib|sample-cmdapp)$/ }) { apply plugin: "org.springframework.boot" dependencyManagement { imports { mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) } } ext { jdbcDriver = "mysql:mysql-connector-java:8.0.15" domaVersion = "2.24.0" } dependencies { def lombokVersion = "1.18.6" testImplementation("org.springframework.boot:spring-boot-starter-test") runtimeOnly(jdbcDriver) implementation("org.seasar.doma.boot:doma-spring-boot-starter:1.1.1") implementation("org.seasar.doma:doma:${domaVersion}") implementation("org.flywaydb:flyway-core:5.2.4") implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2") // for lombok // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している annotationProcessor("org.projectlombok:lombok:${lombokVersion}") compileOnly("org.projectlombok:lombok:${lombokVersion}") } }
implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")
を追加します。
doma2-lib の src/main/resources の下に application-develop.properties、application-product.properties を作成する
develop profile の時には log4jdbc-log4j2 でログが出力されるようにし、product profile の時にはログが出力されないようにします。設定ファイルは doma2-lib プロジェクトの下に作成します(ここに作成しても sample-cmdapp に適用されます)。
doma2-lib/src/main/resources の下に application-develop.properties、application-product.properties を新規作成し、以下の内容を記述します。
■application-develop.properties
# log4jdbc-log4j2 logging.level.jdbc.sqlonly=DEBUG logging.level.jdbc.sqltiming=INFO logging.level.jdbc.audit=INFO logging.level.jdbc.resultset=ERROR logging.level.jdbc.resultsettable=ERROR logging.level.jdbc.connection=DEBUG
■application-product.properties
spring.autoconfigure.exclude=com.integralblue.log4jdbc.spring.Log4jdbcAutoConfiguration
doma2-lib の db-product.properties と同じ内容を db-develop.properties に記述する
develop profile ではログが出力され product profile ではログが出力されないことを確認できるようにするために doma2-lib/src/main/resources/db-product.properties の内容を db-develop.properties と同じにします。
■db-product.properties
doma.dialect=mysql spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost/sampledb?sslMode=DISABLED&characterEncoding=utf8 spring.datasource.hikari.username=sampledb_user spring.datasource.hikari.password=xxxxxxxx spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.hikari.leak-detection-threshold=60000 spring.datasource.hikari.register-mbeans=true
EmployeeDataCsvToDbLoaderTest クラスでトランザクションが有効か確認する
sample-cmdapp/src/test/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoaderTest.groovy のテストを実行して、employeeDataCsvToDbLoader = (EmployeeDataCsvToDbLoader) context.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(employeeDataCsvToDbLoader, "employeeDataCsvToDbLoader")
で手動で Bean に登録した場合にトランザクションが有効か確認します。
テストを実行すると insert 前に Connection.setAutoCommit(false)
が呼び出されて、employeeDataCsvToDbLoader.run(...)
で2件 insert した後に Connection.commit()
が呼び出されていることが確認できました。
わざと RuntimeException を throw して rollback されるかも確認します。 sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoader.java を以下のように変更します。
@Component @ConditionalOnProperty(value = {"batch.execute"}, havingValue = "EmployeeDataCsvToDbLoader") public class EmployeeDataCsvToDbLoader implements CommandLineRunner { .......... @Override @Transactional public void run(String... args) throws Exception { .......... // CSV ファイルを1行ずつ読み込み employee テーブルに insert する try (BufferedReader br = Files.newBufferedReader(Paths.get(this.csvfile), StandardCharsets.UTF_8)) { Employee employee = new Employee(); for (EmployeeCsvRecord employeeCsvRecord : routines.iterate(EmployeeCsvRecord.class, br)) { modelMapper.map(employeeCsvRecord, employee); employeeDao.insert(employee); throw new RuntimeException("error"); } } } }
throw new RuntimeException("error");
を追加します。
テストを実行すると1件目の insert 文が実行された後に RuntimeException が throw された結果 Connection.rollback()
が呼び出されていました。
トランザクションが有効になっていることが確認できました。
また spring.profiles.active=product
に変更すると、
@SpringBootTest(properties = ["spring.profiles.active=product"]) class EmployeeDataCsvToDbLoaderTest extends Specification {
log4jdbc-log4j2 のログは出力されなくなりました。
java.sql.SQLSyntaxErrorException: SELECT command denied to user 'sampledb_user'@'172.18.0.1' for table 'user_variables_by_thread'
を解消する
上でテストを実行した時に java.sql.SQLSyntaxErrorException: SELECT command denied to user 'sampledb_user'@'172.18.0.1' for table 'user_variables_by_thread'
というエラーメッセージが出ていることに気づきました。flyway が performance_schema.user_variables_by_thread テーブルにアクセスしようとして出来なかったために出力されていました。
doma2-lib/src/main/resources/db/init/create_database.sql を以下のように変更します。
create database if not exists sampledb character set utf8mb4 collate utf8mb4_ja_0900_as_cs_ks; create user 'sampledb_user'@'%' identified by 'xxxxxxxx'; grant all privileges ON sampledb.* to 'sampledb_user'@'%' with grant option; grant select ON performance_schema.user_variables_by_thread to 'sampledb_user'@'%'; flush privileges;
grant select ON performance_schema.user_variables_by_thread to 'sampledb_user'@'%';
を追加します。
コンテナを再起動しても create_database.sql に追加した grant select ON ...
文が実行されなかったので、adminer に root ユーザでログインして直接 SQL 文を実行します。
テストを実行すると今度はエラーメッセージが出力されなくなりました。
sample-cmdapp をコマンドラインから実行した時にトランザクションが有効か確認する
コマンドラインから実行した時もトランザクションが有効になっているか確認します。まずは clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、sample-cmdapp-1.0.0-RELEASE.jar を build し直します。
java -Dspring.profiles.active=develop -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv
コマンドを実行すると Connection.commit()
が呼び出されています。
(.....途中省略.....)
sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoader.java に throw new RuntimeException("error");
を追加してから build し直した後、再度コマンドを実行すると1件目を insert した後 Connection.rollback()
が呼び出されていました。
コマンドラインから実行した時もトランザクションが有効になっていることが確認できました。
また -Dspring.profiles.active=product
に変更した java -Dspring.profiles.active=product -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv
コマンドを実行すると log4jdbc-log4j2 のログは出力されませんでした。
履歴
2019/05/01
初版発行。
Gradle で Multi-project を作成する ( その10 )( doma2lib+cmdapp+webapp編、sample-cmdapp プロジェクトを作成する2 )
概要
記事一覧はこちらです。
Gradle で Multi-project を作成する ( その9 )( doma2lib+cmdapp+webapp編、sample-cmdapp プロジェクトを作成する ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
Shortest way to get File object from resource in Groovy
https://stackoverflow.com/questions/39245934/shortest-way-to-get-file-object-from-resource-in-groovySpring start a transaction with object created by new
https://stackoverflow.com/questions/4707193/spring-start-a-transaction-with-object-created-by-newapplicationContext.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(...)
で手動で Bean を登録する方法を参考にしました。
目次
手順
テストを作成する
最初にテストで利用する CSV ファイルを作成します。sample-cmdapp/src/test/resources/employee.csv を新規作成し、以下の内容を記述します。
"name","age","sex" "高橋 蓮","15","男" "渡辺 結月","24","女"
次に sample-cmdapp/src/main/java/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoader.java のテストクラスを作成します。
@SpringBootTest アノテーションが付与されたクラスのテストメソッドを実行するとテストメソッド内の処理が実行される前に CommandLineRunner インターフェースを実装したクラスの run メソッドが実行されてしまうのですが、以下の方法で run メソッドが自動で実行されないようにします。
- EmployeeDataCsvToDbLoader クラスに
@ConditionalOnProperty(value = { "batch.execute" }, havingValue = "EmployeeDataCsvToDbLoader")
アノテーションを付与して、batch.execute=EmployeeDataCsvToDbLoader
が設定されない限り自動で Bean に登録されないようにします。 - EmployeeDataCsvToDbLoader クラスのコンストラクタに渡すオブジェクト(Bean)は、テストクラスのフィールドに
@Autowired
アノテーションを付与して定義しておきます。 - テストメソッドの最初で EmployeeDataCsvToDbLoader クラスのインスタンスを生成してから
context.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(...)
を呼び出して手動で Bean に登録します。手動で登録すると CommandLineRunner#run メソッドは自動で実行されません。
sample-cmdapp/src/test/groovy/ksbysample/app/samplecmdapp/EmployeeDataCsvToDbLoaderTest.groovy が新規作成されますので、以下の内容を記述します。
package ksbysample.app.samplecmdapp import groovy.sql.Sql import ksbysample.lib.doma2lib.dao.EmployeeDao import org.modelmapper.ModelMapper import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.context.ApplicationContext import spock.lang.Specification import javax.sql.DataSource // 通常は spring.profiles.active は IntelliJ IDEA の JUnit の Run/Debug Configuration と build.gradle に定義するが、 // 今回はテストが1つしかないので @SpringBootTest の properties 属性で指定する @SpringBootTest(properties = ["spring.profiles.active=develop"]) class EmployeeDataCsvToDbLoaderTest extends Specification { @Autowired private ApplicationContext context @Autowired private DataSource dataSource @Autowired private final EmployeeDao employeeDao @Autowired private final ModelMapper modelMapper def sql void setup() { sql = new Sql(dataSource) } void cleanup() { sql.close() } def "EmployeeDataCsvToDbLoader.run メソッドを実行すると employee.csv のデータが employee テーブルに登録される"() { setup: // EmployeeDataCsvToDbLoader クラスは batch.execute=EmployeeDataCsvToDbLoader が指定されていないと Bean として // 登録されず run メソッドが自動で実行されない。テストでは手動で Bean に登録することで run メソッドが自動実行されることを // 回避する。 EmployeeDataCsvToDbLoader employeeDataCsvToDbLoader = new EmployeeDataCsvToDbLoader(employeeDao, modelMapper) employeeDataCsvToDbLoader = (EmployeeDataCsvToDbLoader) context .getAutowireCapableBeanFactory() .applyBeanPostProcessorsAfterInitialization(employeeDataCsvToDbLoader, "employeeDataCsvToDbLoader") // src/test/resources の下の employee.csv の File オブジェクトを取得する def url = getClass().getResource("/employee.csv") def employeeCsvFile = new File(url.toURI()) // employee テーブルからテストデータを削除する sql.execute("delete from employee where name in ('高橋 蓮', '渡辺 結月')") expect: employeeDataCsvToDbLoader.run("-csvfile=${employeeCsvFile.absolutePath}") def results = sql.rows("select name, age, sex from employee where name in ('高橋 蓮', '渡辺 結月')") results == [ [name: "高橋 蓮", age: 15, sex: "男"], [name: "渡辺 結月", age: 24, sex: "女"] ] cleanup: sql.execute("delete from employee where name in ('高橋 蓮', '渡辺 結月')") } }
テストを実行して成功することを確認します。
コマンドラインから実行して動作確認する
employee テーブルにテストデータが登録されていないことを確認した後、
sample-cmdapp-1.0.0-RELEASE.jar が生成されている D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\build\libs
に移動してから java -Dspring.profiles.active=develop -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv
コマンドを実行します。
employee テーブルを確認するとテストデータが登録されています。
問題なさそうです。
最後に sample-cmdapp プロジェクトのディレクトリ構成を記載します。
sample-cmdapp-1.0.0-RELEASE.jar の中身を見てみると以下のようになっており doma2-lib-1.0.0-RELEASE.jar が lib ディレクトリの下に入っています。
補足
sample-cmdapp でも Flyway は動作している
現在の設定では sample-cmdapp を実行した時も Flyway は動作しています。確認してみます。
docker-compose down
コマンドを実行後、docker/mysql/data の下をクリアします。
docker-compose.yml の flyway の設定をコメントアウトした後、
version: '3' services: .......... # flyway: # image: boxfuse/flyway:${FLYWAY_VERSION}-alpine # container_name: flyway # environment: # - TZ=Asia/Tokyo # volumes: # - ./doma2-lib/src/main/resources/db/migration:/flyway/sql # command: -url=${FLYWAY_URL} -user=${FLYWAY_USER} -password=${FLYWAY_PASSWORD} -connectRetries=60 migrate # depends_on: - mysql # 下の3行は debug 用 # うまく動かない時はコメントアウトを解除した後、 # docker exec -it flyway /bin/sh # で接続してから # flyway <command に記述した文字列> # を実行してみる # # entrypoint: /bin/sh # stdin_open: true # tty: true
docker-compose up -d
コマンドを実行します。
adminer にログインして sampledb を見るとテーブルは何もありませんが、
コマンドラインから java -Dspring.profiles.active=develop -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv
を実行してから、
sampledb を確認すると employee と flyway_schema_history の2つのテーブルが作成されており、
employee テーブルにも doma2-lib/src/main/resources/db/migration/V1__create_table.sql の2件と sample-cmdapp/src/test/resources/employee.csv の2件の合計4件のデータが登録されています。
履歴
2019/05/01
初版発行。