Typical [enterprise] Java projects use multi-module Maven configuration. You have the parent pom.xml
file at the root of your project and you refer to the modules from the parent pom.xml
. The motivation for this structure is that you want to configure the compiler, testing and perhaps the reporting components once and apply them to all modules; also, the modules depend on each other and you need to use the multi-module project to compile the modules in the right order.
Think about a typical JEE application: you have the domain, repository, services and web app; the dependencies between the modules are that:
- domain does not have any dependencies
- repository depends on domain
- services depends on domain and repository
- web app depends on domain and services
In this post, I am not going to be talking about Spring Java EE application. I will show you how I have moved Specs2 Spring from Maven to SBT.
Specs2 Spring was a multi-module Maven project, with five modules:
- org.specs2.spring with no dependencies
- org.specs2.spring-example depends on org.specs2.spring
- org.specs2.spring.web depends on org.specs2.spring
- org.specs2.spring.web-example depends on org.specs2.spring.web
- org.specs2.spring.documentation with no dependencies
In addition to expressing the structure in the Maven poms, I needed to configure the Scala compiler to run during the compile
and testCompile
phases. Also, I wanted to generate DocBooks as part of the build process. All this made the parent pom.xml
rather verbose, reaching 215 lines of XML; the most notable ones being:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.specs2</groupId>
<artifactId>parent</artifactId>
<version>0.3</version>
<packaging>pom</packaging>
<properties>
...
<specs2.version>1.7</specs2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.9.1</version>
</dependency>
</dependencies>
<modules>
<module>org.specs2.spring</module>
<module>org.specs2.spring.web</module>
<module>org.specs2.spring.documentation</module>
<module>org.specs2.spring-example</module>
<module>org.specs2.spring.web-example</module>
</modules>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
</plugin>
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<version>2.15.3-SNAPSHOT</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.7.2</version>
<configuration>
<classesDirectory>target/classes</classesDirectory>
<includes>
<include>**/*Test.class</include>
</includes>
<argLine>-Xmx1024M -XX:MaxPermSize=256m</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.1</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.scala-tools</groupId>
<artifactId>maven-scala-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.agilejava.docbkx</groupId>
<artifactId>docbkx-maven-plugin</artifactId>
<version>2.0.13</version>
<configuration>
<xincludeSupported>true</xincludeSupported>
<highlightSource>1</highlightSource>
<foCustomization>
${project.basedir}/src/docbkx/styles/pdf/custom.xsl
</foCustomization>
</configuration>
<dependencies>
<dependency>
<groupId>org.docbook</groupId>
<artifactId>docbook-xml</artifactId>
<version>4.4</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.sf.xslthl</groupId>
<artifactId>xslthl</artifactId>
<version>2.0.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.sf.offo</groupId>
<artifactId>fop-hyph</artifactId>
<version>1.2</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<executions>
<execution>
<phase>pre-site</phase>
<goals>
<goal>generate-html</goal>
<goal>generate-pdf</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
This parent pom.xml
refers to modules, which must be sub-directories with another pom.xml
file in them. The org.specs2.spring/pom.xml
with no dependencies amongst the project modules simply listed all other dependencies:
123456789101112131415161718192021222324252627<?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/maven-v4_0_0.xsd">
<parent>
<groupId>org.specs2</groupId>
<artifactId>parent</artifactId>
<version>0.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.specs2</groupId>
<artifactId>spring</artifactId>
<packaging>jar</packaging>
<version>0.3</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
...
</dependencies>
</project>
The org.specs2.spring-example module depends on the org.specs2.spring module, so its pom.xml
had to include the org.specs2.spring-example module like so:
1234567891011121314151617181920212223242526272829303132<?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/maven-v4_0_0.xsd">
<parent>
<groupId>org.specs2</groupId>
<artifactId>parent</artifactId>
<version>0.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.specs2</groupId>
<artifactId>spring-example</artifactId>
<packaging>jar</packaging>
<version>0.3</version>
<dependencies>
<dependency>
<groupId>org.specs2</groupId>
<artifactId>spring</artifactId>
<version>0.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
...
</dependencies>
</project>
Oh, the humanity!
Quite. All this XML became a bit too uncomfortable to navigate around. Furthermore, most of the Scala projects out there use SBT, which promises to be as powerful, but much more concise build tool. So, I set out to replace Maven's verbose XML with SBT's... call it scripts.
SBT Scripts?
SBT is essentially a domain-specific language for building projects. SBT (the tool) then runs the Scala program that is assembled from the various script files as well as full-blown Scala sources. To make life easier, SBT maintains two sets of files: the .sbt
files are decorated into the full Scala syntax and then compiled together with the grown-up Scala code. <ins>SBT decorates the body of the .sbt
file to become a compilation unit (put simply, a class with all imports and functions resolved). Every block in the .sbt
file then becomes a function in the resulting compilation unit. SBT uses empty lines to demarcate what is to become the functions, which is why every "statement" in the .sbt
file needs to be on its own line and why you cannot have empty lines in multi-line "statements". (Many thanks to @plalloni for clarification and comments!)</ins> SBT then executes the resulting Scala program to build your project. There is much more detail at SBT's documentation at https://github.com/harrah/xsbt/wiki. Let's take a look at how I've transformed the multi-module Maven beast into SBT.
Multi-module SBT
First, let's take a look at a typical SBT project. It contains the build.sbt
file (that gets rewritten into the grown-up Scala program that then compiles your code, but you already knew that!). A SBT project also needs the source files, which are in the usual Maven structure. So, a single SBT project typically looks like this:
1234567891011src
main
java
scala
resources
test
java
scala
resources
build.sbt
SBT is smart enough to work out that the files in the java
directory are to be compiled using the Java compiler; that the files in the scala
directory need to be compiled using the Scala compiler and that the files in resources
are not to be compiled, simply copied to the output.
Now, in Specs2 Spring, I have five projects, so the first approach was to include the build.sbt
in every sub-project:
1234567891011121314151617181920212223org.specs2.spring
src
...
build.sbt
org.specs2.spring-example
src
...
build.sbt
org.specs2.spring.web
src
...
build.sbt
org.specs2.spring.web-example
src
...
build.sbt
org.specs2.spring.documentation
src
...
build.sbt
build.sbt
The structure is clear[-ish]: we have five modules, each module's build.sbt
describes how it is to be built; and there is a main build.sbt
, which should build all modules in the right sequence.
The situation is slightly more complicated. There is no provision for project dependencies in the simplified syntax of the .sbt
files. (Recall that they are transformed into Scala by SBT.)
In order to have multi-module SBT projects, we need to define a Scala source that represents the project build. We do so by creating the project
directory at the same level as the modules and adding the Build.scala
file, which contains object that extends sbt.Build
. So, we have:
12345678910111213141516org.specs2.spring
build.sbt
org.specs2.spring-example
build.sbt
org.specs2.spring.web
build.sbt
org.specs2.spring.web-example
build.sbt
org.specs2.spring.documentation
build.sbt
project
Build.scala
build.sbt
The interesting file is the Build.scala
, which defines the "modules" that make up the project and that sets the dependencies between the modules. It is surprisingly simple:
12345678910111213141516171819202122232425import sbt._
object Specs2Spring extends Build {
lazy val root =
Project("specs2-spring", file("."))
aggregate(core, coreExample, documentation, web, webExample)
lazy val core =
Project("org.specs2.spring", file("org.specs2.spring"))
lazy val coreExample =
Project("org.specs2.spring-example",
file("org.specs2.spring-example")) dependsOn(core)
lazy val web =
Project("org.specs2.spring.web",
file("org.specs2.spring.web")) dependsOn(core)
lazy val webExample =
Project("org.specs2.spring.web-example",
file("org.specs2.spring.web-example")) dependsOn(web)
lazy val documentation =
Project("org.specs2.spring.documentation",
file("org.specs2.spring.documentation"))
}
Notice that the object Specs2Spring
extends sbt.Build
and defines the variables that represent the projects. We have the root
project, which is simply an aggregate
of the remaining projects, which are each defined in their own variable. Projects can define dependencies using the dependsOn
function, specifying the project variable of the dependency. How simple!
Now that we have the Specs2Spring
project build source out of the way, let's take a look at the smaller build.sbt
files that complete the picture. By far the most complex is the org.specs2.spring/build.sbt
:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354/** Project */
name := "specs2-spring"
version := "0.3"
organization := "org.specs2"
scalaVersion := "2.9.1"
crossScalaVersions := Seq("2.9.0")
/** Dependencies */
resolvers ++= Seq("snapshots-repo" at "http://scala-tools.org/repo-snapshots")
libraryDependencies <<= scalaVersion { scala_version => Seq(
"org.specs2" %% "specs2" % "1.7.1",
"junit" % "junit" % "4.7" % "optional",
"org.springframework" % "spring-core" % "3.1.0.RELEASE",
"org.springframework" % "spring-beans" % "3.1.0.RELEASE",
"org.springframework" % "spring-jdbc" % "3.1.0.RELEASE",
"org.springframework" % "spring-tx" % "3.1.0.RELEASE",
"org.springframework" % "spring-orm" % "3.1.0.RELEASE",
"org.springframework" % "spring-test" % "3.1.0.RELEASE",
"org.hibernate" % "hibernate-core" % "3.6.0.CR1",
"javax.mail" % "mail" % "1.4.1",
"javax.transaction" % "jta" % "1.1",
"com.atomikos" % "transactions-jta" % "3.7.0",
"com.atomikos" % "transactions-jdbc" % "3.7.0",
"org.apache.activemq" % "activemq-core" % "5.4.1"
)
}
/** Compilation */
javacOptions ++= Seq()
javaOptions += "-Xmx2G"
scalacOptions ++= Seq("-deprecation", "-unchecked")
maxErrors := 20
pollInterval := 1000
logBuffered := false
cancelable := true
testOptions := Seq(Tests.Filter(s =>
Seq("Spec", "Suite", "Unit", "all").exists(s.endsWith(_)) &&
!s.endsWith("FeaturesSpec") ||
s.contains("UserGuide") ||
s.contains("index") ||
s.matches("org.specs2.guide.*")))
In the build.sbt
file, you can see that I specify some common settings (name, version, Scala version); the managed libraries (dependencies in Maven speak) and I configure the parameters of the compiler. But that's all I need to do!
The remaining build.sbt
files can be much simpler, because SBT compiles the project/Build.scala
and all transformed build.sbt
files into a single program that then builds your project. Let's pick the org.specs2.spring-example/build.sbt
as an example:
12345678910111213libraryDependencies <<= scalaVersion { scala_version => Seq(
"org.springframework" % "spring-core" % "3.1.0.RELEASE",
"org.springframework" % "spring-beans" % "3.1.0.RELEASE",
"org.springframework" % "spring-jdbc" % "3.1.0.RELEASE",
"org.springframework" % "spring-tx" % "3.1.0.RELEASE",
"org.springframework" % "spring-orm" % "3.1.0.RELEASE",
"org.hibernate" % "hibernate-core" % "3.6.0.CR1",
"org.hibernate" % "hibernate-validator" % "4.0.2.GA",
"javassist" % "javassist" % "3.4.GA",
"org.hsqldb" % "hsqldb" % "2.2.4"
)
}
That is all!; you can now run sbt compile
, sbt test
, sbt publish-local
and many others. sbt
is a shell script; one example is at https://github.com/harrah/xsbt/wiki/Getting-Started-Setup, but Paul Phillips created far more powerful version, which you can download at https://github.com/paulp/sbt-extras.
The devil in the detail
Now, we have a successful multi-module project in SBT. Everything builds, all libraries are downloaded and the dependencies between the projects work as well. But what about DocBook? In Maven, I used to use the com.agilejava.docbkx:docbkx-maven-plugin
plugin, which took care of generating the HTMLs and PDFs.
Luckily, SBT supports similar plugin infrastructure. All we need to do is to include the appropriate plugins in our project and we can run tasks that the plugins expose. To do what I just described, we need to create the project/plugins.sbt
file that lists the required plugins in the project that requires the plugin. So, taking our org.specs2.spring.documentation
module (or sub-project, if you like), we need to create the following structure:
123456789101112131415org.specs2.spring
...
org.specs2.spring.documentation
project
plugins.sbt
src
main
docbook
...
build.sbt
...
project
Build.scala
build.sbt
The interesting file is the plugins.sbt
, which defines the plugins our project requires. The org.specs2.spring.documentation/project/plugins.sbt
contains just a single line:
12addSbtPlugin("de.undercouch" % "sbt-docbook-plugin" % "0.2-SNAPSHOT")
The "de.undercouch" % "sbt-docbook-plugin" % "0.2-SNAPSHOT"
plugin requires some properties to be set in the build.sbt
, namely the sourceFilter
and, because we have our own XSL for the PDF, the docBookXslFoStyleSheet
. So, in the org.specs2.spring.documentation/build.sbt
, we have:
1234sourceFilter := "**/*.xml"
docBookXslFoStyleSheet in DocBook:= "src/docbkx/styles/pdf/custom.xsl"
I updated the plugin to the latest version of Scala and SBT and sent a pull request to the original author; my sources are at https://github.com/janm399/sbt-docbook-plugin (for the future of the plugin, see the open issues).
Summary
So, what can you achieve? Put simply, the same build functionality in approximately quarter lines of code. Even if you do not use any of the advanced features of SBT, you have gained a more intuitive way of building Java and Scala applications; you can also release your libraries for the correct version of the Scala compiler (no more appending _2.9.1
, _2.9.0_1
and similar to your Maven dependencies!). The artefacts that SBT produces fit directly into the Scala repositories without additional effort.
With this structure, the project will build just like the Maven monster it replaced! As usual, the devil was in the details, so keep reading.
P.S. All SBT goodness is at the SBT master branch of https://github.com/janm399/specs2-spring I will make some final modifications (namely get rid of the now superfluous root (With all the changes applied.)project
directory) in preparation for the 0.4 release.
P.P.S. All signs point to Specs2 Spring being available on the scala-tools.org repo in the next few days!
Come from: http://www.cakesolutions.net/teamblogs/2012/01/04/maven-to-sbt