一、理论篇:
持续集成鼓励尽量短周期内项目团队的代码提交,同时保证每次check in都不会损害我们的构建通过。它跟每日构建的区别就在于代码提交频率更高(一般为一个小时),构建的频率也更高,这样做的目的就是为了快速反馈,使得BUG越早被发现,并能以邮件或者消息(甚至短信)的形式快速反馈给开发人员,从而快速解决问题,并保证构建成功。
二、工具篇:
持续集成重在COC(Conversion Over Configuration:约定由于配置),这样选择合适的支持持续集成的工具就相当重要。庆幸的是我们有许多开源的选择,但是首先我们需要了解持续集成的实现架构:
从上图中我们看到,客户端提交代码更改到源代码仓库,CI服务器会检测到代码库的修改,它会检出代码,在本地构建,构建成功,会将构建结果反馈回客户端,同时可能将构建的可运行代码发布到WEB服务器上。
所以,我们就需要各个节点的工具支持:
对于SCM工具,我们的选择的开源工具有CVS、SVN等,这也没有特殊的取舍,就自己的爱好和公司的已有平台而定。我们这里假设使用SVN作为版本管理工具,它的中文站是:http://www.subversion.org.cn/ ;
对于构建工具,我们的选择的开源工具有Ant和Maven等,Ant通过一些内置的和扩展的Task来实现包括文件操作、编译、测试、代码检查、打包等操作,Eclipse默认提供了对Ant的支持。通过在build.xml中配置一系列相互依赖的任务来实现我们定义的构建过程。Maven是一个以项目为模型的构建,项目管理工具,注意它并不是为了替代Ant(同时支持运行Ant脚本),而是以另一种视觉提供了对软件生命周期的管理,它通过插件的方式提供了类似于Ant任务的功能,它的特色之处在于对项目依赖组件的统一管理,同时它的生成站点功能也是一个不错的特性,具体不再赘述,后面的构建我们会分别用Ant和Maven来说明。
对于持续集成工具,我们的选择的开源工具有CruiseControl(后面简称CC)和Hudson。BuildLoop是CC的核心, 这个BuildLoop包含插件支持,详细介绍可以参照附件中的电子书。CC的实施结构如下图所示:
为了完成上面的结构,CC提供的插件按照下图所示的流程完成构建:
通过对CC的cruisecontrol项目的配置,支持图形化显示checkstyle, pmd, findbugs, cobertura, javadoc等报告。同时CC-Config也提供了对项目的图形化配置,比较方便。
Hudson也提供了持续集成服务器的大多数功能,详细参考官方站点:https://hudson.dev.java.net/
三、实践篇:
我们模拟了两个项目,一个AntBasedCI是基于Ant构建的客户端应用程序,一个MavenBasedCI是基于Maven构建的Web应用程序,我们的SCM由SVN自带的Server提供,启动这个服务,我们需要在命令行运行: svnserve -d -r D:\repos 其中-r指定repository磁盘位置。
CC的项目配置:
<cruisecontrol> <project requiremodification="false" forceonly="false" name="MavenBasedCI"> <modificationset QuietPeriod="30"> <svn LocalWorkingCopy="${checkout.dir}\${project.name}" CheckExternals="false" UseLocalRevision="false" /> </modificationset> <schedule Interval="300"> <maven2 Goal="-e clean site install" MvnHome="D:\OpenSource\maven-2.0.4" PomFile="${checkout.dir}/${project.name}/pom.xml" ShowProgress="false" /> </schedule> <bootstrappers> <svnbootstrapper LocalWorkingCopy="${checkout.dir}\${project.name}" /> </bootstrappers> <listeners> <currentbuildstatuslistener File="${logs.dir}\${project.name}\status.txt" /> </listeners> <log> <merge Dir="${logs.dir}\${project.name}" /> <merge Dir="${checkout.dir}\${project.name}\target" Pattern="*.xml" /> </log> <publishers> <onsuccess> <artifactspublisher Dest="${artifact.dir}\${project.name}" File="${checkout.dir}\${project.name}\target\${project.name}-1.0-SNAPSHOT.war" /> </onsuccess> <artifactspublisher File="${checkout.dir}\${project.name}\target\${project.name}-1.0-SNAPSHOT.war" Dest="artifacts/${project.name}" /> <artifactspublisher Dir="${checkout.dir}\${project.name}\target\site" Dest="artifacts/${project.name}" /> </publishers> <property name="checkout.dir" value="${basedir}\checkout" /> <property name="logs.dir" value="${basedir}\logs" /> <property name="artifact.dir" value="${basedir}\artifacts" /> </project> <project requiremodification="false" forceonly="false" name="AntBasedCI"> <modificationset QuietPeriod="30"> <svn LocalWorkingCopy="${checkout.dir}\${project.name}" CheckExternals="false" UseLocalRevision="false" /> </modificationset> <schedule Interval="300"> <ant AntHome="D:\OpenSource\apache-ant-1.7.1" BuildFile="${checkout.dir}\${project.name}\build.xml" Target="all" /> </schedule> <bootstrappers> <svnbootstrapper LocalWorkingCopy="${checkout.dir}\${project.name}" /> </bootstrappers> <listeners> <currentbuildstatuslistener File="${logs.dir}\${project.name}\status.txt" /> </listeners> <log> <merge Dir="${logs.dir}\${project.name}" /> <merge Dir="${checkout.dir}\${project.name}\target" Pattern="*.xml" /> </log> <publishers> <onsuccess> <artifactspublisher Dest="${artifact.dir}\${project.name}" File="${checkout.dir}\${project.name}\target\${project.name}.jar" /> </onsuccess> <artifactspublisher Dest="artifacts/${project.name}" File="${checkout.dir}\${project.name}\target\${project.name}.jar" /> <artifactspublisher Dir="${checkout.dir}\${project.name}\target" Dest="artifacts/${project.name}" /> </publishers> </project> <dashboard /> <property name="basedir" value="E:\CI\ccworkspace" /> <property name="checkout.dir" value="${basedir}\checkout" /> <property name="logs.dir" value="${basedir}\logs" /> <property name="artifact.dir" value="${basedir}\artifacts" /> </cruisecontrol>
上面的配置是通过CC-Config图形配置自动生成的,包含我们的两个工程。
Ant配置build.xml
<?xml version="1.0" encoding="UTF-8"?> <project name="AntBasedCI" default="all"> <property name="default.target.dir" value="target" /> <property name="classes.dir" value="${default.target.dir}/classes" /> <property name="test.classes.dir" value="${default.target.dir}/test-classes" /> <property name="test.report.dir" value="${default.target.dir}/test-reports" /> <property name="lib.dir" value="${basedir}/lib" /> <property name="javadoc.dir" value="${default.target.dir}/apidocs" /> <property name="source.dir" value="src" /> <property name="test.source.dir" value="test" /> <property name="test.pattern" value="**/**Test.java" /> <!-- Coverage reports are deposited into these directories --> <property name="cobertura.dir" value="${default.target.dir}/cobertura"/> <!-- Instrumented classes are deposited into this directory --> <property name="instrumented.dir" value="instrumented" /> <path id="cobertura.classpath"> <fileset dir="${lib.dir}"> <include name="*.jar" /> </fileset> </path> <taskdef classpathref="cobertura.classpath" resource="tasks.properties"/> <target name="clean"> <delete dir="${classes.dir}"/> <delete dir="${test.classes.dir}"/> <delete dir="${default.target.dir}"/> </target> <target name="init" depends="clean"> <mkdir dir="${classes.dir}" /> <mkdir dir="${test.classes.dir}" /> <mkdir dir="${javadoc.dir}" /> <mkdir dir="${default.target.dir}"/> <mkdir dir="${instrumented.dir}"/> <path id="build.classpath"> <fileset dir="${lib.dir}"> <include name="**/*.jar" /> </fileset> <fileset dir="${default.target.dir}"> <include name="**/*.jar" /> </fileset> </path> </target> <target name="compile-source" depends="init" description="compiles all .java files in source directory "> <javac destdir="${classes.dir}" srcdir="${source.dir}" classpathref="build.classpath" /> </target> <target name="instrument" depends="compile-source"> <delete file="cobertura.ser"/> <delete dir="${instrumented.dir}" /> <!--Instrument the application classes, writing the instrumented classes into ${build.instrumented.dir}.--> <cobertura-instrument todir="${instrumented.dir}"> <ignore regex="org.apache.log4j.*" /> <fileset dir="${classes.dir}"> <!-- Instrument all the application classes, but don't instrument the test classes.--> <include name="**/*.class" /> <exclude name="**/*Test.class" /> </fileset> </cobertura-instrument> </target> <target name="jar" depends="instrument"> <jar jarfile="${default.target.dir}/${ant.project.name}.jar" basedir="${classes.dir}" /> </target> <target name="compile-tests" depends="jar" description="compiles all .java files in test directory "> <javac destdir="${test.classes.dir}" srcdir="${test.source.dir}" classpathref="build.classpath" /> </target> <target name="javadoc" depends="init"> <javadoc author="true" charset="gbk" classpathref="build.classpath" destdir="${javadoc.dir}" version="true" use="true" sourcepath="${source.dir}"></javadoc> </target> <target name="test" depends="compile-tests" description="runs JUnit tests"> <mkdir dir="${test.report.dir}" /> <junit dir="${basedir}" printSummary="on" fork="true" haltonfailure="true"> <sysproperty key="basedir" value="${basedir}" /> <formatter type="xml" /> <classpath> <path refid="build.classpath" /> <pathelement path="${test.classes.dir}" /> <pathelement path="${classes.dir}" /> </classpath> <batchtest todir="${test.report.dir}"> <fileset dir="${test.source.dir}"> <include name="${test.pattern}" /> </fileset> </batchtest> </junit> </target> <target name="coverage-check"> <cobertura-check branchrate="40" totallinerate="100" /> </target> <target name="coverage-report"> <cobertura-report srcdir="${source.dir}" destdir="${cobertura.dir}" format="html" /> </target> <target name="alternate-coverage-report"> <!-- Generate a series of HTML files containing the coverage data in a user-readable form using nested source filesets. --> <cobertura-report destdir="${cobertura.dir}"> <fileset dir="${source.dir}"> <include name="**/*.java"/> </fileset> </cobertura-report> </target> <target name="coverage" depends="jar,instrument,test,coverage-report,alternate-coverage-report"/> <target name="pmd" depends="test"> <taskdef name="pmd" classname="net.sourceforge.pmd.ant.PMDTask" classpathref="build.classpath"/> <pmd> <ruleset>rulesets/basic.xml</ruleset> <ruleset>rulesets/braces.xml</ruleset> <ruleset>rulesets/javabeans.xml</ruleset> <ruleset>rulesets/unusedcode.xml</ruleset> <ruleset>rulesets/strings.xml</ruleset> <ruleset>rulesets/design.xml</ruleset> <ruleset>rulesets/coupling.xml</ruleset> <ruleset>rulesets/codesize.xml</ruleset> <ruleset>rulesets/imports.xml</ruleset> <ruleset>rulesets/naming.xml</ruleset> <formatter type="xml" toFile="${default.target.dir}/pmd_report.xml" /> <fileset dir="${source.dir}"> <include name="**/*.java" /> </fileset> </pmd> </target> <target name="findbugs" depends="jar"> <taskdef name="findbugs" classname="edu.umd.cs.findbugs.anttask.FindBugsTask" classpathref="build.classpath" /> <findbugs classpathref="build.classpath" pluginlist="${lib.dir}/coreplugin-1.0.jar" output="xml" outputFile="${default.target.dir}/findbugs.xml"> <sourcePath path="${source.dir}" /> <class location="${default.target.dir}/${ant.project.name}.jar" /> </findbugs> </target> <target name="all" depends="coverage,pmd,findbugs,javadoc" /> </project>
从上面的配置我们看到这个构建包括:编译、测试、测试覆盖率统计、代码检查、BUG查找、生成Javadoc和打包。
Maven的配置pom.xml
<?xml version="1.0" encoding="GBK"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.samueli.webapp</groupId> <artifactId>MavenBasedCI</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>MavenBasedCI</name> <description>持续集成测试项目</description> <url>http://www.samueli.com</url> <licenses> <license> <name>Fake License</name> <url>http://127.0.0.1:8080/dc-license.txt</url> <distribution>repo</distribution> </license> </licenses> <!-- 问题管理 --> <issueManagement> <system>jira</system> <url>http://169.254.11.166:8088/secure/Dashboard.jspa</url> </issueManagement> <!-- 源代码管理 --> <scm> <connection>scm:svn://127.0.0.1/repos/MavenBasedCI</connection> <developerConnection> scm:svn://127.0.0.1/repos/MavenBasedCI </developerConnection> <tag>head</tag> <url>svn://127.0.0.1/repos/MavenBasedCI</url> </scm> <!-- 开发人员以及角色定义 --> <developers> <developer> <name>aaa</name> <id>aaa</id> <email>aaa@samueli.com</email> <organization>samueli</organization> <roles> <role>PM</role> </roles> </developer> <developer> <name>bbb</name> <id>bbb</id> <email>bbb@samueli.com</email> <organization>samueli</organization> <roles> <role>设计人员</role> <role>开发人员</role> </roles> </developer> </developers> <!-- 依赖定义 --> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.3</version> </dependency> </dependencies> <!--依赖版本管理--> <dependencyManagement /> <organization> <name>XXX有限责任公司</name> <url>http://www.samueli.com</url> </organization> <!--项目模块管理--> <modules /> <build> <plugins> <plugin> <artifactId>maven-site-plugin</artifactId> <configuration> <locales>zh_CN</locales> <outputEncoding>GBK</outputEncoding> </configuration> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> <encoding>GBK</encoding> </configuration> </plugin> <plugin> <artifactId>maven-checkstyle-plugin</artifactId> <configuration> <outputEncoding>GBK</outputEncoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-pmd-plugin</artifactId> <executions> <execution> <goals> <goal>check</goal> </goals> </execution> </executions> <configuration> <linkXref>true</linkXref> <sourceEncoding>GBK</sourceEncoding> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>cobertura-maven-plugin</artifactId> <version>2.0</version> <configuration> <instrumentation> <excludes> <exclude> com/samueli/MavenBasedCI/util/*.class </exclude> </excludes> </instrumentation> <check> <haltOnFailure>true</haltOnFailure> <totalLineRate>40</totalLineRate> <totalBranchRate>100</totalBranchRate> </check> <outputEncoding>GBK</outputEncoding> </configuration> <executions> <execution> <id>clean</id> <goals> <goal>clean</goal> </goals> </execution> <execution> <id>check</id> <goals> <goal>check</goal> </goals> </execution> </executions> </plugin> </plugins> <extensions> <extension> <groupId>org.apache.maven.wagon</groupId> <artifactId>wagon-ftp</artifactId> <version>1.0-alpha-6</version> </extension> </extensions> </build> <distributionManagement> <repository> <id>ftp-repository</id> <url>ftp://192.168.3.241</url> </repository> <site> <id>lmss.site</id> <name>MavenBasedCI</name> <url>file:///Y:\</url> </site> </distributionManagement> <!--各种报告--> <reporting> <plugins> <plugin> <artifactId>maven-site-plugin</artifactId> <configuration> <locales>zh_CN</locales> <outputEncoding>GBK</outputEncoding> </configuration> </plugin> <plugin> <artifactId>maven-surefire-report-plugin</artifactId> <configuration> <showSuccess>false</showSuccess> <outputEncoding>GBK</outputEncoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jxr-plugin</artifactId> <configuration> <outputEncoding>GBK</outputEncoding> <inputEncoding>GBK</inputEncoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-javadoc-plugin</artifactId> <configuration> <aggregate>true</aggregate> <charset>UTF16</charset> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-pmd-plugin</artifactId> <configuration> <rulesets> <ruleset>/rulesets/basic.xml</ruleset> <ruleset>/rulesets/imports.xml</ruleset> <ruleset>/rulesets/unusedcode.xml</ruleset> <ruleset>/rulesets/finalizers.xml</ruleset> </rulesets> <outputEncoding>GBK</outputEncoding> <linkXref>true</linkXref> <excludes> <exclude> com/samueli/MavenBasedCI/util/*.java </exclude> </excludes> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-checkstyle-plugin</artifactId> <configuration> <configLocation>lmss_checks.xml</configLocation> <outputEncoding>GBK</outputEncoding> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>taglist-maven-plugin</artifactId> <configuration> <tags> <tag>TODO</tag> <tag>@todo</tag> <tag>FIXME</tag> <tag>XXX</tag> </tags> <outputEncoding>GBK</outputEncoding> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>cobertura-maven-plugin</artifactId> <version>2.0</version> <configuration> <outputEncoding>GBK</outputEncoding> </configuration> </plugin> <!-- <plugin>--> <!-- <groupId>org.codehaus.mojo</groupId>--> <!-- <artifactId>changes-maven-plugin</artifactId>--> <!-- </plugin>--> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>changelog-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>findbugs-maven-plugin</artifactId> <configuration> <xmlOutput>true</xmlOutput> </configuration> </plugin> </plugins> </reporting> </project>
Maven详细的站点生成可以参考这里:http://www.duduwolf.com/wiki/2008/766.html
Maven生成的站点例子:
CC生成的集成报告截图如下:
四、总结篇:
通过上面的简单介绍,我们基本掌握了持续集成的目的和基本理论,在Martin Fowler的文章中提到了一些最佳实践也值得参考。当然持续集成是一个在实践中不断发展和完善的过程,对于一个团队而言,引入持续集成对于提高开发效率和规范开发过程是必需的,不过在整个持续集成中,我们信赖的依据就是构建,其中的单元测试可靠性就会有一定的要求,这样对于我们开发人员,如何保证写出高质量的单元测试便是一个挑战,TDD是一个不错的实践,它完全从需求出发,逐步完善测试用例,不断减少与需求的偏差来尽量满足需求。同时引入测试覆盖率也利于我们审查我们的单元测试。CC提供的统一出口的各种报告和图表,可以更加直观和快捷的从整体上把握我们代码在构建中表现出来的健壮性(代码检查)和满足需求性(单元测试通过率、测试覆盖率),同时对于出现的问题,能够责任到人,快速反馈也是很有利于问题的解决,对于持续集成的学习刚刚开始,错误偏颇在所难免,越是深入的学习,越会有更多的感悟和思考。
五、参考:
1、Martin Fowler的文章
原文:http://martinfowler.com/articles/continuousIntegration.html
翻译:http://dev.csdn.net/develop/article/12/12286.shtm
2、Juven的一篇原创文章
http://juvenshun.spaces.live.com/blog/cns!CF7D1BC903C111E1!284.entry
3、IBM DW上的一篇教程,以Hudson为例
https://www6.software.ibm.com/developerworks/cn/education/java/j-cq11207/index.html
4、满江红开源上提供的CruiseControl教程下载
http://www.xiaxin.net/blog/OpenDoc-CruiseControl.zip
5、IBM DW上的另外一篇文章讲解如何实现持续集成
http://www.ibm.com/developerworks/cn/rational/rationaledge/content/nov05/lee/