Friday 29 November 2013

Spring-boot with Ant and Ivy

After seeing loads of exciting news about spring-boot I was a little disappointed to find, even though it was mentioned, no examples for building projects using Ant and Ivy. I contacted the team via Twitter and they pointed me at the spring-boot-loader-tools and suggested I give it a go myself. It turns out to be not all that difficult to get something simple going and here are the results in case any other Luddites are still using Ant and Ivy!
First we need to tell Ivy where the Maven repo is:

ivysetting.xml

<ivysettings>
 <!-- this file overrides the default ivysettingsx.xml that is found inside the ivy.jar file -->
 <property name="ivy.checksums" value="sha1,md5" />
 <settings defaultResolver="default" />
 <resolvers>
  <chain name="public">
   <ibiblio name="spring-milestones" m2compatible="true" root="http://repo.springsource.org/libs-milestone/"  />
   <ibiblio name="ibiblio" m2compatible="true" />
  </chain>
 </resolvers>

 <include url="${ivy.default.settings.dir}/ivysettings-shared.xml" />
 <include url="${ivy.default.settings.dir}/ivysettings-local.xml" />
 <include url="${ivy.default.settings.dir}/ivysettings-main-chain.xml" />
 <include url="${ivy.default.settings.dir}/ivysettings-default-chain.xml" />
</ivysettings>
Then we set up our ivy.xml to download the project dependencies

ivy.xml

<ivy-module version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ant.apache.org/ivy/schemas/ivy.xsd" xmlns:m="http://ant.apache.org/ivy/maven"
 xmlns:e="http://ant.apache.org/ivy/extra">
 
 <info organisation="adrian" module="boot" />
 <configurations defaultconfmapping="*->default">
  <conf name="main" />
 </configurations>
 
 <dependencies>
  <dependency org="org.springframework.boot" name="spring-boot" rev="0.5.0.M6" conf="main"/>
  <dependency org="org.springframework.boot" name="spring-boot-starter-web" rev="0.5.0.M6" conf="main"/>
  <dependency org="org.springframework.boot" name="spring-boot-loader-tools" rev="0.5.0.M6" conf="main"/>
 </dependencies>
</ivy-module>
Next, a very simple spring boot application, shamelessly ripped off:
package adrian;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@EnableAutoConfiguration
public class SampleController {

    @RequestMapping("/")
    @ResponseBody
    String home() {
        return "Hello World!";
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(SampleController.class, args);
    }
}
This will compile and run in Eclipse, if we include all the downloaded jars in the classpath but, to make a nice single jar file application, we need to package up the code and the dependencies using some simple code:

Packager

package adrian;

import java.io.File;
import java.io.IOException;

import org.springframework.boot.loader.tools.Libraries;
import org.springframework.boot.loader.tools.LibraryCallback;
import org.springframework.boot.loader.tools.LibraryScope;
import org.springframework.boot.loader.tools.Repackager;

public class Packager {
 public static void main(String[] args) throws Exception {
  String srcJar = args[0];
  String mainClass = args[1];
  final String libDir = args[2];
  
  Repackager repackager = new Repackager(new File(srcJar));
  repackager.setMainClass(mainClass);
  repackager.repackage(new Libraries() {
   @Override
   public void doWithLibraries(LibraryCallback libraryCallback) throws IOException {
    File lib = new File(libDir);
    String[] jars = lib.list();
    for (String jar : jars) {
     System.err.println("adding " + jar);
     libraryCallback.library(new File(lib + "/" + jar), LibraryScope.RUNTIME);
    }
   }
  });
 }
}

An example Ant build.xml

<?xml version="1.0"?>
<project name="boot" default="package" xmlns:ivy="antlib:org.apache.ivy.ant">
 
 <target name="clean">
  <delete dir="build"/>
 </target>
 
 <target name="retrieve" unless="no.retrieve" depends="clean">
  <ivy:retrieve pattern="build/lib/main/[artifact]-[revision].[ext]" conf="main" type="jar,bundle" />
 </target>

 <target name="resolve" description="ivy" depends="retrieve">
  <ivy:cachepath pathid="main.classpath" conf="main" />
 </target>
 
 <target name="compile" depends="resolve">
  <mkdir dir="build/classes"/>
  <javac destdir="build/classes" classpathref="main.classpath" srcdir="src/main/java" includeantruntime="false"/>
 </target>
 
 <target name="jar" depends="compile">
  <jar destfile="build/boot.jar" basedir="build/classes"/>
 </target>
 
 <target name="package" depends="jar">
  <java classname="adrian.Packager">
   <classpath>
    <path refid="main.classpath"/>
    <pathelement location="build/classes"/>
   </classpath>
   <arg value="build/boot.jar"/>
   <arg value="adrian.SampleController"/>
   <arg value="build/lib/main"/>
  </java>
 </target>
</project>
And to run the final application we simply run
java -jar build/boot.jar