一、理論篇:
持續集成鼓勵儘量短週期內項目團隊的代碼提交,同時保證每次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
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/