天天看点

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,往往能事半功倍。