Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その3 )( Run 'All Tests' with Coverage 実行時に出るエラーを解消する )
概要
記事一覧はこちらです。
Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その1 )( 概要 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- Run ‘All Tests’ with Coverage 実行時に出るエラーの解消
参照したサイト・書籍
目次
手順
java.lang.IllegalStateException: Failed to load ApplicationContext
java.lang.IllegalStateException: Failed to load ApplicationContext
の下に出力されるメッセージの内、Caused by:
から始まるエラーメッセージは以下のものでした。
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'webSecurityConfig': Unsatisfied dependency expressed through method 'setContentNegotationStrategy' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$EnableWebMvcConfiguration': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Found unexpected validator configuration. A Spring Boot MVC validator should be registered as bean named 'mvcValidator' and not returned from WebMvcConfigurer.getValidator()
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$EnableWebMvcConfiguration': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Found unexpected validator configuration. A Spring Boot MVC validator should be registered as bean named 'mvcValidator' and not returned from WebMvcConfigurer.getValidator()
Caused by: java.lang.IllegalStateException: Found unexpected validator configuration. A Spring Boot MVC validator should be registered as bean named 'mvcValidator' and not returned from WebMvcConfigurer.getValidator()
Spring Boot MVC validator は mvcValidator
という名前の Bean で登録されているべきなのに、WebMvcConfigurer.getValidator()
が返さない、ということのようです。
例外に出力されているソースを追ってみると、org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration#afterPropertiesSet でこのメッセージを出力していることが確認できます。getValidator() == null
でないと Assert に引っかかります。
getValidator()
は org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration#getValidator のことで、以下のコードです。
configurers.getValidator()
は org.springframework.web.servlet.config.annotation.WebMvcConfigurerComposite#getValidator のことで、以下のコードです。このメソッドが null を返さないのでエラーになるようですね。
デバッガで確認してみると、確かに null ではなく LocalValidatorFactoryBean を返しています。
自分で実装したコードを見ると、確かに Validator インターフェースの Bean は mvcValidator
ではなく validator
という名前にしています。
そして validator Bean を ksbysample.webapp.lending.config.WebMvcConfig#getValidator で返しています。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その20 )( 気になった点を修正する ) の記事で ValidationMessages_ja_JP.properties をやめて messages.properties に1本化するために書いたものでした。
ここまでの調査から対応方法を考えると、以下のようにすれば良い気がします。
- ksbysample.webapp.lending.config.WebMvcConfig#getValidator は null を返さないといけないようなので、ksbysample.webapp.lending.config.WebMvcConfig 自体は削除する。
- validator Bean ではなく mvcValidator Bean にする。
“mvcValidator” で検索してみると org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcValidator に mvcValidator Bean を取得して mvcValidator Bean を生成する?、というよく分からないコードがありました。
Spring Boot 1.4.6 を使用している時のコードに戻してみると、org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcValidator は 1.5.3 と同じでした。
org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.EnableWebMvcConfiguration#afterPropertiesSet は存在しませんでした。
おそらく 1.4 の頃から mvcValidator という名前の Bean として登録しておけばよいだけで、WebMvcConfigurerAdapter の継承クラスで getValidator メソッドを Override する必要はなかった、のではないかと思います。
上に書いた2点の対応方法で良さそうなので、コードを修正します。
まず src/main/java/ksbysample/webapp/lending/config/WebMvcConfig.java を削除します。
次に src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java を リンク先の内容 に変更します。
最後に以下のクラスのフィールド private final Validator validator;
→ private final Validator mvcValidator;
に変更します。
- src/main/java/ksbysample/webapp/lending/web/springmvcmemo/BeanValidationGroupController.java
- src/test/java/ksbysample/webapp/lending/values/validation/ValuesEnumValidatorTest.java
コードを修正したら clean タスク → Rebuild Project → build タスクを実行してみます。今度は “BUILD SUCCESSFUL” が表示されて成功しました。
Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」も実行してみます。こちらも無事全てのテストが成功しました。
Hibernate Validator を使用しているところで messages.properties に記述した日本語のメッセージが使用されるか確認します。
bootRun で Tomcat を起動した後、Hibernate Validator の @NotBlank
アノテーションで入力チェックをしている
http://localhost:8080/springMvcMemo/beanValidationGroup にアクセスします。
「データ更新」ボタンをクリックすると、日本語のメッセージ「必須の入力項目です。」が表示されました。
mvcValidator Bean をコメントアウトしてから Tomcat を再起動して同じ操作をしてみると、
今度は英語のメッセージが表示されました。mvcValidator Bean を定義するだけで messages.properties の日本語メッセージが使用されるようになるようです。
次回は。。。
1.5 系ではこう書くべきという点があるか確認し、変更した方がよいところを修正します。
ソースコード
ApplicationConfig.java
@Bean public Validator mvcValidator() { LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); localValidatorFactoryBean.setValidationMessageSource(this.messageSource); return localValidatorFactoryBean; }
- メソッド名を
validator
→mvcValidator
に変更します。
履歴
2017/05/17
初版発行。
2017/05/20
* http://localhost:8080/springMvcMemo/beanValidationGroup での動作確認を追加しました。
Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その2 )( build.gradle の修正 )
概要
記事一覧はこちらです。
Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その1 )( 概要 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- build.gradle の修正
参照したサイト・書籍
目次
手順
1.5.x ブランチの作成
- master から 1.5.x ブランチを、1.5.x から feature/130-issue ブランチを作成します。
Spring Initializr で 1.5.3 のプロジェクトを作成する
IntelliJ IDEA のメインメニューから「File」-「Close Project」を選択して「Welcome to IntelliJ IDEA」ダイアログに戻ります。
「Welcome to IntelliJ IDEA」ダイアログで「Create New Project」をクリックします。
「New Project」ダイアログが表示されます。画面左側のリストから「Spring Initializr」を選択した後、「Next」ボタンをクリックします。
次の画面が表示されます。「Type」で「Gradle Project」を選択した後、「Next」ボタンをクリックします。
次の画面が表示されます。画面中央上の「Spring Boot」で「1.5.3」を選択してから ksbysample-webapp-lending プロジェクトで使用している以下の項目をチェックした後、「Next」ボタンをクリックします。
次の画面が表示されます。「Project location」を “C:\project-springboot\demo” に変更した後、「Finish」ボタンをクリックします。
「Import Module from Gradle」ダイアログが表示されます。「Create directories for empty content roots automatically」をチェックした後、「OK」ボタンをクリックします。
これでプロジェクトが作成されて以下の build.gradle が作成されました。
buildscript { ext { springBootVersion = '1.5.3.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile('org.springframework.boot:spring-boot-starter-data-redis') compile('org.springframework.boot:spring-boot-starter-freemarker') compile('org.springframework.boot:spring-boot-starter-mail') compile('org.springframework.boot:spring-boot-starter-security') compile('org.springframework.session:spring-session') compile('org.springframework.boot:spring-boot-starter-thymeleaf') compile('org.springframework.boot:spring-boot-starter-web') runtime('org.springframework.boot:spring-boot-devtools') compileOnly('org.projectlombok:lombok') testCompile('org.springframework.boot:spring-boot-starter-test') }
1.4.4 で生成した時と異なる点は特にありませんでした。今回は何も反映しません。
build.gradle を修正して build してみる
build.gradle を リンク先の内容 に変更します。
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。今回は正常に更新できました。
clean タスク実行 → Rebuild Project 実行 を実行します。こちらも正常に終了しました。
build タスクを実行します。が、大量の
java.lang.IllegalStateException
が発生してテストが全く終わりませんでした。。。Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」も実行してみます。
こちらも
java.lang.IllegalStateException: Failed to load ApplicationContext
というエラーメッセージが出てテストは失敗しました。ほとんどのテストが失敗していたので、途中で中断しています。
次回は。。。
Run ‘All Tests’ with Coverage 実行時に出る java.lang.IllegalStateException: Failed to load ApplicationContext
のエラーの解消→ build タスクの再実行(エラーが出れば解消します)の順で進める予定です。
ソースコード
build.gradle
group 'ksbysample' version '1.5.3-RELEASE' buildscript { ext { springBootVersion = '1.5.3.RELEASE' } repositories { jcenter() maven { url "http://repo.spring.io/repo/" } maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") // for Error Prone ( http://errorprone.info/ ) classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.10") // for Grgit classpath("org.ajoberstar:grgit:1.9.2") // Gradle Download Task classpath("de.undercouch:gradle-download-task:3.2.0") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'org.springframework.boot' apply plugin: 'de.undercouch.download' apply plugin: 'groovy' apply plugin: 'net.ltgt.errorprone' apply plugin: 'checkstyle' apply plugin: 'findbugs' sourceCompatibility = 1.8 targetCompatibility = 1.8 task wrapper(type: Wrapper) { gradleVersion = '2.13' } [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path'] compileJava.options.compilerArgs += ['-Xep:RemoveUnusedImports:WARN'] // for Doma 2 // JavaクラスとSQLファイルの出力先ディレクトリを同じにする processResources.destinationDir = compileJava.destinationDir // コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する compileJava.dependsOn processResources idea { module { inheritOutputDirs = false outputDir = file("$buildDir/classes/main/") } } configurations { // for Doma 2 domaGenRuntime } checkstyle { configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml") toolVersion = '7.7' sourceSets = [project.sourceSets.main] } findbugs { toolVersion = '3.0.1' sourceSets = [project.sourceSets.main] ignoreFailures = true effort = "max" excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml") } tasks.withType(FindBugs) { reports { xml.enabled = false html.enabled = true } } repositories { jcenter() } dependencyManagement { imports { mavenBom("io.spring.platform:platform-bom:Brussels-SR2") { bomProperty 'guava.version', '21.0' } } } bootRepackage { mainClass = 'ksbysample.webapp.lending.Application' excludeDevtools = true } dependencies { def jdbcDriver = "org.postgresql:postgresql:42.0.0" def spockVersion = "1.1-groovy-2.4" def domaVersion = "2.16.0" def lombokVersion = "1.16.16" def errorproneVersion = "2.0.15" // dependency-management-plugin によりバージョン番号が自動で設定されるもの // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照 compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity4") compile("org.thymeleaf.extras:thymeleaf-extras-java8time") compile("org.springframework.boot:spring-boot-starter-data-jpa") compile("org.springframework.boot:spring-boot-starter-freemarker") compile("org.springframework.boot:spring-boot-starter-mail") compile("org.springframework.boot:spring-boot-starter-security") compile("org.springframework.boot:spring-boot-starter-data-redis") compile("org.springframework.boot:spring-boot-starter-amqp") compile("org.springframework.boot:spring-boot-devtools") compile("org.springframework.session:spring-session") compile("org.springframework.retry:spring-retry") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") compile("com.google.guava:guava") compile("org.apache.commons:commons-lang3") compile("org.codehaus.janino:janino") testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("org.springframework.security:spring-security-test") testCompile("org.yaml:snakeyaml") testCompile("org.mockito:mockito-core") // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの runtime("${jdbcDriver}") compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.1") compile("org.simpleframework:simple-xml:2.7.1") compile("com.univocity:univocity-parsers:2.4.1") testCompile("org.dbunit:dbunit:2.5.3") testCompile("com.icegreen:greenmail:1.5.4") testCompile("org.assertj:assertj-core:3.7.0") testCompile("com.jayway.jsonpath:json-path:2.2.0") testCompile("org.spockframework:spock-core:${spockVersion}") { exclude module: "groovy-all" } testCompile("org.spockframework:spock-spring:${spockVersion}") { exclude module: "groovy-all" } testCompile("com.google.code.findbugs:jsr305:3.0.2") // for lombok compileOnly("org.projectlombok:lombok:${lombokVersion}") testCompileOnly("org.projectlombok:lombok:${lombokVersion}") // for Doma compile("org.seasar.doma:doma:${domaVersion}") domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}") domaGenRuntime("${jdbcDriver}") // for Error Prone ( http://errorprone.info/ ) errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}") compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}") } bootRun { jvmArgs = ['-Dspring.profiles.active=develop'] } test { jvmArgs = ['-Dspring.profiles.active=unittest'] } // for Doma-Gen task domaGen << { // まず変更が必要なもの def rootPackageName = 'ksbysample.webapp.lending' def daoPackagePath = 'src/main/java/ksbysample/webapp/lending/dao' def dbUrl = 'jdbc:postgresql://localhost/ksbylending' def dbUser = 'ksbylending_user' def dbPassword = 'xxxxxxxx' def tableNamePattern = '.*' // おそらく変更不要なもの def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig" def workDirPath = 'work' def workDaoDirPath = "${workDirPath}/dao" // 作業用ディレクトリを削除する clearDir("${workDirPath}") // 現在の Dao インターフェースのバックアップを取得する copy() { from "${daoPackagePath}" into "${workDaoDirPath}/org" } // Dao インターフェース、Entity クラスを生成する ant.taskdef(resource: 'domagentask.properties', classpath: configurations.domaGenRuntime.asPath) ant.gen(url: "${dbUrl}", user: "${dbUser}", password: "${dbPassword}", tableNamePattern: "${tableNamePattern}") { entityConfig(packageName: "${rootPackageName}.entity", useListener: false) daoConfig(packageName: "${rootPackageName}.dao") sqlConfig() } // 生成された Dao インターフェースを作業用ディレクトリにコピーし、 // @ComponentAndAutowiredDomaConfig アノテーションを付加する copy() { from "${daoPackagePath}" into "${workDaoDirPath}/replace" filter { line -> line.replaceAll('import org.seasar.doma.Dao;', "import ${importOfComponentAndAutowiredDomaConfig};\nimport org.seasar.doma.Dao;") .replaceAll('@Dao', '@Dao\n@ComponentAndAutowiredDomaConfig') } } // @ComponentAndAutowiredDomaConfig アノテーションを付加した Dao インターフェースを // dao パッケージへ戻す copy() { from "${workDaoDirPath}/replace" into "${daoPackagePath}" } // 元々 dao パッケージ内にあったファイルを元に戻す copy() { from "${workDaoDirPath}/org" into "${daoPackagePath}" } // 作業用ディレクトリを削除する clearDir("${workDirPath}") // 自動生成したファイルを git add する addGit() } task downloadCssFontsJs << { def staticDirPath = 'src/main/resources/static' def workDirPath = 'work' def adminLTEVersion = '2.2.0' def jQueryVersion = '2.1.4' def fontAwesomeVersion = '4.3.0' def ioniconsVersion = '2.0.1' def html5shivJsVersion = '3.7.2' def respondMinJsVersion = '1.4.2' // 作業用ディレクトリを削除する clearDir("${workDirPath}") // Bootstrap & AdminLTE Dashboard & Control Panel Template downloadAdminLTE("${adminLTEVersion}", "${jQueryVersion}", "${workDirPath}", "${staticDirPath}") // Font Awesome Icons downloadFontAwesome("${fontAwesomeVersion}", "${workDirPath}", "${staticDirPath}") // Ionicons downloadIonicons("${ioniconsVersion}", "${workDirPath}", "${staticDirPath}") // html5shiv.js downloadHtml5shivJs("${html5shivJsVersion}", "${workDirPath}", "${staticDirPath}") // respond.min.js downloadRespondMinJs("${respondMinJsVersion}", "${workDirPath}", "${staticDirPath}") // fileinput.min.js ( v4.2.7 ) downloadBootstrapFileInputMinJs("${workDirPath}", "${staticDirPath}") // 作業用ディレクトリを削除する clearDir("${workDirPath}") // 追加したファイルを git add する addGit() } task printClassWhatNotMakeTest << { def srcDir = new File("src/main/java") def excludePaths = [ "src/main/java/ksbysample/webapp/lending/Application.java" , "src/main/java/ksbysample/webapp/lending/config" , "src/main/java/ksbysample/webapp/lending/cookie" , "src/main/java/ksbysample/webapp/lending/dao" , "src/main/java/ksbysample/webapp/lending/entity" , "src/main/java/ksbysample/webapp/lending/exception" , "src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv" , "src/main/java/ksbysample/webapp/lending/helper/download/DataDownloadHelper.java" , "src/main/java/ksbysample/webapp/lending/helper/page/PagenationHelper.java" , "src/main/java/ksbysample/webapp/lending/security/LendingUser.java" , "src/main/java/ksbysample/webapp/lending/security/RoleAwareAuthenticationSuccessHandler.java" , "src/main/java/ksbysample/webapp/lending/service/calilapi/response" , "src/main/java/ksbysample/webapp/lending/service/file/BooklistCSVRecord.java" , "src/main/java/ksbysample/webapp/lending/service/openweathermapapi" , "src/main/java/ksbysample/webapp/lending/service/queue/InquiringStatusOfBookQueueMessage.java" , "src/main/java/ksbysample/webapp/lending/util/doma" , "src/main/java/ksbysample/webapp/lending/util/velocity/VelocityUtils.java" , "src/main/java/ksbysample/webapp/lending/values/validation/ValuesEnum.java" , "src/main/java/ksbysample/webapp/lending/view/BookListCsvView.java" , "src/main/java/ksbysample/webapp/lending/web/.+/.+Service.java" , "src/main/java/ksbysample/webapp/lending/webapi/common/CommonWebApiResponse.java" , "src/main/java/ksbysample/webapp/lending/webapi/weather" ] def excludeFileNamePatterns = [ ".*EventListener.java" , ".*Dto.java" , ".*Form.java" , ".*Values.java" ] compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns) } /* ----------------------------------------------------------------------------- * メソッド定義部 ---------------------------------------------------------------------------- */ void clearDir(String dirPath) { delete dirPath } void addGit() { def grgit = org.ajoberstar.grgit.Grgit.open(dir: project.projectDir) grgit.add(patterns: ['.']) } void downloadAdminLTE(String adminLTEVersion, String jQueryVersion, String workDirPath, String staticDirPath) { download { src "https://codeload.github.com/almasaeed2010/AdminLTE/zip/v${adminLTEVersion}" dest new File("${workDirPath}/download/AdminLTE-${adminLTEVersion}.zip") } copy { from zipTree("${workDirPath}/download/AdminLTE-${adminLTEVersion}.zip") into "${workDirPath}/unzip" } copy { from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/bootstrap/css" into "${staticDirPath}/css" } copy { from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/bootstrap/fonts" into "${staticDirPath}/fonts" } copy { from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/bootstrap/js" into "${staticDirPath}/js" } copy { from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/dist/css" into "${staticDirPath}/css" } copy { from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/dist/js" into "${staticDirPath}/js" } copy { from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/plugins/jQuery/jQuery-${jQueryVersion}.min.js" into "${staticDirPath}/js" } delete "${staticDirPath}/js/pages" delete "${staticDirPath}/js/demo.js" } void downloadFontAwesome(String fontAwesomeVersion, String workDirPath, String staticDirPath) { download { src "http://fortawesome.github.io/Font-Awesome/assets/font-awesome-${fontAwesomeVersion}.zip" dest new File("${workDirPath}/download/font-awesome-${fontAwesomeVersion}.zip") } copy { from zipTree("${workDirPath}/download/font-awesome-${fontAwesomeVersion}.zip") into "${workDirPath}/unzip" } copy { from "${workDirPath}/unzip/font-awesome-${fontAwesomeVersion}/css/font-awesome.min.css" into "${staticDirPath}/css" } copy { from "${workDirPath}/unzip/font-awesome-${fontAwesomeVersion}/fonts" into "${staticDirPath}/fonts" } } void downloadIonicons(String ioniconsVersion, String workDirPath, String staticDirPath) { download { src "https://codeload.github.com/driftyco/ionicons/zip/v${ioniconsVersion}" dest new File("${workDirPath}/download/ionicons-${ioniconsVersion}.zip") } copy { from zipTree("${workDirPath}/download/ionicons-${ioniconsVersion}.zip") into "${workDirPath}/unzip" } copy { from "${workDirPath}/unzip/ionicons-${ioniconsVersion}/css/ionicons.min.css" into "${staticDirPath}/css" } copy { from "${workDirPath}/unzip/ionicons-${ioniconsVersion}/fonts" into "${staticDirPath}/fonts" } } void downloadHtml5shivJs(String html5shivJsVersion, String workDirPath, String staticDirPath) { download { src "https://oss.maxcdn.com/html5shiv/${html5shivJsVersion}/html5shiv.min.js" dest new File("${workDirPath}/download/html5shiv.min.js") } copy { from "${workDirPath}/download/html5shiv.min.js" into "${staticDirPath}/js" } } void downloadRespondMinJs(String respondMinJsVersion, String workDirPath, String staticDirPath) { download { src "https://oss.maxcdn.com/respond/${respondMinJsVersion}/respond.min.js" dest new File("${workDirPath}/download/respond.min.js") } copy { from "${workDirPath}/download/respond.min.js" into "${staticDirPath}/js" } } void downloadBootstrapFileInputMinJs(String workDirPath, String staticDirPath) { download { src "https://github.com/kartik-v/bootstrap-fileinput/zipball/master" dest new File("${workDirPath}/download/kartik-v-bootstrap-fileinput.zip") } copy { from zipTree("${workDirPath}/download/kartik-v-bootstrap-fileinput.zip") into "${workDirPath}/unzip" } copy { from "${workDirPath}/unzip/kartik-v-bootstrap-fileinput-883d8b6/js/fileinput.min.js" into "${staticDirPath}/js" } copy { from "${workDirPath}/unzip/kartik-v-bootstrap-fileinput-883d8b6/js/fileinput_locale_ja.js" into "${staticDirPath}/js" } copy { from "${workDirPath}/unzip/kartik-v-bootstrap-fileinput-883d8b6/css/fileinput.min.css" into "${staticDirPath}/css" } } def compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns) { def existFlg def testDirAndTestClassNameList = [ ["src/test/java", "Test.java"] , ["src/test/groovy", "Test.groovy"] ] for (srcFile in srcDir.listFiles()) { String srcFilePath = (srcFile.toPath() as String).replaceAll("\\\\", "/") existFlg = false for (exclude in excludePaths) { if (srcFilePath =~ /^${exclude as String}/) { existFlg = true break } } if (existFlg == true) continue for (exclude in excludeFileNamePatterns) { if (srcFilePath =~ /${exclude as String}/) { existFlg = true break } } if (existFlg == true) continue if (srcFile.isDirectory()) { compareSrcAndTestDir(srcFile, excludePaths, excludeFileNamePatterns) } else { def nextFlg = false for (testDirAndTestClassName in testDirAndTestClassNameList) { def testDir = testDirAndTestClassName[0] def testClassName = testDirAndTestClassName[1] String testFilePath = srcFilePath.replaceFirst(/^src\/main\/java/, testDir) .replaceFirst(/\.java$/, testClassName) def testFile = new File(testFilePath) if (testFile.exists()) { nextFlg = true break } } if (nextFlg) continue println(srcFilePath) } } }
- Spring Boot のバージョンアップ対応として以下の点を変更します。
version '1.4.6-RELEASE'
→version '1.5.3-RELEASE'
に変更します。- buildscript の以下の点を変更します。
springBootVersion = '1.4.6.RELEASE'
→springBootVersion = '1.5.3.RELEASE'
に変更します。
- dependencyManagement の以下の点を変更します。
mavenBom("io.spring.platform:platform-bom:Athens-SR5")
→mavenBom("io.spring.platform:platform-bom:Athens-SR5")
に変更します。
- ライブラリを最新バージョンにアップデートするために以下の点を変更します。
def spockVersion = "1.1-groovy-2.4-rc-4"
→def spockVersion = "1.1-groovy-2.4"
に変更します。testCompile("org.assertj:assertj-core:3.6.2")
→testCompile("org.assertj:assertj-core:3.7.0")
に変更します。
履歴
2017/05/14
初版発行。
Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その1 )( 概要 )
概要
記事一覧はこちらです。
- 「Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る」で作成した Web アプリケーション ( ksbysample-webapp-lending ) の Spring Boot のバージョンを 1.4.6 → 1.5.3 へバージョンアップします。
- 進め方は以下の方針とします。
- Git のブランチは 1.5.x を作成して、そちらで作業します。Spring Boot のバージョンと合わせます。
- 最初に build.gradle を修正します。
- Spring Boot のバージョン番号を 1.5.3 に、Spring IO Platform の BOM を Brussels-SR2 にします。
- Spring Initializr で 1.5.3 のプロジェクトを作成して、修正した方がよさそうな点があれば反映します。
- ライブラリは最新バージョンにアップデートします。
- プロジェクトを build し直してエラーが出る点があれば修正し、まずはここまでで動くようにします。
- その後で 1.5 系ではこう書くべきという点があるか確認し、変更した方がよいところを修正します。
1.5 のリリースノートはこちらです。
Spring Boot 1.5 Release Notes
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.5-Release-Notes
履歴
2017/05/13
初版発行。
Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( 大目次 )
- その1 ( 概要 )
- その2 ( build.gradle の修正 )
- その3 ( Run ‘All Tests’ with Coverage 実行時に出るエラーを解消する )
- その4 ( 1.4系 → 1.5系で変更された点を修正する )
- その5 ( Thymeleaf を 2.1.5 → 3.0.6 へバージョンアップする )
- その6 ( Thymeleaf を 2.1.5 → 3.0.6 へバージョンアップする2 )
- その7 ( Gradle を 2.13 → 3.5 へバージョンアップし、FindBugs Gradle Plugin が出力する大量のログを抑制する )
- その8 ( logback-develop.xml, logback-unittest.xml, logback-product.xml の設定を logback-spring.xml と application.properties に移動してファイルを削除する )
- 番外編 ( static メソッドをモック化してテストするには? )
- その9 ( Spring Boot を 1.5.3 → 1.5.4 にバージョンアップする )
- 番外編 ( Groovy + JUnit4 でテストを書いてみる、Groovy SQL を使ってみる )
- その10 ( 起動時の spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する )
- その11 ( build.gradle への PMD の導入 )
- その12 ( build.gradle への PMD の導入2 )
- その13 ( jar ファイルを作成して動作確認する )
- その14 ( request, response のログを出力する RequestAndResponseLogger クラスを修正する )
- その15 ( -XX:+ExitOnOutOfMemoryError と -XX:+CrashOnOutOfMemoryError オプションのどちらを指定すべきか? )
- 感想
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( 感想 )
記事一覧はこちらです。
1.3系 → 1.4 系へのバージョンアップでは動かなくなるということはあまりなくて、どちらかと言うと書き方がいろいろ変わるので、1.4 系の書き方に変更するのが主な対応になる、という感じでした。
Velocity が非推奨になったので FreeMarker に変えましたが、テキスト形式のテンプレートの場合には Thymeleaf 3 より FreeMarker の方が機能が揃っていて便利な気がします。ただし、デフォルトで数値は3桁毎に “,” 区切りになる等、まだ気付いていない点があるような気がしていて、使用時はちょっと注意が必要そうです。
CheckStyle, FindBugs を入れてみましたが、フォーマットやコードに問題があるところを指摘してくれるので便利でした。ただし Gradle を 3 系の最新版へバージョンアップすると FindBugs Plugin が警告メッセージを大量に出力するのが難点です。Gradle 3 へ上げようと思っていましたが、これのために止めました。FindBugs って今開発が止まっているんですよね。FindBugs を外して Gradle を 3 系へバージョンアップすべきか迷います。。。
ErrorProne は FindBugs では指摘してくれないコード上の問題点や、JDK 9 にした場合の問題点を報告してくれるので、こちらも今後は入れていきたいと思います。ただし lombok と相性が悪いんですよね。。。 バージョン 2.0.19 まで出ていますが、しばらく 2.0.15 で止めたままになる気がします。
以下の2つを今回入れましたが、こちらも便利でした。今後も入れていきます。
- Request mapper Plugin
- Log4jdbc Spring Boot Starter
次は 1.5.x へのバージョンアップをやる予定です。Thymeleaf も 3 に上げます。1.4.x → 1.5.x はそんなに大きな変更点はなさそうなので、短期間で終わるはず!(と言いつつ、たぶん寄り道するんだろうな。。。)
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その27 )( Thymeleaf parser-level comment blocks で @thymesVar のコメント文が HTML に出力されないようにする )
概要
記事一覧はこちらです。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その26 )( jar ファイルを作成して動作確認する2 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- Thymeleaf 3 で何が変わったのか確認したくて Tutorial: Using Thymeleaf を読んだのですが、Thymeleaf parser-level comment blocks というものがあることに今さら気づきました(Thymeleaf 3 の新機能ではありません)。
- @thymesVar のコメント文を HTML に出力しないようにするために使えそうなので試してみます。
参照したサイト・書籍
- Tutorial: Using Thymeleaf - 11.2. Thymeleaf parser-level comment blocks
http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#thymeleaf-parser-level-comment-blocks
目次
手順
@thymesVar のコメント文を HTML に出力しないようにする
IntelliJ IDEA Ulitimate Editon では <!-- @thymesVar id="beanValidationGroupForm" type="ksbysample.webapp.lending.web.springmvcmemo.BeanValidationGroupForm" -->
のようなコメント文を書いておくと、Thymeleaf テンプレート内で変数の補完が効くようになります。
コメント文を書いていないと、以下の画像の赤枠の部分で Ctrl+Enter を押しても何も表示されませんが、
<!-- @thymesVar id="beanValidationGroupForm" type="ksbysample.webapp.lending.web.springmvcmemo.BeanValidationGroupForm" -->
のコメントを付けると、Ctrl+Enter を押すと候補が表示されます。
ただし HTML を出力した時にこのコメントがそのまま残ります。
HTML を見た時に Thymeleaf が使われていることが分からないようにしたくて、出力されない方法をずっと探していたんですよね。。。
そこで見つけたのが Thymeleaf parser-level comment blocks です。Thymeleaf テンプレート内で記述するコメントの書き方を HTML のコメント文 <!-- ... -->
ではなく <!--/* ... */-->
にすれば、補完も効くし、HTML には出力されないようになります。
<!--/* @thymesVar id="beanValidationGroupForm" type="ksbysample.webapp.lending.web.springmvcmemo.BeanValidationGroupForm" */-->
に変えても候補は表示されます。
そしてコメントは HTML には出力されません。
以下のファイル内の <!-- @thymesVar ... -->
→ <!--/* @thymesVar ... */-->
に修正します。
- src/main/resources/templates/sessionsample/confirm.html
- src/main/resources/templates/sessionsample/first.html
- src/main/resources/templates/sessionsample/next.html
- src/main/resources/templates/springmvcmemo/beanValidationGroup.html
- src/main/resources/templates/textareamemo/display.html
- src/main/resources/templates/textareamemo/index.html
Alt+Enter で @thymesVar のコメント文は自動補完できました。。。
IntelliJ IDEA Ulitimate Editon では Thymeleaf テンプレート上で Alt+Enter を押すことで <!--/*@thymesVar ... */-->
のコメント文を自動補完できることに気づきました。
まず <!--/*@thymesVar ... */-->
のコメント文がない変数には赤波下線が表示されます。
赤波下線が表示されている変数にカーソルを移動した後、Alt+Enter を押してコンテキストメニューを表示した後「Declare external variable in comment annotation」を選択します。
<!--/*@thymesVar ... */-->
のコメント文が補完されます。type=""
の中は空でクラスの補完メニューが出ますので、入力します。
履歴
2017/05/13
初版発行。
2017/05/13
* <!--/*@thymesVar ... */-->
のコメント文の自動補完について追記しました。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( 番外編 )( Thymeleaf 3 へのバージョンアップを試してみる2 )
概要
記事一覧はこちらです。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( 番外編 )( Thymeleaf 3 へのバージョンアップを試してみる ) の続きです。Thymeleaf 3 ten-minute migration guide に書かれている新機能を試してみます。
今回も試してみるだけでコミットはしません。
参照したサイト・書籍
Thymeleaf 3 ten-minute migration guide
http://www.thymeleaf.org/doc/articles/thymeleaf3migration.htmlTutorial: Using Thymeleaf
http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
目次
手順
SpELコンパイラを有効にする
前回、「参照したサイト・書籍」にリンクを書いたのに設定するのを忘れていたので、Spring Boot 1.4+Thymeleaf 3.0でSpELコンパイラを有効にしてパフォーマンスを向上させよう!! の記事に従い SpELコンパイラを有効にします。
src/main/java/ksbysample/webapp/lending/config/WebMvcConfig.java を以下のように変更します。
@Configuration public class WebMvcConfig extends WebMvcConfigurerAdapter { .......... /** * Thymeleaf 3 のパフォーマンスを向上させるために SpEL コンパイラを有効にする * * @param templateEngine {@link SpringTemplateEngine} オブジェクト */ @Autowired public void configureThymeleafSpringTemplateEngine(SpringTemplateEngine templateEngine) { templateEngine.setEnableSpringELCompiler(true); } }
configureThymeleafSpringTemplateEngine
メソッドを追加します。
Fragment Expressions
Fragment Expressions は ~{ ... :: ... }
の形式でかなり自由に Thymeleaf テンプレートファイルに記載された HTML の一部を取得できるようになるような機能ですが、これを使ってヘッダーの共通化等がかなりやりやすくなっています。
例えば head タグ内の共通部分を定義するのに、これまでは共通部分を別の html ファイルに記述して th:replace
で読み込んでいました。
src/main/resources/templates/booklist/booklist.html では以下のように記述して、
<head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>貸出希望書籍 CSV ファイルアップロード</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <link th:replace="common/head-cssjs"/> <!-- Bootstrap File Input --> <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css"/> <style type="text/css"> <!-- .callout ul li { margin-left: -30px; } --> </style> </head>
<link th:replace="common/head-cssjs"/>
で読み込んでいる src/main/resources/templates/common/head-cssjs.html は以下のように記述しています。
<!-- Bootstrap 3.3.4 --> <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <!-- Font Awesome Icons --> <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"/> <!-- Ionicons --> <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"/> <!-- Theme style --> <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/> <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style>
これで画面を表示すると以下の HTML が出力されます。
<head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>貸出希望書籍 CSV ファイルアップロード</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <!-- Bootstrap 3.3.4 --> <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <!-- Font Awesome Icons --> <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"/> <!-- Ionicons --> <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"/> <!-- Theme style --> <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/> <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style> <!-- Bootstrap File Input --> <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css"/> <style type="text/css"> <!-- .callout ul li { margin-left: -30px; } --> </style> </head>
これが Fragment Expressions の機能を利用すると head タグの共通部分を全て別ファイルに定義して、差分の箇所だけ各 html ファイルに記述できるようになります。
例えば src/main/resources/templates/common/head.html に head タグの共通部分を以下のように記述します。
<head th:fragment="common_header(title, links, style)"> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title th:replace="${title}">画面のタイトル</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <!-- Bootstrap 3.3.4 --> <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <!-- Font Awesome Icons --> <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"/> <!-- Ionicons --> <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"/> <!-- Theme style --> <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/> <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- ここに各htmlで定義された link タグが追加される --> <th:block th:replace="${links}"/> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style> <!-- ここに各htmlで定義された style タグが追加される --> <th:block th:replace="${style}"/> </head>
src/main/resources/templates/booklist/booklist.html を以下のように修正します。変更したい title タグや、追加したい link, style タグだけ記述します。
<head th:replace="~{common/head :: common_header(~{::title}, ~{::link}, ~{::style})}"> <title>貸出希望書籍 CSV ファイルアップロード</title> <!-- Bootstrap File Input --> <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css"/> <style type="text/css"> <!-- .callout ul li { margin-left: -30px; } --> </style> </head>
これで画面を表示すると以下の HTML が出力されます。src/main/resources/templates/booklist/booklist.html に記述した title, link, style タグの内容が反映されています。この例では link, style タグはそれぞれ1つしか記述していませんが、例えば link タグを2つ書けば、2つとも反映されます。
<head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>貸出希望書籍 CSV ファイルアップロード</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <!-- Bootstrap 3.3.4 --> <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <!-- Font Awesome Icons --> <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"/> <!-- Ionicons --> <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"/> <!-- Theme style --> <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/> <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- ここに各htmlで定義された link タグが追加される --> <link href="/css/fileinput.min.css" rel="stylesheet" type="text/css"/> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style> <!-- ここに各htmlで定義された style タグが追加される --> <style type="text/css"> <!-- .callout ul li { margin-left: -30px; } --> </style> </head>
src/main/resources/templates/booklist/booklist.html の head タグの中に何も書かないと、
<head th:replace="~{common/head :: common_header(~{::title}, ~{::link}, ~{::style})}"> </head>
エラー画面が表示されました。。。
ログを見ると org.thymeleaf.exceptions.TemplateInputException: Error resolving fragment: "${title}": template or fragment could not be resolved (template: "common/head" - line 4, col 12)
というログが出力されていました。head タグの中に title タグを記述しなかったので、引数の ~{::title}
が空になりエラーになったようです。
Fragment Expressions と同じく新規に追加された The No-Operation token の機能を利用して src/main/resources/templates/common/head.html を以下のように修正します。?: _
を付けると、値がセットされていなければ th:replace
の処理が行われなくなります。
<head th:fragment="common_header(title, links, style)"> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title th:replace="${title} ?: _">画面のタイトル</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <!-- Bootstrap 3.3.4 --> <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <!-- Font Awesome Icons --> <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"/> <!-- Ionicons --> <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"/> <!-- Theme style --> <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/> <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- ここに各htmlで定義された link タグが追加される --> <th:block th:replace="${links} ?: _"/> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style> <!-- ここに各htmlで定義された style タグが追加される --> <th:block th:replace="${style} ?: _"/> </head>
今度は画面が表示されて、
以下の HTML が出力されました。title タグは head.html の記述がそのまま出力されて、th:block
で書いていたところには何も出力されていません。
<head> <meta charset="UTF-8"/> <meta http-equiv="X-UA-Compatible" content="IE=edge"/> <title>画面のタイトル</title> <!-- Tell the browser to be responsive to screen width --> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"/> <!-- Bootstrap 3.3.4 --> <link href="/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <!-- Font Awesome Icons --> <link href="/css/font-awesome.min.css" rel="stylesheet" type="text/css"/> <!-- Ionicons --> <link href="/css/ionicons.min.css" rel="stylesheet" type="text/css"/> <!-- Theme style --> <link href="/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/> <!-- AdminLTE Skins. Choose a skin from the css/skins folder instead of downloading all of them to reduce the load. --> <link href="/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="/js/html5shiv.min.js"></script> <script src="/js/respond.min.js"></script> <![endif]--> <!-- ここに各htmlで定義された link タグが追加される --> <style type="text/css"> <!-- .jp-gothic { font-family: Verdana, "游ゴシック", YuGothic, "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; } .......... --> </style> <!-- ここに各htmlで定義された style タグが追加される --> </head>
また <th:block th:replace="${style} ?: _"/>
は The EMPTY fragment を利用して <th:block th:replace="${style} ?: ~{}"/>
と書いても結果は同じになります。
Fragment Expressions の詳細は以下のリンク先の Issue, Tutorial に書かれています。
- Fragment Expressions
https://github.com/thymeleaf/thymeleaf/issues/451 - Tutorial: Using Thymeleaf("fragment" で検索するといろいろヒットします)
http://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
共通部分をまとめることができ、かつ部分的な変更もしやすくなるので、かなり便利な機能だと思います。
The No-Operation token
The No-Operation token は記述した条件にマッチしなければ th:*
の内容が反映されない機能です。
既に Fragment Expressions に The No-Operation token の例を記述しましたが、別の例を書いてみます。
画面右上の図書館の選択状態を表示するために、src/main/resources/templates/common/mainparts.html に以下のように記述していますが、
<!-- Navbar Right Menu --> <div class="navbar-custom-menu"> <p class="navbar-text" th:classappend="${#strings.startsWith(@libraryHelper.getSelectedLibrary(), '※')} ? 'noselected-library' : 'selected-library'" th:text="${@libraryHelper.getSelectedLibrary()}">※図書館が選択されていません</p> <ul class="nav navbar-nav"> <li><a href="/logout">ログアウト</a></li> </ul> </div> <!-- /.navbar-custom-menu -->
これを以下のように変更します。@libraryHelper.getSelectedLibrary()
が値を返さなければ、HTML の ※図書館が選択されていません
をそのまま表示します。
<!-- Navbar Right Menu --> <div class="navbar-custom-menu"> <p class="navbar-text" th:with="selectedLibrary=${@libraryHelper.getSelectedLibrary()}" th:classappend="${selectedLibrary} ? 'selected-library' : 'noselected-library'" th:text="${selectedLibrary} ?: _">※図書館が選択されていません</p> <ul class="nav navbar-nav"> <li><a href="/logout">ログアウト</a></li> </ul> </div> <!-- /.navbar-custom-menu -->
@libraryHelper.getSelectedLibrary()
の実装である src/main/java/ksbysample/webapp/lending/helper/library/LibraryHelper.java は現在以下のコードですが、
@Component public class LibraryHelper { .......... /** * @return ??? */ public String getSelectedLibrary() { String result; LibraryForsearch libraryForsearch = libraryForsearchDao.selectSelectedLibrary(); if (libraryForsearch == null) { result = "※図書館が選択されていません"; } else { result = "選択中:" + libraryForsearch.getFormal(); } return result; } }
getSelectedLibrary メソッドを以下のように変更します。図書館が未選択なら null を返します。
public String getSelectedLibrary() { String result = null; LibraryForsearch libraryForsearch = libraryForsearchDao.selectSelectedLibrary(); if (libraryForsearch != null) { result = "選択中:" + libraryForsearch.getFormal(); } return result; }
library_search テーブルをクリアして検索対象図書館登録画面を表示すると、「※図書館が選択されていません」のメッセージが表示されます。
図書館を選択すると、選択された図書館が表示されます。
Decoupled Template Logic
Decoupled Template Logic は HTML から Thymeleaf の記述を分離して、HTML ファイル自体には th:*
タグがないままに出来る機能です。
サンプルを作ろうと思いましたが、th:*
の記述を HTML に直接書いてもそんなに邪魔にならないと自分では思っているのと、th:*
を記述するところに id や class 等で指定しやすくしておかないと別ファイルから HTML 本体への位置指定が書きにくそうに思えたので、止めました。今の自分ではこの機能を使うことはないという印象です。
最後に
Thymeleaf 3 でいろいろ便利になっている印象を受けました。Thymeleaf 3 ten-minute migration guide だけでなく Tutorial も読んでみようと思います。
履歴
2017/05/11
初版発行。