2005年8月22日

Web應用程式自動部署作業-使用 Tomcat 及 Ant ( Part II )

記得在去年在 Web應用程式自動部署作業-使用 Tomcat 及 Ant 一文中介紹過如何透過 Ant 完成 Tomcat 上的 Web 應用程式自動部署作業。過了一年後相關的設定也有了修正,且目前因為計畫將部署程序移轉到 Maven 上,所以先在此將作法登錄下來,算是一個歷程的完結。

第二版的部署作業最大的改變就是將原本那個超大的 build.xml 檔予以解散,並將系統的、專案專屬的設定項目都切割開來,目的當然在簡化每一個設定檔的大小。以下從目錄結構開始介紹起:

網頁應用系統標準目錄結構

現行所有的網頁應用程式目錄結構係依據 Application Developer's Guide: Deployment (也可參考 Web Application Archives) 文中所描述基本結構,配合後續單一檔案維護要求而設定。目錄結構如下:
網頁應用系統標準目錄結構
WEB-ROOT       網站根目錄,目錄中會包含 Ant 建置主檔
├─build        建置過程中之暫存目錄
├─build.ant    Ant 建置主檔附屬支援目錄
├─conf         網站參考設定檔
├─css          應用系統目錄:依各專案不同而有增減
├─distribute   建置完成輸出目錄
├─images       應用系統目錄:依各專案不同而有增減
├─src          原始程式檔:主要為該專案之 servlet、javabean 等原始檔存放目錄
├─war.backup   遠端程式備份目錄:儲存遠端伺服器上原本運作中之網頁應用系統壓縮包
└─WEB-INF      Web Application Archives 標準目錄
    └─lib      該應用系統有關之類別庫一律置放於此目錄下

ANT 部署說明

所有的部署動作由 WEB-ROOT 下的 build.xml 開始,透過 build.ant/ 目錄下的輔助設定及類似 Ant 副程式的機制,完成自動化部署作業。而在系統建置過程中將參考 conf/ 目錄下之內容以建構出完整的網頁應用系統部署套件。

如何使用 build.xml 部署建置檔

常在取得一份 .war 檔後,先使用 7-zip 等檔案解壓縮工具將檔案解壓到專案目錄中,此例為取得一個 QuerySalary.war 檔案。附圖是這個壓縮檔案的內容。(註:war 檔的本質是 Zip 格式)
取得 war 壓縮檔
解壓縮 .war 檔後,即可從命令列中檢查該專案所附的 build.xml 設定檔提供那些功能,請切換到專案所在目錄中並鍵入以下指令:
檢查 build.xml 提供功能
ant -projecthelp
以下是 QuerySalary 專案的 ant -projecthelp
CHECK_PROJECT_HELP
另一個專案的 ant -projecthelp
CHECK_PROJECT_HELP_OTHER
通常來說要部署一個專案時只要鍵入以下指令即可!當然請先確定該建置檔是否提供相對應之部署功能。
各階段部署應用系統的方式
# 部署至正式主機
ant production

# 部署至開發用主機 ant dev

# 部署至使用者測試主機 ant uat

下圖為某次部署作業之畫面擷圖:
某次部署作業之畫面擷圖
  • 每個專案總會有些差異,這類的差異由 build.ant/ 目錄下的檔案進行調整,稍候介紹。

build.xml 內容說明

以下為 build.xml 之基本設定:
build.xml
<?xml version="1.0" encoding="Big5"?>
<!-- 專案名稱,識別用 -->
<project name="Forms">

<!-- 初始設定區段 --> <target description="初始設定作業" name="init"> <!-- 設定系統環境變數之識別碼 --> <property environment="env"/> <!-- 載入基本屬性設定檔 --> <property file="${basedir}/build.ant/build.properties"/> <!-- 載入資料庫屬性設定檔 --> <property file="${basedir}/build.ant/database.properties"/> <!-- 載入部署作業屬性設定檔 --> <property file="${basedir}/build.ant/deploy.properties"/> <!-- 開始建置時間 --> <tstamp> <format pattern="yyyy-MM-dd HH:mm:ss" property="build.time"/> </tstamp> <!-- 建立 CLASSPATH 設定 --> <path id="build.classpath"> <fileset dir="${lib.path}"> <include name="**/*.jar"/> </fileset> </path> <!-- Configure the custom Ant tasks for the Tomcat 5.0 Manager application --> <taskdef name="deploy" classname="org.apache.catalina.ant.DeployTask"/> <taskdef name="list" classname="org.apache.catalina.ant.ListTask"/> <taskdef name="reload" classname="org.apache.catalina.ant.ReloadTask"/> <taskdef name="resources" classname="org.apache.catalina.ant.ResourcesTask"/> <taskdef name="roles" classname="org.apache.catalina.ant.RolesTask"/> <taskdef name="start" classname="org.apache.catalina.ant.StartTask"/> <taskdef name="stop" classname="org.apache.catalina.ant.StopTask"/> <taskdef name="undeploy" classname="org.apache.catalina.ant.UndeployTask"/> </target>

<!-- 顯示 ant 目前的 properties 設定值 --> <target depends="init" name="echoProperties" description="顯示 ant 當前 properties 設定值" > <echoproperties/> </target>

<target depends="init" description="清除輸出目錄" name="clean"> <!-- 刪掉打包過程暫存目錄 --> <delete dir="${build.path}" quiet="true"/> <!-- 刪掉最終輸出檔案目錄 --> <delete dir="${target.path}" quiet="true"/> <!-- 刪掉所有名為 .bak 之檔案 --> <delete quiet="true"> <fileset dir="${basedir}"> <include name="**/*.bak"/> </fileset> </delete> </target>

<!-- 建立系統所需環境(目錄及檔案) --> <target depends="clean" name="prepare-system"> <ant antfile="${basedir}/build.ant/system.xml" inheritrefs="true"/> </target>

<!-- 建立專案所需環境(目錄及檔案) --> <target depends="prepare-system" name="prepare-project"> <ant antfile="${basedir}/build.ant/project.xml" inheritrefs="true"/> </target>

<!-- 測試是否有 struts.xml ant 設定檔,目前未使用 --> <target depends="prepare-project" name="test-struts"> <available property="struts.xml.present" file="${basedir}/build.ant/struts.xml" type="file" /> </target>

<!-- 建立專案所需環境(目錄及檔案) --> <target name="prepare-struts" depends="test-struts" if="struts.xml.present" > <ant antfile="${basedir}/build.ant/struts.xml" inheritrefs="true" /> </target>

<!-- 編譯 java 原始程式(包含 servlet 及一般類別庫)--> <target name="compile" depends="prepare-struts" description="編譯 java 原始程式" > <javac deprecation="${deprecation}" destdir="${classes.path}" srcdir="${java.source.path}" debug="on" debuglevel="lines"> <classpath refid="build.classpath"/> </javac> </target>

<!-- 建立 javadoc 文件,僅限於已設定 JAVADOC 環境變數時才會啟動 --> <target if="env.JAVADOC" name="javadoc" depends="compile" description="建立 JavaDoc 文件,只在已設定環境變數 JAVADOC 時執行此作業" > <ant antfile="${basedir}/build.ant/javadoc.xml" inheritrefs="true" /> </target>

<!-- 開發測試環境中需要準備的動作 --> <target depends="javadoc" name="dev-prepare"> <!-- 開發環境中所需的 log4j 相關設定資料 --> <property name="log4j.properties.path" value="${dev.deploy.webroot}"/> <property name="logfile.path" value="${dev.deploy.webroot}"/> <!-- 呼叫 genConfig task 以便建立 web.xml 檔案 --> <antcall target="genConfig" inheritAll="true"/> </target>

<!-- 使用者驗收測試環境中需要準備的動作 --> <target depends="compile" name="uat-prepare"> <!-- 使用者驗收測試環境中所需的 log4j 相關設定資料 --> <property name="log4j.properties.path" value="${uat.deploy.webroot}"/> <property name="logfile.path" value="${uat.deploy.webroot}"/> <!-- 呼叫 genConfig task 以便建立 web.xml 檔案 --> <antcall target="genConfig" inheritAll="true"/> </target>

<!-- 正式環境中所需要先行準備的動作 --> <target depends="compile" name="production-prepare"> <!-- 開發環境中所需的 log4j 相關設定資料 --> <property name="log4j.properties.path" value="${production.deploy.webroot}"/> <property name="logfile.path" value="${production.deploy.webroot}"/> <!-- 呼叫 genConfig task 以便建立 web.xml 檔案 --> <antcall target="genConfig" inheritAll="true"/> </target>

<!-- 初始化 XDoclet 設定 --> <target name="genWeb.xml"> <path id="xdoclet.classpath"> <!-- xdoclet 類別庫路徑在 build.properties 檔中自行捉取環境變數設定 --> <fileset dir="${xdoclet.lib.path}"> <include name="*.jar"/> </fileset> <pathelement location="${classes.path}"/> </path> <taskdef name="webdoclet" classname="xdoclet.modules.web.WebDocletTask" classpathref="xdoclet.classpath"/> <!-- Generate servlet and JSP Tag "stuff" --> <webdoclet destDir="${basedir}/WEB-INF" mergeDir="${basedir}/conf" force="true" verbose="false"> <fileset dir="${java.source.path}"> <include name="**/*.java" /> </fileset> <deploymentdescriptor distributable="true" displayname="${application.name}" description="${application.description}" > <!-- 要特別傳給 xDoclet 用的設定變數 --> <configParam name="application-name" value="${application.name}"/> <configParam name="application-description" value="${application.description}"/> <configParam name="log4j-init-file" value="${log4j.properties.path}/${log4j.properties.file}"/> </deploymentdescriptor> </webdoclet> </target>

<!-- 建立網頁設定檔 - web.xml --> <target name="genConfig" depends="genWeb.xml"> <!-- 自動調整 log4j.properties 中的 log 檔檔名 --> <replaceregexp byline="true" file="${target.path}/${log4j.properties.file}" match="log4j.appender.FileLog.file=default.exception.log" replace="log4j.appender.FileLog.file=${logfile.path}/${log4j.exception.file}"/>

<!-- 調整的變數內容 --> <echo>LogFile.Path: ${logfile.path}</echo> <echo>LogFile.Name: ${log4j.properties.file}</echo> <echo>${target.path}/${log4j.exception.file}</echo>

<!-- 用於修改 datasource 設定之內容 --> <!-- <replaceregexp byline="true" file="${web.xml}" match="${DATASOURCE}" replace="${data.source}"/> --> </target>

<!-- 將編譯出來的 .class 予以打包 --> <target depends="compile" name="package" description="將編譯出的 .class 檔壓製成單一 .jar 檔,放入 web root 下的 WEB-INF/lib 目錄中"> <jar basedir="${classes.path}" jarfile="${jar.name}"/> <delete dir="${classes.path}" quiet="true"/> </target>

<!-- 將整個WEB ROOT目錄打包成.war部署檔,此部署檔已完整包含開發所需各原始程式。 --> <target depends="package" name="build"> <war warfile="${war.name}" webxml="${web.xml}"> <fileset dir="${build.path}" excludes="**/*.jar"/> <lib dir="${lib.path}"/> </war> </target>

<!-- DEV,通常會採用 JSP Container 的自動部署功能,因此使用 FTP 上傳方式處理 --> <target name="dev" if="dev.deploy.server" depends="dev-prepare, build" description="部署 DEV 測試環境" > <echo>Sending file(s) to ${dev.deploy.server}...</echo> <ftp binary="yes" depends="yes" password="${dev.deploy.password}" server="${dev.deploy.server}" userid="${dev.deploy.user}"> <fileset dir="${target.path}"/> </ftp> </target>

<!-- UAT,通常與 DEV 同一台主機,因此也採 FTP 部署方式處理 --> <target name="uat" if="uat.deploy.server" depends="uat-prepare, build" description="部署 UAT 測試環境" > <echo>Sending file(s) to ${uat.ftp.server}...</echo> <ftp binary="yes" depends="yes" password="${uat.deploy.password}" server="${uat.deploy.server}" userid="${uat.deploy.user}"> <fileset dir="${output.path}"/> </ftp> </target>

<!-- Production, --> <!-- 若使用 JRun 4 時必須採用 FTP 上傳方式,但目前多數已改用 Tomcat Application Server, 可在關閉自動部署(校能考量)後,透過 Tomcat 內建的管理功能進行部署作業! --> <!-- catalina-ant.jar --> <target name="production" if="production.deploy.server.1" depends="production-prepare, build" description="部署 production 正式環境" > <echo> Deploying ${application.name}.war to ${production.deploy.server.1}… </echo> <!-- 移除原指定路徑上之應用系統,應用系統名稱為 deploy.properties 中所定義 --> <undeploy url="http://${production.deploy.server.1}/manager" username="${production.tomcat.user.1}" password="${production.tomcat.password.1}" path="/${application.name}"/> <!-- 傳送檔案 ( log4j.properties 設定檔 ) --> <echo> Sending ${log4j.properties.file} to ${production.deploy.server.1}… </echo> <ftp binary="yes" depends="yes" server="${production.deploy.server.1}" userid="${production.deploy.user.1}" password="${production.deploy.password.1}"> <!-- 避開 .war 檔 --> <fileset dir="${target.path}" excludes="**/*.war"/> </ftp> <sleep seconds="5" /> <!-- 重新部署 --> <deploy url="http://${production.deploy.server.1}/manager" username="${production.tomcat.user.1}" password="${production.tomcat.password.1}" path="/${application.name}" war="${war.name}"/> <tstamp> <format pattern="yyyy-MM-dd HH:mm:ss" property="complete.time"/> </tstamp> <echo>Deploy complete at ${complete.time}</echo> </target> </project>

build.ant/ 相關配置檔案說明

build.ant/build.properties 基本參數設定

此檔為 Ant 建置過程中的標準參數設定檔,通常使用時直接保留原始設定檔即可,不需另行修改。

build.ant/database.properties 資料庫參數設定

此檔為資料庫連線有關的參數設定檔,通常是在 xdoclet 建置 web.xml 配置檔時修改相關參數時使用。目前因採用 Servlet Containner 提供的 Connection Pool 或其他原因之故並未真正使用本設定檔。

build.ant/deploy.properties 部署參數設定

此檔為專案部署有關之參數設定檔。內容及說明如下:
build.ant/deploy.properties 內容說明
# 定義應用程式名稱, 請用英文,且 .war 壓縮包將以此為名
application.name=QuerySalary
# 定義應用程式名稱, 請用英文
application.description=FITEL QuerySalary System

# log4j 記錄器設定 - 給 log4j init servlet 使用, # 為避免不同專案混淆造成混亂,直接以專案名稱設定 log4j.properties.file=${application.name}-log4j.properties # 此檔案會放在指定之 ${deploy.webroot}/ 下 log4j.exception.file=${application.name}-exception.log

# 正式環境之 FTP 部署設定 # 若有多台時以 .2, .3, … 方式登記,但系統的 webroot 應統一! production.deploy.server.1=production.server production.deploy.user.1=production.user production.deploy.password.1=production.password # webroot 只有一組 production.deploy.webroot=C:/JRun4/servers/EHR # 此為 Tomcat 管理界面使用,JRun 不使用此法 production.tomcat.user.1= production.tomcat.password.1=

# 開發人員環境 FTP 部署設定 # 無測試主機,故設定與 正式環境 相同! dev.deploy.server=${production.deploy.server.1} dev.deploy.user=${production.deploy.user.1} dev.deploy.password=${production.deploy.password.1} dev.deploy.webroot=${production.deploy.webroot} # 此為 Tomcat 管理界面使用,JRun 不使用此法 dev.tomcat.user=${production.tomcat.user} dev.tomcat.password=${production.tomcat.password}

# 使用者驗收環境 FTP 部署設定 # 無測試主機,故設定與 開發環境 相同! uat.deploy.server=${dev.deploy.server} uat.deploy.user=${dev.deploy.user} uat.deploy.password=${dev.deploy.password} uat.deploy.webroot=${dev.deploy.webroot} # 此為 Tomcat 管理界面使用,JRun 不使用此法 uat.tomcat.user=${dev.tomcat.user} uat.tomcat.password=${dev.tomcat.password}

若有兩台以上正式主機時,除了在 build.ant/deploy.properties 中設定 production.deploy.server.2 等等變數外,尚必須連帶修改 build.xml 檔內容如下: (其他 Application Server 亦請比照辦理!)
build.xml
  <!-- Production, JRun 4 -->
  <target depends="production-prepare, build" 
          description="部署 Production 正式環境" 
          name="production" if="production.deploy.server.1">
    <echo>Sending files to ${production.deploy.server.1}...</echo>
    <ftp binary="yes" depends="yes" 
         server="${production.deploy.server.1}" 
         userid="${production.deploy.user.1}"
         password="${production.deploy.password.1}">
      <fileset dir="${distribute.path}"/>
    </ftp>
    <!-- 若有兩台以上主機時,移除以下區段之註解並修改主機編號即可! -->
    <!--
      <echo>Sending files to ${production.deploy.server.2}...</echo>
      <ftp binary="yes" depends="yes" 
           server="${production.deploy.server.2}"
           userid="${production.deploy.user.2}" 
           password="${production.deploy.password.2}">
        <fileset dir="${distribute.path}"/>
      </ftp>
    -->
    <tstamp>
      <format pattern="yyyy-MM-dd HH:mm:ss" property="complete.time"/>
    </tstamp>
    <echo>Deploy complete at ${complete.time}</echo>
  </target>

build.ant/system.xml 系統基礎作業部署設定檔

此檔受 build.xml 呼叫,用於建置網頁應用程式之基本目錄結構。整個網頁應用程式之暫存目錄會放在 WEB-ROOT/build/ (在 build.ant/build.properties 內設定)下,而且通常不做任何修改。

build.ant/project.xml 專案作業部署設定檔

此檔受 build.xml 呼叫,用於配置專案所需目錄或檔案之用。必須視專案之目錄需求進行對應之調整。
build.ant/project.xml
<?xml version="1.0" encoding="Big5"?>
<project name="project.prepare" default="project.all">
  <target description="建立輸出所需相關目錄" name="project.mkdir">
    <!-- 有額外的目錄要納進專案時,請在此處增加之 -->
    <!-- 注意:所有要打包的目錄必須先行建立在 ${build.path} 指定的目錄下 -->
    <mkdir dir="${build.path}/images"/>
    <mkdir dir="${build.path}/css"/>
  </target>

<target description="複製系統檔案" depends="project.mkdir" name="project.copy"> <!-- 若有額外的檔案要納進專案時,請在此處增加之 --> <!-- 注意:所有要打包的檔案/目錄必須複製到 ${build.path} 指定的目錄中 --> <copy todir="${build.path}"> <fileset dir="${basedir}" excludes="**/*.bak" includes="**/*.jsp"/> <fileset dir="${basedir}" excludes="**/*.bak" includes="**/*.html"/> <fileset dir="${basedir}" includes="**/*.xml" excludes="**/build.xml,**/web.xml,**/build.ant/**,**/build/**" /> </copy> <copy todir="${build.path}/images"> <fileset dir="${basedir}/images"/> </copy> <copy todir="${build.path}/css"> <fileset dir="${basedir}/css"/> </copy> </target>

<target description="執行所有動作" depends="project.copy" name="project.all" /> </project>

build.ant/javadoc.xml javadoc 文件建置設定檔

此檔受 build.xml 呼叫,用於建置 src/ 下所有 java 原始碼之 javadoc 文字檔。必須先設定一個環境變數 JAVADOC 時,才能在建置過程中觸發此項作業。
build.ant/javadoc.xml
<?xml version="1.0" encoding="Big5"?>
<project name="javadoc" default="javadoc">
  <target name="javadoc" if="env.JAVADOC" 
          description="建立 JavaDoc 文件,只在已設定環境變數 JAVADOC 時執行此作業">
    <mkdir dir="${javadoc.path}"/>
    <!-- 必須設定欲產生那一個 package 下的文件,如紅色字所標示的內容 -->
    <javadoc author="true" 
             bottom="Copyright by FITEL. Co., 2005" 
             classpath="${classes.path}" 
             classpathref="build.classpath" 
             description="請視需要調整 packagenames 屬性之內容" 
             destdir="${javadoc.path}" 
             doctitle="○○企業 ${ant.project.name} 專案類別庫說明文件" 
             header="&lt;b&gt;FITEL ${ant.project.name} JavaDcos &lt;/b&gt;" 
             packagenames="tw.net.fitel.ehr" 
             protected="true" sourcepath="${java.source.path}" 
             use="true" 
             version="true" 
             windowtitle="○○企業 ${ant.project.name} 專案類別庫說明文件">
      <link href="http://java.sun.com/j2se/1.4.2/docs/api/index.html"/>
      <link href="http://java.sun.com/j2ee/1.4/docs/api/index.html"/>
    </javadoc>
  </target>
</project>
以下是執行情形:
  1. 未設定 JAVADOC 環境變數時:
    NO_JAVADOC
  2. 已設定 JAVADOC 環境變數時:
    SET_JAVADOC

conf/ 目錄說明

conf/commons-logging.properties

conf/ejb-resourcerefs.xml

conf/error-pages.xml

conf/log4j.properties

conf/mime-mappings.xml

conf/servlets.xml

conf/web-settings.xml

conf/welcomefiles.xml