О чем эта статья
Одной из отличительных особенностей платформы 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>
Заключение
Вот собственно и все. Это почти готовый пример взятый из реального проекта. Пользуйтесь на здоровье, не забывайте писать комментарии.
Ссылки
- ant.apache.org
- maven.apache.org
- habrahabr.ru/blogs/webdev/77333 статья про Maven