I've been away for a while and I've been playing with Finatra, so, I decided to write this post because I really enjoyed using it.
Lately I've been using Scala a lot in my personal projects and even for testing my Java applications (it is much better than using JUnit, believe me), so, I also tried a few scala web frameworks out and I found out that Finatra is the easiest one (at least for me).
First of all, what is Finatra? Well, it is a web framework created by Twitter and built on TwitterServer and Finagle. You can check their home page over here. With Finatra, you can build http and thrift services that are fast and testable.
We also are using Slick, which is, according to their web site, a modern database query and access library for Scala. With Slick you can easily access your database using the Scala functional paradigm in a reactive way.
For testing porpuses, we are using H2 as our DB, but, you can use any database that Slick supports. You can check if your database is supported over here.
I also am using Gradle in this post (because it is simpler and much less verbose than SBT), so, if you want to use SBT, please, check this github repository.
We should start with our build.gradle:
apply plugin: 'scala'
apply plugin: 'gradle-one-jar'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.github.rholder:gradle-one-jar:1.0.4'
}
}
repositories {
jcenter()
maven {
url "https://maven.twttr.com"
}
}
jar.archiveName = "finatra-slick-microservice-nolibs.jar"
task uberJar(type: OneJar) {
mainClass = 'org.thedevpiece.finatra.slick.microservices.ApplicationHttpServerMain'
archiveName = 'finatra-slick-microservice.jar'
}
final scalaVersion = "_2.11"
final finatra = "2.6.0"
final slick = "3.1.1"
final scalaAsync = "0.9.5"
final twitterBijection = "0.9.2"
final logback = "0.9.28"
dependencies {
compile 'org.scala-lang:scala-library:2.11.8'
compile "com.twitter:finatra-http$scalaVersion:$finatra"
compile "com.twitter:finatra-slf4j$scalaVersion:$finatra"
compile "com.twitter:finatra-jackson$scalaVersion:$finatra"
compile "com.twitter:inject-server$scalaVersion:$finatra"
compile "com.twitter:inject-app$scalaVersion:$finatra"
compile "com.twitter:inject-modules$scalaVersion:$finatra"
compile "com.typesafe.slick:slick$scalaVersion:$slick"
compile "com.h2database:h2:1.4.181"
compile "org.scala-lang.modules:scala-async$scalaVersion:$scalaAsync"
compile "org.scala-lang.modules:scala-async$scalaVersion:$scalaAsync"
compile "com.twitter:bijection-util$scalaVersion:$twitterBijection"
compile "com.twitter:bijection-core$scalaVersion:$twitterBijection"
compile "ch.qos.logback:logback-classic:$logback"
testCompile "com.twitter:finatra-http$scalaVersion:$finatra:tests"
testCompile "com.twitter:finatra-slf4j$scalaVersion:$finatra:tests"
testCompile "com.twitter:finatra-jackson$scalaVersion:$finatra:tests"
testCompile "com.twitter:inject-server$scalaVersion:$finatra:tests"
testCompile "com.twitter:inject-app$scalaVersion:$finatra:tests"
testCompile "com.twitter:inject-core$scalaVersion:$finatra:tests"
testCompile "com.twitter:inject-modules$scalaVersion:$finatra:tests"
testCompile 'junit:junit:4.12'
testCompile "org.scalatest:scalatest$scalaVersion:3.0.0"
testCompile "org.specs2:specs2$scalaVersion:2.3.12"
testCompile "com.google.inject.extensions:guice-testlib:4.1.0"
}
Our first class is a repository using Slick to access the database and Guice as our dependency injector:
package org.thedevpiece.finatra.slick.microservices.domain
import com.google.inject.{Inject, Singleton}
import slick.driver.JdbcProfile
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
@Singleton
class UserRepository @Inject()(val driver: JdbcProfile) {
import driver.api._
/*
We are assuming we have a table named 'USERS' that has the following columns:
- IDT_USER (number, auto inc, PK)
- DES_USERNAME (varchar)
- NUM_AGE (number)
- DES_OCCUPATION (varchar)
*/
class Users(tag: Tag) extends Table[(Option[Long], String, Int, String)](tag, "USERS") {
def id = column[Long]("IDT_USER", O.AutoInc, O.PrimaryKey)
def username = column[String]("DES_USERNAME")
def age = column[Int]("NUM_AGE")
def occupation = column[String]("DES_OCCUPATION")
override def * = (id.?, username, age, occupation)
}
val users: TableQuery[Users] = TableQuery[Users]
val db = Database.forConfig("database")
def findById(id: Long): Future[Seq[(Option[Long], String, Int, String)]] = {
val query = users.filter(_.id === id).result
db.run(query).map({
case Nil => None
case x +: Nil => Some(x)
})
}
def create(user: (Option[Long], String, Int, String)): Future[Option[Long]] = {
val action: DBIO[Seq[Long]] = (users returning users.map(_.id)) ++= List(user)
db.run(action).map({
case Nil => None
case x +: Nil => Some(x)
})
}
}
We need to provide a JdbcProfile to Guice inject into our UserRepository class, so:
package org.thedevpiece.finatra.slick.microservices.modules
import com.google.inject.{ Provides, Singleton }
import com.twitter.inject.TwitterModule
import slick.driver.{ H2Driver, JdbcProfile }
object DefaultModule extends TwitterModule {
@Singleton
@Provides
def jdbcDriver(): JdbcProfile = H2Driver
}
Finatra uses Jackson to serialize Scala objects into JSON, so, we need to create our models to be serialized:
package org.thedevpiece.finatra.slick.microservices.api
object Resources {
case class Message(message: String)
case class User(id: Option[Long], username: String, age: Int, occupation: String)
}
We have two services in our UserEndpoint: create and findById. Since Finatra uses the Twitter util classes, we must convert our scala.concurrent.Future to their com.twitter.util.Future, knowing that Finatra is reactive, so, you should not block threads in your app:
package org.thedevpiece.finatra.slick.microservices.utils
import com.twitter.bijection.Conversion._
import com.twitter.bijection.twitter_util.UtilBijections.twitter2ScalaFuture
import com.twitter.util.{ Future => TwitterFuture }
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.{ Future => ScalaFuture }
object FutureUtils {
implicit class FutureImplicit[A](val x: ScalaFuture[A]) {
def asTwitterFuture: TwitterFuture[A] = x.as[TwitterFuture[A]]
}
}
And then we can create our Endpoint:
package org.thedevpiece.finatra.slick.microservices.api
import com.google.inject.{Inject, Singleton}
import com.twitter.finagle.http.Request
import com.twitter.finatra.http.Controller
import org.thedevpiece.finatra.slick.microservices.utils.FutureUtils._
import org.thedevpiece.finatra.slick.microservices.api.Resources.{Message, User}
import org.thedevpiece.finatra.slick.microservices.domain.UserRepository
import scala.concurrent.ExecutionContext.Implicits.global
import scala.language.postfixOps
@Singleton
class UserEndpoint @Inject()(repository: UserRepository) extends Controller {
get("/users/:id") { request: Request =>
repository
.findById(request.getLongParam("id"))
.map({
case None => response.notFound(Message(s"Resource with id: ${request.getLongParam("id")} not found"))
case Some(user) => response.ok((User.apply _).tupled(user))
}) asTwitterFuture //converting scala future to twitter future
}
post("/users") { user: User =>
repository
.create(User.unapply(user).get)
.map({
case None => response.internalServerError(Message("Error while creating the user"))
case Some(id) => response.created.location("/users/" + id)
}) asTwitterFuture //converting scala future to twitter future
}
}
Our last class will be ApplicationHttpServer:
package org.thedevpiece.finatra.slick.microservices
import com.twitter.finatra.http.HttpServer
import com.twitter.finatra.http.routing.HttpRouter
import org.thedevpiece.finatra.slick.microservices.api.UserEndpoint
import org.thedevpiece.finatra.slick.microservices.modules.DefaultModule
class ApplicationHttpServer extends HttpServer {
override val defaultFinatraHttpPort: String = ":8080"
override val modules = Seq(DefaultModule)
override protected def configureHttp(router: HttpRouter): Unit = {
router
.add[UserEndpoint]
}
}
object ApplicationHttpServerMain extends ApplicationHttpServer {
}
In this class we configure our endpoint, but, you can also configure several other stuff, such as exception mappers, filters, etc. If you want to run your app, just run the ApplicationHttpServerMain and your app will be up.
Finally, add a file named application.conf to src/main/resources with the following lines to configure your database access:
database = {
url = "jdbc:h2:mem:test1"
driver = org.h2.Driver
connectionPool = disabled
keepAliveConnection = true
}
Just to show how it is easy to create tests using Finatra, we are going to create a feature test that assures that our services are working properly:
package org.thedevpiece.finatra.slick.microservices.api
import com.twitter.finagle.http.Status
import com.twitter.finatra.http.EmbeddedHttpServer
import com.twitter.inject.server.FeatureTest
import org.thedevpiece.finatra.slick.microservices.ApplicationHttpServer
import org.thedevpiece.finatra.slick.microservices.domain.UserRepository
import slick.driver.JdbcProfile
class UserEndpointTest extends FeatureTest {
override val server = new EmbeddedHttpServer(new ApplicationHttpServer)
override protected def beforeAll(): Unit = {
val driver = injector.instance[JdbcProfile]
val repository: UserRepository = injector.instance[UserRepository]
import driver.api._
//creating the schema of the table users
val insertActions = DBIO.seq(
repository.users.schema.create
)
repository.db.run(insertActions)
}
"Clients" should {
"be able to create user resources" in {
val response = server.httpPost(
path = "/users",
postBody =
"""{
"username": "Gabriel",
"age": 23,
"occupation": "Software Engineer"
}"""
)
response.status shouldBe Status.Created
response.location should not be empty
}
}
"Clients" should {
"be able to fetch a created user" in {
val response = server.httpPost(
path = "/users",
postBody =
"""{
"username": "Gabriel",
"age": 23,
"occupation": "Software Engineer"
}""",
andExpect = Status.Created
)
response.location should not be empty
val location = response.location.get
val id = location.split("/users/")(1)
server.httpGet(
path = location,
andExpect = Status.Ok,
withJsonBody = s"""{
"id": $id,
"username": "Gabriel",
"age": 23,
"occupation": "Software Engineer"
}"""
)
}
}
}
And that's all. I hope this post can be helpful to you. Probably soon I'll post more about other Scala frameworks.
Other posts about microservices
Building microservices with Akka HTTP and MongoDB
Building microservices using Undertow, CDI and JAX-RS
Building microservices with Kotlin and Spring Boot
Thank you, and, any questions, please, leave a comment!