Create spring-boot-antlib module

Create a new spring-boot-antlib module which allows Apache Ant users
to easily create executable jars.

Fixes gh-3339
pull/3357/merge
Matt Benson 10 years ago committed by Phillip Webb
parent 8d948dfa6c
commit ae4559eb4f

@ -607,11 +607,155 @@ further information.
[[build-tool-plugins-antlib]]
== Spring Boot AntLib module
The Spring Boot AntLib module provides basic Spring Boot support for Apache Ant. You can
use the module to create executable jars. To use the module you need to declare an
additional `spring-boot` namespace in your `build.xml`:
[source,xml,indent=0]
----
<project xmlns:ivy="antlib:org.apache.ivy.ant"
xmlns:spring-boot="antlib:org.springframework.boot.ant"
name="myapp" default="build">
...
</project>
----
You'll need to remember to start Ant using the `-lib` option, for example:
[indent=0,subs="verbatim,quotes,attributes"]
----
$ ant -lib <folder containing spring-boot-antlib-{spring-boot-version}.jar>
----
TIP: The "`Using Spring Boot`" section includes a more complete example of
<<using-spring-boot.adoc#using-boot-ant, using Apache Ant with `spring-boot-antlib`>>
=== Spring Boot Ant tasks
Once the `spring-boot-antlib` namespace has been declared, the following additional
tasks are available.
==== spring-boot:exejar
The `exejar` task can be used to creates a Spring Boot executable jar. The following
attributes are supported by the task:
[cols="1,2,2"]
|====
|Attribute |Description |Required
|`destfile`
|The destination jar file to create
|Yes
|`classes`
|The root directory of Java classfiles
|Yes
|`start-class`
|The main application class to run
|No _(default is first class found declaring a `main` method)_
|====
The following nested elements can be used with the task:
[cols="1,4"]
|====
|Element |Description
|`resources`
|One or more {ant-manual}/Types/resources.html#collection[Resource Collections]
describing a set of {ant-manual}/Types/resources.html[Resources] that should be added to
the content of the created +jar+ file.
|`lib`
|One or more {ant-manual}/Types/resources.html#collection[Resource Collections]
that should be added to the set of jar libraries that make up the runtime dependency
classpath of the application.
|====
===== Examples
.Specify +start-class+
[source,xml,indent=0]
----
<spring-boot:exejar destfile="target/my-application.jar"
classes="target/classes" start-class="com.foo.MyApplication">
<resources>
<fileset dir="src/main/resources" />
</resources>
<lib>
<fileset dir="lib" />
</lib>
</spring-boot:exejar>
----
.Detect +start-class+
[source,xml,indent=0]
----
<exejar destfile="target/my-application.jar" classes="target/classes">
<lib>
<fileset dir="lib" />
</lib>
</exejar>
----
=== spring-boot:findmainclass
The `findmainclass` task is used internally by `exejar` to locate a class declaring a
`main`. You can also use this task directly in your build if needed. The following
attributes are supported
[cols="1,2,2"]
|====
|Attribute |Description |Required
|`classesroot`
|The root directory of Java classfiles
|Yes _(unless `mainclass` is specified)_
|`mainclass`
|Can be used to short-circuit the `main` class search
|No
|`property`
|The Ant property that should be set with the result
|No _(result will be logged if unspecified)_
|====
===== Examples
.Find and log
[source,xml,indent=0]
----
<findmainclass classesroot="target/classes" />
----
.Find and set
[source,xml,indent=0]
----
<findmainclass classesroot="target/classes" property="main-class" />
----
.Override and set
[source,xml,indent=0]
----
<findmainclass mainclass="com.foo.MainClass" property="main-class" />
----
[[build-tool-plugins-other-build-systems]]
== Supporting other build systems
If you want to use a build tool other than Maven or Gradle, you will likely need to develop
your own plugin. Executable jars need to follow a specific format and certain entries need
to be written in an uncompressed form (see the
If you want to use a build tool other than Maven, Gradle or Ant, you will likely need to
develop your own plugin. Executable jars need to follow a specific format and certain
entries need to be written in an uncompressed form (see the
_<<appendix-executable-jar-format.adoc#executable-jar, executable jar format>>_ section
in the appendix for details).
@ -668,6 +812,8 @@ Here is a typical example repackage:
});
----
[[build-tool-plugins-whats-next]]
== What to read next
If you're interested in how the build tool plugins work you can

@ -2198,9 +2198,10 @@ details.
[[howto-build-an-executable-archive-with-ant]]
=== Build an executable archive with Ant
=== Build an executable archive from Ant without using spring-boot-antlib
To build with Ant you need to grab dependencies, compile and then create a jar or war
archive as normal. To make it executable:
archive as normal. To make it executable you can either use the `spring-boot-antlib`
module, or you can follow these instructions:
. Use the appropriate launcher as a `Main-Class`, e.g. `JarLauncher` for a jar file, and
specify the other properties it needs as manifest entries, principally a `Start-Class`.
@ -2236,7 +2237,7 @@ The Actuator Sample has a `build.xml` that should work if you run it with
[indent=0,subs="verbatim,quotes,attributes"]
----
$ ant -lib <path_to>/ivy-2.2.jar
$ ant -lib <folder containing ivy-2.2.jar>
----
after which you can run the application with

@ -39,6 +39,7 @@ Phillip Webb; Dave Syer; Josh Long; Stéphane Nicoll; Rob Winch; Andy Wilkinson;
:spring-data-mongo-javadoc: http://docs.spring.io/spring-data/mongodb/docs/current/api/org/springframework/data/mongodb
:spring-data-rest-javadoc: http://docs.spring.io/spring-data/rest/docs/current/api/org/springframework/data/rest
:gradle-userguide: http://www.gradle.org/docs/current/userguide
:ant-manual: http://ant.apache.org/manual
// ======================================================================================
include::documentation-overview.adoc[]

@ -210,12 +210,68 @@ endif::[]
[[using-boot-ant]]
=== Ant
It is possible to build a Spring Boot project using Apache Ant, however, no special
support or plugins are provided. Ant scripts can use the Ivy dependency system to import
starter POMs.
It is possible to build a Spring Boot project using Apache Ant+Ivy. The
`spring-boot-antlib` "`AntLib`" module is also available to help Ant create executable
jars.
See the _<<howto.adoc#howto-build-an-executable-archive-with-ant>>_ "`How-to`" for more
complete instructions.
To declare dependencies a typical `ivy.xml` file will look something like this:
[source,xml,indent=0]
----
<ivy-module version="2.0">
<info organisation="org.springframework.boot" module="spring-boot-sample-ant" />
<configurations>
<conf name="compile" description="everything needed to compile this module" />
<conf name="runtime" extends="compile" description="everything needed to run this module" />
</configurations>
<dependencies>
<dependency org="org.springframework.boot" name="spring-boot-starter"
rev="${spring-boot.version}" conf="compile" />
</dependencies>
</ivy-module>
----
A typical `build.xml` will look like this:
[source,xml,indent=0]
----
<project
xmlns:ivy="antlib:org.apache.ivy.ant"
xmlns:spring-boot="antlib:org.springframework.boot.ant"
name="myapp" default="build">
<property name="spring-boot.version" value="1.3.0.BUILD-SNAPSHOT" />
<target name="resolve" description="--> retrieve dependencies with ivy">
<ivy:retrieve pattern="lib/[conf]/[artifact]-[type]-[revision].[ext]" />
</target>
<target name="classpaths" depends="resolve">
<path id="compile.classpath">
<fileset dir="lib/compile" includes="*.jar" />
</path>
</target>
<target name="init" depends="classpaths">
<mkdir dir="build/classes" />
</target>
<target name="compile" depends="init" description="compile">
<javac srcdir="src/main/java" destdir="build/classes" classpathref="compile.classpath" />
</target>
<target name="build" depends="compile">
<spring-boot:exejar destfile="build/myapp.jar" classes="build/classes">
<spring-boot:lib>
<fileset dir="lib/runtime" />
</spring-boot:lib>
</spring-boot:exejar>
</target>
</project>
----
TIP: See the _<<howto.adoc#howto-build-an-executable-archive-with-ant>>_ "`How-to`" if
you don't want to use the `spring-boot-antlib` module.

@ -25,5 +25,6 @@
<module>spring-boot-loader-tools</module>
<module>spring-boot-maven-plugin</module>
<module>spring-boot-gradle-plugin</module>
<module>spring-boot-antlib</module>
</modules>
</project>

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-tools</artifactId>
<version>1.3.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-antlib</artifactId>
<name>Spring Boot Antlib</name>
<description>Spring Boot Antlib</description>
<url>http://projects.spring.io/spring-boot/</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>http://www.spring.io</url>
</organization>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
<ant.version>1.9.3</ant.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader-tools</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>${ant.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<artifactSet>
<includes>
<include>org.springframework.boot:spring-boot-loader-tools</include>
<include>org.springframework:spring-core</include>
</includes>
</artifactSet>
<minimizeJar>true</minimizeJar>
<shadedArtifactAttached>false</shadedArtifactAttached>
<keepDependenciesWithProvidedScope>true</keepDependenciesWithProvidedScope>
</configuration>
<executions>
<execution>
<id>shade-runtime-dependencies</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>antunit</id>
<phase>integration-test</phase>
<configuration>
<target>
<mkdir dir="${project.build.directory}/it" />
<copy todir="${project.build.directory}/it">
<fileset dir="${project.basedir}/src/it" />
</copy>
<path id="taskpath">
<fileset dir="${project.build.directory}"
includes="${project.build.finalName}.${project.packaging}" />
</path>
<pathconvert refid="taskpath" />
<typedef resource="org/springframework/boot/ant/antlib.xml"
classpathref="taskpath" uri="antlib:org.springframework.boot.ant" />
<property name="build.compiler"
value="org.eclipse.jdt.core.JDTCompilerAdapter" />
<antunit xmlns="antlib:org.apache.ant.antunit">
<propertyset>
<propertyref name="build.compiler" />
</propertyset>
<plainlistener />
<fileset dir="${project.build.directory}/it" includes="**/build.xml"
xmlns="antlib:org.apache.tools.ant" />
</antunit>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>${ant.version}</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant-launcher</artifactId>
<version>${ant.version}</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant-antunit</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.apache.ivy</groupId>
<artifactId>ivy</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.4.2</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,93 @@
<?xml version="1.0"?>
<project name="spring-boot-antlib-test" default="antunit" xmlns:au="antlib:org.apache.ant.antunit" xmlns:spring-boot="antlib:org.springframework.boot.ant" xmlns:ivy="antlib:org.apache.ivy.ant">
<property name="src.dir" location="src/main/java" />
<property name="resource.dir" location="src/main/resources" />
<property name="target.dir" location="target" />
<property name="classes.dir" location="${target.dir}/classes" />
<target name="setUp">
<ivy:cachepath inline="true" pathid="compile.classpath" organisation="joda-time" module="joda-time" type="jar" revision="2.8.1" />
<mkdir dir="${classes.dir}" />
<javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="compile.classpath" />
</target>
<target name="tearDown">
<delete dir="${classes.dir}" />
</target>
<target name="test-findmainclass-set-property" depends="setUp">
<spring-boot:findmainclass classesroot="${classes.dir}" property="main-class" />
<au:assertEquals expected="org.test.SampleApplication" actual="${main-class}" />
</target>
<target name="test-findmainclass-log-message" depends="setUp">
<spring-boot:findmainclass classesroot="${classes.dir}" />
<au:assertLogContains text="org.test.SampleApplication" />
</target>
<target name="test-findmainclass-override" depends="setUp">
<spring-boot:findmainclass mainclass="OVERRIDDEN" property="main-class" />
<au:assertEquals expected="OVERRIDDEN" actual="${main-class}" />
</target>
<macrodef name="check-exejar">
<attribute name="jar" />
<sequential>
<echo>Checking @{jar}</echo>
<au:assertFileExists file="@{jar}" />
<javaresource id="foo" name="foo">
<classpath>
<pathelement location="@{jar}" />
</classpath>
</javaresource>
<au:assertRefResourceExists refid="foo" />
<au:assertRefResourceContains refid="foo" value="FOO" />
<java jar="@{jar}" fork="true" />
<au:assertLogContains text="LocalDate" />
</sequential>
</macrodef>
<target name="test-exejar-explicit-start-class" depends="setUp">
<local name="jar" />
<property name="jar" location="${target.dir}/explicit.jar" />
<spring-boot:exejar destfile="${jar}" classes="${classes.dir}" start-class="org.test.SampleApplication">
<resources>
<fileset dir="${resource.dir}" />
</resources>
<lib>
<path refid="compile.classpath" />
</lib>
</spring-boot:exejar>
<check-exejar jar="${jar}" />
</target>
<target name="test-exejar-find-start-class" depends="setUp">
<local name="jar" />
<property name="jar" location="${target.dir}/found.jar" />
<spring-boot:exejar destfile="${jar}" classes="${classes.dir}">
<resources>
<fileset dir="${resource.dir}" />
</resources>
<lib>
<path refid="compile.classpath" />
</lib>
</spring-boot:exejar>
<check-exejar jar="${jar}" />
</target>
<target name="antunit">
<au:antunit>
<au:plainlistener />
<file file="${ant.file}" />
</au:antunit>
</target>
<target name="clean">
<delete dir="${target.dir}" />
</target>
</project>

@ -0,0 +1,16 @@
<ivysettings>
<settings defaultResolver="chain" />
<resolvers>
<chain name="chain">
<!-- NOTE: You should declare only repositories that you need here -->
<filesystem name="local" local="true" m2compatible="true">
<artifact pattern="${user.home}/.m2/[organisation]/[module]/[revision]/[module]-[revision].[ext]" />
<ivy pattern="${user.home}/.m2/[organisation]/[module]/[revision]/[module]-[revision].pom" />
</filesystem>
<ibiblio name="ibiblio" m2compatible="true" />
<ibiblio name="spring-milestones" m2compatible="true" root="http://repo.spring.io/release" />
<ibiblio name="spring-milestones" m2compatible="true" root="http://repo.spring.io/milestone" />
<ibiblio name="spring-snapshots" m2compatible="true" root="http://repo.spring.io/snapshot" />
</chain>
</resolvers>
</ivysettings>

@ -0,0 +1,27 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.test;
import org.joda.time.LocalDate;
public class SampleApplication {
public static void main(String[] args) {
System.out.println(LocalDate.class.getSimpleName());
}
}

@ -0,0 +1,115 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ant;
import java.io.File;
import java.io.IOException;
import java.util.jar.JarFile;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.springframework.boot.loader.tools.MainClassFinder;
import org.springframework.util.StringUtils;
/**
* Ant task to find a main class.
*
* @author Matt Benson
* @since 1.3.0
*/
public class FindMainClass extends Task {
private String mainClass;
private File classesRoot;
private String property;
public FindMainClass(Project project) {
setProject(project);
}
@Override
public void execute() throws BuildException {
String mainClass = this.mainClass;
if (!StringUtils.hasText(mainClass)) {
mainClass = findMainClass();
if (!StringUtils.hasText(mainClass)) {
throw new BuildException(
"Could not determine main class given @classesRoot "
+ this.classesRoot);
}
}
handle(mainClass);
}
private String findMainClass() {
if (this.classesRoot == null) {
throw new BuildException(
"one of @mainClass or @classesRoot must be specified");
}
if (!this.classesRoot.exists()) {
throw new BuildException("@classesRoot " + this.classesRoot
+ " does not exist");
}
try {
if (this.classesRoot.isDirectory()) {
return MainClassFinder.findSingleMainClass(this.classesRoot);
}
return MainClassFinder
.findSingleMainClass(new JarFile(this.classesRoot), "/");
}
catch (IOException ex) {
throw new BuildException(ex);
}
}
private void handle(String mainClass) {
if (StringUtils.hasText(this.property)) {
getProject().setProperty(this.property, mainClass);
}
else {
log("Found main class " + mainClass);
}
}
/**
* Set the main class, which will cause the search to be bypassed.
* @param mainClass
*/
public void setMainClass(String mainClass) {
this.mainClass = mainClass;
}
/**
* Set the root location of classes to be searched.
* @param classesRoot
*/
public void setClassesRoot(File classesRoot) {
this.classesRoot = classesRoot;
}
/**
* Set the property to set (if unset, result will be printed to the log).
* @param property
*/
public void setProperty(String property) {
this.property = property;
}
}

@ -0,0 +1,51 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ant;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.springframework.util.StringUtils;
/**
* Quiet task that establishes a reference to its loader.
*
* @author Matt Benson
* @since 1.3.0
*/
public class ShareAntlibLoader extends Task {
private String refid;
public ShareAntlibLoader(Project project) {
super();
setProject(project);
}
@Override
public void execute() throws BuildException {
if (!StringUtils.hasText(this.refid)) {
throw new BuildException("@refid has no text");
}
getProject().addReference(this.refid, getClass().getClassLoader());
}
public void setRefid(String refid) {
this.refid = refid;
}
}

@ -0,0 +1,78 @@
<?xml version="1.0"?>
<antlib>
<taskdef name="findmainclass" classname="org.springframework.boot.ant.FindMainClass" />
<!-- unadvertised taskdef to help pull antlib resources from within the antlib.xml file -->
<taskdef name="spring-boot-antlib-share-antlib-loader" classname="org.springframework.boot.ant.ShareAntlibLoader" />
<macrodef name="exejar" description="Create a spring-boot executable jar">
<attribute name="destfile" />
<attribute name="classes" />
<attribute name="start-class" default="" />
<element name="resources" optional="true"
description="includes resource collections specifying additional Java resources" />
<element name="lib" optional="true"
description="includes resource collections containing (jar) dependencies" />
<sequential>
<local name="start-class" />
<findmainclass
xmlns="antlib:org.springframework.boot.ant"
property="start-class"
mainclass="@{start-class}"
classesroot="@{classes}" />
<echo>Using start class ${start-class}</echo>
<spring-boot-antlib-share-antlib-loader
xmlns="antlib:org.springframework.boot.ant"
refid="spring.boot.antlib.loader" />
<local name="spring-boot.version" />
<loadproperties prefix="spring-boot">
<javaresource
name="META-INF/maven/org.springframework.boot/spring-boot-antlib/pom.properties"
loaderref="spring.boot.antlib.loader" />
<filterchain>
<linecontainsregexp>
<regexp pattern="^version=" />
</linecontainsregexp>
</filterchain>
</loadproperties>
<local name="destdir" />
<dirname file="@{destfile}" property="destdir" />
<echo>Using destination directory ${destdir}</echo>
<mkdir dir="${destdir}/dependency" />
<echo>Extracting spring-boot-loader to ${destdir}/dependency</echo>
<copy todir="${destdir}/dependency">
<javaresource name="META-INF/loader/spring-boot-loader.jar"
loaderref="spring.boot.antlib.loader" />
<flattenmapper />
</copy>
<echo>Embedding spring-boot-loader v${spring-boot.version}...</echo>
<jar destfile="@{destfile}" compress="false">
<fileset dir="@{classes}" />
<resources />
<mappedresources>
<lib />
<globmapper from="*" to="lib/*" />
</mappedresources>
<zipfileset src="${destdir}/dependency/spring-boot-loader.jar" />
<manifest>
<attribute name="Main-Class"
value="org.springframework.boot.loader.JarLauncher" />
<attribute name="Start-Class" value="${start-class}" />
</manifest>
</jar>
</sequential>
</macrodef>
</antlib>
Loading…
Cancel
Save