Your build tool is your good friend: what sbt can do for Java developer
I think for developers picking the right build tool is a very important choice. For years I have been sticking to Apache Maven and, honestly, it does the job well enough, even nowadays it’s a good tool to use. But I always feel it could be done much better … and then Gradle came along …
Despite many hours I have spent getting accustomed to Gradle way to do things, I finally gave up and switched back to Apache Maven. The reason – I didn’t feel comfortable with it, mostly because of Groovy DSL. Anyway, I think Gradle is great, powerful and extensible build tool which is able to perform any task your build process needs.
But engaging myself more and more with Scala, I quickly discovered sbt. Though sbt is acronym for “simple build tool“, my first impression was quite a contrary: I found it complicated and hard to understand. For some reasons, I liked it nonetheless and by spending more time reading the documentation (which is getting better and better), many experiments, I finally would say the choice is made. In this post I would like to show up couple of great things sbt can do to make Java developer’s life easy (some knowledge of Scala would be very handy, but it’s not required).
Before moving on to real example, couple of facts about sbt. It uses Scala as a language for build scenario and requires a launcher which could be downloaded from here (the version we’ll be using is 0.13.1). There are several ways to describe build in sbt, the one this post demonstrates is by using Build.scala with single project.
Our example is a simple Spring console application with couple of JUnit test cases: just enough to see how build with external dependencies is structured and tests are run. Application contains only two classes:
package com.example; import org.springframework.stereotype.Service; @Service public class SimpleService { public String getResult() { return "Result"; } }
and
package com.example; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.GenericApplicationContext; public class Starter { @Configuration @ComponentScan( basePackageClasses = SimpleService.class ) public static class AppConfig { } public static void main( String[] args ) { try( GenericApplicationContext context = new AnnotationConfigApplicationContext( AppConfig.class ) ) { final SimpleService service = context.getBean( SimpleService.class ); System.out.println( service.getResult() ); } } }
Now, let see how sbt build looks like. By convention, Build.scala should be located in project subfolder. Additionally, there should be present build.properties file with desired sbt version and plugins.sbt with external plugins (we will use sbteclipse plugin to generate Eclipse project files). We will start with build.properties which contains only one line:
sbt.version=0.13.1
and continue with plugins.sbt, which in our case is also just one line:
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
Lastly, let’s start with the heart of our build: Build.scala. There would be two parts in it: common settings for all projects in our build (useful for multi-project builds but we have only one now) and here is the snippet of this part:
import sbt._ import Keys._ import com.typesafe.sbteclipse.core.EclipsePlugin._ object ProjectBuild extends Build { override val settings = super.settings ++ Seq( organization := "com.example", name := "sbt-java", version := "0.0.1-SNAPSHOT", scalaVersion := "2.10.3", scalacOptions ++= Seq( "-encoding", "UTF-8", "-target:jvm-1.7" ), javacOptions ++= Seq( "-encoding", "UTF-8", "-source", "1.7", "-target", "1.7" ), outputStrategy := Some( StdoutOutput ), compileOrder := CompileOrder.JavaThenScala, resolvers ++= Seq( Resolver.mavenLocal, Resolver.sonatypeRepo( "releases" ), Resolver.typesafeRepo( "releases" ) ), crossPaths := false, fork in run := true, connectInput in run := true, EclipseKeys.executionEnvironment := Some(EclipseExecutionEnvironment.JavaSE17) ) }
The build above looks quite clean and understandable: resolvers is a straight analogy of Apache Maven repositories, EclipseKeys.executionEnvironment is customization for execution environment (Java SE 7) for generated Eclipse project. All these keys are very well documented.
Second part is much smaller and defines our main project in terms of dependencies and main class:
lazy val main = Project( id = "sbt-java", base = file("."), settings = Project.defaultSettings ++ Seq( mainClass := Some( "com.example.Starter" ), initialCommands in console += """ import com.example._ import com.example.Starter._ import org.springframework.context.annotation._ """, libraryDependencies ++= Seq( "org.springframework" % "spring-context" % "4.0.0.RELEASE", "org.springframework" % "spring-beans" % "4.0.0.RELEASE", "org.springframework" % "spring-test" % "4.0.0.RELEASE" % "test", "com.novocode" % "junit-interface" % "0.10" % "test", "junit" % "junit" % "4.11" % "test" ) ) )
The initialCommands requires a bit of explanation here: sbt is able to run Scala console (REPL) and this setting allows to add default import statements so we can use our classes immediately. The dependency to junit-interface allows sbt to run JUnit test cases and it’s the first thing we’ll do: add some tests. Before creating actual tests, we will start sbt and ask it to run test cases on every code change, just like that:
sbt ~test
While sbt is running, we will add a test case:
package com.example; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertThat; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.support.GenericApplicationContext; import com.example.Starter.AppConfig; public class SimpleServiceTestCase { private GenericApplicationContext context; private SimpleService service; @Before public void setUp() { context = new AnnotationConfigApplicationContext( AppConfig.class ); service = context.getBean( SimpleService.class ); } @After public void tearDown() { context.close(); } @Test public void testSampleTest() { assertThat( service.getResult(), equalTo( "Result" ) ); } }
In a console we should see that sbt picked the change automatically and run all test cases. Sadly, because of this issue which is already fixed and should be available in next release of junit-interface, we cannot use @RunWith and @ContextConfiguration annotation to run Spring test cases yet.
For TDD practitioners it’s an awesome feature to have. The next terrific feature we are going to look at is Scala console (RELP) which gives as the ability to play with application without actually running it. It could be invoked by typing:
sbt console
and observing something like this in the terminal (as we can see, the imports from initialCommands are automatically included):
At this moment playground is established and we can do a lot of very interesting things, for example: create context, get beans and call any methods on them:
sbt takes care about classpath so all your classes and external dependencies are available for use. I found this way to discover things much faster than by using debugger or other techniques.
At the moment, there is no good support for sbt in Eclipse but it’s very easy to generate Eclipse project files by using sbteclipse plugin we’ve touched before:
sbt eclipse
Awesome! Not to mention other great plugins which are kindly listed here and the ability to import Apache Maven POM files using externalPom() which really simplifies the migration. As a conclusion from my side, if you are looking for better, modern, extensible build tool for your project, please take a look at sbt. It’s a great piece of software built on top of awesome, consise language.
- Complete project is available on GitHub.