聊聊构建工具-Ant,Maven,Gradle(上)

(转载请注明作者和出处‘https://fourthringroad.com/’,请勿用于任何商业用途)

一直想写篇文章整理一下几个构建工具,先从Ant 和Maven入手吧。

Make和Ant

我用过最简单的构建工具是make。它的功能就是控制编译的过程,底层本质上是依赖不同的编译器,shell命令,操作系统CLI等等,make就像是一个编排器。它的语法如下:

<target> : <prerequisites> 
[tab]  <commands>

譬如之前一个golang的项目,涉及多个模块的构建。截取部分makefile配置:

manager: bootstrap_node bootstrap_kibana restart_node op_failover_node replace_text_file execute_shell
	gb build cmd/manager

bootstrap_node:
	gb build manager/exe/cmd/bootstrap_node

bootstrap_kibana:
	gb build manager/exe/cmd/bootstrap_kibana

含义是:

如果要使用make manager构建manager模块,那么底层执行的是一个gb vendor编译命令:

gb build cmd/manager

但是在这之前,manager模块还依赖其他模块,包括bootstrap_node bootstrap_kibana restart_node op_failover_node replace_text_file execute_shell的构建,所以make bootstrap_node会在make manager之前执行,它的底层是执行另一个gb vendor编译命令:

gb build manager/exe/cmd/bootstrap_node

当然也可以在<commands>中用shell命令控制编译后的文件目录,文件名称,位置,权限等等。

make很灵活,没有语言限制,但是基本上什么都需要用户自己做。

Ant 是一个基于java的构建工具, 定义使用XML文件(build.xml,类似makefile),里面封装了一些代表某些基本功能的XML标签;Ant同样非常灵活;用户可以定义定义大量的targets以及他们的依赖关系,当构建触发一个上层的target执行时,这个target的底层依赖会提前执行。

复制一个网上的sample:

<project>
    <target name="clean">
        <delete dir="classes" />
    </target>

    <target name="compile" depends="clean">
        <mkdir dir="classes" />
        <javac srcdir="src" destdir="classes" />
    </target>

    <target name="jar" depends="compile">
        <mkdir dir="jar" />
        <jar destfile="jar/HelloWorld.jar" basedir="classes">
            <manifest>
                <attribute name="Main-Class" 
                  value="antExample.HelloWorld" />
            </manifest>
        </jar>
    </target>

    <target name="run" depends="jar">
        <java jar="jar/HelloWorld.jar" fork="true" />
    </target>
</project>

我还在Amazon工作时,内部的构建工具brazil就是基于ant。Ant的缺点是缺失依赖管理功能,brazil对此功能进行了增强,利用dependency标签进行依赖的管理。

灵活对应的缺点就是复杂度。在makefile或者build.xml中,用户需要定义的东西太多,往往需要从target目录的创建开始,包括后续的清理,编译,连接,打包。。。等等,任务的依赖也可能会错综复杂。我看到过一个词来形容build.xml文件-write-only,来形容它糟糕的可读性。

Maven则用另一种思路解决这个问题。

Maven – Convention over configuration 约定优先

在京东云工作时,除了ES源码开发用的gradle,其他大部分java项目都是maven管理的,直观感受就是简单。

maven对构建过程进行了标准化定义,形成了一个约定,所有的用户需要在这个约定上进行使用和开发。本质上就像一个框架,用户使用上更简单了,用户只需要去specify unconventional aspects of the application。

standard conventions包括了:项目结构的约定,生命周期的约定,依赖管理的约定等等多个方面。引入了约定,灵活性肯定会下降,衡量一个框架是否灵活可以用它的可拓展性,maven的可拓展性依赖它的插件机制(maven的所有功能其实都是以插件形式提供的),如果需要进行功能拓展则需要进行插件开发,插件的开发代价就成为了影响灵活性的关键因素。很多人认为相比gradle,maven的‘不灵活’,也正是在于所有的定制化功能都需要进行相对复杂的插件开发(当然还有固定的生命周期等难以改动的conventions)

maven构建生命周期

Maven的构建的生命周期分为三种类型:clean,default,site,分别对应:清理,默认构建,生成项目介绍页面。

其中default显然是最重要的。default生命周期可以细分成多个按顺序执行的阶段:

其中比较重要的过程为

  1. Validate 验证
  2. Compile 编译
  3. Test 测试
  4. Package 打包
  5. Verify 运行集成测试
  6. Install 安装到本地仓库
  7. Deploy 安装到远程仓库

常用的执行命令为:

mvn clean package

含义:(如果是多模块项目,每个子项目都会被遍历执行)依次执行clean,validate,compile,test,package。

maven插件

maven这样形容自己:Maven is – at its heart – a plugin execution framework。可见插件的重要程度。

上述是build lifecycle,是maven对构建流程从顶层的划分。每个具体的阶段(phase)执行什么操作,就是由插件以及插件中的goals决定的。

goal就是一个任务(类似ant中的一个target),它可以被绑定在生命周期的某个阶段,也可以脱离阶段独自存在。譬如我们经常使用的一个命令:

mvn dependency:tree

就是直接调用dependency插件的一个goal。

一个阶段可以包括多个goals,如果没有任何goals,显然这个phase就不会被执行。

同一个phase中的goals执行顺序是根据它们在pom文件中的声明先后。(版本2.0.5及以上)

如何绑定插件的goals到生命周期?

对于default lifecycle,是没有为任何phase绑定默认goals的,有以下两种方式可以绑定:

最简单的方式利用<packaging>标签

以下是packaging标签为jar时绑定的goals:

关于3.8.5的详细插件版本:

当然packaging还有多种属性可以配置,war,pom等。如果没有使用packaging标签,默认意味着设定了packaging类型为jar。

packaging标签是打包组合的绑定方式当然也可以逐个指定plugin以及要绑定的goal

可以用这种方式去指定packaging中指定插件的version,事实上为了开发过程中出现的问题的可复性,我们应该始终在pluginManagement中指定所有plugin的版本号,这是一个很好的习惯,而不是使用一个隐式的默认值;版本管理使用的标签是PluginManagement

除此之外,plugin将它的goal绑定到生命周期中的哪一个阶段,相应的goal就会在对应阶段被执行。这种绑定关系可以是隐式的,也可以是显式的。

同一个阶段多个goals的执行顺序是:packaging绑定的goals先执行,剩下的按照pom的声明顺序。

Clean lifecycle 默认已经绑定了一个goals:

org.apache.maven.plugins:maven-clean-plugin:2.5:clean

Site lifecycle 默认绑定了两个goals:

org.apache.maven.plugins:maven-site-plugin:3.3:site

org.apache.maven.plugins:maven-site-plugin:3.3:deploy

基本原理和示例

插件可以分为两类:构建插件(build plugin)和报告插件(reporting plugin),多数情况下使用前者,下文也围绕构建插件展开。

插件一个goal最重要的‘参数配置’和‘执行逻辑’其实可以简单对应到一个自定义类中,官方称其为mojo(m for maven)。举个例子,下面是一个mojo的定义,和省略的执行逻辑:

/**
 * @goal query
 * @phase package
 */
//上面定义goal的名称为query,和插件默认绑定的生命周期phase为package
public class MyQueryMojo
    extends AbstractMojo
{
    @Parameter(property = "query.url", required = true)
    private String url;
 
    @Parameter(property = "timeout", required = false, defaultValue = "50")
    private int timeout;
 
    @Parameter(property = "options")
    private String[] options;
 
    public void execute()
        throws MojoExecutionException
    {
        ...
    }
}

一种常见的配置方式是利用<configuration>标签:

<build>
  <plugins>
    <plugin>
      <artifactId>maven-myquery-plugin</artifactId>
      <version>1.0</version>
      <configuration>
        <url>http://www.foobar.com/query</url>
        <timeout>10</timeout>
        <options>
          <option>one</option>
          <option>two</option>
          <option>three</option>
        </options>
      </configuration>
    </plugin>
  </plugins>
</build>

另一种方式是通过<execution>标签进行定义,这种定义方式可以进行多次绑定,即绑定到不同的phase,多次执行:

<build> 
  <plugins> 
    <plugin> 
      <artifactId>maven-myquery-plugin</artifactId>  
      <version>1.0</version>  
      <executions> 
        <execution> 
          <id>execution1</id>  
          <phase>test</phase>  
          <configuration> 
            <url>http://www.foo.com/query</url>  
            <timeout>10</timeout>  
            <options> 
              <option>one</option>  
              <option>two</option>  
              <option>three</option> 
            </options> 
          </configuration>  
          <goals> 
            <goal>query</goal> 
          </goals> 
        </execution>  
        <execution> 
          <id>execution2</id>  
          <configuration> 
            <url>http://www.bar.com/query</url>  
            <timeout>15</timeout>  
            <options> 
              <option>four</option>  
              <option>five</option>  
              <option>six</option> 
            </options> 
          </configuration>  
          <goals> 
            <goal>query</goal> 
          </goals> 
        </execution> 
      </executions> 
    </plugin> 
  </plugins> 
</build>

(注意:这里是在<plugins>标签声明,区别于<pluginManagement>标签)

第一个execution绑定的执行phase是test;第二个execution没有绑定phase,则会在plugin的默认phase(package)执行。

上面是从官网扒的一个抽象的例子,下面拿个真实的插件举例:maven-surefire-plugin

这个插件只有一个goal:

surefire:test

这个goal被绑定到了test阶段,是用来执行应用的所有单元测试-它定义会扫描所有以Test开始,或者Test,Tests,TestCase结尾的类。

它有个非常大的优点就是兼容Junit,TestNG等多个单元测试框架。譬如如果用户想使用junit,只需引入junit的依赖

<dependency> 
  <groupId>junit</groupId>  
  <artifactId>junit</artifactId>  
  <version>4.11</version>  
  <scope>test</scope> 
</dependency>

然后在src/test/java下添加测试类。

如果用户使用的是testNG,将上面的依赖替换为testNG的依赖

<dependency> 
  <groupId>org.testng</groupId>  
  <artifactId>testng</artifactId>  
  <version>6.9.8</version>  
  <scope>test</scope> 
</dependency>

如果想做一些testNG框架级别的配置,通过参数配置即可:

<plugin> 
  <artifactId>maven-surefire-plugin</artifactId>  
  <configuration> 
    <suiteXmlFiles> 
      <suiteXmlFile>testng-qa.xml</suiteXmlFile> 
    </suiteXmlFiles>  
    <parallel>methods</parallel>  
    <threadCount>10</threadCount>  
    <argLine>-Dfile.encoding=UTF-8</argLine>  
    <testFailureIgnore>true</testFailureIgnore> 
  </configuration> 
</plugin>

接着聊聊其他方面:

服务架构

几个重要组件:

  • 中央仓库:central repo,服务端,用来存储三方(3rd party)依赖和插件的文件
  • 本地仓库:local repo
  • 私有仓库:公司可以在自己的私有服务器上搭建仓库存储三方依赖和插件
  • 本地客户端:本地访问远端repo的client

目录结构

目录结构也是一个convention,maven做了下面的规定:

配置文件

全局配置文件settings.xml

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
      <localRepository/>
      <interactiveMode/>
      <offline/>
      <pluginGroups/>
      <servers/>
      <mirrors/>
      <proxies/>
      <profiles/>
      <activeProfiles/>
</settings>

譬如我们常配置的中央仓库镜像:

<mirrors> 
  <mirror> 
    <id>alimaven</id>  
    <name>aliyun maven</name>  
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url>  
    <mirrorOf>central</mirrorOf> 
  </mirror> 
</mirrors>

项目的配置文件:pom.xml

这个不详述了,需要知道优先级 pom.xml > settings.xml

依赖

两个比较重要的点:

依赖的scope:

scope用来定义一个依赖的传递性,即在某个阶段是否需要这个依赖的存在。一个依赖包可能在三个阶段被用到:编译,测试,运行时。相应的有以下几个scope:

  1. compile:默认scope;在所有的阶段都会存在
  2. provided:在编译和测试阶段会使用,但是在运行时不会使用;运行时由容器或者jdk提供依赖包。
  3. runtime:编译阶段不会使用,但是测试和运行时会使用。
  4. test:只在测试阶段被使用,例如JUnit和TestNG
  5. system:类似provided,需要提供一个存放jar的目录路径
  6. import:很少用,懒得写了。

使用原则:当然是最小化原则,如果只在测试阶段使用的依赖,应当加上test scope,可以避免无意义的包冲突,减少部署文件的大小。

依赖冲突解决:

人工通过<exclusions>标签进行排除某一个依赖包。通常是引入最新版本,因为java包设计时,通常是向后兼容(Backwards Compatibility)的。

当然,maven也有自动解决冲突的方式:短路径优先,声明优先(路径长度相同,谁先声明听谁的)

一种可控的方式是在父项目中通过dependencyManagement对依赖的版本进行锁定。

父子项目配置

maven中允许给两个项目之间赋予继承关系,即构成父子项目。子项目会继承父项目pom中的一些属性,可以避免重复的定义。

  • 父项目配置:
    • 打包方式:pom
    • 父项目中的基本依赖管理 
    • 父项目中的统一依赖管理<dependencyManagement>(只有在子项目中使用时才会引入)
  • 子项目配置
    • 父项目的基本依赖一定会引入
    • 父项目的统一依赖,可以不写版本号

类似还可以定义聚合项目:将一个庞大复杂的项目拆分成多个模块(通过new ->module的方式),最上级项目为聚合项目,里面的模块都是它的‘子项目’;这样能够方便更好协作/管理。聚合项目在父项目中会有modules标签。

对聚合项目执行打包,会对下属所有模块执行打包。

一些功能性标签

还有一些功通过一些maven定义的标签来实现,譬如Filtering Resources功能

<build> 
  <filters> 
    <filter>src/main/filters/filter.properties</filter> 
  </filters>  
  <resources> 
    <resource> 
      <directory>src/main/resources</directory>  
      <filtering>true</filtering> 
    </resource> 
  </resources> 
</build>

# application.properties
application.name=${project.name}
application.version=${project.version}
message=${my.filter.value}

# filter.properties
my.filter.value=hello!

利用上面的filter标签,可以对资源里的占位符进行值的填充。

 

小结maven,一个规范化的依赖管理/项目构建工具,对于熟悉其定义的conventions(like lifecycle)的用户来讲,项目是非常容易维护的。另一方面这些定义的conventions也限制了它的灵活性。maven的插件机制从一定程度上缓解了这个问题。市面上有大量的插件几乎可以满足所有日常构建需求,但是少数的场景还是需要用户定制,如果你恰巧擅长并喜欢maven插件开发,那么congrats,maven就非常适合你。而对于一些大型复杂的项目,构建过程中需要很多定制化的逻辑,这时候maven的不灵活成为了它很大的短板,于是就有人转向了gradle,后者可以直接用groovy,scala等脚本语言直接定义构建过程。这个就下篇文章再整理吧。

 

此条目发表在Uncategorized分类目录。将固定链接加入收藏夹。

发表评论

您的电子邮箱地址不会被公开。