构建工具的进化:ant, maven, gradle

在讲解基础知识的过程中,我们也要动手去写。而Java发展到现在,可以帮助我们写程序,构建,发布的工具有一大堆。今天就来讨论一下构建工具。在开始之前,我们先讲点别的。

如何学习琳琅满目的框架和工具

学Java的新人,最头疼的事情,莫过于工具太多,挑花了眼。不管你要做什么,几乎都要面临各种各样的选择。我是应该选hibernate呢?还是mybatis。我新建一个工程,要不要加上spring?mvc, tx都是什么?tomcat,jetty是什么?网络编程要不要用netty?要在服务端布署多个机器,用akka行不行?DMQ呢?protobuf是什么鬼?类似的问题还有很多。

如果利用搜索引擎,想去看看这些工具分别是干嘛的,会发现,搜索结果五花八门,说什么的都有,甚至有些说法都相互矛盾。造成这些乱象的原因,一是Java的应用过于广泛,在不同的软件体系中都能看到Java的身影,而不同的需求,会使用不同的工具,甚至是同样的工具,在不同的环境下,都会有不同的用法。二是,Java的流行已经有很长时间了,随着时间的推移,肯定会有很多工具没有跟上历史的潮流而慢慢被淘汰。比如J2EE现在的市场占有率只有不足5%。再比如java swing的图形界面编程,现在几乎没有公司再使用swing进行图形界面编程了,最早,互联网公司就几乎没有采用过swing的方案。swing只在某些企业级软件里发挥作用,而随着HTML5的兴起,越来越多的软件公司也把自己的界面搬到web前端,现在的html5对2d, 3d都有非常好的支持,而一套简洁的CSS就能定制出漂亮的界面。所以swing被html5挤出历史进程实属正常。再比如JSF,JavaFX等等,也算是出师未捷身先死。有用的,没用的一堆堆地都堆到新手面前,新手的感觉肯定就是“那叫一个乱”啊。

说到这里,吐槽一下,某些培训机构和出版机构。为了凑篇幅,还会把一些已经不常用的技术加上,这就给新手们,尤其是自学的新手们带来误导,以为这些东西很重要。例如《Java核心技术》这本书的第一卷里,还为swing准备了大段的篇幅,甚至还有awt,而这些东西早就不应该被列入学习计划了。这一点《Java编程思想》会好一点,在第4版里,图形界面编程已经压缩到最后一章了,做为参考提醒一下读者,Java里还有这么个玩意。

关于这些框架,工具的学习,我这里有一个建议:什么都别学,从基础开始,把根基搞扎实了。然后自己动手做工程,遇到问题了,再去学习会更好。我觉得一个好的老师,不是手把手地把知识教给你,而是能够指出问题的所在。我举一个例子。我之前指导过一个师弟学习Java,我让他自己用socket写一个web server ,他使用了一个线程一个连接的原始的线程方案,并且使用本机测试,开了十来个客户端,觉得还不错。我就告诉他,你这样的测试是不对的,你要去找一个client端的benchmark,或者自己写个工具,创建成百上千个连接,然后再看看你的web server的表现如何。后来,他发现,当创建几百个连接以后,服务端几乎就跑不动了。然后,我就提醒他,可以考虑一下,业界有哪些解决方案。这时候,他找到了nio,netty, 线程池等等方案。然后自己去重构,重构过程中,发现逻辑和页面合在一起,很难处理,又引入了spring mvc,为了操作数据库,又引入了hibernate等等。

回顾他学习的过程可以发现,对于工具和框架,被动地学习会比主动地学习效果好。到了不得不学的时候再去学习,有具体的场景,学起来必然事半功倍。我再举一个例子,上次在北京和朋友一起吃饭,他抱怨说Java不好招。我说,北京那么多培训班,新人一把一把地,不是随便招吗?他就讲了自己的面试经历。有一个小伙子,简历上写了熟悉servlet, spring,ibatis等框架。他就和这个小伙子聊怎么用这些框架,小伙子答的还可以。然后他就问到了http协议和Java中的Proxy(这是实现Spring的核心机制)以及reflection,小伙子就答不上来了。这就有点舍本逐末了,且不说,是不是所有公司都会使用spring,就算spring是标准配置,被写到Java语言标准中去,那也不能基础太薄弱了,出了诡异BUG,都不知道怎么查。框架这个东西,遇到问题再去学吧,如果你跟着框架走,三天两头有新的东西出来,你只能疲于奔命。今天出个rabbitmq,你要学,明天出个kafka,你还要学,没准哪天又出来个什么样的新DMQ,你还是得学。这学到什么时候是个头了。

回到今天的主题,我们讨论三个构建工程用的工具。

构建工具

我们要写一个Java程序,一般的步骤也就是编译,测试,打包。这个构建的过程,如果文件比较少,我们可以手动使用java, javac, jar命令去做这些事情。但当工程越来越大,文件越来越多,这个事情就不是那么地令人开心了。因为这些命令往往都是很机械的操作。但是我们可以把机械的东西交给机器去做。

在linux上,有一个工具叫make。我们可以通过编写Makefile来执行工程的构建。windows上相应的工具是nmake。这个工具写起来比较罗嗦,所以从早期,Java的构建就没有选择它,而是新建了一个叫做ant的工具。ant的思想和makefile比较像。定义一个任务,规定它的依赖,然后就可以通过ant来执行这个任务了。我们通过例子看一下。下面列出一个ant工具所使用的build.xml:

<?xml version="1.0" encoding="UTF-8" ?>  
<project name="HelloWorld" default="run" basedir=".">  
<property name="src" value="src"/>  
<property name="dest" value="classes"/>  
<property name="jarfile" value="hello.jar"/>  
<target name="init">  
   <mkdir dir="${dest}"/>  
</target>  
<target name="compile" depends="init">  
   <javac srcdir="${src}" destdir="${dest}"/>  
</target>  
<target name="build" depends="compile">  
   <jar jarfile="${jarfile}" basedir="${dest}"/>  
</target>  
<target name="test" depends="build">  
   <java classname="test.ant.HelloWorld" classpath="${hello_jar}"/>  
</target>  
<target name="clean">  
   <delete dir="${dest}" />  
   <delete file="${hello_jar}" />  
</target>  
</project>  

可以看到ant的构建脚本还是比较清楚的。ant定义了五个任务,init, compile, build, test, clean。每个任务做什么都定义清楚了。打包之前要先编译,所以通过depends来指定依赖的路径。如果在命令行里执行ant build,那就会先执行compile,而compile又依赖于init,所以就会先执行init。看起来很合理,对吧?有了这个东西以后,我们只要一条命令:

ant test

就可以执行编程,打包,测试了。为开发者带来了很大的便利。

但是ant有一个很致命的缺陷,那就是没办法管理依赖。我们一个工程,要使用很多第三方工具,不同的工具,不同的版本。每次打包都要自己手动去把正确的版本拷到lib下面去,不用说,这个工作既枯燥还特别容易出错。为了解决这个问题,maven闪亮登场。

maven最核心的改进就在于提出仓库这个概念。我可以把所有依赖的包,都放到仓库里去,在我的工程管理文件里,标明我需要什么什么包,什么什么版本。在构建的时候,maven就自动帮我把这些包打到我的包里来了。我们再也不用操心着自己去管理几十上百个jar文件了。

这了达到这个目标,maven提出,要给每个包都标上坐标,这样,便于在仓库里进行查找。所以,使用maven构建和发布的包都会按照这个约定定义自己的坐标,例如:

<?xml version="1.0" encoding="utf-8"?>
<project ...xmlns...>
    <groupId>cn.hinus.recruit</groupId>
    <artifactId>Example</artifactId>
    <version>0.1.0-SNAPSHOT</version>
		 
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.10</version>
        </dependency>
    </dependencies>
</project>

这样,我就定义了我自己的包的坐标是cn.hinus.recruit:Example:0.1.0-SNAPSHOT,而我的工程要依赖junit:junit:4.10。那么maven就会自动去帮我把junit打包进来。如果我本地没有junit,maven还会帮我去网上下载。下载的地方就是远程仓库,我们可以通过repository标签来指定远程仓库。

maven里抛弃了ant中通过target定义任务的做法,而是引入了生命周期的概念。这个问题要讲下去,就是一个大的话题了。我们暂时放一下。因为我们要看今天的最终BOSS,gradle

Gradle

maven已经很好了,可以满足绝大多数工程的构建。那为什么我们还需要新的构建工具呢?第一,maven是使用xml进行配置的,语法不简洁。第二,最关键的,maven在约定优于配置这条路上走太远了。就是说,maven不鼓励你自己定义任务,它要求用户在maven的生命周期中使用插件的方式去工作。这有点像设计模式中的模板方法模式。说通俗一点,就是我使用maven的话,想灵活地定义自己的任务是不行的。基于这个原因,gradle做了很多改进。

gradle并不是另起炉灶,它充分地使用了maven的现有资源。继承了maven中仓库,坐标,依赖这些核心概念。文件的布局也和maven相同。但同时,它又继承了ant中target的概念,我们又可以重新定义自己的任务了。(gradle中叫做task)

我们来体验一下,新建一个空目录,在命令行,执行

gradle init --type java-library

可以看到新创建了一个工程,工程根目录下,有这几项:

build.gradle  gradle  settings.gradle  src

我们看一下,build.gradle的内容:

// Apply the java plugin to add support for Java
apply plugin: 'java'

// In this section you declare where to find the dependencies of your project
repositories {
    // Use 'jcenter' for resolving your dependencies.
    // You can declare any Maven/Ivy/file repository here.
    jcenter()
}

// In this section you declare the dependencies for your production and test code
dependencies {
    // The production code uses the SLF4J logging API at compile time
    compile 'org.slf4j:slf4j-api:1.7.21'

    // Declare the dependency for your favourite test framework you want to use in your tests.
    // TestNG is also supported by the Gradle Test task. Just change the 
    // testCompile dependency to testCompile 'org.testng:testng:6.8.1' and add 
    // 'test.useTestNG()' to your build script.
    testCompile 'junit:junit:4.12'
}

内容很简单,引入了java插件,指定仓库,指定依赖。可以看到依赖的设定相比起xml的写法,变得大大简化了。

使用gradle,任务又变成了核心概念了。我们就来体验一下任务。

在build.gradle里添加这样的任务:

task hello << { 
    println 'welcome to gradle';
}

然后在命令行执行

gradle -q hello

就可以看见打印一行"welcome to gradle"。在使用maven构建的时候,如果想临时对某一个构建任务加一点log,会是个非常困难的事情 。但在gradle里,就变得非常简单,因为gradle的背后其实是groovy这个编程语言在起作用。为了验证这一点,我们再改一下:

task hello << {
    3.times {
        println 'welcome to gradle';
    }   
}

然后执行gradle -q hello,就可以看到连续打印了三行。使用脚本语言进行构建,这几乎给了我们任何的能力,我们可以在构建的时候做任何的事情,甚至你可以直接让gradle帮你做表达式求值 :)

导入到 IDE

在Java的开发中,我们不可能脱离集成开发环境(Integrated Develop Environment)。因为IDE提供了代码补全和方便的代码跳转,这是普通的文本编辑软件(比如vim)很难做到的。所以我们还是要通过ide来进行代码开发。

以前在ide里使用spring的时候,我们要手动下载spring的包,如果spring依赖了其他的第三方的库,我们还要去下载那个库并且添加到IDE中去。现在,我们就不必再这样做了。只需要在build.gradle里定义好依赖,然后更新它,IDE就可以自动帮我们把包导进来。

例如,我要在新建的工程里使用spring,只需要在build.gradle里添加一行:

然后,在gradle窗口里,点击更新:

然后,intellij就自动把spring所依赖的所有包都下载下来了。

非常方便。

好了,今天的课程就到这里了。作业:

使用gradle新建一个工程,并把它导入到你喜欢的IDE中去。添加spring依赖,并更新,观察一下,gradle的工作方式。

小密圈《进击的Java新人》中,有更详细的内容,以及我已经建好的一个工程。已经加入小密圈的同学请去git上clone下来,我们接下来的课程会在这个工程上写代码咯。知乎专栏只负责讲解知识,不负责指导动手。

上一节:进制习题课

下一节:Java中的设计模式:适配与装饰

课程目录:总目录

编辑于 2020-08-25 20:20