かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その15 )( -XX:+ExitOnOutOfMemoryError と -XX:+CrashOnOutOfMemoryError オプションのどちらを指定すべきか? )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その14 )( request, response のログを出力する RequestAndResponseLogger クラスを修正する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • stackoverflow の Java - shutting down on Out of Memory Error の QA で 8u92 から -XX:+ExitOnOutOfMemoryError-XX:+CrashOnOutOfMemoryError のオプションが追加になったと見かけて、そういえばどちらも指定していなかったことに気づきました。
    • ただし、ほとんど似たようなオプションに見えてどちらを指定した方がよいのかよく分からなかったので、その調査をします。

参照したサイト・書籍

  1. Java - shutting down on Out of Memory Error
    https://stackoverflow.com/questions/12096403/java-shutting-down-on-out-of-memory-error

  2. Print All JVM Flags
    https://stackoverflow.com/questions/10486375/print-all-jvm-flags

  3. Implement ExitOnOutOfMemory and CrashOnOutOfMemory in HotSpot
    https://bugs.openjdk.java.net/browse/JDK-8138745

  4. Release Note: New JVM Options: ExitOnOutOfMemoryError and CrashOnOutOfMemoryError
    https://bugs.openjdk.java.net/browse/JDK-8152669

  5. 8u92 Update Release Notes
    http://www.oracle.com/technetwork/java/javase/8u92-relnotes-2949471.html

  6. terazzoの日記 - JavaでOutOfMemoryErrorを出す方法
    http://d.hatena.ne.jp/terazzo/20130320/1363731368

目次

  1. 確認の前に -XX:HeapDumpPath, -XX:ErrorFile で指定するファイル名に日時が付くようにする
  2. -XX:+ExitOnOutOfMemoryError と -XX:+CrashOnOutOfMemoryError のオプションが追加されていることを確認する
  3. 実際に OutOfMemory が発生する処理を追加して相違点を確認する
    1. OutOfMemory が発生する処理を追加する
    2. どちらのオプションも指定していない場合、どうなるのか?
    3. -XX:+ExitOnOutOfMemoryError オプションを指定するとどうなるか?
    4. -XX:+CrashOnOutOfMemoryError オプションを指定するとどうなるか?
  4. まとめ
  5. webapp_startup.bat を修正する

手順

確認の前に -XX:HeapDumpPath, -XX:ErrorFile で指定するファイル名に日時が付くようにする

/webapps/ksbysample-webapp-lending/bat/webapp_startup.bat は現在以下の内容ですが、

@echo on

setlocal
set JAVA_HOME=C:\Java\jdk1.8.0_131
set PATH=%JAVA_HOME%\bin;%PATH%
set WEBAPP_HOME=C:\webapps\ksbysample-webapp-lending
set WEBAPP_JAR=ksbysample-webapp-lending-1.5.4-RELEASE.jar

cd /d %WEBAPP_HOME%
java -server ^
     -Xms1024m -Xmx1024m ^
     -XX:MaxMetaspaceSize=384m ^
     -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled ^
     -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75 ^
     -XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark ^
     -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps ^
     -Xloggc:%WEBAPP_HOME%/logs/gc.log ^
     -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M ^
     -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=%WEBAPP_HOME%/logs/%date:~0,4%%date:~5,2%%date:~8,2%.hprof ^
     -XX:ErrorFile=%WEBAPP_HOME%/logs/hs_err_pid_%p.log ^
     -Dsun.net.inetaddr.ttl=100 ^
     -Dcom.sun.management.jmxremote ^
     -Dcom.sun.management.jmxremote.port=7900 ^
     -Dcom.sun.management.jmxremote.ssl=false ^
     -Dcom.sun.management.jmxremote.authenticate=false ^
     -Dspring.profiles.active=product ^
     -jar lib\%WEBAPP_JAR%

以下の問題があることに気づきました。

  • -XX:HeapDumpPath=%WEBAPP_HOME%/logs/%date:~0,4%%date:~5,2%%date:~8,2%.hprof だとファイルに日付しか付かず、同じ日に HeapDump が出力された場合に前のファイルが上書きされてしまう。
  • -XX:ErrorFile=%WEBAPP_HOME%/logs/hs_err_pid_%p.log の %p でプロセス ID が付くものと思っていましたが、Windows だからなのかプロセス ID ではなくただの “p” になってしまう。

どちらもファイル名に日時が付くように以下のように変更します。この修正はファイル生成時に日時を付けるものではなく、最終的には -XX:+CrashOnOutOfMemoryError オプションを付けて、java コマンドが終了した後に日時の付いたファイル名に rename する想定のものです。

@echo on

setlocal
set JAVA_HOME=C:\Java\jdk1.8.0_131
set PATH=%JAVA_HOME%\bin;%PATH%
set WEBAPP_HOME=C:\webapps\ksbysample-webapp-lending
set WEBAPP_JAR=ksbysample-webapp-lending-1.5.4-RELEASE.jar
set LOGS_DIR=%WEBAPP_HOME%\logs
set PATH_HEAPDUMPFILE=%LOGS_DIR%\heapdump.hprof
set PATH_ERRORFILE=%LOGS_DIR%\hs_err.log

cd /d %WEBAPP_HOME%
java -server ^
     -Xms1024m -Xmx1024m ^
     -XX:MaxMetaspaceSize=384m ^
     -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled ^
     -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75 ^
     -XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark ^
     -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps ^
     -Xloggc:%WEBAPP_HOME%/logs/gc.log ^
     -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M ^
     -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=%PATH_HEAPDUMPFILE% ^
     -XX:ErrorFile=%PATH_ERRORFILE% ^
     -Dsun.net.inetaddr.ttl=100 ^
     -Dcom.sun.management.jmxremote ^
     -Dcom.sun.management.jmxremote.port=7900 ^
     -Dcom.sun.management.jmxremote.ssl=false ^
     -Dcom.sun.management.jmxremote.authenticate=false ^
     -Dspring.profiles.active=product ^
     -jar lib\%WEBAPP_JAR%

set YYYYMMDD=%date:~0,4%%date:~5,2%%date:~8,2%
set HHMMSS=%time:~0,8%
set HHMMSS=%HHMMSS::=%
set HHMMSS=%HHMMSS: =%
if exist %PATH_HEAPDUMPFILE% rename %PATH_HEAPDUMPFILE% heapdump_%YYYYMMDD%%HHMMSS%.hprof
if exist %PATH_ERRORFILE% rename %PATH_ERRORFILE% hs_err_%YYYYMMDD%%HHMMSS%.log

-XX:+ExitOnOutOfMemoryError と -XX:+CrashOnOutOfMemoryError のオプションが追加されていることを確認する

オプションが追加されたのは分かったのですが、実際に java.exe を実行する等してオプションを確認する方法があるのか気になったので調べて見ました。Print All JVM Flags を見ると java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal を実行すれば表示されるようです。

8u131 で実行すると以下の行が出力されました。

     bool CrashOnOutOfMemoryError                   = false                               {product}
     bool ExitOnOutOfMemoryError                    = false                               {product}

実際に OutOfMemory が発生する処理を追加して相違点を確認する

OutOfMemory が発生する処理を追加する

src/main/java/ksbysample/webapp/lending/SampleController.java を以下のように変更します。-Xmx1024mJava ヒープの最大サイズを 1024m で指定しているので、1024m の String を生成しようとすれば OutOfMemory になるはずです。

@Controller
@RequestMapping("/sample")
public class SampleController {

    ..........

    @RequestMapping("/outofmemory")
    @ResponseBody
    public void outofmemory() {
        String msg = StringUtils.repeat('x', 1024 * 1024 * 1024);
    }

}

clean タスク実行 → Rebuild Project 実行 → build タスクを実行して ksbysample-webapp-lending-1.5.4-RELEASE.jar を生成した後、C:\webapps\ksbysample-webapp-lending\lib の下へコピーします。

どちらのオプションも指定していない場合、どうなるのか?

webapp_startup.bat を実行して Tomcat を起動した後、http://localhost:8080/sample/outofmemory にアクセスすると OutOfMemoryError が発生します。

ログファイル ksbysample-webapp-lending.log には at ksbysample.webapp.lending.SampleController.outofmemory(SampleController.java:31) が出力されており、OutOfMemoryError が発生した原因の箇所が分かります。

org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:982)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:317)
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:127)
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:91)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:114)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:137)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:111)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:150)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:170)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:200)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:100)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:64)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:214)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:105)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:167)
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:80)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:478)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:799)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:861)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1455)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.OutOfMemoryError: Java heap space
    at org.apache.commons.lang3.StringUtils.repeat(StringUtils.java:6087)
    at ksbysample.webapp.lending.SampleController.outofmemory(SampleController.java:31)
    at ksbysample.webapp.lending.SampleController$$FastClassBySpringCGLIB$$709005bd.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:85)
    at ksbysample.webapp.lending.aspect.logging.RequestAndResponseLogger.logginRequestAndResponse(RequestAndResponseLogger.java:79)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:629)
    at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:618)
    at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:168)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673)
    at ksbysample.webapp.lending.SampleController$$EnhancerBySpringCGLIB$$a1afe1d0.outofmemory(<generated>)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901)

コンソールには Heap dump file created のログが出力されるだけで、Web アプリケーションは終了しません。

f:id:ksby:20170717154509p:plain

C:\webapps\ksbysample-webapp-lending\logs の下には heapdump.hprof が作成されています。

f:id:ksby:20170717154620p:plain

コンソールで Ctrl+C を押して Tomcat を停止します。

-XX:+ExitOnOutOfMemoryError オプションを指定するとどうなるか?

webapp_startup.bat を以下のように変更します。

  • -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=%WEBAPP_HOME%/logs/%date:~0,4%%date:~5,2%%date:~8,2%.hprof ^ オプションを削除します。
  • -XX:+ExitOnOutOfMemoryError を追加します。
     -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M ^
     -XX:+ExitOnOutOfMemoryError ^
     -XX:ErrorFile=%PATH_ERRORFILE% ^

webapp_startup.bat を実行して Tomcat を起動した後、http://localhost:8080/sample/outofmemory にアクセスして OutOfMemoryError を発生させます。

ログファイル ksbysample-webapp-lending.log には何も出力されませんでした。

コンソールには Terminating due to java.lang.OutOfMemoryError: Java heap space と出力されて Web アプリケーションは終了しています。

f:id:ksby:20170717155253p:plain

C:\webapps\ksbysample-webapp-lending\logs の下には何も作成されません。

f:id:ksby:20170717155352p:plain

-XX:+CrashOnOutOfMemoryError オプションを指定するとどうなるか?

webapp_startup.bat を以下のように変更します。

  • -XX:+ExitOnOutOfMemoryError を削除して、-XX:+CrashOnOutOfMemoryError を追加します。
     -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M ^
     -XX:+CrashOnOutOfMemoryError ^
     -XX:ErrorFile=%PATH_ERRORFILE% ^

webapp_startup.bat を実行して Tomcat を起動した後、http://localhost:8080/sample/outofmemory にアクセスして OutOfMemoryError を発生させます。

-XX:+ExitOnOutOfMemoryError オプションの時と同じくログファイル ksbysample-webapp-lending.log には何も出力されません。

コンソールには Aborting due to java.lang.OutOfMemoryError: Java heap space と出力されて Web アプリケーションは終了しています。

f:id:ksby:20170717160028p:plain

C:\webapps\ksbysample-webapp-lending\logs の下には hs_err_20170717155857.log というファイルが作成されて、

f:id:ksby:20170717160226p:plain

hs_err_20170717155857.log を開くと以下のようなログが出力されています。このログファイルだとクラスとメソッドは分かりますが、行数までは分かりません。

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (debug.cpp:308), pid=10920, tid=0x0000000000002d64
#  fatal error: OutOfMemory encountered: Java heap space
#
# JRE version: Java(TM) SE Runtime Environment (8.0_131-b11) (build 1.8.0_131-b11)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.131-b11 mixed mode windows-amd64 compressed oops)
# Failed to write core dump. Minidumps are not enabled by default on client versions of Windows
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.java.com/bugreport/crash.jsp
#

---------------  T H R E A D  ---------------

Current thread (0x000000005f7b4000):  JavaThread "http-nio-8080-exec-1" daemon [_thread_in_vm, id=11620, stack(0x0000000065f00000,0x0000000066000000)]

Stack: [0x0000000065f00000,0x0000000066000000]
[error occurred during error reporting (printing stack bounds), id 0xc0000005]

Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)

Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
j  org.apache.commons.lang3.StringUtils.repeat(CI)Ljava/lang/String;+8
j  ksbysample.webapp.lending.SampleController.outofmemory()V+4
..........

まとめ

  • -XX:+ExitOnOutOfMemoryError オプションを指定すると何もログを出力せずに Web アプリケーションが終了します。
  • -XX:+CrashOnOutOfMemoryError オプションを指定すると -XX:ErrorFile オプションで指定されたファイルにエラーファイルが生成されて Web アプリケーションが終了します。-XX:ErrorFile オプションを指定していない場合にはデフォルトの設定場所(ksbysample-webapp-lending の場合は C:\webapps\ksbysample-webapp-lending でした)に生成されます。このファイルを見ると OutOfMemory が発生したクラスとメソッドは分かりますが、行数までは分かりません。
  • -XX:+ExitOnOutOfMemoryError あるいは -XX:+CrashOnOutOfMemoryError オプションを指定すると Web アプリケーションのログファイルには Caused by: java.lang.OutOfMemoryError: Java heap space のログは出力されません。
  • どちらのオプションも指定しなければ Web アプリケーションは終了しません。また Web アプリケーションのログファイルに Caused by: java.lang.OutOfMemoryError: Java heap space のログが出力されます。このログが出力されると OutOfMemory が発生したファイルと行数が分かります。
  • HeapDump は -XX:+ExitOnOutOfMemoryError あるいは -XX:+CrashOnOutOfMemoryError オプションの有無とは全く関係がなく、-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=... オプションを指定すれば出力されます。

個人的な感想としては、

  • OutOfMemory 発生時に Web アプリケーションのログファイルに発生箇所のファイル名、行番号のログが出力された方がよくて、かつ Web アプリケーションが終了せずエラーが発生し続けても構わないのであれば -XX:+ExitOnOutOfMemoryError-XX:+CrashOnOutOfMemoryError も指定しません。
  • Web アプリケーションが終了すれば自動的に再起動する仕組みがあり、Web アプリケーションのログファイルに発生箇所のファイル名、行番号のログが出力される必要がないのであれば -XX:+CrashOnOutOfMemoryError を指定して終了するようにして、エラーファイルが生成されるようにします。
  • -XX:+ExitOnOutOfMemoryError あるいは -XX:+CrashOnOutOfMemoryError オプションを指定する/しないに関わらず、-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=... オプションは指定して HeapDump は出力した方がよいです。
  • -XX:+ExitOnOutOfMemoryError は存在意義がよく分かりませんでした。HeapDump だけで OutOfMemory の原因を調査するのは難しい気がします。今回出力された HeapDump を Memory Analyzer で開いてみましたが、今回のような実装が原因であれば Caused by: java.lang.OutOfMemoryError: Java heap space のログが出てくれた方が分かりやすいかな、と思いました。

webapp_startup.bat を修正する

オプションを忘れないようにするために、webapp_startup.bat には -XX:+CrashOnOutOfMemoryError を追加することにします。

@echo on

setlocal
set JAVA_HOME=C:\Java\jdk1.8.0_131
set PATH=%JAVA_HOME%\bin;%PATH%
set WEBAPP_HOME=C:\webapps\ksbysample-webapp-lending
set WEBAPP_JAR=ksbysample-webapp-lending-1.5.4-RELEASE.jar
set LOGS_DIR=%WEBAPP_HOME%\logs
set PATH_HEAPDUMPFILE=%LOGS_DIR%\heapdump.hprof
set PATH_ERRORFILE=%LOGS_DIR%\hs_err.log

cd /d %WEBAPP_HOME%
java -server ^
     -Xms1024m -Xmx1024m ^
     -XX:MaxMetaspaceSize=384m ^
     -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled ^
     -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75 ^
     -XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark ^
     -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps ^
     -Xloggc:%WEBAPP_HOME%/logs/gc.log ^
     -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M ^
     -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=%PATH_HEAPDUMPFILE% ^
     -XX:+CrashOnOutOfMemoryError ^
     -XX:ErrorFile=%PATH_ERRORFILE% ^
     -Dsun.net.inetaddr.ttl=100 ^
     -Dcom.sun.management.jmxremote ^
     -Dcom.sun.management.jmxremote.port=7900 ^
     -Dcom.sun.management.jmxremote.ssl=false ^
     -Dcom.sun.management.jmxremote.authenticate=false ^
     -Dspring.profiles.active=product ^
     -jar lib\%WEBAPP_JAR%

set YYYYMMDD=%date:~0,4%%date:~5,2%%date:~8,2%
set HHMMSS=%time:~0,8%
set HHMMSS=%HHMMSS::=%
set HHMMSS=%HHMMSS: =%
if exist %PATH_HEAPDUMPFILE% rename %PATH_HEAPDUMPFILE% heapdump_%YYYYMMDD%%HHMMSS%.hprof
if exist %PATH_ERRORFILE% rename %PATH_ERRORFILE% hs_err_%YYYYMMDD%%HHMMSS%.log

履歴

2017/07/17
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その14 )( request, response のログを出力する RequestAndResponseLogger クラスを修正する )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その13 )( jar ファイルを作成して動作確認する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • request, response のデータを出力する ksbysample.webapp.lending.aspect.logging.RequestAndResponseLogger を作成していますが、以下2つの問題があることに気づきました。今回はそれを修正します。
      • ログを出力する対象を @RequestMapping アノテーションが付加されたメソッドとしていたのですが、この条件だと Spring Framework 4.3 から追加された @GetMapping, @PostMapping アノテーションが付加されたメソッドではログが出力されないことに気づきました。ログが出力されるよう変更します。
      • @RequestMapping アノテーションが付加されたメソッドで例外が throw されて、応答を返すのが @ExceptionHandler メソッドが付加されたメソッドになった時に response のログが出力されないことに気づきました。こちらもログが出力されるよう変更します。

参照したサイト・書籍

  1. Spring Aspectj @Before all rest method
    http://techqa.info/programming/question/42642681/Spring-Aspectj–Before-all-rest-method

目次

  1. 今回の修正・動作確認のための前準備
    1. 今回の動作確認で使用する SampleController クラスを Spring Security による認証の対象外にする
    2. RequestAndResponseLogger の対象となるパッケージを ksbysample.webapp.lending.webksbysample.webapp.lending へ変更する
  2. @GetMapping, @PostMapping アノテーションが付加されたメソッドでも request, response のログが出力されるようにする
  3. エラーが発生して @ExceptionHandler アノテーションを付加したメソッドが応答を返す時でも response のログが出力されるようにする

手順

今回の修正・動作確認のための前準備

今回の動作確認で使用する SampleController クラスを Spring Security による認証の対象外にする

今回の RequestAndResponseLogger クラスの修正では、動作確認に ksbysample.webapp.lending.SampleController クラスを使用することにします。ただし SampleController クラスに関連付けている URL /sample は Spring Security の認証が必要な状態なので、認証の対象外にします。

src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java を以下のように変更します。

@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    ..........

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 認証の対象外にしたいURLがある場合には、以下のような記述を追加します
                // 複数URLがある場合はantMatchersメソッドにカンマ区切りで対象URLを複数列挙します
                // .antMatchers("/country/**").permitAll()
                ..........
                .antMatchers("/sample/**").permitAll()
                .anyRequest().authenticated();
        ..........
  • .antMatchers("/sample/**").permitAll() を追加します。

RequestAndResponseLogger の対象となるパッケージを ksbysample.webapp.lending.webksbysample.webapp.lending へ変更する

ksbysample.webapp.lending.SampleController クラスを使用するので、RequestAndResponseLogger の対象となるパッケージを ksbysample.webapp.lending.webksbysample.webapp.lending へ変更します。

src/main/java/ksbysample/webapp/lending/aspect/logging/RequestAndResponseLogger.java を以下のように変更します。

@Aspect
@Component
public class RequestAndResponseLogger {

    ..........

    /**
     * @param pjp ???
     * @return ???
     * @throws Throwable
     */
    @Around(value = "execution(* ksbysample.webapp.lending..*.*(..))"
            + "&& @annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public Object logginRequestAndResponse(ProceedingJoinPoint pjp)
            throws Throwable {
  • execution(* ksbysample.webapp.lending.web..*.*(..))execution(* ksbysample.webapp.lending..*.*(..)) へ変更します。

@GetMapping, @PostMapping アノテーションが付加されたメソッドでも request, response のログが出力されるようにする

例えば ksbysample.webapp.lending.SampleController クラスに @RequestMapping アノテーションを付加したメソッドを追加し、

@Controller
@RequestMapping("/sample")
public class SampleController {

    ..........

    /**
     * {@link RequestMapping}, {@link GetMapping}, {@link PostMapping} アノテーションを付加
     * したメソッドが呼び出された時に {@link RequestAndResponseLogger} クラスで request, response
     * が出力されることを確認するための動作確認用のメソッド
     *
     * @return 画面に出力する文字列
     */
    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

}

Tomcat を起動して http://localhost:8080/sample/hello にアクセスすると、RequestAndResponseLogger クラスにより request, response のログが出力されます。

f:id:ksby:20170713002633p:plain

hello メソッドに付加するアノテーションを @GetMapping に変更すると、

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

request, response のログが出力されません。

f:id:ksby:20170713003220p:plain

@GetMapping は以下の実装の composed annotation ですが、AspectJ@annotation(org.springframework.web.bind.annotation.RequestMapping) という Pointcut の指定では @GetMapping は対象にならないようです。

f:id:ksby:20170713003759p:plain

単純に対象にしたいアノテーションを || で結合して指定すると request, response のログが出力されるようにはなるのですが、出来れば @RequestMapping アノテーションを使用した composed annotation をシンプルに指定できるようにしたいです。

    @Around(value = "execution(* ksbysample.webapp.lending..*.*(..))"
            + " && (@annotation(org.springframework.web.bind.annotation.RequestMapping)"
            + " || @annotation(org.springframework.web.bind.annotation.GetMapping)"
            + " || @annotation(org.springframework.web.bind.annotation.PostMapping)"
            + " || @annotation(org.springframework.web.bind.annotation.PutMapping)"
            + " || @annotation(org.springframework.web.bind.annotation.DeleteMapping)"
            + " || @annotation(org.springframework.web.bind.annotation.PatchMapping))")
    public Object logginRequestAndResponse(ProceedingJoinPoint pjp)
            throws Throwable {

指定する方法がないか調べてみたところ、Spring Aspectj @Before all rest method という記事を見つけました。execution(@(@org.springframework.web.bind.annotation.RequestMapping *) * *(..)) と指定すれば @RequestMapping アノテーションを使用した composed annotation が付加されたメソッドが全て対象になるようです。

ただしこの条件だけだと @RequestMapping アノテーションが対象に含まれないので、以下のように変更することにします。

@Aspect
@Component
public class RequestAndResponseLogger {

    ..........

    /**
     * {@link RequestMapping} アノテーションが付加されたメソッドを Join Point にする Pointcut
     */
    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void requestMappingAnnotation() {
    }

    /**
     * {@link GetMapping}, {@link PostMapping} 等 {@link RequestMapping} アノテーションを使用した
     * composed annotation が付加されたメソッドを Join Point にする Pointcut
     */
    @Pointcut("execution(@(@org.springframework.web.bind.annotation.RequestMapping *) * *(..))")
    public void requestMappingComposedAnnotations() {
    }

    /**
     * @param pjp ???
     * @return ???
     * @throws Throwable
     */
    @Around(value = "execution(* ksbysample.webapp.lending..*.*(..))"
            + " && (requestMappingAnnotation() || requestMappingComposedAnnotations())")
    public Object logginRequestAndResponse(ProceedingJoinPoint pjp)
            throws Throwable {

これで @RequestMapping の時でも request, response のログが出力され、

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

f:id:ksby:20170713011034p:plain

@GetMapping の時でも request, response のログが出力されます。

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }

f:id:ksby:20170713011351p:plain

エラーが発生して @ExceptionHandler アノテーションを付加したメソッドが応答を返す時でも response のログが出力されるようにする

ksbysample.webapp.lending.SampleController クラスに以下の hello メソッド、handlerException メソッドを追加し、hello メソッドでリクエストを受けて handlerException メソッドでレスポンスが返るようにします。

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        if (true) {
            throw new RuntimeException();
        }
        return "hello";
    }

    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public String handlerException() {
        return "RuntimeException";
    }

Tomcat を起動して http://localhost:8080/sample/hello にアクセスすると、RequestAndResponseLogger クラスにより request のログは出力されますが、response のログは出力されません。

f:id:ksby:20170713013930p:plain

RequestAndResponseLogger クラスに @ExceptionHandler アノテーションが付加されたメソッドが呼び出された後に response のログを出力するための After Returning Advice のメソッドを追加します。以下のように変更します。

@Aspect
@Component
public class RequestAndResponseLogger {

    private static final Logger logger = LoggerFactory.getLogger(RequestAndResponseLogger.class);

    private static final String POINTCUT_ALL_CLASS_AND_METHOD_UNDER_APPLICATION_PACKAGE
            = "execution(* ksbysample.webapp.lending..*.*(..))";

    ..........

    /**
     * Web アプリケーションの package 配下の全てのクラスのメソッドを Join Point にする Pointcut
     */
    @Pointcut(POINTCUT_ALL_CLASS_AND_METHOD_UNDER_APPLICATION_PACKAGE)
    public void allClassAndMethodUnderApplicationPackage() {
    }

    ..........

    @Around(value = "allClassAndMethodUnderApplicationPackage()"
            + " && (requestMappingAnnotation() || requestMappingComposedAnnotations())")
    public Object logginRequestAndResponse(ProceedingJoinPoint pjp)
            throws Throwable {
        ..........
    }

    @After(value = "allClassAndMethodUnderApplicationPackage()"
            + " && @annotation(org.springframework.web.bind.annotation.ExceptionHandler)")
    public void logginResponseAfterExceptionHandler() {
        HttpServletResponse response
                = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        loggingResponse(response);
    }
  • Web アプリケーションの package 配下の全てのクラスのメソッドを対象にする PointCut 式 execution(* ksbysample.webapp.lending..*.*(..)) を今回追加する After Returning Advice のメソッドでも使用したいので、定数文字列 POINTCUT_ALL_CLASS_AND_METHOD_UNDER_APPLICATION_PACKAGE と allClassAndMethodUnderApplicationPackage メソッドを追加します。
  • logginRequestAndResponse メソッドの @Around アノテーションに指定する Pointcut 式を allClassAndMethodUnderApplicationPackage() に変更します。
  • @ExceptionHandler アノテーションが付加されたメソッドが呼び出された後に処理を行う @After アノテーションが付加された logginResponseAfterExceptionHandler メソッドを追加します。

再度 Tomcat を起動して http://localhost:8080/sample/hello にアクセスすると、今度は reponse のログも出力されました。

f:id:ksby:20170713023614p:plain

最後に src/main/java/ksbysample/webapp/lending/aspect/logging/RequestAndResponseLogger.java のソースを記載します。

ソースコード

RequestAndResponseLogger.java

package ksbysample.webapp.lending.aspect.logging;

import com.google.common.collect.Iterators;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.StreamSupport;

/**
 * ???
 */
@Aspect
@Component
public class RequestAndResponseLogger {

    private static final Logger logger = LoggerFactory.getLogger(RequestAndResponseLogger.class);

    private static final String POINTCUT_ALL_CLASS_AND_METHOD_UNDER_APPLICATION_PACKAGE
            = "execution(* ksbysample.webapp.lending..*.*(..))";

    private static final String LOG_REQUEST_INFO = "[req][info  ] ";
    private static final String LOG_REQUEST_HEADER = "[req][header] ";
    private static final String LOG_REQUEST_COOKIE = "[req][cookie] ";
    private static final String LOG_REQUEST_PARAMETER = "[req][param ] ";

    private static final String LOG_RESPONSE_INFO = "[res][info  ] ";
    private static final String LOG_RESPONSE_HEADER = "[res][header] ";

    /**
     * Web アプリケーションの package 配下の全てのクラスのメソッドを Join Point にする Pointcut
     */
    @Pointcut(POINTCUT_ALL_CLASS_AND_METHOD_UNDER_APPLICATION_PACKAGE)
    public void allClassAndMethodUnderApplicationPackage() {
    }

    /**
     * {@link RequestMapping} アノテーションが付加されたメソッドを Join Point にする Pointcut
     */
    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void requestMappingAnnotation() {
    }

    /**
     * {@link GetMapping}, {@link PostMapping} 等 {@link RequestMapping} アノテーションを使用した
     * composed annotation が付加されたメソッドを Join Point にする Pointcut
     */
    @Pointcut("execution(@(@org.springframework.web.bind.annotation.RequestMapping *) * *(..))")
    public void requestMappingComposedAnnotations() {
    }

    /**
     * @param pjp ???
     * @return ???
     * @throws Throwable
     */
    @Around(value = "allClassAndMethodUnderApplicationPackage()"
            + " && (requestMappingAnnotation() || requestMappingComposedAnnotations())")
    public Object logginRequestAndResponse(ProceedingJoinPoint pjp)
            throws Throwable {
        HttpServletRequest request
                = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        loggingRequest(request);
        Object ret = pjp.proceed();
        HttpServletResponse response
                = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        loggingResponse(response);

        return ret;
    }

    /**
     * ???
     */
    @After(value = "allClassAndMethodUnderApplicationPackage()"
            + " && @annotation(org.springframework.web.bind.annotation.ExceptionHandler)")
    public void logginResponseAfterExceptionHandler() {
        HttpServletResponse response
                = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        loggingResponse(response);
    }

    private void loggingRequest(HttpServletRequest request) {
        loggingRequestInfo(request);
        loggingRequestHeaders(request);
        loggingRequestCookies(request);
        loggingRequestParameters(request);
    }

    private void loggingResponse(HttpServletResponse response) {
        loggingResponseInfo(response);
        loggingResponseHeaders(response);
    }

    private void loggingRequestInfo(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        append(sb, "REQUEST_URI", request.getRequestURI());
        append(sb, ", SERVLET_PATH", request.getServletPath());
        logging(LOG_REQUEST_INFO, null, sb.toString());

        sb = new StringBuilder();
        append(sb, "CHARACTER_ENCODING", request.getCharacterEncoding());
        append(sb, ", CONTENT_LENGTH", String.valueOf(request.getContentLength()));
        append(sb, ", CONTENT_TYPE", request.getContentType());
        append(sb, ", LOCALE", String.valueOf(request.getLocale()));
        append(sb, ", SCHEME", request.getScheme());
        logging(LOG_REQUEST_INFO, null, sb.toString());

        sb = new StringBuilder();
        append(sb, "REMOTE_ADDR", request.getRemoteAddr());
        append(sb, ", REMOTE_HOST", request.getRemoteHost());
        append(sb, ", SERVER_NAME", request.getServerName());
        append(sb, ", SERVER_PORT", String.valueOf(request.getServerPort()));
        logging(LOG_REQUEST_INFO, null, sb.toString());

        sb = new StringBuilder();
        append(sb, "CONTEXT_PATH", request.getContextPath());
        append(sb, ", REQUEST_METHOD", request.getMethod());
        append(sb, ", QUERY_STRING", request.getQueryString());
        append(sb, ", PATH_INFO", request.getPathInfo());
        append(sb, ", REMOTE_USER", request.getRemoteUser());
        logging(LOG_REQUEST_INFO, null, sb.toString());
    }

    private void loggingRequestHeaders(HttpServletRequest request) {
        Iterable<String> headerNameList = () -> Iterators.forEnumeration(request.getHeaderNames());
        StreamSupport.stream(headerNameList.spliterator(), false)
                .filter(headerName -> !StringUtils.equals(headerName, "cookie"))
                .forEach(headerName -> {
                    Iterable<String> headerList = () -> Iterators.forEnumeration(request.getHeaders(headerName));
                    headerList.forEach(header -> logging(LOG_REQUEST_HEADER, headerName, header));
                });
    }

    private void loggingRequestCookies(HttpServletRequest request) {
        if (request.getCookies() != null) {
            Arrays.asList(request.getCookies()).forEach(cookie -> {
                StringBuilder sb = new StringBuilder();
                sb.append("name = ")
                        .append(cookie.getName())
                        .append(", value = ")
                        .append(cookie.getValue())
                        .append(", domain = ")
                        .append(cookie.getDomain())
                        .append(", path = ")
                        .append(cookie.getPath())
                        .append(", maxage = ")
                        .append(cookie.getMaxAge())
                        .append(", secure = ")
                        .append(cookie.getSecure())
                        .append(", httponly = ")
                        .append(cookie.isHttpOnly());
                logging(LOG_REQUEST_COOKIE, null, sb.toString());
            });
        }
    }

    private void loggingRequestParameters(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        parameterMap.forEach((key, values) -> {
            Arrays.asList(values).forEach(value -> logging(LOG_REQUEST_PARAMETER, key, value));
        });
    }

    private void loggingResponseInfo(HttpServletResponse response) {
        StringBuilder sb = new StringBuilder();
        append(sb, "RESPONSE_STATUS", String.valueOf(response.getStatus()));
        append(sb, ", CHARACTER_ENCODING", response.getCharacterEncoding());
        append(sb, ", CONTENT_TYPE", response.getContentType());
        append(sb, ", LOCALE", String.valueOf(response.getLocale()));
        logging(LOG_RESPONSE_INFO, null, sb.toString());
    }

    private void loggingResponseHeaders(HttpServletResponse response) {
        response.getHeaderNames().forEach(headerName -> {
            response.getHeaders(headerName).forEach(header -> logging(LOG_RESPONSE_HEADER, headerName, header));
        });
    }

    private void logging(String title, String name, String value) {
        StringBuilder sb = new StringBuilder(title);
        if (name != null) {
            sb.append(name);
            sb.append(" = ");
        }
        sb.append(value);
        logger.info(sb.toString());
    }

    private void append(StringBuilder sb, String name, String value) {
        sb.append(name);
        sb.append(" = ");
        sb.append(value);
    }

}

履歴

2017/07/13
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その13 )( jar ファイルを作成して動作確認する )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その12 )( build.gradle への PMD の導入2 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • jar ファイルを作成して動作確認する。

参照したサイト・書籍

目次

  1. jar ファイルを作成、配置する
  2. サービスに登録する
  3. 動作確認
  4. サービスから削除する
  5. feature/130-issue -> 1.5.x へ Pull Request、1.5.x へマージ、feature/130-issue ブランチを削除
  6. 次回は。。。

手順

jar ファイルを作成、配置する

clean タスク実行 → Rebuild Project 実行 → build タスク実行します。

C:\project-springboot\ksbysample-webapp-lending\build\libs の下に ksbysample-webapp-lending-1.5.4-RELEASE.jar が作成されますので、C:\webapps\ksbysample-webapp-lending\lib の下に配置します。

webapps/ksbysample-webapp-lending/bat/webapp_startup.bat を リンク先の内容 に変更した後、C:\webapps\ksbysample-webapp-lending\bat の下にコピーします。

サービスに登録する

コマンドプロンプトを「管理者として実行…」モードで起動した後、以下のコマンドを実行します。

> cd /d C:\webapps\ksbysample-webapp-lending\nssm
> nssm.exe install ksbysample-webapp-lending

「NSSM service installer」画面が表示されます。以下の画像の値を入力した後、「Install service」ボタンをクリックします。サービスの登録に成功すると「Service “ksbysample-webapp-lending” installed successfully!」のダイアログが表示されますので「OK」ボタンをクリックします。

f:id:ksby:20170709120507p:plain

  • 「Path」に C:\webapps\ksbysample-webapp-lending\bat\webapp_startup.bat を入力します。
  • 「Startup directory」に C:\webapps\ksbysample-webapp-lending\bat を入力します。

動作確認

  1. 動作確認前に DB のデータを以下の状態にします。

    • user_info, user_role テーブルのデータは開発時のままにします。
    • lending_app, lending_book, library_forsearch テーブルのデータはクリアします。
  2. サービス画面を開きます。サービス一覧から「ksbysample-webapp-lending」を選択し、「サービスの開始」リンクをクリックしてサービスを開始します。

    f:id:ksby:20170709202133p:plain

    f:id:ksby:20170709202400p:plain

  3. C:\webapps\ksbysample-webapp-lending\logs の下の ksbysample-webapp-lending.log をエディタで開き、最後に “Started Application in …” のログが出力されていることを確認します。

  4. メールを受信するので smtp4dev を起動します。

  5. 以下の手順で動作確認します ( 画面キャプチャは省略します )。

    • ブラウザを起動して http://localhost:8080/ にアクセスしてログイン画面を表示します。tanaka.taro@sample.com / taro でログインします。
    • 検索対象図書館登録画面が表示されます。"東京都" で検索した後、一覧表示されている図書館から「国立国会図書館東京本館」を選択します。
    • ログアウトします。
    • ログイン画面に戻るので suzuki.hanako@test.co.jp / hanako でログインします。
    • 貸出希望書籍 CSV ファイルアップロード画面が表示されます。以下の内容が記述された CSV ファイルをアップロードします。

      “ISBN”,“書名”
      “978-4-7741-6366-6”,“GitHub実践入門”
      “978-4-7741-5377-3”,“JUnit実践入門”
      “978-4-7973-8014-9”,“Java最強リファレンス”
      “978-4-7973-4778-4”,“アジャイルソフトウェア開発の奥義”
      “978-4-87311-704-1”,“Javaによる関数型プログラミング

    • 「貸出状況を確認しました」のメールが送信されるので、メールに記述されている URL にアクセスします。
    • 貸出申請画面が表示されます。3冊程「申請する」を選択して申請します。
    • ログアウトします。
    • 「貸出申請がありました」のメールが送信されるので、メールに記述されている URL にアクセスします。ログイン画面が表示されるので、tanaka.taro@sample.com / taro でログインします。
    • 貸出承認画面が表示されます。「承認」あるいは「却下」を選択して確定させます。
    • ログアウトします。
    • 「貸出申請が承認・却下されました」のメールが送信されるので、メールに記述されている URL にアクセスします。ログイン画面が表示されるので、suzuki.hanako@test.co.jp / hanako でログインします。
    • 貸出申請結果確認画面が表示されるので内容を確認します。

    動作確認は特に問題ありませんでした。

  6. smtp4dev を終了します。

  7. サービス画面で「ksbysample-webapp-lending」サービスを停止します。

サービスから削除する

サービスを削除します。管理者モードで起動しているコマンドプロンプトから以下のコマンドを実行します。

> nssm.exe remove ksbysample-webapp-lending

「Remove the service?」のダイアログが表示されますので、「はい」ボタンをクリックします。サービスが削除されると「Service “ksbysample-webapp-lending” removed successfully!」のダイアログが表示されますので「OK」ボタンをクリックします。

feature/130-issue -> 1.5.x へ Pull Request、1.5.x へマージ、feature/130-issue ブランチを削除

feature/130-issue -> 1.5.x へ Pull Request、1.5.x へマージ、feature/130-issue ブランチを削除します。

1.5.x -> master へもマージします。

次回は。。。

この後は感想を書いて完了させる予定です。もしかすると番外編を入れるかもしれません。

ソースコード

webapp_startup.bat

@echo on

setlocal
set JAVA_HOME=C:\Java\jdk1.8.0_131
set PATH=%JAVA_HOME%\bin;%PATH%
set WEBAPP_HOME=C:\webapps\ksbysample-webapp-lending
set WEBAPP_JAR=ksbysample-webapp-lending-1.5.4-RELEASE.jar

cd /d %WEBAPP_HOME%
java -server ^
     -Xms1024m -Xmx1024m ^
     -XX:MaxMetaspaceSize=384m ^
     -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled ^
     -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75 ^
     -XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark ^
     -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps ^
     -Xloggc:%WEBAPP_HOME%/logs/gc.log ^
     -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M ^
     -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=%WEBAPP_HOME%/logs/%date:~0,4%%date:~5,2%%date:~8,2%.hprof ^
     -XX:ErrorFile=%WEBAPP_HOME%/logs/hs_err_pid_%p.log ^
     -Dsun.net.inetaddr.ttl=100 ^
     -Dcom.sun.management.jmxremote ^
     -Dcom.sun.management.jmxremote.port=7900 ^
     -Dcom.sun.management.jmxremote.ssl=false ^
     -Dcom.sun.management.jmxremote.authenticate=false ^
     -Dspring.profiles.active=product ^
     -jar lib\%WEBAPP_JAR%

履歴

2017/07/09
初版発行。

IntelliJ IDEA を 2017.1.4 → 2017.1.5 へ、Git for Windows を 2.13.1(2) → 2.13.2 へバージョンアップ

IntelliJ IDEA を 2017.1.4 → 2017.1.5 へバージョンアップする

IntelliJ IDEA の 2017.1.5 がリリースされたのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

  2. 「Platform and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170708130450p:plain

  3. Plugin の update も表示されました。「Error-prone Compiler Integration」はバージョンアップすると動かなくなりますので、これだけチェックを外して「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170708130607p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. メイン画面が表示されます。IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

  6. 処理が終了すると Gradle Tool Window のツリーの表示が other グループしかない初期の状態に戻っていますので、左上の「Refresh all Gradle projects」ボタンをクリックして更新します。更新が完了すると build グループ等が表示されます。

  7. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.1.5 へバージョンアップされていることを確認します。

  8. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して “BUILD SUCCESSFUL” のメッセージが表示されることを確認します。

    f:id:ksby:20170708132252p:plain

  9. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20170708134552p:plain

Git for Windows を 2.13.1(2) → 2.13.2 へバージョンアップする

Git for Windows の 2.13.2 がリリースされていたのでバージョンアップします。

  1. https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.13.2-64-bit.exe をダウンロードします。

  2. Git-2.13.2-64-bit.exe を実行します。

  3. 「Git 2.13.2 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。

  4. 「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [Next >]ボタンをクリックします。

  5. 「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。

  6. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Configuring the line ending conversions」画面が表示されます。「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  8. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  9. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Install]ボタンをクリックします。

  10. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View Release Notes」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

  11. コマンドプロンプトを起動して git のバージョンが git version 2.13.2.windows.1 になっていることを確認します。

    f:id:ksby:20170708140552p:plain

  12. git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。

    f:id:ksby:20170708140817p:plain

  13. 特に問題はないようですので、2.13.2 で作業を進めたいと思います。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その12 )( build.gradle への PMD の導入2 )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その11 )( build.gradle への PMD の導入 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 前回 PMD の設定をしたので、今回は指摘を受けた箇所を修正します。
    • PMD のバージョンも 5.7.0 → 5.8.0 → 5.8.1 へバージョンアップします。

参照したサイト・書籍

目次

  1. PMD を 5.8.0 へ上げて build タスクを実行する
  2. 指摘を受けた箇所を修正する
    1. rulesets/java/comments.xml/CommentRequired
    2. rulesets/java/design.xml
    3. rulesets/java/imports.xml
    4. rulesets/java/logging-java.xml
    5. rulesets/java/naming.xml
    6. rulesets/java/optimizations.xml
    7. rulesets/java/strings.xml
    8. rulesets/java/unnecessary.xml
    9. rulesets/java/unusedcode.xml
  3. clean タスク → Rebuild Project → build タスクを実行する
  4. メモ書き
    1. コンソールに出力されるメッセージがどの RuleSet のどの Rule なのかをどうやって探すのか?
    2. RuleSet を複数適用してメッセージが大量に出力されたらどうするか?
  5. PMD を 5.8.0 → 5.8.1 へ上げて build タスクを実行する
  6. 最後に

手順

PMD を 5.8.0 へ上げて build タスクを実行する

build.gradle の以下の点を変更します。

pmd {
    toolVersion = "5.8.0"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}
  • toolVersion = "5.7.0"toolVersion = "5.8.0" に変更します。

clean タスク → Rebuild Project → build タスク の順に実行します。

f:id:ksby:20170701065604p:plain

199 PMD rule violations were found. という結果でした。5.7.0 の時は 219 PMD rule violations were found. でしたので、バージョンを上げたら 20 も減りましたね。。。?

減ったところを確認したら、以下のメッセージの部分でした。このメッセージが全て出なくなっているのではなく、一部だけのようです。

  • Private field ... could be made final; it is only initialized in the declaration or constructor.

5.7.0 で指摘を受けた箇所を見てみましたが、final を付けるよう指摘されてもおかしくはないようなところが指摘されなくなっているようでした。出なくなった理由は分かりませんが、そのまま進めます。

指摘を受けた箇所を修正する

rulesets/java/comments.xml/CommentRequired

  • CommentRequired の headerCommentRequirement(headerCommentRequirement Required)は、今回は指摘を受けたクラスに以下のコメントを追加して、指摘が出ないようにだけします。
/**
 * ???
 */

rulesets/java/design.xml

  • UseVarargs(Consider using varargs for methods or constructors which take an array the last parameter.)は、メソッドの最後の引数を Object[] argsObject... args のように変更します。。。と思いましたが、指摘された2箇所はどちらも可変引数ではなく Object[] で受けないといけないところでした。特に null が渡されることがあるところを Object... に変更すると、呼び出し元を null(Object) null に変更しないといけなくなるので、さすがにそれは避けたいと思いました。@SuppressWarnings({"PMD.UseVarargs"}) を付けて指摘されないようにします。
  • UnnecessaryLocalBeforeReturn(Consider simply returning the value vs storing it in local variable ...)は、途中に変数を入れていたところを入れないように修正します。ただし、spring-retry の RetryTemplate#execute を使用している部分はコードを修正せず @SuppressWarnings({"PMD.UnnecessaryLocalBeforeReturn"}) を付加してメッセージが出力されないようにします。
    public MessageConverter messageConverter() {
        // 変更前
        // Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
        // return converter;

        // 変更後
        return new Jackson2JsonMessageConverter();
    }
  • UncommentedEmptyConstructor(Document empty constructor)は、指摘された箇所のコンストラクタを削除できないことに気付いたので、pmd-project-rulesets.xml<exclude name="UncommentedEmptyConstructor"/> を追加して、この rule を外すことにします。
  • ImmutableField(Private field ... could be made final; it is only initialized in the declaration or constructor.)は、フィールド変数に final を付けるよう変更します。
  • AccessorMethodGeneration(Avoid autogenerated methods to access private fields and methods of inner / outer classes)はインナークラスからアクセスしているアウタークラスのフィールド変数のアクセス識別子を private → public へ変更します。でも、これはアウタークラスのフィールド変数にアクセスしていること自体が問題なんだろうな、という気がしています。。。
  • PreserveStackTrace(New exception is thrown in catch block, original stack trace may be lost)は、catch 句の中で throw している例外クラスの引数に catch した例外クラスのインスタンスを追加するよう変更します。
  • ClassWithOnlyPrivateConstructorsShouldBeFinal(A class which only has private constructors should be final)は、private コンストラクタのみのクラスに final を付けるよう変更します。
  • AvoidDeeplyNestedIfStmts(Deeply nested if..then statements are hard to read)は、指摘を付けた箇所の if 文内の処理を private メソッドへ分離して if 文のネストの深さを減らすように変更します。

rulesets/java/imports.xml

  • UnnecessaryFullyQualifiedName(Unnecessary use of fully qualified name ... due to existing import ...)は外部のライブラリの存在チェックをしている @ConditionalOnClass(com.fasterxml.jackson.dataformat.xml.XmlMapper.class) のところで指摘されたのですが、ここはパッケージ込みで記述しておきたいので @SuppressWarnings({"PMD.UnnecessaryFullyQualifiedName"}) を付けて指摘されないようにします。

rulesets/java/logging-java.xml

  • LoggerIsNotStaticFinal(The Logger variable declaration does not contain the static and final modifiers)は、Logger の変数に static を付けていなかったので static を付けるよう変更します。

rulesets/java/naming.xml

  • VariableNamingConventions(Variables that are final and static should be all capitals, ... is not all capitals.)は private static final Logger logger = LoggerFactory.getLogger(~.class); の logger を大文字&アンダーバーに変更するよう指摘されることに気付いたので、pmd-project-rulesets.xml<exclude name="VariableNamingConventions"/> を追加して、この rule を外すことにします。
  • SuspiciousConstantFieldName(The field name indicates a constant but its modifiers do not)は定数のフィールド変数に final を付けていなかったので final を付けるよう変更します。

rulesets/java/optimizations.xml

  • PrematureDeclaration(Avoid declaring a variable if it is unreferenced before a possible exit point.)は、変数を定義する位置と最初に使用している位置が離れていたので、定義する位置を使用する位置の直前に移動します。
  • AvoidInstantiatingObjectsInLoops(Avoid instantiating new objects inside loops)は指摘箇所を修正しようと思ったのですが、ループの外でインスタンスを生成するよう修正できなかったので、pmd-project-rulesets.xml<exclude name="AvoidInstantiatingObjectsInLoops"/> を追加して、この rule を外すことにします。

rulesets/java/strings.xml

  • ConsecutiveAppendsShouldReuse(StringBuffer (or StringBuilder).append is called consecutively without reusing the target variable.)は、指摘通り StringBuilder#append をチェーンするよう変更します。
  • AppendCharacterWithChar(Avoid appending characters as strings in StringBuffer.append.)は、StringBuilder#append の引数が1文字だけの場合には "a"'a' のようにシングルクォーテーションに変更します。
  • AvoidDuplicateLiterals(The String literal "..." appears ... times in this file; the first occurrence is on line ...)は、フィールド定数を定義して、それを使用するよう変更します。

rulesets/java/unnecessary.xml

  • UnnecessaryModifier(Avoid modifiers which are implied by the context)は、インターフェースに定義したメソッドに public を付けていたので削除します。

rulesets/java/unusedcode.xml

  • UnusedPrivateMethod(Avoid unused private methods such as ...)は、@SuppressWarnings({"PMD.UnusedPrivateMethod"}) を付加してメッセージが出力されないようにします。
  • UnusedLocalVariable(Avoid unused local variables such as ...)は、未使用の変数が必要のないよう処理を変更するか、未使用の変数を削除します。

clean タスク → Rebuild Project → build タスクを実行する

上の修正では /config/pmd/pmd-project-rulesets.xml に記載した ReleSet を一旦全てコメントアウトしてから、1つずつコメントアウトを解除して適用しながら確認していました。最後に全て適用した形で build タスクを実行します。

リンク先の内容 が最終版の /config/pmd/pmd-project-rulesets.xml です。

clean タスク → Rebuild Project → build タスク の順に実行し、"BUILD SUCCESSFUL" が出力されることを確認します。

f:id:ksby:20170708093657p:plain

メモ書き

コンソールに出力されるメッセージがどの RuleSet のどの Rule なのかをどうやって探すのか?

例えば rulesets/java/design.xml だけ適用してチェックしていた時に Consider using varargs for methods or constructors which take an array the last parameter. のメッセージが出力された場合、以下の手順で該当の Rule を探しました。

  1. https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java の下の design.xml をクリックする。
  2. ページ内で Consider using varargs for methods or constructors which take an array the last parameter. の文字列を検索する。message="Consider using varargs for methods or constructors which take an array the last parameter." がヒットして、これが UseVarargs の Rule であることが分かる。
  3. design.xml の UseVarargs の description を読んだり、https://pmd.github.io/pmd-5.8.0/pmd-java/rules/java/design.html の UseVarargs の説明を読んで変更方法を考える。

適用している RuleSet の xml を開いて、出力されているメッセージの文字列で検索する方法になります。複数の RuleSet を適用している場合には、https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java の一番上に表示されている GitHub の検索フィールドに文字列を入力して検索してもいいかもしれません。

ただし、この方法では Rule が見つからない場合があります。

  • メッセージ内のメソッド名や変数名等動的に変わる部分はそのまま検索しても当然ヒットしないので、それ以外の部分で検索すること。
  • メッセージで動的な部分以外で検索しても全然ヒットしない場合があります。例えば naming.xml の VariableNamingConventions(Variables that are final and static should be all capitals, ... is not all capitals.)は “final and static” で検索してもヒットしませんでした。結局 “final” だけで検索してヒットした Rule を1つずつ確認して判断しています。

    メッセージが出力されている Rule の RuleSet が分からず、メッセージで検索してもヒットしない状態だとすごく探しづらいと思います。分からない場合にはまず1つずつ RuleSet を適用していきながら、メッセージが出力されている Rule が含まれる RuleSet を探すところから始めましょう(たぶん)。

RuleSet を複数適用してメッセージが大量に出力されたらどうするか?

一番最初がこの状態でしたが、この場合には RuleSet を1つずつ適用しながら(他の RuleSet はコメントアウトしながら)対応方法を考えるのが良いと思います。

PMD を 5.8.0 → 5.8.1 へ上げて build タスクを実行する

ここまで書いてから PMD のバージョンを確認したところ、5.8.1 がリリースされていました。5.8.1 はバージョンアップしておきます。

build.gradle の以下の点を変更します。

pmd {
    toolVersion = "5.8.1"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}
  • toolVersion = "5.8.0"toolVersion = "5.8.1" に変更します。

clean タスク → Rebuild Project → build タスク の順に実行します。5.8.1 にバージョンアップしても追加で指摘されたところはありませんでした。

f:id:ksby:20170708114354p:plain

※上の実行前に試しに1度実行したので、PMD 5.8.1 のモジュールのダウンロード処理は出力されていません。

最後に

PMD は checkstyleFindBugs とは別の観点から指摘してくれる(一部重複するところはある)ようで、これも入れた方が良いと思いました。ただし全ての RuleSet を無条件に適用するのは無理があるので(不要と思う RuleSet あるいは Rule は必ず出て来ます)、プロジェクトにあったものだけ適用するよう調整が必要です。

ソースコード

pmd-project-rulesets.xml

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="mybraces"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
    <description>project rulesets</description>

    <!--
        rulesets の種類・説明は 以下の URL 参照
        https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java
        https://pmd.github.io/pmd-5.8.0/pmd-java/rules/index.html
        ※"pmd-5.8.0" の部分は適用しているバージョンに変更すること。
    -->
    <rule ref="rulesets/java/basic.xml"/>
    <rule ref="rulesets/java/braces.xml"/>
    <rule ref="rulesets/java/clone.xml"/>
    <rule ref="rulesets/java/codesize.xml">
        <exclude name="TooManyMethods"/>
    </rule>
    <rule ref="rulesets/java/comments.xml/CommentRequired">
        <properties>
            <property name="fieldCommentRequirement" value="Ignored"/>
            <property name="publicMethodCommentRequirement" value="Ignored"/>
            <property name="protectedMethodCommentRequirement" value="Ignored"/>
            <property name="enumCommentRequirement" value="Ignored"/>
        </properties>
    </rule>
    <rule ref="rulesets/java/design.xml">
        <exclude name="UseUtilityClass"/>
        <exclude name="UncommentedEmptyMethodBody"/>
        <exclude name="UncommentedEmptyConstructor"/>
        <exclude name="MissingStaticMethodInNonInstantiatableClass"/>
    </rule>
    <rule ref="rulesets/java/empty.xml"/>
    <rule ref="rulesets/java/finalizers.xml"/>
    <rule ref="rulesets/java/imports.xml"/>
    <rule ref="rulesets/java/logging-java.xml"/>
    <rule ref="rulesets/java/naming.xml">
        <exclude name="ShortVariable"/>
        <exclude name="LongVariable"/>
        <exclude name="ShortMethodName"/>
        <exclude name="VariableNamingConventions"/>
        <exclude name="ShortClassName"/>
    </rule>
    <rule ref="rulesets/java/optimizations.xml">
        <exclude name="LocalVariableCouldBeFinal"/>
        <exclude name="AvoidInstantiatingObjectsInLoops"/>
        <exclude name="MethodArgumentCouldBeFinal"/>
        <exclude name="UseStringBufferForStringAppends"/>
        <exclude name="RedundantFieldInitializer"/>
    </rule>
    <rule ref="rulesets/java/strictexception.xml">
        <exclude name="SignatureDeclareThrowsException"/>
        <exclude name="AvoidThrowingRawExceptionTypes"/>
        <exclude name="AvoidCatchingGenericException"/>
    </rule>
    <rule ref="rulesets/java/strings.xml"/>
    <rule ref="rulesets/java/sunsecure.xml"/>
    <rule ref="rulesets/java/typeresolution.xml">
        <exclude name="SignatureDeclareThrowsException"/>
    </rule>
    <rule ref="rulesets/java/unnecessary.xml">
        <exclude name="UselessParentheses"/>
    </rule>
    <rule ref="rulesets/java/unusedcode.xml"/>
</ruleset>

履歴

2017/07/08
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その11 )( build.gradle への PMD の導入 )

概要

記事一覧こちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その10 )( 起動時の spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する ) の続きです。

参照したサイト・書籍

  1. PMD
    https://pmd.github.io/

  2. Gradle で PMD による静的解析を実行する
    http://maku77.github.io/gradle/pmd/pmd.html

  3. mychaelstyle/build.gradle - Example Gradle build Java with FindBugs and PMD and CPD
    https://gist.github.com/mychaelstyle/9826322

  4. How to find PMD Rulesets names in Gradle >2.0
    https://stackoverflow.com/questions/25584501/how-to-find-pmd-rulesets-names-in-gradle-2-0

  5. pmd/pmd - pmd/pmd-java/src/main/resources/rulesets/java/
    https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java

    • build.gradle の ruleSets に記述する名称はどこを見ればよいのかがよく分からなかったのですが、どうもこのディレクトリを見ればよいらしいです。末尾の “.xml” を取り除いて、先頭に “java-” を付ければ ruleSet 名になります。
  6. How can getters/setters be ignored in the PMD CommentRequired rule?
    https://stackoverflow.com/questions/30869538/how-can-getters-setters-be-ignored-in-the-pmd-commentrequired-rule

  7. How to make a new rule set
    https://pmd.github.io/pmd-5.7.0/customizing/howtomakearuleset.html#

  8. @SuppressWarnings more than one rule not working
    https://stackoverflow.com/questions/22855796/suppresswarnings-more-than-one-rule-not-working

目次

  1. build.gradle を変更する
  2. build タスクを実行する
  3. RuleSets を1つずつ適用して出力されるメッセージを確認し、適用するか否か判断する
    1. java-basic
    2. java-braces
    3. java-clone
    4. java-codesize
    5. java-comments
    6. java-controversial
    7. java-coupling
    8. java-design
    9. java-empty
    10. java-finalizers
    11. java-imports
    12. java-logging-jakarta-commons
    13. java-logging-java
    14. java-naming
    15. java-optimizations
    16. java-strictexception
    17. java-strings
    18. java-sunsecure
    19. java-typeresolution
    20. java-unnecessary
    21. java-unusedcode
    22. pmd-project-rulesets.xml を作成し、build.gradle を変更する
  4. 再び build タスクを実行する
  5. 続きます。。。

手順

build.gradle を変更する

  1. build.gradle を リンク先のその1の内容 に変更します。

  2. 変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

build タスクを実行する

  1. clean タスク → Rebuild Project → build タスク の順に実行します。

    build タスクが実行されるといろいろダウンロードされます。また Removed misconfigured rule: LoosePackageCoupling cause: No packages or classes specified というメッセージも出力されていました。

    f:id:ksby:20170624153320p:plain

    その後 PMD のチェック処理が実行されて 1761 PMD rule violations were found. というメッセージが出力されました。checkstyle, findbugs で警告・エラーが出なくなっているので大丈夫なのでは?と思っていましたが、かなり大量にメッセージが出ますね。。。

    f:id:ksby:20170624154136p:plain

    そんなに大量に指摘されても全て対応することはできないので、RuleSet を1つずつ確認して、どのようなメッセージが出るのか、またその RuleSet を適用すべきか、を判断します。

RuleSet を1つずつ適用して出力されるメッセージを確認し、適用するか否か判断する

build.gradle の ruleSets に指定する RuleSet を1つだけにして clean タスク → Rebuild Project → build タスク の順に実行し、以下の方針で判断します。

  • Web で PMD を設定している build.gradle を調べてみると java-basic, java-braces の2つを適用している例がよく見られたので、この2つは必ず適用することにします。ただしメッセージが出力されるのか否かは確認します。
  • それ以外は出力されるメッセージと PMD Rulesets index: Current Rulesets のマニュアルでチェックされる内容を確認して、適用するか否かを判断します。

また適用の方法については、最終的には build.gradle の ruleSets に指定する方法ではなく、別の xml ファイルで細かく条件を指定する方法に変更します。作成する xml ファイルは /config/pmd/pmd-project-rulesets.xml とし、以下の内容のファイルに RuleSet を追加していきます。

<?xml version="1.0"?>
<ruleset name="mybraces"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
    <description>custom rulesets</description>

    .....(ここに RuleSet を記述します).....

</ruleset>

java-basic

java-basic は何もメッセージは出ませんでした。これはそのまま適用します。

    <rule ref="rulesets/java/basic.xml"/>

java-braces

java-braces は何もメッセージは出ませんでした。これはそのまま適用します。

    <rule ref="rulesets/java/braces.xml"/>

java-clone

java-clone は何もメッセージは出ませんでした。

Clone Implementation でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/clone.xml"/>

java-codesize

java-codesize は 2 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • This class has too many methods, consider refactoring it.

Code Size でチェック内容を確認した上で、以下のルールで適用することにします。

  • TooManyMethods(This class has too many methods, consider refactoring it.)は指摘されてもすぐに修正できなさそうなので、外します。
  • それ以外はそのまま適用します。
    <rule ref="rulesets/java/codesize.xml">
        <exclude name="TooManyMethods"/>
    </rule>

java-comments

java-comments は 479 PMD rule violations were found. という結果でした。メッセージの種類は8種類で、以下のメッセージでした。

  • headerCommentRequirement Required
  • fieldCommentRequirement Required
  • Comment is too large: Line too long
  • publicMethodCommentRequirement Required
  • protectedMethodCommentRequirement Required
  • To avoid mistakes add a comment at the beginning of the ... field if you want a default access modifier
  • Comment is too large: Too many lines
  • enumCommentRequirement Required

Comments でチェック内容を確認した上で、以下のルールで適用することにします。

  • CommentRequired の headerCommentRequirement(headerCommentRequirement Required)だけ指摘して欲しいと思ったので(checkstyle でクラスのコメントがないのをチェックできていないことに気付いたので)、これだけ適用します。
    <rule ref="rulesets/java/comments.xml/CommentRequired">
        <properties>
            <property name="fieldCommentRequirement" value="Ignored"/>
            <property name="publicMethodCommentRequirement" value="Ignored"/>
            <property name="protectedMethodCommentRequirement" value="Ignored"/>
            <property name="enumCommentRequirement" value="Ignored"/>
        </properties>
    </rule>

java-controversial

java-controversial は 169 PMD rule violations were found. という結果でした。メッセージの種類は12種類で、以下のメッセージでした。

  • Each class should declare at least one constructor
  • Avoid unnecessary constructors - the compiler will generate these for you
  • It is a good practice to call super() in a constructor
  • Use explicit scoping instead of the default package private level
  • Found 'DD'-anomaly for variable ...
  • Found 'DU'-anomaly for variable ...
  • If you run in Java5 or newer and have concurrent access, you should use the ConcurrentHashMap implementation
  • A method should have only one exit point, and that should be the last statement in the method
  • Found 'UR'-anomaly for variable ...
  • Avoid using Literals in Conditional Statements
  • This statement may have some unnecessary parentheses
  • Assigning an Object to null is a code smell. Consider refactoring.

Controversial でチェック内容を確認しましたが、この RuleSet に必要性を感じなかったので、適用しないことにします。

java-coupling

java-coupling は 247 PMD rule violations were found. という結果でした。メッセージの種類は3種類で、以下のメッセージでした。またこの RuleSets を適用すると Removed misconfigured rule: LoosePackageCoupling cause: No packages or classes specified のメッセージも表示されました。

  • Potential violation of Law of Demeter (method chain calls)
  • Potential violation of Law of Demeter (object not created locally)
  • Potential violation of Law of Demeter (static property access)

Coupling でチェック内容を確認しましたが、この RuleSet に必要性を感じなかったので、適用しないことにします。

java-design

java-design は 55 PMD rule violations were found. という結果でした。メッセージの種類は11種類で、以下のメッセージでした。

  • All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning.
  • Document empty method body
  • Consider using varargs for methods or constructors which take an array the last parameter.
  • Consider simply returning the value vs storing it in local variable '...'
  • Document empty constructor
  • Private field '...' could be made final; it is only initialized in the declaration or constructor.
  • Avoid autogenerated methods to access private fields and methods of inner / outer classes
  • New exception is thrown in catch block, original stack trace may be lost
  • A class which only has private constructors should be final
  • Class cannot be instantiated and does not provide any static methods or fields
  • Deeply nested if..then statements are hard to read

Design でチェック内容を確認した上で、以下のルールで適用することにします。

  • UseUtilityClassAll methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning.)は Application.javaSpring Framework の @Component アノテーションを付加していて Helper クラスにしているものが検知されてしまうので、外します。
  • UncommentedEmptyMethodBodyDocument empty method body)は @Aspect を付加したクラスの @Pointcut アノテーションを付加したメソッドが検知されていたので、外します。
  • MissingStaticMethodInNonInstantiatableClassClass cannot be instantiated and does not provide any static methods or fields)は ValuesHelper クラスが検知されていて修正のしようがないので、外します。
  • それ以外はそのまま適用します。
    <rule ref="rulesets/java/design.xml">
        <exclude name="UseUtilityClass"/>
        <exclude name="UncommentedEmptyMethodBody"/>
        <exclude name="MissingStaticMethodInNonInstantiatableClass"/>
    </rule>

java-empty

java-empty は何もメッセージは出ませんでした。

Empty Code でチェック内容を確認すると指摘してもらいたい点が多かったので、そのまま適用することにします。

    <rule ref="rulesets/java/empty.xml"/>

java-finalizers

java-finalizers は何もメッセージは出ませんでした。

Finalizer でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/finalizers.xml"/>

java-imports

java-design は 55 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • Unnecessary use of fully qualified name ... due to existing import ...

Import Statements でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/imports.xml"/>

java-logging-jakarta-commons

java-logging-jakarta-commons は 2 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • There is log block not surrounded by if

Jakarta Commons Logging でチェック内容を確認すると指摘して欲しい点が記載されているのですが、試してみると期待通り動作しないようです。

  • There is log block not surrounded by if が出力されたところは確かに logger.info(...) しか記述しておらず if (logger.isInfoEnabled()) { ... } は記述していなかったのですが、他に logger.info(...) しか記述していないところでメッセージが出ていない箇所があります。
  • GuardDebugLogging の rule は検知して欲しいと思ったのですが、logger.debug(...) だけ記述してもメッセージが何も表示されませんでした。

org.slf4j.Logger を使用しているからでしょうか。。。? 動作していないようなので、外すことにします。

java-logging-java

java-logging-java7 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • The Logger variable declaration does not contain the static and final modifiers

Java Logging でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/logging-java.xml"/>

java-naming

java-naming は 175 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • Variables that are final and static should be all capitals, ... is not all capitals.
  • Avoid excessively long variable names like ...
  • Avoid variables with short names like ...
  • Only variables that are final should contain underscores (except for underscores in standard prefix/suffix), ... is not final.
  • The field name indicates a constant but its modifiers do not
  • Variables should start with a lowercase character, ... starts with uppercase character.
  • Avoid short class names like ...

Naming でチェック内容を確認した上で、以下のルールで適用することにします。

  • 以下の rule は必要性を感じなかったので外します。
    • ShortVariable(Avoid variables with short names like ...
    • LongVariable(Avoid excessively long variable names like ...
    • ShortMethodName
    • ShortClassName(Avoid short class names like ...
  • それ以外はそのまま適用します。
    <rule ref="rulesets/java/naming.xml">
        <exclude name="ShortVariable"/>
        <exclude name="LongVariable"/>
        <exclude name="ShortMethodName"/>
        <exclude name="ShortClassName"/>
    </rule>

java-optimizations

java-optimizations は 527 PMD rule violations were found. という結果でした。メッセージの種類は6種類で、以下のメッセージでした。

  • Parameter ... is not assigned and could be declared final
  • Local variable ... could be declared final
  • Avoid declaring a variable if it is unreferenced before a possible exit point.
  • Avoid instantiating new objects inside loops
  • Prefer StringBuffer over += for concatenating strings
  • Avoid using redundant field initializer for ...

Optimization でチェック内容を確認した上で、以下のルールで適用することにします。

  • 以下の2つの rule はそこまで final だらけにしたくないと思ったので、外します。
    • MethodArgumentCouldBeFinal(Parameter ... is not assigned and could be declared final
    • LocalVariableCouldBeFinal(Local variable ... could be declared final
  • AvoidInstantiatingObjectsInLoops(Avoid instantiating new objects inside loops)は適用しますが、CSV ファイルのレコードをループでチェックしている処理の中の errors.reject(..., new Object[]{...}, ...) のように指摘を受けても修正しようがない箇所もあったので、その場合には @SuppressWarnings({PMD.AvoidInstantiatingObjectsInLoops}) アノテーションを付加して PMD のチェックが行われないようにします。
  • UseStringBufferForStringAppends(Prefer StringBuffer over += for concatenating strings) は、簡単な文字列結合でも指摘を受けていて、さすがにそこまで全部 StringBuffer で処理したくないので外します。
  • RedundantFieldInitializer(Avoid using redundant field initializer for ...)は入れた方がよいのか否かちょっと悩みましたが、例えデフォルトでも明示的に書きたい場合もあると思ったので、外すことにします。
    <rule ref="rulesets/java/optimizations.xml">
        <exclude name="LocalVariableCouldBeFinal"/>
        <exclude name="MethodArgumentCouldBeFinal"/>
        <exclude name="UseStringBufferForStringAppends"/>
        <exclude name="RedundantFieldInitializer"/>
    </rule>

java-strictexception

java-strictexception は 14 PMD rule violations were found. という結果でした。メッセージの種類は3種類で、以下のメッセージでした。

  • A method/constructor shouldnt explicitly throw java.lang.Exception
  • Avoid throwing raw exception types.
  • Avoid catching generic exceptions such as NullPointerException, RuntimeException, Exception in try-catch block

Strict Exceptions でチェック内容を確認した上で、以下のルールで適用することにします。

  • SignatureDeclareThrowsException(A method/constructor shouldnt explicitly throw java.lang.Exception)は throws Exception としか書きようがないところまで指摘されていたので、外します。
  • AvoidThrowingRawExceptionTypes(Avoid throwing raw exception types.)は RuntimeException ではなくてきちんと場面に応じた例外を throw しようという指摘なのですが、安直に RuntimeException を使いたい時もあるので、外すことにします。
  • AvoidCatchingGenericException(Avoid catching generic exceptions such as NullPointerException, RuntimeException, Exception in try-catch block)も Exception を catch したい時があるので外します。
  • それ以外はそのまま適用します。
    <rule ref="rulesets/java/strictexception.xml">
        <exclude name="SignatureDeclareThrowsException"/>
        <exclude name="AvoidThrowingRawExceptionTypes"/>
        <exclude name="AvoidCatchingGenericException"/>
    </rule>

java-strings

java-strings は 30 PMD rule violations were found. という結果でした。メッセージの種類は3種類で、以下のメッセージでした。

  • StringBuffer (or StringBuilder).append is called consecutively without reusing the target variable.
  • Avoid appending characters as strings in StringBuffer.append.
  • The String literal ... appears ... times in this file; the first occurrence is on line ...

String and StringBuffer でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/strings.xml"/>

java-sunsecure

java-sunsecure は何もメッセージは出ませんでした。

Security Code Guidelines でチェック内容を確認すると適用しても問題なさそうだったので、そのまま適用することにします。

    <rule ref="rulesets/java/sunsecure.xml"/>

java-typeresolution

java-typeresolution は 6 PMD rule violations were found. という結果でした。メッセージの種類は1種類で、以下のメッセージでした。

  • A method/constructor shouldnt explicitly throw java.lang.Exception

Type Resolution でチェック内容を確認した上で、以下のルールで適用することにします。

  • SignatureDeclareThrowsException(A method/constructor shouldnt explicitly throw java.lang.Exception)は削除できない throws Exception を指摘されていたので、外します。
  • それ以外の rule は適用します。
    <rule ref="rulesets/java/typeresolution.xml">
        <exclude name="SignatureDeclareThrowsException"/>
    </rule>

java-unnecessary

java-unnecessary は 41 PMD rule violations were found. という結果でした。メッセージの種類は2種類で、以下のメッセージでした。

  • Avoid modifiers which are implied by the context
  • Useless parentheses.

Unnecessary でチェック内容を確認した上で、 以下のルールで適用することにします。

  • UselessParentheses(Useless parentheses.)は削除できない箇所を指摘されていたので、外します。
  • それ以外の rule は適用します。
    <rule ref="rulesets/java/unnecessary.xml">
        <exclude name="UselessParentheses"/>
    </rule>

java-unusedcode

java-unnecessary は 6 PMD rule violations were found. という結果でした。メッセージの種類は2種類で、以下のメッセージでした。

  • Avoid unused private methods such as ...
  • Avoid unused local variables such as ...

Unused Code でチェック内容を確認した上で、以下のルールで適用することにします。

  • この RuleSet はそのまま適用します。
  • メッセージが出力された箇所で指摘を受ける必要がないところは @SuppressWarnings({...}) アノテーションを付加して PMD のチェックが行われないようにします。
    <rule ref="rulesets/java/unusedcode.xml"/>

pmd-project-rulesets.xml を作成し、build.gradle を変更する

  1. /config/pmd/pmd-project-rulesets.xml を新規作成し、リンク先の内容 を記述します。

  2. build.gradle を リンク先のその2の内容 に変更します。

再び build タスクを実行する

  1. clean タスク → Rebuild Project → build タスク の順に実行します。

    今度は 219 PMD rule violations were found. という結果でした。

    f:id:ksby:20170630053245p:plain

続きます。。。

次回、指摘を受けた箇所を修正します。また今回記事を書き始めた時は PMD のバージョンは 5.7.0 だったのですが、その後で 5.8.0 が出たようです。5.8.0 に変更して確認しながら修正します。

ソースコード

build.gradle

■その1

..........

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'
apply plugin: 'pmd'

..........

pmd {
    toolVersion = "5.7.0"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSets = [
            // ruleSet の種類・説明は 以下の URL 参照
            // https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java
            // https://pmd.github.io/pmd-5.7.0/pmd-java/rules/index.html
            'java-basic'
            , 'java-braces'
            , 'java-clone'
            , 'java-codesize'
            , 'java-comments'
            , 'java-controversial'
            , 'java-coupling'
            , 'java-design'
            , 'java-empty'
            , 'java-finalizers'
            , 'java-imports'
            , 'java-logging-jakarta-commons'
            , 'java-logging-java'
            , 'java-migrating'
            , 'java-naming'
            , 'java-optimizations'
            , 'java-strictexception'
            , 'java-strings'
            , 'java-sunsecure'
            , 'java-typeresolution'
            , 'java-unnecessary'
            , 'java-unusedcode'
    ]
}
  • apply plugin: 'pmd' を追加します。
  • pmd { ... } を追加します。ruleSets は java-j2ee, java-javabeans, java-junit, java-migrating を除き一旦全部入れています。

■その2

pmd {
    toolVersion = "5.7.0"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}
  • ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml") を追加します。
  • ruleSets = [] に変更します。

pmd-project-rulesets.xml

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="mybraces"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
    <description>project rulesets</description>

    <!--
        rulesets の種類・説明は 以下の URL 参照
        https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java
        https://pmd.github.io/pmd-5.7.0/pmd-java/rules/index.html
        ※"pmd-5.7.0" の部分は適用しているバージョンに変更すること。
    -->
    <rule ref="rulesets/java/basic.xml"/>
    <rule ref="rulesets/java/braces.xml"/>
    <rule ref="rulesets/java/clone.xml"/>
    <rule ref="rulesets/java/codesize.xml">
        <exclude name="TooManyMethods"/>
    </rule>
    <rule ref="rulesets/java/comments.xml/CommentRequired">
        <properties>
            <property name="fieldCommentRequirement" value="Ignored"/>
            <property name="publicMethodCommentRequirement" value="Ignored"/>
            <property name="protectedMethodCommentRequirement" value="Ignored"/>
            <property name="enumCommentRequirement" value="Ignored"/>
        </properties>
    </rule>
    <rule ref="rulesets/java/design.xml">
        <exclude name="UseUtilityClass"/>
        <exclude name="UncommentedEmptyMethodBody"/>
        <exclude name="MissingStaticMethodInNonInstantiatableClass"/>
    </rule>
    <rule ref="rulesets/java/empty.xml"/>
    <rule ref="rulesets/java/finalizers.xml"/>
    <rule ref="rulesets/java/imports.xml"/>
    <rule ref="rulesets/java/logging-java.xml"/>
    <rule ref="rulesets/java/naming.xml">
        <exclude name="ShortVariable"/>
        <exclude name="LongVariable"/>
        <exclude name="ShortMethodName"/>
        <exclude name="ShortClassName"/>
    </rule>
    <rule ref="rulesets/java/optimizations.xml">
        <exclude name="LocalVariableCouldBeFinal"/>
        <exclude name="MethodArgumentCouldBeFinal"/>
        <exclude name="UseStringBufferForStringAppends"/>
        <exclude name="RedundantFieldInitializer"/>
    </rule>
    <rule ref="rulesets/java/strictexception.xml">
        <exclude name="SignatureDeclareThrowsException"/>
        <exclude name="AvoidThrowingRawExceptionTypes"/>
        <exclude name="AvoidCatchingGenericException"/>
    </rule>
    <rule ref="rulesets/java/strings.xml"/>
    <rule ref="rulesets/java/sunsecure.xml"/>
    <rule ref="rulesets/java/typeresolution.xml">
        <exclude name="SignatureDeclareThrowsException"/>
    </rule>
    <rule ref="rulesets/java/unnecessary.xml">
        <exclude name="UselessParentheses"/>
    </rule>
    <rule ref="rulesets/java/unusedcode.xml"/>
</ruleset>

履歴

2017/06/30
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その10 )( 起動時の spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する )

概要

記事一覧こちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その9 )( Spring Boot を 1.5.3 → 1.5.4 にバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • IntelliJ IDEA 2017.2 Public Preview の記事が出ていたので読んでみたのですが、その中の「Replacing multiple equals with Set.contains」を見て、Application クラスで spring.profiles.active をチェックしている処理に適用できそうなので、変更することにします。

参照したサイト・書籍

  1. IntelliJ IDEA 2017.2 Public Preview
    https://blog.jetbrains.com/idea/2017/06/intellij-idea-2017-2-public-preview/

目次

  1. Application クラスの spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する

手順

Application クラスの spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する

src/main/java/ksbysample/webapp/lending/Application.java を以下のように変更します。

public class Application {

    private static final Set<String> springProfiles = Collections
            .unmodifiableSet(new HashSet<>(Arrays.asList("product", "develop", "unittest")));

    /**
     * Spring Boot メインメソッド
     *
     * @param args ???
     */
    public static void main(String[] args) {
        String springProfilesActive = System.getProperty("spring.profiles.active");
        if (!springProfiles.contains(springProfilesActive)) {
            throw new UnsupportedOperationException(
                    MessageFormat.format("JVMの起動時引数 -Dspring.profiles.active で "
                                    + "develop か unittest か product を指定して下さい ( -Dspring.profiles.active={0} )。"
                            , springProfilesActive));
        }

        SpringApplication.run(Application.class, args);
    }

}
  • private static final Set<String> springProfiles = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("product", "develop", "unittest"))); を追加します。
  • main メソッド内の if 文で springProfilesActive を StringUtils.equals でチェックしていた処理を !springProfiles.contains(springProfilesActive) に変更します。

動作を確認してみます。bootRun タスクで Tomcat を起動してみると、正常に起動します。

f:id:ksby:20170623061241p:plain

Tomcat を停止した後、build.gradle の bootRun に記述している spring.profiles.active を developdevelopx に変更してから再度 bootRun で Tomcat を起動しようとしてみると、

bootRun {
    jvmArgs = ['-Dspring.profiles.active=developx']
}

今度はエラーになって起動しませんでした。チェックは正常に機能しているようです。

f:id:ksby:20170623061602p:plain

clean タスク → Rebuild Project → build タスクを実行すると “BUILD SUCCESSFUL” のメッセージが出力されることも確認できます。

f:id:ksby:20170623062244p:plain

IntelliJ IDEA 2017.2 は「Spring Boot: dashboard & actuator endpoints」が一番期待している機能なのですが、他にもいろいろ面白そうだったり、よく分からない機能があって、リリースが楽しみです。でも GIFアニメーションがちょっと速すぎて、もう少しゆっくり見たいんだけどな、とも思いました。。。

履歴

2017/06/23
初版発行。