最近在執行單元測試的時候,發現一個奇怪的問題:本地Junit的單測單獨執行,程序正常;但是在集成了JaCoCo覆蓋率插件後,執行maven test命令一直會報ArrayIndexOutOfBounds數組越界異常,查了好久,才找到最終原因。
單測代碼如下:
@Test
public void getSingleProductProperty(){
String productCode = "C030060008";
List<PropertyOptionVO> propertyOptionVOList = waresMallProductFacadeService.getSingleProductProperty(productCode);
Assert.assertTrue(!CollectionUtils.isEmpty(propertyOptionVOList));
}
maven test命令報出的異常:
[20190822-17:46:30.840]-[INFO]-[1566467084465_GqwN]-[Thread-18]-[org.springframework.scheduling.concurrent.ExecutorConfigurationSupport:202]- Shutting down ExecutorService
[INFO]
[INFO] Results:
[INFO]
[ERROR] Errors:
[ERROR] WaresMallProductFacadeServiceImplTest.getSingleProductInfo:62 » ArrayIndexOutOfBounds
[INFO]
[ERROR] Tests run: 41, Failures: 0, Errors: 1, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] wares-mall-api 1.8.0 ............................... SUCCESS [ 2.386 s][INFO] support-platform-user .............................. SUCCESS [ 25.723 s]
[INFO] mall-bootstrap ..................................... FAILURE [01:49 min]
[INFO] mall-rest 3.3-SNAPSHOT ............................. SKIPPED
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 05:21 min
[INFO] Finished at: 2019-08-22T17:46:31+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.21.0:test (default-test) on project mall-bootstrap: There are test failures.
[ERROR]
[ERROR] Please refer to /Users/ding******/wares-manage-center/mall-bootstrap/target/surefire-reports for the individual test results.
[ERROR] Please refer to dump files (if any exist) [date]-jvmRun[N].dump, [date].dumpstream and [date]-jvmRun[N].dumpstream.
[ERROR] -> [Help 1]
org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.21.0:test (default-test) on project mall-bootstrap: There are test failures.
Please refer to /Users/ding******/wares-manage-center/mall-bootstrap/target/surefire-reports for the individual test results.
Please refer to dump files (if any exist) [date]-jvmRun[N].dump, [date].dumpstream and [date]-jvmRun[N].dumpstream.
at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:213)
at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:154)
at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:146)
at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:117)
at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:81)
at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build (SingleThreadedBuilder.java:56)
at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:128)
at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:305)
at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192)
at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105)
at org.apache.maven.cli.MavenCli.execute (MavenCli.java:954)
at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:288)
at org.apache.maven.cli.MavenCli.main (MavenCli.java:192)
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.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:289)
at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:229)
at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:415)
at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:356)
Caused by: org.apache.maven.plugin.MojoFailureException: There are test failures.
Please refer to /Users/ding******/wares-manage-center/mall-bootstrap/target/surefire-reports for the individual test results.
Please refer to dump files (if any exist) [date]-jvmRun[N].dump, [date].dumpstream and [date]-jvmRun[N].dumpstream.
at org.apache.maven.plugin.surefire.SurefireHelper.throwException (SurefireHelper.java:240)
at org.apache.maven.plugin.surefire.SurefireHelper.reportExecution (SurefireHelper.java:112)
at org.apache.maven.plugin.surefire.SurefirePlugin.handleSummary (SurefirePlugin.java:354)
at org.apache.maven.plugin.surefire.AbstractSurefireMojo.executeAfterPreconditionsChecked (AbstractSurefireMojo.java:1008)
at org.apache.maven.plugin.surefire.AbstractSurefireMojo.execute (AbstractSurefireMojo.java:854)
at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo (DefaultBuildPluginManager.java:137)
at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:208)
at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:154)
at org.apache.maven.lifecycle.internal.MojoExecutor.execute (MojoExecutor.java:146)
at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:117)
at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject (LifecycleModuleBuilder.java:81)
at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build (SingleThreadedBuilder.java:56)
at org.apache.maven.lifecycle.internal.LifecycleStarter.execute (LifecycleStarter.java:128)
at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:305)
at org.apache.maven.DefaultMaven.doExecute (DefaultMaven.java:192)
at org.apache.maven.DefaultMaven.execute (DefaultMaven.java:105)
at org.apache.maven.cli.MavenCli.execute (MavenCli.java:954)
at org.apache.maven.cli.MavenCli.doMain (MavenCli.java:288)
at org.apache.maven.cli.MavenCli.main (MavenCli.java:192)
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.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced (Launcher.java:289)
at org.codehaus.plexus.classworlds.launcher.Launcher.launch (Launcher.java:229)
at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode (Launcher.java:415)
at org.codehaus.plexus.classworlds.launcher.Launcher.main (Launcher.java:356)
[ERROR]
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
[ERROR]
[ERROR] After correcting the problems, you can resume the build with the command
[ERROR] mvn <goals> -rf :mall-bootstrap
使用maven -X -e -l,也查不出來具體的信息。一開始不知道是JaCoCo導致的問題,以爲是maven的問題,更換了最新的maven版本也不行,因爲自己一直搜的關鍵詞是maven + ArrayIndexOutOfBounds,谷歌搜索給了一條搜索記錄了,進去之後才發現是JaCoCo覆蓋率插件的問題。
JaCoCo GitHub項目上的issues:https://github.com/jacoco/jacoco/issues/799
JaCoCo官網上FAQ的解釋:https://www.jacoco.org/jacoco/trunk/doc/faq.html
My code uses reflection. Why does it fail when I execute it with JaCoCo?
To collect execution data JaCoCo instruments the classes under test which adds two members to the classes: A private static field $jacocoData
and a private static method $jacocoInit()
. Both members are marked as synthetic.
Please change your code to ignore synthetic members. This is a good practice anyways as also the Java compiler creates synthetic members in certain situation.
是說爲了收集執行數據,jacoco在測試中的類加入兩個成員:私有靜態字段$jacodata 和私有靜態方法160;$jacoinit(),這兩個成員都是合成(synthetic)的。Java 裏面的synthetic機制,有興趣大家可以看一下,是說Java編譯器會給類添加額外的成員信息,這些成員字段在源碼級別往往是看不到的,即這些成員不是程序員定義的,比如常見的內部類,Java編譯器通過生成一些在源代碼中不存在的synthetic方法和類的方式,實現了對private級別的字段和類的訪問,從而繞開了語言限制。我檢查了一下單測中waresMallProductFacadeService.getSingleProductProperty(productCode)方法的實現,裏面果然用到了反射。是一個Bean字段copy的函數:
public static <T> T copyProperties(Object orig, T dest){
if (dest == null || orig == null) return dest;
if (orig instanceof Map) {
Map map = ((Map)orig);
for(Object o:map.keySet()){
setProperties(dest, (String)o, map.get(o));
}
} else /* if (orig is a standard JavaBean) */ {
Class<?> clazz = orig.getClass();
while(clazz.getName().equals(Object.class.getName())==false){
for(Field origField:clazz.getDeclaredFields()){
Object value = null;
try {
origField.setAccessible(true);
value = origField.get(orig);
} catch (IllegalArgumentException e1) {
e1.printStackTrace();
} catch (IllegalAccessException e1) {
e1.printStackTrace();
}
setProperties(dest, origField.getName(), value);
}
clazz = clazz.getSuperclass();
}
}
return dest;
}
在JaCoCo執行的時候,修改了字節碼文件(比如添加了synthetic成員),導致了錯誤,因此如果修改的話,可以看到JDK 反射涉及的Class、Method、Field都有synthetic相關的處理,並且都有一個函數 boolean isSynthetic(); 用來判斷一個類或者類的成員函數或者字段是不是合成的。如果JaCoCo上涉及反射的單測出錯了,可以考慮用這個方法排除synthetic合成成員的干擾。
關於Java compiler synthetic的介紹:https://javapapers.com/core-java/java-synthetic-class-method-field/
這次排查這個問題耗費不少時間,有以下教訓:自己給項目選用了JaCoCo做單測覆蓋率統計插件,沒有仔細閱讀官方文檔資料,以前沒有太關注過一些官網上的FAQ,這是不正確的。很多問題FAQ上都表明了,所以要養成看FAQ的好習慣。另外,排查問題的時候,可以去所在的GitHub項目上,找一找對應的issues,往往能事半功倍。