Ant 脚本在部署过程中实现系统参数的自动发现

邓威
2023-12-01

在使用 Ant 进行软件包的部署时,经常需要输入一些系统环境参数,例如主机名称、IP 地址,一些服务的配置文件路径等等。在一些重复性的场景中,例如软件开发、测试过程中,每次部署都手工去配置一遍系统参数,无疑是对人力的一种浪费。在本文中,我们将对使用 Ant 自身功能实现自动发现系统参数值的技术进行探讨。

引言

Apache Ant 被广泛使用在 Java 开发的自动化编译、打包与部署过程中。在使用 Ant 进行软件包的部署时,经常需要输入一些系统环境参数,例如主机名称、IP 地址,一些服务的配置文件路径以及一些系统服务的端口等等。这些系统参数的值通常是因所在的环境而异的,因此在不同的环境上使用 Ant 脚本进行部署,也需要手动的输入这些参数值。一般的 Ant 部署脚本都会把需要使用者输入的参数放置到配置 property 文件里面,这样每次用户修改这些配置文件之后再运行 Ant 脚本就可以了。

但是在一些重复性的场景中,例如软件开发、测试过程中,每次部署都手工去配置一遍系统参数,无疑是对人力的一种浪费。在本文中,我们将对使用 Ant 自身功能实现自动发现系统参数值的技术进行探讨。


系统参数的分类

在使用 Ant 脚本进行部署工作的系统环境中,通常被用到的系统参数有如下几类:

  • 操作系统的基本参数

    例如主机名称、域名、系统用户名称与密码等等

  • 应用程序服务器参数

    例如 Web 服务器安装路径、配置文件路径、J2EE 服务器安装路径、应用实例名称、Web 服务端口、数据库服务器安装路径、JDBC 驱动路径等等

  • 应用程序本身参数

    例如需要部署的模块名称、路径、用到的数据库记录等等

在这些参数中,操作系统基本参数会因所使用的服务器而异,例如管理员用户在 Unix/Linux 系统中一般是 root,而在 Windows 系统中一般是 Administrator,主机名称和 IP 地址更是每台服务器都不一样;应用程序服务器参数则会因所在的操作系统不同而有所不同,也会因为安装这些服务器的人员习惯不同而不同,例如有的人习惯修改安装目录到 /usr,而有的喜欢 /opt;至于应用程序本身在重复部署时的参数,倒是基本上不会有太多不同。

那么在我们使用 Ant 脚本进行重复部署的工作时,经常需要手工修改的参数就是操作系统基本参数与应用程序服务器参数。下面是一个配置用的 property 文件例子,我们可以看到,如上面所说,每次我们在不同的服务器、不同的操作系统上进行部署时,这个 property 文件里面的很多参数都需要修改,而且一旦改错或者漏改,就可能导致部署失败。

清单 1. deploy.properties 文件
server.host=test.mycompany.com 
 admin.user=Administrator 
 admin.pass=adminPassword 
 was.home=C:/WebSphere/AppServer 
 ejb.module.list=Project-Server 
 web.module.list=ProjectServicesHTTPInterface 
 app.home=C:/WebSphere/AppHome 
 app.instance.name=demo 
 was.profile.name=${app.instance.name} 
 was.server.name=server1 
 was.conntype=SOAP 
 was.port=9066 
 db.type=db2 
 db.name=demo 
 jdbc.url=jdbc:db2://test.mycompany.com:50000/demo 
 jdbc.driver=com.ibm.db2.jcc.DB2Driver 
 jdbc.driver.path=C:/WebSphere/SQLLIB/java/db2jcc4.jar;\ 
 C:/WebSphere/SQLLIB/java/db2jcc_license_cu.jar 
 datasource.jndi.name=jdbc/App DB2 DataSource ${app.instance.name}

那么,有哪些参数可以通过 Ant 自身自动发现并设置呢?

判断操作系统类型

因为在不同的操作系统下,Ant 能够调用的系统命令是不同的,所以我们需要先使用 Ant 发现自身执行所在环境的操作系统类型。

清单 2. 确定操作系统类型和名称
<!-- 探测当前操作系统 --> 
    <condition property="osFamily" value="windows"> 
        <os family="windows" /> 
    </condition> 
    <condition property="osFamily" value="unix"> 
        <os family="unix" /> 
    </condition> 
    <condition property="osName" value="windows"> 
        <os family="windows" /> 
    </condition> 
    <condition property="osName" value="aix"> 
        <os name="AIX" /> 
    </condition> 
    <condition property="osName" value="linux"> 
        <os name="Linux" /> 
    </condition> 
    <condition property="osName" value="sunos"> 
        <os name="SunOS" /> 
    </condition>

通过特定文件发现安装路径

几乎所有的应用服务器都有自己独有的一些文件,亦即在一个系统中只有它一个拥有,别无分店。那么根据这样的特色文件及其所在路径,我们可以使用 Ant 脚本在文件系统中进行搜索,一旦找到该文件,就可以确定应用服务器的安装路径了。

例如,对于 DB2 服务器,我们可以搜索 db2syssv 文件,它位于 DB2 安装路径的 cfg 目录下:

清单 3. 根据 db2syssv 文件找到 DB2 安装路径
<if> 
    <equals arg1="${osFamily}" arg2="windows" /> 
    <then> 
        <exec executable="cmd" outputproperty="db2syssvPath"> 
            <arg line="/c"/> 
            <arg line="where /r c:\ db2syssv"/> 
        </exec> 
        <propertyregex property="db2InstallPath" input="${db2syssvPath}" 
            regexp="(.+)\\cfg\\db2syssv"
            select="\1"
            defaultvalue="Error"
            override="true"/> 
    </then> 

    <else> 
        <exec executable="find" output="${basedir}/temp.txt"> 
            <arg line="/ -name db2syssv"/> 
        </exec> 
        <exec executable="grep" outputproperty="db2syssvPath"> 
            <arg line="db2syssv"/> 
            <arg line="${basedir}/temp.txt"/> 
        </exec> 
        <delete file="${basedir}/temp.txt"/> 
        <propertyregex property="db2InstallPath" input="${db2syssvPath}" 
            regexp="(.+)/cfg/db2syssv"
            select="\1"
            defaultvalue="Error"
            override="true"/> 
    </else> 
 </if> 
 <echo>DB2 install path: ${db2InstallPath}</echo>

这里用到了 ant-contrib 的 if 任务,所以我们需要把 ant-contrib.jar 放置到我们的 ClassPath 下并在 Ant 项目里写上:

<taskdef resource="net/sf/antcontrib/antlib.xml" />

因为 Windows 和 Unix/Linux 的区别,所以我们在脚本里将它们分开处理。在 Unix/Linux 处理的片段中,我们调用了两次 exec,是因为在某些版本的系统中在 Ant 中使用管道会出现问题。当然,我们也可以把脚本中的逻辑写成外部 Shell 脚本然后用 Ant 调用,不过在本文中就不对这种方法进行详细描述了。

在两个片段的后面,我们都调用了 propertyregex 任务,它用来对 db2syssvPath 的值进行正则表达式匹配,将其中 \\cfg\\db2syssv(Windows)或 /cfg/db2syssv(Unix/Linux)字符串前面的 DB2 安装路径取得并赋值给 db2InstallPath 参数。

以下是完整的脚本代码:

清单 4.test1.xml 文件
<project name="test1" default="get_db2_install_path"> 
    <taskdef resource="net/sf/antcontrib/antlib.xml" />
<!-- 探测操作系统 --> 
    <condition property="osFamily" value="windows"> 
        <os family="windows" /> 
    </condition> 
    <condition property="osFamily" value="unix"> 
        <os family="unix" /> 
    </condition> 
    <condition property="osName" value="windows"> 
        <os family="windows" /> 
    </condition> 
    <condition property="osName" value="aix"> 
        <os name="AIX" /> 
    </condition> 
    <condition property="osName" value="linux"> 
        <os name="Linux" /> 
    </condition> 
    <condition property="osName" value="sunos"> 
        <os name="SunOS" /> 
    </condition> 

    <target name="get_db2_install_path"> 
        <if> 
            <equals arg1="${osFamily}" arg2="windows" /> 
            <then> 
                <exec executable="cmd" outputproperty="db2syssvPath"> 
                    <arg line="/c"/> 
                    <arg line="where /r c:\ db2syssv"/> 
                </exec> 
                <propertyregex property="db2InstallPath" input="${db2syssvPath}" 
                    regexp="(.+)\\cfg\\db2syssv"
                    select="\1"
                    defaultvalue="Error"
                    override="true"/> 
            </then> 

            <else> 
                <exec executable="find" output="${basedir}/temp.txt"> 
                    <arg line="/ -name db2syssv"/> 
                </exec> 
                <exec executable="grep" outputproperty="db2syssvPath"> 
                    <arg line="db2syssv"/> 
                    <arg line="${basedir}/temp.txt"/> 
                </exec> 
                <delete file="${basedir}/temp.txt"/> 
                <propertyregex property="db2InstallPath" input="${db2syssvPath}" 
                    regexp="(.+)/cfg/db2syssv"
                    select="\1"
                    defaultvalue="Error"
                    override="true"/> 
            </else> 
        </if> 
        <echo>DB2 install path: ${db2InstallPath}</echo> 
    </target> 
    
 </project>

如果一个系统中安装了多个应用服务器,例如 WebSphere Application Server(以下简称 WAS),那么这种方法就不那么可靠了。此时我们就需要根据我们需要使用的特定应用服务器包含的特定应用的独有文件来定位这个应用服务器路径。具体文件当然是因应用不同而不同,但基本方法还是一样的。

通过系统命令发现系统参数

在各个操作系统里,都内置了一些系统命令,通过它们我们能够在命令行窗口查询本机系统的一些信息。那么使用 Ant 调用这些系统命令并对返回的结果进行分析,就可以得到我们想要的某些系统参数。

例如我们想得到当前主机的主机名(包含域名):

清单 5.get_hostname 脚本
<target name="get_hostname"> 
    <if> 
        <equals arg1="${osFamily}" arg2="windows" /> 
        <then> 
            <!-- 在 Windows 上取得 hostname.domain 的值 --> 
            <exec executable="hostname" outputproperty="host.name" />    

            <exec executable="cmd" output="${basedir}/ipconfig.txt"> 
                <arg value="/c"/> 
                <arg value="ipconfig /all|find &quot;DNS Suffix Search List&quot; "/> 
            </exec> 
            
            <replaceregexp file="${basedir}/ipconfig.txt"
                        match="DNS Suffix Search List(.*): "
                        replace="dns=" flags="g"/> 
                        
            <property file="${basedir}/ipconfig.txt" /> 
            
            <property name="hostname.domain" value="${host.name}.${dns}" />    
        </then> 

        <else> 
            <if> 
                <equals arg1="${osName}" arg2="linux" /> 
                <then> 
                    <!-- 在 Linux 上取得 hostname.domain 的值 --> 
                    <exec executable="hostname" outputproperty="hostname.domain" > 
                        <arg value="-f"/> 
                    </exec> 
                </then> 
                <else> 
                    <!-- 在 Unix 上取得 hostname.domain 的值 --> 
                    <exec executable="hostname" outputproperty="hostname.domain" /> 
                </else> 
            </if> 
        </else> 
    </if> 

    <echo>The Hostname is: ${hostname.domain}</echo> 
 </target>

在这个脚本里,我们针对不同的操作系统同样做了区别处理。在 Windows 系统中,hostname 命令只能得到主机名,而无法取得域名,所以我们使用 ipconfig /all 的结果进行解析。我们把 ipconfig /all 的结果保存到 ipconfig.txt 文件中,然后调用 replaceregexp 任务把该文件中含“DNS Suffix Search List(.*): ”正则表达式的串替换为“dns=”,这样该行就符合 Property 文件格式了;然后我们用 property 任务加载该文件,就取得了 DNS 后缀参数 ${dns};最后我们把 hostname 短名与 DNS 后缀拼接 ${host.name}.${dns},就得到了长主机名。

在 Linux 上的 hostname 命令需要加 -f 开关才能返回包含域名的主机名。而在 AIX 等其它 Unix 上,直接调用 hostname 命令即可。

通过应用实例的配置文件发现系统参数

在有的情况下,我们只是对已经部署好的应用程序进行更改,即将修改过或新添加的模块部署到当前应用程序中。在已经部署好的应用程序中,一般会将当前系统参数记录到配置文件当中,以便随时调用。那么我们也就可以根据这个配置文件取得我们想要的参数值。

例如在 WAS 的应用程序里,我们想要知道 wsadmin 工具所使用的 SOAP 服务端口(这个端口不是固定的,因此在不同的服务器上它的值也可能不同),只需要在 WAS 安装目录 /profiles/< 应用程序 profile>/properties/portdef.props 文件中找到 SOAP_CONNECTOR_ADDRESS 对应的参数值即可。

清单 6.get_port 脚本
<property name="defaultAppProfile" value="demo" /> 

 <target name="get_port"> 
    <if> 
        <equals arg1="${osFamily}" arg2="windows" /> 
        <then> 
            <exec executable="cmd" outputproperty="portPropertyFile"> 
                <arg line="/c"/> 
                <arg line="
where /r c:\ portdef.props | findstr /C:&quot;${defaultAppProfile}\\&quot; "/> 
            </exec> 
            <copy todir="${basedir}" file="${portPropertyFile}" overwrite="true" /> 
        </then> 

        <else> 
            <exec executable="find" output="${basedir}/temp.txt"> 
                <arg line="/ -name portdef.props"/> 
            </exec> 
            <exec executable="grep" outputproperty="portPropertyFile"> 
                <arg line="${defaultAppProfile}/"/> 
                <arg line="${basedir}/temp.txt"/> 
            </exec> 
            <delete file="${basedir}/temp.txt"/> 
            <copy todir="${basedir}" file="${portPropertyFile}" overwrite="true" /> 
        </else> 
    </if> 
    <replace_properties 
 propFile="${portPropertyFile}" 
 propName="SOAP_CONNECTOR_ADDRESS" propToReplace="was.port" />
 </target> 

 <macrodef name="replace_properties"> 
    <attribute name="propFile" /> 
    <attribute name="propName" /> 
    <attribute name="propToReplace" /> 
    
    <sequential> 
        <property file="@{propFile}" /> 
        
        <propertyfile file="${basedir}/deploy.properties"> 
            <entry key="@{propToReplace}" value="${@{propName}}" /> 
        </propertyfile> 
        
    </sequential> 
 </macrodef>

在上面这个脚本里,我们利用了与清单 3、4 类似的方法定位 portdef.props 文件路径,由于在同一个系统中可能会有多个 profile,所以我们不得不先定义 defaultAppProfile 的值。因为 portdef.props 文件本身就是一个 property 格式的文件,在 replace_properties 这个宏里面我们利用 Ant 的 property 任务将其中的参数名值对取出,然后用 propertyfile 任务替换 deploy.properties 里面的对应参数值。在本例里面,我们替换掉了 was.port 的值。

对于那些使用 xml 格式文件存储的参数值,例如 tomcat 的 server.xml,我们可以用 Ant 的 XmlProperty 任务来获取它们。

通过数据库记录发现应用参数

很多应用在部署过程中,会对数据库记录进行读、写操作,而这些操作往往需要用户事先提供一些对应的数据库记录参数值。我们自然也可以使用 Ant脚本将这个步骤自动化。

Ant 提供了 SQL 任务来让用户访问数据库,要执行这个任务,用户必须提供数据库的 JDBC 驱动信息以及用户信息。我们在这里假定已经通过前面的方法或者用户输入的方法获得了这些信息。

清单 7.get_storeId 脚本
<if> 
    <equals arg1="${db.type}" arg2="oracle" casesensitive="false" /> 
    <then> 
        <property name="toChar" value="to_char" /> 
        <property name="bigInt" value="NUMBER(19)" /> 
        <property name="integer" value="NUMBER(10)" /> 
        <property name="smallInt" value="NUMBER(5)" /> 
        <property name="varChar" value="VARCHAR2" /> 
    </then> 
    <else> 
        <property name="toChar" value="char" /> 
        <property name="bigInt" value="BIGINT" /> 
        <property name="integer" value="INTEGER" /> 
        <property name="smallInt" value="SMALLINT" /> 
        <property name="varChar" value="VARCHAR" /> 
    </else> 
 </if> 

 <target name="get_storeId"> 
    <sql 
        driver="${jdbc.driver}"
        url="${jdbc.url}"
        userid="${db.user.name}"
        password="${db.user.password}"
        output="${basedir}/storeId.txt"
        print="true"
        classpath="${jdbc.driver.path}"> 
        <![CDATA[ 
            select directory||'='||trim(${toChar}(store_id)) from store; 
         ]]> 
    </sql>  
 </target>

在这个脚本里,我们为了让 SQL 请求能够适应不同数据库类型,对 Oracle 和 DB2 的一些函数和类型用 Ant 参数做了映射,然后在 get_storeId 的 SQl 任务里调用它们。为了让输出的 storeId.txt 拥有 property 文件格式,我们在 SQL 请求中使用了一个小技巧,我们不是用逗号连接 directory(此列为 store 的名称)和 store_id(此列为 store 的唯一编号),而是将它们输出为等号连接。

结束语

本文通过利用 Apache Ant 提供的各种功能,实现了通过特定文件自动发现安装路径、通过系统命令和应用配置文件自动发现系统参数以及通过数据库记录自动发现应用参数。使用这些方法,我们可以根据所部署的应用及部署环境灵活的撰写自动发现系统和应用参数的 Ant 脚本,从而节省更多开发和测试人员的精力。

参考资料

学习

  • 参考 Apache Ant网站,下载 Ant 和了解更多关于 Ant 的内容。
  • 参考 Ant-Contrib Tasks网站,下载 Ant-contrib 和了解更多关于 Ant-contrib 的内容。

 类似资料: