如何提高Java Maven工程的编译速度
【引言】
最近接到一个任务,是分析一下关于Java Maven工程编译时间过长的问题。本文我们就来探讨一下这个话题。
【1. 代码优化重构】
复杂度涉及到的内容有时间上的复杂度,空间上的复杂度,程序设计上的复杂度,模块设计上的复杂度。
Java代码的优化在于维持代码复杂度的最小化和模块设计的依赖复杂度的最小化。
模块设计的依赖复杂度取决于具体的业务设计,基本原则是越简单越好。
【1.1避免一行中多个独立语句的调用】
代码复杂度的基本原则也是越简单越好。下面看一个例子:
static Object slow(DSLContext sql) {
return sql.select().having(round(sum(field("a").cast(Double.class)).cast(Double.class), 2).ne(0d));
}
以上这种代码很常见,有些人写代码喜欢把几个语句放在一起组成一条语句,看着很酷,实际上会:
1. 降低程序的可读性。
2. 增加程序设计的复杂度。
把多个语句放在一个语句里面势必会影响程序的可读性,同时也增加了增加程序的复杂度。
3. 可维护性差。
如果程序出错了,我们很难看出到底是哪一部分出错了,因为程序报错的时候顶多告诉我们在哪一行出错了。
具体是哪一部份,我们不得不把这一句话分解开。无形中增加了调试工作的难度。
4. 让编译器疲于奔命。
编译器在编译的时候,需要把这么复杂的一句话拆分开,再一句句的解析出来。由于是程序自动去做,编译器需要临时做很多工作。
5. 有时候程序的执行效率也会打折扣。
【1.1.1改进】
static Object fast(DSLContext sql) {
Field<Double> a = field("a").cast(Double.class);
Sum<Double> s = sum(a).cast(Double.class);
return sql.select().having(round(s, 2).ne(0d));
}
【1.2 Streams Chaining的正确使用】
有人问了Java8引入的流处理函数的链接使用。这一小节,我们就说一下这一部分的内容。
【1.2.1 什么是流处理】
首先要知道这个Streams不同于I/O Streams(InputStream, OutputStream, etc)。两者是完全不同的东西。
在这里的Streams只是围绕数据源的一层外壳,是为了快速方便的对数据进行操作,它不保存数据。
【1.2.2 流处理的操作类别】
流处理有两种操作类别:
Terminal Operation(终端操作): 意思是说,这种操作执行以后,Streams会被销毁,不能再重复使用了。这类函数有forEach, collect, toArray等等。
Intermediate Operation(中间操作): 这类操作会返回一个新的Streams,我们可以继续在新的Streams上面进行操作,这类函数有map, filter, flatMap等等。
一个流处理必须有且仅有一个Terminal Operation,没有Terminal Operation会导致内存泄漏和无效代码。可以有0个或多个Intermediate Operation。
流处理的一个最大优势是惰性评价(Lazy Evaluation)。 对于数据源的计算只会在调用Terminal Operation以后发生, 对于数据的获取是基于按需分配的原则获取的。
所有的Intermediate Operations都是惰性的,在没有调用Terminal Operation之前都不会被处理。
现在来看一个例子:
Stream<Integer> infiniteStream = Stream.iterate(2, i-> i * 2);
List<Integer> collect = infiniteStream.skip(3).limit(5).collect(Collectors.toList());
Assert.assertEquals(collect, Arrays.asList(new Integer[]{16, 32, 64, 128, 256}));
【1.2.3 流处理的使用】
对于流处理的使用,最重要的是辨别出Terminal Operation和Intermediate Operation。
如果Terminal Operation的操作,建议一定要独立成行,不要多个Terminal Operation的调用放在一行中。
对于Intermediate Operation的操作,可以根据可读性和具体场景决定是否分行使用。可参见上面的例子。
【2.并行编译可以提高编译速度】
使用并行编译可以充分利用电脑的硬件性能来提高编译速度。我们来看如下的命令行示例:
-mvn –T 4 install
上面这个命令行是启动四个线程来编译。
-mvn –T 1C install
上面这个命令行是在每个可用的CPU core上面启动一个线程。
在启动多线程选项以后,maven会对整个项目的模块结构进行分析,然后按照依赖关系进行并行编译。这样子会最大限度地利用现有的硬件资源,提高编译的效率。
【3.测试相关的速度改进】
在开发阶段,我们会经常使用-DskipTests=true来跳过测试的执行。但是在开发阶段之外的编译中我们依然要进行测试程序的运行。提高测试执行效率的可行方案就是使用并行技术。
下面是一个参考配置:
3.1添加Surefire插件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version>
</plugin>
3.2配置并行参数
3.2.1 并行测试的适用对象有如下类别:
-methods: 支持Methods。
-classes; 支持Classes。
-classesAndMethods: 支持Classes 和 Methods。
-suites支持Suites。
-suitesAndClasses支持Suites 和 Classes。
-suiteAndMethods支持Suites和 Methods。
-all 支持Suites, Classes 和 Methods。
示例:支持Suites, Classes 和 Methods。
<parallel>all</parallel>
3.2.2 我们指定线程的数量
示例:最多启用10个线程
<threadCount>10</threadCount>
示例:一个Core创建一个线程
<useUnlimitedThreads>true</useUnlimitedThreads>
示例:缺省情况先的线程数是指一个CPU core可创建的线程数。我们可以通过如下的参数来启用或者终止:
<perCoreThreadCount>true</perCoreThreadCount>
3.2.3指定对应内容的线程数
我们可以再细化一下指定对应内容使用的线程数,比如:
<threadCountSuites>2</threadCountSuites>
以上指定Suites的线程数。
<threadCountClasses>2</threadCountClasses>
以上指定了类的线程数。
<threadCountMethods>6</threadCountMethods>
以上指定了方法的线程数。
有时候我们会既指定总内容的线程数,又指定子内容线程数。此时,只要我们能确保总内容的线程数大于等于子内容的线程数之和就可以了。
下面的例子可供参考:
<threadCount>10</threadCount>
<threadCountClasses>2</threadCountClasses>
3.2.4超时设置
在使用并行的过程中,我们可能需要设置超时参数,这样可以避免过长时间的等待,当然如果通过超时方式退出测试程序,对应的错误信息会显示出来。
如下的参数配置会在五秒的超时到来时终止正在运行的测试并且不会执行队列中的测试。
<parallelTestTimeoutForcedInSeconds>5</parallelTestTimeoutForcedInSeconds>
如下的参数配置会在五秒超时到来时,不会执行队列中未执行的测试。
<parallelTestTimeoutInSeconds>3.5</parallelTestTimeoutInSeconds>
3.3 内存不一致和条件竞争
Surefire会在父线程中调用使用了@Parameter, @BeforeClass和@AfterClass注解的静态方法。因此,在使用并行模式前一定要确保避免内存不一致和条件竞争的情况出现。
同样的,如果你的测试用例中存在修改共享状态的情况,那么测试使用并行模式是不合适的。
3.4 在多个模块下运行并行测试
以上的并行配置是针对一个模块的。那么,如何对多个模块使用并行模式呢?可以通过如下方式:
3.4.1 指定整个工程可使用的线程数
mvn -T 4 surefire:test
3.4.2 指定每个Core可使用的线程数
mvn -T 1C surefire:test
【4.只编译需要编译的模块】
很多程序员在编译maven工程的时候,会使用下面的命令行:
-mvn clean install
-clean 的意思是清除所有已经生成的成品,半成品,临时文件。这就意味着整个编译过程要把这些文件全部再创建一遍。我们说这种清理模式在一些情况下是非常有用的,比如遇到了缓存相关的问题,难以重现的Bug等等。
但是,在绝大多数时候,我们不需要这个清理过程。尤其是当在一个工程中有多个模块时,你可能只会修改一个模块,你并不想把时间花费在不停的清理再重新编译上。
此时最好的方案时使用下面的命令行:
-mvn install –pl $moduleName –am
与上面的命令行相比,我们去掉了clean,因为我们不想每次都清理工程。关于其他的参数,我们诸个来看看:
-pl 指定模块来进行编译。
-am 让编译器来找出指定模块的依赖进行编译。
通过以上的参数组合配置,我们编译过程既能保证编译的准确性和灵活性,又能提高编译的速度。
【5.使用离线参数改善网络访问问题】
众所周知,使用maven会从英特网上下载指定的程序库及其依赖,这在其他的编译系统中也是一样的,如npm, gradle, yarn等等。如果网络速度不给力,你会发觉整个编译过程非常缓慢,痛苦至极。
其实,在maven环境下我们有一个非常简单的解决方案,那就是offline参数。
mvn install --offline
在mvn命令行中加入offline以后,maven不会再尝试连接英特网。
如果你不想完全的使用离线模式,你可以在MAVEN_OPTS中添加如下参数-DdependencyLocationsEnabled=false。
【6.Maven 开发环境下编译命令行示例】
mvn clean
mvn -T 1C install -pl $moduleName –am
mvn -T 1C install -pl $moduleName -am —offline
通过以上的参数配置,我们就可以在开发环境下提高maven工程的编译速度了。
【小结】
本文从代码优化,maven编译参数配置以及并行测试的角度对java maven工程遇到的编译时间过长的问题进行了剖析,希望可以对相关的项目提供帮助,欢迎留言,批评和指正。
- 点赞
- 收藏
- 关注作者
评论(0)