1. Introduction
If you’re a big, and I mean a seriously big fan of the Spring
Framework as I am, and never read something like the title of this article describes, you probably just got cold feet only by reading it. No annotations? It may sound like a bit of an insane idea. When I achieved the Spring
certification, it gave me the feeling of accomplishment. There I was Spring
certified. But a huge chunk of that certification path was about precisely the use of annotations. I had to learn all sorts of annotations. And there are quite a few, to say the least. The whole Spring
Framework is given to us as an annotation-driven framework nowadays. We can do anything with it, from programmatically changing the way beans start, how beans are created, we can create components, services, repositories, apply the decorator, IoC, adaptor, MVC
patterns, we can create triggers for specific exceptions, handlers for events, health endpoints, we can implement the Observer pattern in a seamless way, use AOP
with OOP
, implement batch jobs, STOMP
pattern for WebSockets
, conditional beans, use reactive programming, and I could go on forever. The point I’m trying to make is that for years we’ve been educated and pressured to leave an XML-driven framework as it happened up to Spring
2.5. to move on to a full and complete annotation-driven capable framework since Spring
4. Finally, in Spring
5 we get support for reactive implementations over project reactor:
Now that we’ve learned all of this, at the same time that we have Spring
5 and Spring Boot
2.5.6.
moving forward, a team of the Spring
Experimental Projects is developing a whole new version of Spring
called Spring-Fu
. Fu, means functional. They have developed two sub-versions of this: KoFu
and JaFu
. Spring KoFu
is a Kotlin-based DSL
and Spring JaFu
is a Java-based DSL
. The project has started on GitHub on the 31st of May 2018
. There is currently some buzz around it given that the Spring-Fu
team has apparently achieved better service startups and faster processing. This article is my research into this matter to find signs of these claims. All the results and implementations are available on GitHub.
2. What we are trying to find
We are going to explore the responses of three simple services. Two are reactive and one is non-reactive. One is implemented with Spring-Fu
for Kotlin also known as Spring KoFu
. The other two are implemented in a traditional MVC
way. One is reactive and the other one is not.
The ultimate goal of reactive programming is to allow services to be more resilient and use resources in the most effective way in order to process as many possible requests as possible. The way it does this is to deliver the responsibility of processing our requests to publishers. In Spring
, they are known as Mono<T>
’s and Flux<T>
’s. For fast processing, this causes the service to be highly available, which means that a non-reactive service will be unavailable much quicker than a reactive service. This is because the response in the former case is given back directly and so the client must wait until the request is processed, before performing a new one. For long processing requests, however, the story can be different. Since the reactive service keeps on accepting requests regardless of the previous ones being processed or not, this also means that it needs to be much more resilient than the traditional non-reactive service. As a result, a reactive service will become unresponsive much quicker, should the processing of one request take too long.
Finally, what we want to prove is, on one hand, that described theory stands. On the other hand, by comparing the two reactive services, we want to see if one is faster than the other while processing multiple fast processing requests. We also want to prove that the Spring-Fu
services start faster. We may also not want to prove anything and just see signs that the Spring-Fu
team is on to something.
Please keep in mind that terms like non-reactive, reactive, and KoFu are used in this article interchangeably with terms like Spring Web
, Spring WebFlux
, and Spring KoFu
.
3. Biases
Proving and testing something like this can be very difficult and inconclusive on a local machine. A MacBook Pro or a Chrome machine or any Linux machine, even on Windows, can have its own ways of processing, interpreting, and performing. We also, when looking at new technologies, may project some of our own personal feeling to it. This is the reason why all the test results I’m presenting throughout the article have been done multiple times and I only present the ones that gave consistent results. All the other tests, including these also, are available in the reporting folder of the repo on GitHub.
4. Case
The case for this article is essentially just a list of randomly generated ISBN 13 numbers. There are two static sources of ISBN
13 codes. One is a big list of half a million ISBN
and the other is a source of 50 ISBNs
. The first case will simulate the case of long-lasting and heavy data requests. The latter will simulate fast processing requests. We could argue that there is actually no processing involved. The idea is not to block any part of the running application and simulate fast requests. The processing will indeed spend some short time on de server and that is what we want to explore. We will then have three services implemented as discussed before as the following diagram explains:
For these kinds of things, I think it's also important to understand this from a sequence diagram perspective.
Essentially for every service, we request ISBN from designated endpoints and then wait for a reply. We are interested in seeing in practical terms, how the services behave, how long it takes to get the requests, how many requests can they take, and how responsive and possibly resilient they are.
Now that we have had a look into the design, let’s now have a look at the following step, which is the implementation.
5. Implementation
Assuming you already know how to implement services in Spring/Spring boot
, I just want to, in any case, have a quick run through all of the different implementations and the effort I made to minimize the differences on a functional level. Also, in this case, there is still a small chance of bias or inaccurate reading. The way we implement the simplest of REST
responses can dictate how reliable our tests are going to be. In these implementations, the service layer and the data layer do not exist. Data is being fetched directly from memory out of two lists initialized upon startup.
5.1. Traditional Spring Web MVC
Blocking REST
service
It is now fairly accepted that the old Spring Web MVC
is slowly being replaced by better software design patterns or an extension of it. In this implementation, however, we just want to make sure of two important aspects. One, that we have a very basic implementation. Two, that the responses are identical or similar. Out of all the dependencies we need, the one we are actually investigating is this one:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
In regard to the implementation let’s look at how it’s being made:
@RestController
@RequestMapping("/api/traditional/non")
class ISBNController {
var mapper = ObjectMapper()
@GetMapping("hello")
fun getMessage(): String {
return MESSAGE
}
@GetMapping
fun getJsonMessage(): ObjectNode? {
val createObjectNode = mapper.createObjectNode()
createObjectNode.put("message", MESSAGE)
return createObjectNode
}
@GetMapping("isbns")
fun getIsbns(): List<IsbnDto> {
return ISBNS
}
@GetMapping("small/isbns")
fun getSmallIsbns(): List<IsbnDto> {
return SMALL_ISBNS
}
companion object {
const val MESSAGE = "I will now give you 1.000.000 ISBN numbers in a Spring MVC Non-Reactive way"
}
}
For this implementation and the following, we need to keep in mind that ISBNS
is our list of half a million ISBN
13 records and that SMALL_ISBNS
is our list of 50 records. Since the KoFu
code is declarative, I also wanted all the other code to be as declarative as possible where reason applies.The application context (the root path of the webservice) doesn’t have to be configured in the application.properties for these tests. Not only doesn’t have to, but it can be detrimental to a valid result. This is why I’m not using it. For the rest of the implementation we just need to keep two endpoints in mind: http://localhost:8080/api/traditional/non/isbns and http://localhost:8080/api/traditional/non/small/isbns.
5.2. Traditional Spring WebFlux MVC Reactive REST service
Reactive programming and the possibilities Spring has to offer can be very quickly reduced in practical terms to the use of Flux
and Mono
. As explained before, these are publishers responsible for processing the data. They may have extra subscribers associated with them. The dependencies we are studying in this case are:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>${spring-boot.version}</version>
</dependency>
And the implementation of the REST controller can be done in a very similar way:
@RestController
@RequestMapping("/api/traditional")
class ISBNController {
var mapper = ObjectMapper()
@GetMapping("hello")
fun getMessage(): Mono<String> {
return Mono.just(MESSAGE)
}
@GetMapping
fun getJsonMessage(): Mono<JsonNode> {
return Mono.fromCallable {
val createObjectNode = mapper.createObjectNode()
createObjectNode.put("message", MESSAGE)
createObjectNode
}
}
@GetMapping("isbns")
fun getIsbns(): Flux<IsbnDto> {
return Flux.fromIterable(ISBNS)
}
@GetMapping("small/isbns")
fun getSmallIsbns(): Flux<IsbnDto> {
return Flux.fromIterable(SMALL_ISBNS)
}
companion object {
const val MESSAGE = "I will now give you 1.000.000 ISBN numbers in a Spring MVC Reactive way"
}
}
As we can see here, we’ll be using Flux only. In very simple terms, this is how we publish a list of elements in a reactive way. This is thus the only difference in implementation which allows us to check for differences in behavior between the first and the second implementations knowing that those differences should come only from the fact that the second is reactive and the first one is not. This service will be, as we can see from the implementation, available on http://localhost:8081/api/traditional/isbns and http://localhost:8081/api/traditional/small/isbns.
5.3. Spring KoFu implementation
This implementation is where our case is centered. Here we use a radically different approach. Instead of annotation and as we said before, we will be implementing everything in a declarative way. In my opinion, it looks quite good right off the bat. For this, we’ll need a few very important dependencies. Before adding them to our repo, we need to make sure we can reach the spring experimental libraries repo. We need to add this to our pom file:
<repositories>
<repository>
<id>spring-milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
Then we can add the necessary dependencies:
<dependency>
<groupId>org.springframework.fu</groupId>
<artifactId>spring-fu-kofu</artifactId>
<version>${spring-fu-kofu.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>${spring-boot.version}</version>
</dependency>
We can see very clearly that we are also using web-flux behind Spring KoFu.
With this, we can implement the rest:
val app = reactiveWebApplication {
beans {
bean<ISBNSampleService>()
bean<ISBNSampleHandler>()
}
webFlux {
port = if (profiles.contains("test")) 8181 else 8080
router {
val handler = ref<ISBNSampleHandler>()
GET("/api/kofu/hello", handler::hello)
GET("/api/kofu", handler::json)
GET("/api/kofu/isbns", handler::isbns)
GET("/api/kofu/small/isbns", handler::smallIsbns)
}
codecs {
string()
jackson()
}
}
}
data class ISBNMessage(val message: String)
class ISBNSampleService {
fun generateMessage() = "I will now give you 1.000.000 ISBN numbers in a Spring KoFu Reactive way"
}
class ISBNSampleHandler(private val isbnSampleService: ISBNSampleService) {
fun hello(request: ServerRequest) = ServerResponse.ok().bodyValue(isbnSampleService.generateMessage())
fun json(request: ServerRequest) = ServerResponse.ok().bodyValue(ISBNMessage(isbnSampleService.generateMessage()))
fun isbns(request: ServerRequest) = ServerResponse.ok().bodyValue(ISBNS)
fun smallIsbns(request: ServerRequest) = ServerResponse.ok().bodyValue(SMALL_ISBNS)
}
class ISBNStacksKoFuReactiveLauncher{
companion object{
@JvmStatic
fun main(args: Array<String>) {
app.run()
}
}
}
If you are familiar with NodeJS
, I’m very sure that you are very surprised at the similarities between this and the same creation of a service like this in NodeJS
. An example very similar to this one can be found on their GitHub repo. One of the advantages of going annotationless
we can already see. A simple web service like this one only costs us a few lines of code. We can also see everything add make addon’s like the string() and jackson() codes in a completely declarative way. The implementation is quite self-explanatory, and we can see clearly that this service and the different data lists will be available on http://localhost:8080/api/kofu/isbns and http://localhost:8080/api/kofu/small/isbns.
6. Starting the services
To make it easy to perform our tests, I’ve created a docker-compose.yml file, which allows us to build and start all three services at once. I’ve also created a Makefile, with several written commands which will help us perform our performance tests. Before we start testing or doing anything with our environment we need to run this command:
make docker-clean-build-start
Once the command finishes running, we should see three running containers. We will however perform tests individually. If we perform tests while the three containers are running at the same time, they might influence each other, they may start consuming too much CPU
or too much memory leading to unreliable or misleading results.
I have performed such tests locally before and couldn’t reach any conclusive result. As mentioned before, we will perform tests that I know point to some important conclusions. All reports of all my tests are placed in folder docs/reports from the root folder of the project.
We will perform tests with the locust performance tool. All locust python scripts are located in the root locust folder of the project.
To avoid deviations in our tests, we’ll also run the headless version of Locust. This means that we will see results in generated graphics from the CSV files generated.
6.1. locust-sequence-start test
In this test, we are going to perform load requests to the services, one by one, using this script located in the Makefile
of the project:
locust-sequence-start:
docker stop jofisaes_isbn_stacks_reactive
docker stop jofisaes_isbn_stacks_mvc
docker restart jofisaes_isbn_stacks_kofu
sleep 5
cd locust/kofu && locust --host=localhost --headless -u 2000 -r 1 --run-time 5m --csv kofu --exit-code-on-error 0
docker stop jofisaes_isbn_stacks_reactive
docker restart jofisaes_isbn_stacks_mvc
docker stop jofisaes_isbn_stacks_kofu
sleep 5
cd locust/web && locust --host=localhost --headless -u 2000 -r 1 --run-time 5m --csv web --exit-code-on-error 0
docker restart jofisaes_isbn_stacks_reactive
docker stop jofisaes_isbn_stacks_mvc
docker stop jofisaes_isbn_stacks_kofu
sleep 5
cd locust/webflux && locust --host=localhost --headless -u 2000 -r 1 --run-time 5m --csv webflux --exit-code-on-error 0
The locust root folder contains several sub-folders. The kofu, web and webflux sub-folders contain the scripts responsible to connect to large content endpoints and they represent Spring Kofu
, Spring Web
(Non-reactive) and Spring WebFlux
respectively. From the script we see that we stop non-required containers for each single test, and we restart the target container. We give it a 5-second
delay to ensure that our service has started. On average, and on a current machine, all of these implementations take less than 5 seconds to start. Locust then starts with 1 user and ramps up by a rate of 1 user per second up to 2000
users per second making requests. This ensures that we’ll reach a threshold for all services. We will, however not reach 2000
users, purely because we are also limited to 5 minutes on each test. The complete test combined takes about 15 minutes to complete, yielding the results presented in the following graphs.
We must first interpret the graphs correctly. On both graphs, we see an axis that goes from 0
to 300
. These are the number of spawned users. We initially want to ramp up from 0
to 2000
. However, due to machine restrictions, I found out that for my case, 300
was de best number to get to. It is not a huge number, but significant enough to measure which service starts to get disrupted first for long processing requests. As seen in the graph, the first service to start failing is KoFu, at about 60 spawned users. This is followed by the WebFlux
service which starts failing at around 170 spawned users. The Web service doesn’t fail at all for this ramp-up. At this point, this could mean bad or good news. For KoFu
and WebFlux
the bad could mean that these architectures are just not good. But we know already, and therefore already slightly biased that WebFlux
is just better at being resilient than a non-reactive service. On the good side, it could mean that they took too many requests and since they take longer to process, they got stuck in reactive limbo. If this is so, then we are actually getting a positive result. Reactive programming is not really thought out for long-lasting requests. It actually goes firmly against the handling of long-lasting requests. If we have long-lasting requests and want to comply with reactive programming then firstly we have something wrong in how we handle requests and secondly, we need to solve this problem.
For the moment the take from this test, and the other consistent ones I’ve made, is that when dealing with long-lasting requests, Spring KoFu fails first and fast, followed by the reactive service with Spring WebFlux
, and finally we see that the non-reactive Spring Web service never failed in our tests. We also know that these could mean bad or good results. And this is why we now move on to our fast processing requests test.
6.2. locust-small-load-sequence-start
In local tests using my own computer, I realized that the best way to test the way the three services behave is simply to let them run for a long time. If we want to repeat such a test, then it will take about 30 minutes to complete:
locust-small-load-sequence-start:
docker stop jofisaes_isbn_stacks_reactive
docker stop jofisaes_isbn_stacks_mvc
docker restart jofisaes_isbn_stacks_kofu
sleep 5
cd locust/small/kofu && locust --host=localhost --headless -u 2000 -r 2000 --run-time 10m --csv kofu --exit-code-on-error 0
docker stop jofisaes_isbn_stacks_reactive
docker restart jofisaes_isbn_stacks_mvc
docker stop jofisaes_isbn_stacks_kofu
sleep 5
cd locust/small/web && locust --host=localhost --headless -u 2000 -r 2000 --run-time 10m --csv web --exit-code-on-error 0
docker restart jofisaes_isbn_stacks_reactive
docker stop jofisaes_isbn_stacks_mvc
docker stop jofisaes_isbn_stacks_kofu
sleep 5
cd locust/small/webflux && locust --host=localhost --headless -u 2000 -r 2000 --run-time 10m --csv webflux --exit-code-on-error 0
This means that we’ll immediately begin the tests with a spawning rate of 2000
users per second. This will continue for 10 minutes on each service on separate occasions. With the resulting CSV
files, I was able to build the following graphs:
In the graph above, it is not easy to distinguish between the performance of the different services. This has to do with the startup of the services and how requests are handled the first time they get to the service after a restart. This, combined with the way records are placed on the CSV
files, generates a first ramp-up-like curve, which is not useful for our tests. If we cut off the startup section and visualize only the stable data afterward we see something more conclusive.
If we look at the different data, we see that in general that our Spring KoFu
service was able to process much more requests than the other two. We also see that our Spring Web
and Spring WebFlux
services seem to duel with each other. If we look more closely, we also see that on average WebFlux does seem to process more requests than just the plain Web. Another thing we observe and that was also very surprising to me is that the Spring KoFu requests/second curve has a somewhat mirror shape to the other services. Spring Kofu seems to have, instead of low-performance peaks, it has instead high-performance peaks.
So now it seems that Spring KoFu can process more requests at a time than the other Spring services.
The Locust performance tests also provide us with an average response times table. Here we can see how fast our requests are processed using the different implementations:
Type | Name | Request Count | Failure Count | Median Response Time | Average Response Time | Min Response Time | Max Response Time | Requests/s |
---|---|---|---|---|---|---|---|---|
GET | /api/kofu/small/isbns | 488189 | 0 | 1700.0 | 1663.5698557514636 | 868.2196040000001 | 28160.157482 | 812.7795532454088 |
GET | /api/traditional/non/small/isbns | 459502 | 0 | 1700.0 | 1810.3353489623726 | 319.1146300000014 | 76340.247267 | 765.9678731389113 |
GET | /api/traditional/small/isbns | 462978 | 0 | 1700.0 | 1768.4598885850269 | 304.2018510000162 | 46094.92280300003 | 770.7296145412935 |
Reading the data from the table, we can see yet again, that Spring KoFu does seem to offer better performance than the other Spring services. In terms of the number of requests, the Spring Non-Reactive Service
processed 459502
requests. This was followed by Spring Reactive WebFlux
which processed 462978
requests. This is already a significant difference of 3476
requests. This is made even better by Spring KoFu which was able to process 488189
requests. This is further an increase of 25211
requests! This is also reflected in the average response times. We can see that we have no errors on any of the tests, which means that our services were always available during testing. Another interesting metric that we are interested in is the longest request of all our requests. Clearly, the longest comes from the non-reactive service where a potential user would have to wait 76 seconds. This is improved by the plain reactive service with a wait time of 46 seconds and finally the Kofu service with a wait time of 28 seconds. If these numbers sound long to you it is because they are. But this is also because these tests are running on a local machine where the scale of time, failure, and delay is very different than with real servers. Finally, we also see that this all matches up with the requests per second where we also see improvements going from non-reactive to KoFu.
The take-away from this test and the previous one is that now it seems that Spring KoFu
will react much faster to fast processing requests, it will be able to process more requests at the same time and pushes itself to performance peaks.
6.3. Service startup
This is our last test for the Spring KoFu
framework. In this case, we are going to test how long does the service takes to startup. In this case, I’ve also created a command in the Makefile to help us in our tests:
local: no-test
mkdir -p bin
cp isbn-stacks-rest-kofu-mvc-reactive/target/isbn-stacks-rest-kofu-mvc-reactive-*.jar bin/isbn-stacks-rest-kofu-mvc-reactive.jar
cp isbn-stacks-rest-traditional-mvc-non-reactive/target/isbn-stacks-rest-traditional-mvc-non-reactive-*.jar bin/isbn-stacks-rest-traditional-mvc-non-reactive.jar
cp isbn-stacks-rest-traditional-mvc-reactive/target/isbn-stacks-rest-traditional-mvc-reactive-*.jar bin/isbn-stacks-rest-traditional-mvc-reactive.jar
cp isbn-stacks-rest-kofu-plain/target/isbn-stacks-rest-kofu-plain-*.jar bin/isbn-stacks-rest-kofu-plain.jar
What this command does is copy all jars into a bin folder in the root.
This way we can start them by simply running java -jar , registering their startup time, and then stopping them with Ctrl-C.
Description | KoFu | WebFlux | Web | Control |
---|---|---|---|---|
2.133 | 2.42 | 2.065 | 1.383 | |
1.436 | 2.026 | 1.895 | 1.566 | |
1.349 | 2.233 | 1.876 | 1.412 | |
1.699 | 2.128 | 1.858 | 1.329 | |
1.471 | 2.079 | 1.875 | 1.350 | |
1.492 | 2.34 | 1.87 | 1.431 | |
1.364 | 2.199 | 1.917 | 1.577 | |
1.591 | 1.994 | 1.912 | 1.427 | |
1.477 | 2.056 | 1.838 | 2.020 | |
1.426 | 2.013 | 1.94 | 1.354 | |
AVERAGE | 1.544 | 2.149 | 1.905 | 1.485 |
We can also see here a sign that Spring KoFu does seem to start faster. If we compare these results to WebFlux or Web we do see a remarkable improvement.
7. Conclusion
We started this journey by making 3 services and making them as simple as possible with only two endpoints. A fast endpoint to support only 50 ISBNs
and a slow endpoint to support half a million ISBNs
. We performed tests against them, and although we cannot make definite conclusions, we can, however, observe some behavior as a sign of confirmation of the statement of the Spring-Fu
team on GitHub:
Spring Fu is an incubator for JaFu
(Java DSL) and KoFu
(Kotlin DSL) designed to configure Spring Boot explicitly with code in a declarative way with great discoverability thanks to auto-complete. It provides fast startup (40% faster than regular autoconfiguration
on a minimal Spring MVC app), low memory consumption and is a good fit with GraalVM
native thanks to its (almost) reflection-less approach.
We did see that, while coding, IntelliJ does seem to offer great auto-complete support. Though this is all true, it is likely not the most interesting aspect of Spring Fu. We did also observe that the service startup seems to be faster, although we couldn’t exactly see a 40% reduction in startup time. Although performance is not something explicit in any of the Spring Fu sources, it is in some way implicit because of its reflection-less nature. They also mention that a functional approach is much more efficient in the JVM. This at least is the theory and we do observe a remarkable difference in improvement with Spring-KoFu in our tests. We have seen this in response times and load capacity.
Making these tests was quite laborious, but it is very rewarding to see the very compelling signs of how worthwhile might be to go to a completely declarative way of working and leaving annotations behind.
As I mentioned in the introduction, I did take the Spring Certification and a lot of it was about annotations. I still feel and I always do that it is worthwhile to take certifications. Nonetheless, technology does move fast, and moving forward what we seem to achieve is an ongoing improvement in our path to enrich our knowledge in technology in general. What we know to be efficient and fantastic today may change tomorrow in the world of technology and that is ok. I’m very down with this to be very honest. My best advice to you if you feel that all of those years of learning annotations in the past are somehow being threatened by these new developments, is to just let it go and move on. What I have learned in the past is great, and I can take that and modify it to adapt it to this new way of working. If we see a Spring Functional production-ready version in the future, probably Spring-Fu
, then that is all for the better and working hard to keep up with the changes gives us all a bit more magic to whatever we do and this is what we need to keep doing.
I have placed all the source code of this application on GitHub
I hope that you have enjoyed this article as much as I enjoyed writing it. I tried to keep it small, concise and I left many small details out.
Thanks in advance for reading!
8. References
- The evolution of Spring Fu
- The State of Kotlin Support in Spring
- Spring Fu 0.3.0 and beyond
- Mermaid Live Editor
- Spring Framework 5.0 Released
- JAX Innovation Award Winners
- Agitar Wins 2006 Software Development Magazine Productivity Award
- 16th Jolt Awards 2006
- The 16th annual jolt product Excellence Award winners)
- JetBrains Products Win Jolt Productivity Awards
- Spring Framework
- java spring - natashasweety7/Software Wiki
- A Guide to Spring Framework Annotations
- What is New in Spring Framework 4.x
- Nicolas Fränkel-Annotation-free Spring
- Spring-Fu GitHub Repo
- Spring KoFu
Top comments (0)