JaCoCo覆蓋率插件引起的Maven報ArrayIndexOutOfBounds異常問題

    最近在執行單元測試的時候,發現一個奇怪的問題:本地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,往往能事半功倍。


 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章