从Apache Ant到Gradle再到Travis CI——构建!构建!构建!

概述

本文原载于我的博客,地址:https://blog.guoziyang.top/archives/17/

从上古时期的命令行构建,到当代形形色色的自动构建工具,再到未来科技的在线构建,您已经历许多。现在,开启您最伟大的探索吧:从Apache Ant到Gradle再到Travis CI!

无聊地玩了一下文明的梗……

但是项目的构建确实是项目生命周期中的一项重要的环节。远古时期,大家都是手动构建,大都是命令行。但当IDE出现之后,好多人就丧失了构建的能力,满足于一键编译运行,隐藏了诸多细节。但是,软件构造课程要求上明确表示,要求“脱离IDE构建”。所以,我们还是来看一看当今使用的一些构建工具。

Java可用构建工具有Apache Ant、Apache Maven、Gradle和Travis CI。其中,Travis CI是线上构建工具,关联一个Github仓库,在每个commit后对整个仓库进行构建测试。线下的Apache Ant、Apache Maven和Gradle则是各有千秋。配置文件的书写是必不可少的,但是Apache Ant和其它两个有一个很大的不同:Ant的配置文件是在模拟命令行的工作,而Maven和Gradle则将细节隐藏的很好,不需要用户过度关注底层的细节,自动依赖引入,但是也牺牲了一定的自由。

Travis CI是关联GitHub仓库,对每一个commit进行整体构建。Travis CI会启动一个虚拟机(操作系统貌似是Ubuntu 14.04),将代码clone到虚拟机中,进行构建,并返回构建结果。Travis CI到构建脚本可以基于Apache Maven、Gradle和Apache Ant,系统在项目目录中查找这三个工具的配置文件以决定使用哪种工具。

由于软件构造实验的特殊性(src与test平级,依赖的jar包以文件的形式存在),我们选择Apache Ant作为线下构建工具,Travis CI作为线上构建工具。(补充,添加了Gradle的构建)

目录如下:

GuodeMacBook-Air:Lab1-1170300520 guoziyang$ tree -L 2
.
├── lib							------项目所需依赖jar
│   ├── hamcrest-core-1.3.jar
│   ├── javax.json-1.0.jar
│   └── junit.jar
├── src							------项目的Java源码
│   ├── P1
│   ├── P2
│   ├── P3
│   └── P4
└── test						------项目的测试类
    ├── P2
    ├── P3
    └── P4

PS:travis-ci.org只可以构建共有项目,私有项目需要在travis-ci.com中构建。

Apache Ant

Apache Ant是一个Apache基金会的自动构建工具。与其它线下构建工具(Maven和Gradle)不同的是,Ant不仅仅可以构建Java系(Java、Kotlin和Scala)的项目。事实上,它可以构建所有的可以通过命令行构建的项目。

Ant的官方网站是http://ant.apache.org 。

Apache Ant的安装不再赘述,无非是下载、解压、配置系统环境变量,最后成功的标志是在命令行中运行ant -version,出现类似以下的响应:

GuodeMacBook-Air:~ guoziyang$ ant -version
Apache Ant(TM) version 1.10.5 compiled on July 10 2018

Ant识别一个项目的方式是通过一个配置文件build.xml,Ant将与该配置文件同级的所有文件(夹)及其子文件(夹)视作同一项目。

配置文件build.xml是需要我们手动书写的,只需要在项目的根目录下新建build.xml即可。

配置文件示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 指定项目的名称,默认运行的指令,以及项目根目录 -->
<project name="SC_Lab1" default="build" basedir=".">
 	
    <!-- 以下设置一些目录的宏定义 -->
	<property name="src.dir" value="src"/>
	<property name="lib.dir" value="lib"/>
	<property name="build.dir" value="build"/>
	<property name="build.classes" value="${build.dir}/classes"/>
	<property name="build.apidocs" value="build/doc"/>
	<property name="testSrc.dir" value="test"/>
	<property name="reports.dir" value="build/report"/>
	<property name="correctreports.dir" value="${reports.dir}/html"/>
	
    <!-- 设置依赖目录的宏定义 -->
	<path id="classpath">
    	<fileset dir="${lib.dir}">  
        	<include name="*.jar"/>  
    	</fileset>
	</path>
	
    <!-- available根据classname是否存在而设置property -->
	<target name="JUNIT">
		<available property="junit.present" classname="junit.framework.TestCase"/>
	</target>
	
    <!-- 编译src下的所有Java文件,depends属性表示在执行该任务前,会事先执行的任务 -->
	<target name="compile" depends="JUNIT">
        <!-- 建立一些输出的目录 -->
		<mkdir dir="${build.dir}"/>
		<mkdir dir="${build.classes}"/>
		<depend srcdir="${src.dir}" destdir="${build.classes}"/>
        <!-- javac命令编译java文件,srcdir指定java文件位置,destdir指定class文件输出位置,fork表示是否创建新的jvm执行 -->
		<javac srcdir="${src.dir}" destdir="${build.classes}" fork="true" includeantruntime="on">
            <!-- classpath设置依赖路径 -->
			<classpath>
				<pathelement path="${build.classes}"/>
				<pathelement path="${java.class.path}/"/>
			</classpath>
			<classpath refid="classpath"/>
            <!-- include设置源文件为srcdir下的所有java文件 -->
			<include name="**/*.java"/>
            <!-- 编译参数,设置字符集编码 -->
			<compilerarg line="-encoding UTF-8 "/>
		</javac>
	</target>
	
    <!-- 编译所有的测试用例 -->
	<target name="testcompile" depends="compile">
		<depend srcdir="${testSrc.dir}" destdir="${build.classes}"/>
        <!-- class文件输出目录与compile中的输出目录一致,可保持包结构不变 -->
		<javac srcdir="${testSrc.dir}" destdir="${build.classes}" fork="true" includeantruntime="on">
			<classpath>
				<pathelement path="${build.classes}"/>
				<pathelement path="${java.class.path}/"/>
				<fileset dir="${lib.dir}">  
        			<include name="*.jar"/>  
    			</fileset>
			</classpath>
			<compilerarg line="-encoding UTF-8 "/>
		</javac>
	</target>
	
    <!-- 运行junit测试 -->
	<target name="runtests" depends="testcompile">
		<mkdir dir="${reports.dir}"/>
		<delete>
			<fileset dir="${reports.dir}" includes="**/*" />
		</delete>
		<junit printsummary="on" failureProperty="fail">
            <!-- 断言所需的参数 -->
			<jvmarg value="-ea"/>
			<classpath>
				<pathelement location="lib/***.jar" />
				<pathelement path="${build.classes}"/>
				<pathelement path="${java.class.path}/"/>
			</classpath>
			<classpath refid="classpath"/>
			<formatter type="xml"/>
			<batchtest fork="yes" todir="${reports.dir}">
				<fileset dir="${testSrc.dir}">
					<include name="**/*Test.java"/>
				</fileset>
			</batchtest>
		</junit>
		
        <!-- 测试报告输出路径 -->
		<junitreport todir="${reports.dir}">
			<fileset dir="${reports.dir}">
    			<include name="TEST-*.xml"/>
   			</fileset>
   			<report format="frames" todir="${correctreports.dir}"/>
  		</junitreport>
	</target>
	
    <!-- 编译源文件为jar包 -->
	<target name="makejar" depends="compile" description="Jar生成">
  		<delete file="build/lib/main.jar"/>
  		<jar jarfile="build/lib/main.jar" basedir="${build.classes}">
   			<fileset dir="${build.classes}">
                <!-- 编译时排除Test文件 -->
    			<exclude name="**/*Test"/>
   			</fileset>
			<manifest>
				<attribute name="Main-Class" value="Main"/>
		   </manifest>
  		</jar>
 	</target>
	
    <!-- 以下为运行的target,需要指定main的类名 -->
	<target name="run_MagicSquare" depends="makejar">
		<java classname="P1.MagicSquare" classpath="build/lib/main.jar">
			<classpath refid="classpath"/>
		</java>
	</target>

	<target name="run_TurtleSoup" depends="makejar">
		<java classname="P2.turtle.TurtleSoup" classpath="build/lib/main.jar" fork="true">
			<classpath refid="classpath"/>
		</java>
	</target>

	<target name="run_FriendshipGraph" depends="makejar">
		<java classname="P3.FriendshipGraph" classpath="build/lib/main.jar">
			<classpath refid="classpath"/>
		</java>
	</target>

	<target name="run_tweet" depends="makejar">
		<java classname="P4.twitter.Main" classpath="build/lib/main.jar" fork="true">
			<classpath refid="classpath"/>
			<jvmarg value="-ea"/>
		</java>
	</target>

	<target name="run_all" depends="run_MagicSquare, run_FriendshipGraph, run_tweet">
	</target>

	<target name="build" depends="run_all, runtests">
	</target>

</project>

通过Ant运行junit测试时,在classpath中除了要有junit.jar以外,还需要hamcrest-core-1.3.jar这个jar包,否则会出现错误。

将以上配置文件放在项目的根目录后,在终端中cd进根目录,运行ant + target名来执行某个target,如ant build,如果直接使用ant,则会执行project标签下的default属性指定的target。

使用以上配置文件的项目,会将junit报告输出在build/report中,报告以xml的形式呈现,在html文件夹中也可以找到网页版报告。

Gradle

Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化构建开源工具。它使用一种基于Groovy的特定领域语言(DSL)来声明项目设置,抛弃了基于XML的各种繁琐配置。面向Java应用为主。

简而言之,gradle很简单、很简单、很简单。

gradle的配置文件为build.gradle,也是放置于项目的根目录下。Gradle可以在https://gradle.org 下载。

一个典型的gradle的项目的目录结构如下,如果项目使用该目录结构则不需要特殊配置:

.
└── src							
    ├── main			------源码目录
    |   ├──java
    |   └──resource
    └── test			------测试目录
        ├──java
        └──resource

最大的问题还是目录结构的不符,需要在配置文件中重新指定各个目录的位置,还有个问题就是依赖的引入了,gradle默认从maven中央仓库获取依赖包,需要配置成从本地文件夹获取,以下就要展现gradle的极简配置了:

apply plugin: 'java'	//必选,项目类型

//设置source目录结构
sourceSets {
    main {
        java.srcDirs = ['src']
        resources.srcDirs = ['src']
    }
    test {
        java.srcDirs = ['test']
        resources.srcDirs = ['test']
    }
}

//依赖文件来源
dependencies {
    compile fileTree(dir: 'lib', includes: ['*.jar'])
}

没错,就这么点儿!在根目录运行gradle build即可。

GuodeMacBook-Air:Lab2-1170300520 guoziyang$ gradle build
Starting a Gradle Daemon (subsequent builds will be faster)

BUILD SUCCESSFUL in 15s
5 actionable tasks: 5 executed

测试报告可以在build/reports/tests下查看。

Travis-CI

Travis CI是目前新兴的开源持续集成构建项目,其中CI代表持续集成(continuous integration)。Travis-CI可以与你的Github账号绑定,监控Github中仓库的状态,一旦有新的commit,就会试图构建该项目。

Travis-CI的构建本质上是基于Apache Ant、Apache Maven和Gradle的,取决于你的项目使用的项目管理工具。在一个commit到来时,Travis-CI会自动开启一台虚拟机,并在虚拟机上执行事先设置好的命令来构建。所以,Travis-CI本质上不是一个构建工具,只是一个帮你在线使用其它工具构建的工具(有点拗口)。

使用Travis-CI,首先需要将Github账号与Travis-CI绑定。如果你只需要构建公有项目,那么在https://travis-ci.org ,若要构建私有项目,则需要在https://travis-ci.com 绑定。

绑定完成后,Travis-CI就开始监控你每个项目的每一个commit。Travis-CI通过识别项目根目录下的.travis.yml来构建项目。这又是一个需要自己写的配置文件了……

以下以一个gradle项目为例。gradle构建项目的命令为gradle build。.travis.yml文件如下:

language: java
jdk: oraclejdk11
script: gradle build

很简单。该配置文件指定了项目的语言(java)、jdk的版本(oraclejdk11)以及自动构建的命令(gradle build)。将项目push到github后,即可自动开始构建。

如果你的项目是基于jdk8的,那么可以不用配置jdk属性,然而如果使用的不是jdk8,而你希望使用你的版本来编译,就需要书写jdk属性。

然而,如果你和我一样使用jdk11编译,就可能会遇到编译失败的情况,报错信息大致是gradle无法识别jdk11。根本原因在于,travis-ci默认使用的gradle版本较老,不支持最新的jdk,解决办法如下。

在build.gradle的最后加入:

//用于travis-ci的gradle版本同步任务
task update(type: Wrapper) {
    gradleVersion = '5.3'
}

接着在项目根目录下执行gradle update,会生成gradlew和gradlew.bat,并修改.travis.yml中的script为./gradlew即可解决。

使用Travis-CI的一个好处是,可以生成一个小徽章,实时标识了当前项目的构建状态,如build passing或者build fail

然而,Travis-CI的能力远大于此,例如,我的博客就是由Travis-CI自动生成后推送的,不再赘述。

事实上,除了Travis-CI以外,还有一个比较有名的开源在线CI项目——Jenkins。这个项目支持面更加广泛,而且不用花钱(Travis CI构建私有项目时需要会员)……要是我们学校什么时候可以搭建一个就好了,然而学校的垃圾服务器还是算了……(小声BB)

总结

其实没啥好总结的,毕竟我们也不知道老师会用啥构建我们的项目……但是不得不说,gradle简洁的配置还是惊艳到我了,墙裂推荐。

猜你喜欢

转载自blog.csdn.net/qq_40856284/article/details/106499194