Pull to refresh

Сборка Java приложений при помощи Apache Ant, quick start

Reading time12 min
Views51K

О чем эта статья


Одной из отличительных особенностей платформы Java является ее независимость от используемого инструментария. Вы можете разрабатывать сколь угодно большое Java приложение при помощи блокнота (vi) и командной строки. Понятно что так никто не делает и все используют какую-то IDE. Как следствие независимости от инструментов — IDE для Java много. Все это хорошо но есть одна особенность. Если Ваш коллега делал приложение и для сборки проекта использовал IDE_A то в IDE_B которая стоит у Вас — собрать приложение не получится.
В общем-то это давно уже не проблема. Хорошей практикой считается использовать систему сборки не зависящую от IDE. Для Java их две это Apache-Ant и Maven (тоже в общем-то Apache). Но тут есть один подводный камень. Если в Delphi или Visual Studio, чтобы создать и собрать приложение надо кликнуть в кнопку new пройтись по шагам визарда и нажать кнопку собрать, то написание ant скрипта для сборки например web приложения, особенно для начинающего разработчика, задача не тривиальная.
В статье рассматривается сборка и деплой Java web приложения шаг за шагом.

В целом задачу можно решить как с помощью ant так и с помощью maven, здесь будет рассмотрен ant. Для начинающих он проще и нагляднее.

Примечание 10 лет спустя

Решил посмотрел на свою статью написанную 10 лет назад. Звучит старомодно, в 2021 я в общем случае не рекомендую собирать Java приложения при помощи ant. НО если уж у вас возникла такая необходимость, то статья все еще может помочь. Пусть живет.



Зачем нужен скрипт сборки


  • Независимость проектных задач от окружения.
  • 100% повторяемость любого результата (работает у меня – работает у всех)
  • Исключение человеческого фактора из важных операций
  • Превращение деплоя из сложной операции в рутинную задачу.


Собираем и деплоим war



Есть много способов собрать war и есть много способов расположить файлы приложения. В статье дается один способ — может быть не самый лучший но вполне неплохой.

Структура проекта

Файлы проекта располагаем вот так.
+---ant
|   |   step1.ant.xml
|   |   step2.ant.xml
|   |   step3.ant.xml
|   |   step4.ant.xml
|   |   step5.ant.xml
|   |   step6.ant.xml
|   |
|   \---env
|           default.properties
|           semenych.properties
|
+---lib
|   \---google-gson-1.4
|           gson-1.4-javadoc.jar
|           gson-1.4-sources.jar
|           gson-1.4.jar
|
+---src
|   \---java
|       \---com
|           \---dataart
|               \---ant
|                   \---demo
|                           Test.java
|
\---web
    \---WEB-INF
        |   web.xml
        |
        \---lib

В реальном проекте вместо stepN.xml будет build.xml. Библиотек будет больше и каждую следует располагать в отдельном каталоге. Имена пакетов выдают что я работаю в компании DataArt.

Шаг 1: компиляция

Для начала просто скомпилируем весь код, подключив бибилиотеку GSON. Компилированный код идет в каталог .build. Имя может быть любым, но обычно удобно если имя каталога начинается с точки.
<project name="step1" default="compile">
<target name="compile"> <mkdir dir="../.build/classes"/> <javac srcdir="../src/java" destdir="../.build/classes"> <classpath location="../lib/google-gson-1.4/gson-1.4.jar"/> </javac> </target> </project>


Шаг 2: улучшаем скрипт

Скрипт шага 1 довольно не гибкий и ряд путей там прописан дав раза. Давайте его улучшим

<project name="step2" default="compile">

    <property name="dir.build" value="../.build"/>

    <property name="dir.classes" value="${dir.build}/classes"/>

    <property name="dir.src.java" value="../src/java"/>

    <target name="clean">
        <delete dir="${dir.build}"/>
    </target>

    <target name="mkdirs">
        <mkdir dir="${dir.classes}"/>
    </target>

    <target name="compile" depends="mkdirs">
        <javac srcdir="${dir.src.java}" destdir="${dir.classes}">
            <classpath location="../lib/google-gson-1.4/gson-1.4.jar"/>
        </javac>
    </target>

</project>


Шаг 3: Пути к библиотекам

Пути к библиотекам прописаны жестко в середине кода. Это не есть хорошо, меняем.

<project name="step3" default="compile">

    <property name="dir.build" value="../.build"/>

    <property name="dir.classes" value="${dir.build}/classes"/>

    <property name="dir.src.java" value="../src/java"/>

    <property name="dir.lib" value="../lib"/>

    <path id="libs.gson">
        <fileset dir="${dir.lib}/google-gson-1.4">
            <include name="*.jar"/>
        </fileset>
    </path>

    <path id="libs.main.module">
        <path refid="libs.gson"/>
    </path>

    <target name="clean">
        <delete dir="${dir.build}"/>
    </target>

    <target name="mkdirs">
        <mkdir dir="${dir.classes}"/>
    </target>

    <target name="compile" depends="mkdirs">
        <javac srcdir="${dir.src.java}" destdir="${dir.classes}">
            <classpath>
                <path refid="libs.main.module"/>
            </classpath>
        </javac>
    </target>

</project>


Шаг 4: Управление кофигурациями

Управление конфигурациями это мега технология о которой меня рассказал матерый американский программер Walden Mathews. Суть в следующем, при сборке вы пишете в файл свойств таймаут или путь до какого-то внешнего каталога или URL какого-то сервиса. На вашей локальной машине он один, на боевом сервере (или на машине коллеги другой). Вопрос — как использовать правильные значения свойств и не поубивать друг друга.
<project name="step4" default="compile">

    <property name="dir.build" value="../.build"/>

    <property name="dir.classes" value="${dir.build}/classes"/>

    <property name="dir.src.java" value="../src/java"/>

    <property name="dir.lib" value="../lib"/>

    <property name="dir.env" value="./env"/>

    <property name="assembled.properties" value="${dir.build}/assembled.properties"/>

    <path id="libs.gson">
        <fileset dir="${dir.lib}/google-gson-1.4">
            <include name="*.jar"/>
        </fileset>
    </path>

    <path id="libs.main.module">
        <path refid="libs.gson"/>
    </path>

    <target name="clean">
        <delete dir="${dir.build}"/>
    </target>

    <target name="mkdirs">
        <mkdir dir="${dir.build}"/>
        <mkdir dir="${dir.classes}"/>
    </target>

    <target name="init" depends="mkdirs">
        <property name="env" value="${user.name}"/>
        <echo level="info" message="env=${env}"/>
        <available file="${dir.env}/${env}.properties" property="env.props.available"/>
        <fail unless="env.props.available"
              message="No such file: ${dir.env}/${env}.properties"/>
        <property file="${dir.env}/${env}.properties"/>
        <property file="${dir.env}/default.properties"/>
        <echoproperties destfile="${assembled.properties}"/>
        <filter filtersfile="${assembled.properties}"/>
    </target>

    <target name="compile" depends="init">
        <javac srcdir="${dir.src.java}" destdir="${dir.classes}">
            <classpath>
                <path refid="libs.main.module"/>
            </classpath>
        </javac>
    </target>

</project>


Здесь в target init читается файл с именем, совпадающим с Вашим именем пользователя. Если такой не находится сборка дальше не идет. А уже потом читаются дефолтные свойства из default. Так как в ant значения свойств переопределять нельзя то в default должны быть все свойства, а в Вашем файле только те значения которых отличаются.

default.properties

welcome.file=default.html

tomcat.service.name=tomcat6
tomcat.home=C:/bin/tomcat6


semenych.properties

welcome.file=index.html



Если Вы хотите собрать проект с файлом свойств имя которого отличается от имени пользователя — пишете так
ant -Denv=mihalych compile

Обратите внимание что в команде написано просто mihalych а не mihalych.properties

Шаг 5: Давайте уже соберем jar и war файл

Да действительно давайте.
<project name="step5" default="compile">

    <property name="dir.build" value="../.build"/>

    <property name="dir.classes" value="${dir.build}/classes"/>

    <property name="dir.src.java" value="../src/java"/>

    <property name="dir.lib" value="../lib"/>

    <property name="dir.env" value="./env"/>

    <property name="dir.war.content" value="${dir.build}/war.content"/>

    <property name="assembled.properties" value="${dir.build}/assembled.properties"/>

    <property name="file.jar" value="${dir.build}/main.module.jar"/>

    <property name="name.application" value="demo"/>

    <property name="file.war" value="${dir.build}/${name.application}.war"/>

    <path id="libs.gson">
        <fileset dir="${dir.lib}/google-gson-1.4">
            <include name="*.jar"/>
        </fileset>
    </path>

    <path id="libs.main.module">
        <path refid="libs.gson"/>
    </path>

    <target name="clean">
        <delete dir="${dir.build}"/>
    </target>

    <target name="mkdirs">
        <mkdir dir="${dir.build}"/>
        <mkdir dir="${dir.classes}"/>
        <mkdir dir="${dir.war.content}"/>
    </target>

    <target name="init" depends="mkdirs">
        <property name="env" value="${user.name}"/>
        <echo level="info" message="env=${env}"/>
        <available file="${dir.env}/${env}.properties" property="env.props.available"/>
        <fail unless="env.props.available"
              message="No such file: ${dir.env}/${env}.properties"/>
        <property file="${dir.env}/${env}.properties"/>
        <property file="${dir.env}/default.properties"/>
        <echoproperties destfile="${assembled.properties}"/>
        <filter filtersfile="${assembled.properties}"/>
    </target>

    <target name="compile" depends="init">
        <javac srcdir="${dir.src.java}" destdir="${dir.classes}">
            <classpath>
                <path refid="libs.main.module"/>
            </classpath>
        </javac>
    </target>

    <target name="build.jar" depends="compile">
        <jar destfile="${file.jar}"
             basedir="${dir.classes}"
             compress="false"
             index="true">
        </jar>
    </target>

    <target name="build.war.content"
            depends="build.jar">
        <copy todir="${dir.war.content}" preservelastmodified="true" overwrite="true">
            <fileset dir="../web"/>
        </copy>
        <copy todir="${dir.war.content}/WEB-INF/lib" preservelastmodified="true">
            <path refid="libs.main.module"/>
        </copy>

        <copy todir="${dir.war.content}/WEB-INF/lib"
              preservelastmodified="true"
              file="${file.jar}">
        </copy>

        <replace dir="${dir.war.content}/WEB-INF/"
                 propertyfile="${assembled.properties}">
            <include name="*.xml"/>

            <replacefilter token="@welcome.file@" property="welcome.file"/>

        </replace>

    </target>

    <target name="build.war" depends="build.war.content">
        <delete file="${file.war}"/>
        <war
                compress="true"
                encoding="utf-8"
                warfile="${file.war}"
                webxml="${dir.war.content}/WEB-INF/web.xml">
            <fileset dir="${dir.war.content}" excludes="WEB-INF/web.xml"/>
        </war>
    </target>


</project>


Один комментарий:


        <replace dir="${dir.war.content}/WEB-INF/"
                 propertyfile="${assembled.properties}">
            <include name="*.xml"/>

            <replacefilter token="@welcome.file@" property="welcome.file"/>

        </replace>


вот эта секция делает замену внутри web.xml при этом web.xml выглядит так:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
         xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

    <welcome-file-list>
        <welcome-file>@welcome.file@</welcome-file>
    </welcome-file-list>

</web-app>



Шаг 6: war готов, последний штрих, деплой

Скрипт будет делать так называемый «холодный» деплой т.е. развертывание приложения с остановкой сервера. В ряде случаев «холодный» деплой позволяет избежать ряда проблем связанных с освобождением ресурсов и чисткой кэша.

<project name="step6" default="compile">

    <property name="dir.build" value="../.build"/>

    <property name="dir.classes" value="${dir.build}/classes"/>

    <property name="dir.src.java" value="../src/java"/>

    <property name="dir.lib" value="../lib"/>

    <property name="dir.env" value="./env"/>

    <property name="dir.war.content" value="${dir.build}/war.content"/>

    <property name="assembled.properties" value="${dir.build}/assembled.properties"/>

    <property name="file.jar" value="${dir.build}/main.module.jar"/>

    <property name="name.application" value="demo"/>

    <property name="file.war" value="${dir.build}/${name.application}.war"/>

    <path id="libs.gson">
        <fileset dir="${dir.lib}/google-gson-1.4">
            <include name="*.jar"/>
        </fileset>
    </path>

    <path id="libs.main.module">
        <path refid="libs.gson"/>
    </path>

    <target name="clean">
        <delete dir="${dir.build}"/>
    </target>

    <target name="mkdirs">
        <mkdir dir="${dir.build}"/>
        <mkdir dir="${dir.classes}"/>
        <mkdir dir="${dir.war.content}"/>
    </target>

    <target name="init" depends="mkdirs">
        <property name="env" value="${user.name}"/>
        <echo level="info" message="env=${env}"/>
        <available file="${dir.env}/${env}.properties" property="env.props.available"/>
        <fail unless="env.props.available"
              message="No such file: ${dir.env}/${env}.properties"/>
        <property file="${dir.env}/${env}.properties"/>
        <property file="${dir.env}/default.properties"/>
        <echoproperties destfile="${assembled.properties}"/>
        <filter filtersfile="${assembled.properties}"/>
    </target>

    <target name="compile" depends="init">
        <javac srcdir="${dir.src.java}" destdir="${dir.classes}">
            <classpath>
                <path refid="libs.main.module"/>
            </classpath>
        </javac>
    </target>

    <target name="build.jar" depends="compile">
        <jar destfile="${file.jar}"
             basedir="${dir.classes}"
             compress="false"
             index="true">
        </jar>
    </target>

    <target name="build.war.content"
            depends="build.jar">
        <copy todir="${dir.war.content}" preservelastmodified="true" overwrite="true">
            <fileset dir="../web"/>
        </copy>
        <copy todir="${dir.war.content}/WEB-INF/lib" preservelastmodified="true">
            <path refid="libs.main.module"/>
        </copy>

        <copy todir="${dir.war.content}/WEB-INF/lib"
              preservelastmodified="true"
              file="${file.jar}">
        </copy>

        <replace dir="${dir.war.content}/WEB-INF/"
                 propertyfile="${assembled.properties}">
            <include name="*.xml"/>

            <replacefilter token="@welcome.file@" property="welcome.file"/>

        </replace>

    </target>

    <target name="build.war" depends="build.war.content">
        <delete file="${file.war}"/>
        <war
                compress="true"
                encoding="utf-8"
                warfile="${file.war}"
                webxml="${dir.war.content}/WEB-INF/web.xml">
            <fileset dir="${dir.war.content}" excludes="WEB-INF/web.xml"/>
        </war>
    </target>

    <target name="deploy"
            depends="build.war,do.undeploy.war,do.deploy.war">
    </target>

    <target name="do.undeploy.war" depends="init">
        <service.stop.win32 service.name="${tomcat.service.name}"/>
        <delete file="${tomcat.home}/webapps/${name.application}.war" failonerror="false"/>
        <delete dir="${tomcat.home}/webapps/${name.application}" failonerror="false"/>
    </target>

    <target name="do.deploy.war" depends="init">
        <copy todir="${tomcat.home}/webapps" failonerror="yes">
            <fileset file="${file.war}"/>
        </copy>
        <service.start.win32 service.name="${tomcat.service.name}"/>
    </target>

    <!-- macro -->

    <macrodef name="service.stop.win32">
        <attribute name="service.name"/>
        <sequential>
            <echo>Stoping service: @{service.name}</echo>
            <exec executable="net" outputproperty="whatsRunning">
                <arg value="stop"/>
                <arg value="@{service.name}"/>
            </exec>
        </sequential>
    </macrodef>

    <macrodef name="service.start.win32">
        <attribute name="service.name"/>
        <sequential>
            <echo>Starting service: @{service.name}</echo>
            <exec executable="net" failonerror="yes">
                <arg value="start"/>
                <arg value="@{service.name}"/>
            </exec>
        </sequential>
    </macrodef>

</project>


Заключение


Вот собственно и все. Это почти готовый пример взятый из реального проекта. Пользуйтесь на здоровье, не забывайте писать комментарии.

Ссылки


Tags:
Hubs:
+29
Comments35

Articles

Change theme settings