Maven, SBT and Modularisation
Join the DZone community and get the full member experience.
Join For FreeLet me revisit the Maven to SBT post, and expand on the modularisation of Akka applications. I will also outline the Akka AMQP addition, which allows you to use AMQP brokers like my favourite RabbitMQ in your Akka applications. Again, it is a long post, so gather some food & drink to make it all the way to the end.
Maven to SBT
The first thing to tackle is the source code modularisation. How do we build & deploy our application and how do our choices influence the packaging and building approaches. I shall start with typical Maven strcutrue. Imagine a typical Akka application that constructs the important actor structure in the core
module, operating on the instances of classes in the domain
. We then expose the functionality in the api
and expose the API on HTTP in the web
module. To support our efforts throughout the code, we may even have the test
module, which helps us write code in the tests.
In a typical Maven beast, you’d create these modules (domain
, core
, …) as separate modules, with explicitly defined dependencies on the other modules. The reasoning is that this ensures that you don’t skip tiers; and that you can reuse portions of your application’s code. (Assuming you have some sort of company-wide artifact repository.)
On the face of it, this sounds good. But my experience with large project tells me that this almost never works as well as you think. The clear dependencies between the modules become tangled; and often the strict “separate project” structure prevents large refactorings. Finally, you almost always deploy the entire application, packaged up in, say a WAR. Our reasons for using multi-module Maven projects are no longer that relevant; and in most cases, they actually hurt us.
Out with Maven!
This is the line you’ve no doubt been waiting for. Out with Maven, in with SBT; and while we’re at it, let’s build single-module SBT project. We’ll build a small Akka application whose small portion connects RabbitMQ to deliver & route the messages. Naturally, we’ll include tests, including tests that exercise the AMQP componentry.
First of all, all our source code will sit in the src
directory. Because we also need some SBT plugins, we’ll create the project
directory. We will complete the picture with the build.sbt
file; along with a few good housekeeping files. We need something like this:
$ ls -la -rw-r--r-- 1 janmachacek staff 1155 17 Nov 16:42 README.md -rw-r--r-- 1 janmachacek staff 2389 17 Nov 16:42 build.sbt drwxr-xr-x 2 janmachacek staff 102 11 Nov 17:00 project drwxr-xr-x 4 janmachacek staff 136 11 Nov 17:00 src -rw-r--r-- 1 janmachacek staff 31 11 Nov 17:00 version.sbt
Turning to build.sbt
, we have the dependencies and other bits and pieces. Here it is in its entirety:
import sbtrelease._ /** Project */ name := "Akka Patterns" version := "1.0" organization := "org.cakesolutions.akkapatterns" scalaVersion := "2.10.0-RC2" /** Shell */ shellPrompt := { state => System.getProperty("user.name") + "> " } shellPrompt in ThisBuild := { state => Project.extract(state).currentRef.project + "> " } /** Dependencies */ resolvers += "spray repo" at "http://repo.spray.io" resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/" resolvers += "Sonatype OSS Releases" at "http://oss.sonatype.org/content/repositories/releases/" resolvers += "Sonatype OSS Snapshots" at "http://oss.sonatype.org/content/repositories/snapshots/" libraryDependencies <<= scalaVersion { scala_version => val sprayVersion = "1.1-M5" val akkaVersion = "2.1.0-RC2" Seq( "com.typesafe.akka" % "akka-kernel" % akkaVersion cross CrossVersion.full, "io.spray" % "spray-can" % sprayVersion, "io.spray" % "spray-routing" % sprayVersion, "io.spray" % "spray-httpx" % sprayVersion, "io.spray" % "spray-util" % sprayVersion, "io.spray" % "spray-client" % sprayVersion, "io.spray" % "spray-json" % "1.2.2" cross ..., "org.mongodb" % "mongo-java-driver" % "2.9.3", "com.aphelia" %% "amqp-client" % "1.0", "io.spray" % "spray-testkit" % sprayVersion % "test", "com.typesafe.akka" % "akka-testkit" % akkaVersion % "test" cross ..., "org.specs2" % "classycle" % "1.4.1" % "test", "org.specs2" % "specs2" % "1.12.2" % "test" cross ... ) } /** Compilation */ javacOptions ++= Seq("-Xmx1812m", "-Xms512m", "-Xss6m") 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.*"))) /** Console */ initialCommands in console := "import org.cakesolutions.akkapatterns._"
Nothing too funky. Let’s take a look at our src
directory, which contains the entire source code, split into the main
and the test
codebase. We run the main
code on the server and we run the test
when we want to check that the main
does the right thing. The packages we create in the bowels of the src
directory mimicks our Maven modules:
$ ls -la total 8 drwxr-xr-x 9 janmachacek staff 340 17 Nov 16:42 . drwxr-xr-x 3 janmachacek staff 102 10 Nov 13:06 .. drwxr-xr-x 2 janmachacek staff 238 17 Nov 16:42 api drwxr-xr-x 4 janmachacek staff 272 17 Nov 16:42 core drwxr-xr-x 2 janmachacek staff 272 17 Nov 16:42 domain drwxr-xr-x 2 janmachacek staff 136 17 Nov 16:42 main drwxr-xr-x 2 janmachacek staff 102 17 Nov 16:42 test drwxr-xr-x 2 janmachacek staff 102 17 Nov 16:42 web
This structure requires more discipline; you’re free to tangle your packages. Instead of relying on the rigid structure of our Maven build (or the multi-project SBT build), we will include tests that verify the architectural soundness of our codebase. We shall be using Specs2, so the test that verifies our architecture should be no surprise. (Note that you’ll need to compile & run the code using JDK 1.7.)
class ArchitectureSpec extends Specification with Analysis with ClassycleDependencyFinder { "The architecture" should { "Have properly defined layers" in { val ls = layers( "main", "web", "api", "core", "domain" ).withPrefix("org.cakesolutions.akkapatterns"). inTargetDir("target/scala-2.10") ls must beRespected } } }
So, instead of having separate Maven modules and having some of our architectual constraints expressed in the modules’ dependencies, we have moved our architectural tests to our testing code.
Deploying & running the modules
If our project includes just a single main
method, all that you have to do to boot your project is to type sbt run
and you’re up!
Suppose you now have two main components in your applicaiton; and you
connect these two components over AMQP. So, we now have two main
methods. Trying to execute sbt run
will ask which main
you want to execute? So, instead of sbt run
, we’ll need to say sbt run-main org.cakesolutions.akkapatterns.main.Server
to boot the core actors and sbt run-main org.cakesolutions.akkapatterns.Keystore
to run the key store component.
Even though we can control which component to run, we always deploy the entire codebase.
I’ll give you the details of the AMQP-based actor communication in the next few days.
Published at DZone with permission of Jan Machacek, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments