The Spring Framework and especially
Spring Boot, are very
popular and widely used for programming applications in
Java. For Scala, there are other popular frameworks, particularly for
programming web servers, such as the Play
Framework.
In this small series of articles, we want to explore whether and
to what extent we can program functionally in Scala while still
using Spring Boot.
The Spring Framework consists of a whole range of elements,
some of which form a kind of central core while others, building on
the core, have more of a library character.
Dependency Injection
Among the central elements is a mechanism for dependency
injection, also called auto-wiring. In various highly configurable
ways, this finds classes that appear as arguments or members in other classes
or methods and instantiates
them automatically at runtime.
Annotations
Spring and Spring Boot also rely heavily on annotations to
mark classes and methods for various purposes and to
add code to them. Alternatively, this can at least partially be
done via a configuration file (XML or YAML), but that
isn‘t very popular since these files can quickly become very large.
Spring Boot
Spring Boot is a collection of libraries designed to make handling and
getting started with the Spring Framework easier. This happens primarily
through so-called starters, which bring many defaults with them
and are intended to make it easier to implement typical use cases.
A list of starters can be found
here.
The focus is primarily on web servers and database connections.
Example Application
In a small example, we‘ll now explore what this looks like in practice with
Scala and as much functional programming as possible.
We‘ll implement a very simple (and morbid) game in which armadillos venture
onto a highway and may get run over. Based on a functional core, we‘ll use
Spring Boot to implement a REST API for the game.
Let‘s start with the functional core. A Dillo (short for Armadillo) has a name
and a health level. When it gets run over, the health level decreases depending
on the speed. Additionally, it can say hello as long as it‘s still alive:
case class Dillo(name: String, health: Int)
object Dillo {
def runOver(d: Dillo, speed: Int): Dillo = {
d.copy(health = Math.max(0, d.health - speed))
}
def speak(d: Dillo): String = {
if (d.health > 0)
s"${d.name} says Hi!"
else
s"${d.name} is dead"
}
}
Then we define a highway with armadillos that are
on it, and a method that lets the Dillos say something:
case class Highway(dillos: Map[UUID, Dillo]) {
def report(): Iterable[String] = {
for (_id, d) <- dillos
yield Dillo.speak(d)
}
}
We abstract functions that do something with the highway as Highway.Op - a
so-called arrow. One of them is driveAlong, which runs over all armadillos
once. A second one adds a new armadillo to the highway:
object Highway {
type Op = Highway => Highway
val empty: Highway = Highway(Map.empty)
def driveAlong(speed: Int): Op = { highway =>
highway.copy(dillos = highway.dillos.view.mapValues(Dillo.runOver(_, speed)).toMap)
}
def spawn(id: UUID, dillo: Dillo): Op = { highway =>
highway.copy(dillos = highway.dillos + (id -> dillo))
}
}
So much for the functional core of our application. Now let‘s
move on to the REST API.
For our example, it‘s sufficient to include the Spring Boot starter for web
applications: spring-boot-starter-web. In SBT we can do this as follows:
libraryDependencies += "org.springframework.boot" % "spring-boot-starter-web" % "3.4.2"
Then we first define a class that represents our application:
import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication
class DemoApp {}
The SpringBootApplication annotation implicitly brings a whole series of
additional annotations with it. In particular, the so-called ComponentScan,
which causes the Spring Framework to find classes in the application‘s
classpath for dependency injection (so-called components). Also the
EnableAutoConfiguration annotation from Spring Boot, which sets certain
defaults depending on the libraries in the classpath and the presence (or
absence!) of certain classes in the namespace of the application class.
Therefore, just by adding a class or annotation, or by adding a library, the
behavior of the application can change.
As the entry and starting point of our application, we now
define a main method for the companion object of DemoApp:
object DemoApp {
def main(args: Array[String]): Unit = {
import org.springframework.boot.SpringApplication
SpringApplication.run(classOf[DemoApp], args: _*)
}
}
This is enough to have an executable application that first
outputs a wonderful Spring logo:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.4.2)
followed by some log messages showing that
Spring Boot by default starts an embedded Apache Tomcat web server
on port 8080 and registers our application in it as a servlet.
However, this is initially still empty. So let‘s define a
so-called Controller to process HTTP requests. The
easiest way to do this is with a class using the
RestController annotation:
import org.springframework.web.bind.annotation.{GetMapping, RestController}
@RestController
class DemoController {
@GetMapping() def index: String = "Hello world"
}
Spring Boot finds this class and binds it accordingly into
the servlet. If we now start the application and send a
request to "/", we get a corresponding response:
$ curl http://localhost:8080/
Hello World
However, to make our game „playable“ via such a RestController, we need to
figure out how to manage the state of the game (or of the highway) over time.
Since Spring Boot unfortunately doesn‘t offer any functional interfaces for
this, we have to work imperatively. What we can do, however, to delay this
decision as long as possible, is define a trait:
trait IAppState {
def get: Highway
def update(op: Highway.Op): Unit
}
We can then use this in our controller to return the highway report as a
response to "/":
@RestController
class DemoController(state: IAppState) {
@GetMapping(value = Array("/")) def index: String =
state.get.report().mkString("\n")
}
If we now start our application, Spring complains that it can‘t find an
implementation of IAppState. So let‘s define an implementation that stores
the state in an AtomicReference:
import org.springframework.stereotype.Component
@Component
class MemAppState extends IAppState {
import java.util.concurrent.atomic.AtomicReference
private val highway: AtomicReference[Highway] = AtomicReference[Highway](Highway.empty)
def get: Highway = highway.get()
def update(op: Highway.Op): Unit = {
highway.updateAndGet({ highway => op(highway) })
}
}
Through the Component annotation, we make the class discoverable for Spring.
The framework creates an instance of our MemAppState class when the
application starts in order to create the controller with it. If multiple
classes exist that implement IAppState, Spring aborts the application with a
corresponding error, since it can‘t decide which one to use.
So that something can actually be changed on the highway via the HTTP
interface, we now define a request to add an armadillo and one to drive along
the highway:
@RestController
class DemoController(state: IAppState) {
@GetMapping() def index: String =
state.get.report().mkString("\n")
@PostMapping(Array("/dillo")) def dillo(name: String): UUID = {
val id = UUID.randomUUID()
state.update(Highway.spawn(id, Dillo(name, health = 100)))
id
}
@PostMapping(Array("/drive")) def drive(speed: Int): Unit = {
state.update(Highway.driveAlong(speed))
}
}
The default path of the mappings is always „/“, regardless of what the method
is called. The subpaths „/dillo“ and „/drive“ must be explicitly declared as
Array in Scala.
A big pitfall exists in Scala 3 before version 3.6, where you can write the
annotation as it appears in many Java tutorials for Spring:
However, this code compiles to something different than in Java and sets the
name of the annotation instead of the value. You‘ll notice this either when
the requests don‘t work or when Spring complains about multiply declared paths.
Now we can play our game with CURL!
$ curl -X POST -d "name=Sammy" localhost:8080/dillo
"5ee5db86-8906-4a9e-a37c-543d69015455"
$ curl -X POST -d "speed=80" localhost:8080/drive
$ curl -X POST -d "name=Mona" localhost:8080/dillo
"d39f99ee-6405-4bce-8ee3-0157b1c4adcb"
$ curl -X POST -d "speed=90" localhost:8080/drive
$ curl localhost:8080/
Sammy is dead
Mona says Hi!
The names of the parameters of the two methods dillo and drive are by
default also the names of the form parameters in the POST request.
Conclusion
So we were able to create a web server with Spring Boot and connect our
functional application core to it. That‘s not bad at all, and in some
situations it may be a good alternative to the Play Framework or other Scala
libraries.
In a follow-up article, we‘ll explore whether and how we can use Spring Data to
store the application state in a database.