Spring Webflux and Native for Microservices – Ready for Primetime?

The short answer is no, not yet. For the longer answer read on, including to see when the two actually are ready for primetime.

Spring Webflux is a project attempting to fuse Spring MVC and reactive programming. Reactive programming replaces native threads with lightweight threads and cooperative multi-tasking. It is basically like upgrading from Windows 95 to Windows 3.1 or Mac OS X to Mac OS 7. Spring Webflux is based on Project Reactor, which is an implementation of the standard reactive patterns. Spring Webflux aims to co-exist with or replace MVC, and allow developers a relatively smooth transition from the classical way of doing thigs to the new way, going so far as allowing much of the same configuration and annotations as are already familiar from MVC, while also offering more modern DSL-like constructs.

Spring Native is a project attempting to bring Spring Boot to the world of native compilation using GraalVM. GraalVM makes it possible to compile Java applications to native code, lowering start-up time and ensures applications are fast immediately (vs needing JIT compilation).

Spring Webflux and the Quest for the Database

Spring Webflux is relatively mature and supports many usage scenarios. It does require you rewire your brain to thing in reactive terms instead of imperative terms, but given that also Javscript is reactive, this is probably a worthwhile rewiring to do anyway. I’m not going to to into the intricacies of reactive programming here, go read a book or a howto about it instead.

The one thing I do want to mention is a very common usage scenario: web applications backed by a database. This is a very common scenario, and unfortunately one that is not properly supported by Spring Webflux at this time. The go-to way to deal with databases in the Spring world is using Spring Data, typically in the form of Spring Data JPA. Spring Data JPA is basically a Springification of JPA, the Java Persistence API. JPA is the Javaification and standardization of the Hibernate APIs.

The problem is that JPA is inherently synchronous. Most sites online will say that to use JPA in your reactive application, you need to perform your database access in worker threads. Which is fine, except it does not work with transations. The most common way to deal with transactions in the JPA/Spring world is using the @Transactional annotation on methods that need to be atomic database-wise. In the synchronous world, this is very usable: transactions can be nested, allowing developers to perform individual updates each in their own transactions or handle entire webservice calls within a single transaction. This is nice as we often want webservices to be atomic and, you know, consistent, but sometimes need to perform computation or call external services where we do not wish to hold on to a database transaction.

The @Transactional annotation works by essentially wrapping the original method and automatically call BEGIN TRANSACTION before entering the method, calling COMMIT on completion, and calling ROLLBACK on exception. There’s a bit more to it (to handle various types of nested transactions), but that’s the gist: wrap the method call in a transaction.

Unfortunately, this approach does not work in a reactive setting. The method called is not responsible for performing the computation, only for setting up the reactive stream, so wrapping the method in a transaction is useless. Furthermore, the @Transactional annotation works using thread-local values which is entirely useless in a reactive setting, where different bits of the reactive computation is done on different threads or even entirely different thread pools.

This is not unfixable, and Spring does have support for a reactive version of the @Transactional annotation. It even externally works the same as the classical version… except, it is not supported for JPA. This IMO makes it irrelevant to use Spring Data JPA in Webflux applications. If we need to perform an entire webservice call in a single transaction, we need to perform the entire operation as a synchronous operation wrapped with @Transactional, executed on a background worker threadpool, making the use of asynchronous operations superfluous.

Spring does have an answer to this. Kind-of. Spring Data R2DBC aims to be JPA for reactive. R2DBC is short for Reactive Relational Database Connectivity. R2DBC has very similar aims to Webflux: allow using as many concepts from the old world to implement the new world. Entities can still be set up in much the same way as using JPA, and can be accessed using reactive repositories, mirroring classical repositories, and supports reactive transaction handling.

On the surface R2DBC is very tantalizing, and all the online guides makes it seem like it is a drop-in replacement, just reactive. A very nice feature is that R2DBC will stream results from the database. That makes it near effortless to make your entire webservice streaming: stream data from the database, filter or map the results as necessary, and provide them as a streaming reactive result in your webservice. That allows you do deal with millions of database entries without ever needing much memory.

The problem, and one that no R2DBC tutorial is going to tell you about, is that the “relational” part of the name is a lie. R2DBC does not support relations. You cannot have one-to-many or many-to-many relations using R2DBC. This, to me and I guess to anybody considering using it for anything beyond a tutorial application, makes R2DBC completely useless. R2DBC is on version 0.8 with milestones of 0.9 in the working, and suggested for production use. The project has a 16 months old (at the time of writing) feature request to support relations, but there has been no updates for more than half a year. In addition, R2DBC does not support schema initialization/validation, making it a chore to use (though it is possible to work around that by generating a DDL using Hibernate and preloading that). Before at least support for relations is added, I consider R2DBC entirely useless for anything but the simplest applications.

There are alternatives to R2DBC, most notably Hibernative Reactive. Also, Oracle has their own way of doing things, of course. As Pivotal (the creators of Spring) are also behind R2DBC, I do not see such libraries get whole-hearted support in the Spring ecosystem anytime soon.

With no reactive database ORM, I do not see a place for Webflux in applications that rely on databases. Which is every webservice for a modern business application. It absolutely does have a place for services that are themselves stateless or which stores state elsewhere (e.g., in other web-services, like my own Gitlab Project Management application, which uses Webflux and stores all data directly in Gitlab via webservices, or applications storing data in the frontend) or which uses databases that promote data corruption or information leaks, but it is not yet an alternative for most business services.

Spring Native and the One Million Annotations

Java was built almost from day one to be run on a virtual machine. This provides it some advantages not normally seen in compiled languages. Most important to these are dynamic class loading, run-time introspection (reflection), and dynamic proxies. Many users of Java will not know what all (or even any) of these are, as they are complex and largely tools for framework developers. But frameworks will often make use of them.

Thought I wouldn’t be able to shoehorn in a Britney reference? Think again! Reflection -> Mirror -> Britney’s Girl in the Mirror

Spring Boot, for example, makes heavy use of reflection to determine which classes are on the classpath and set up functionality accordingly (so for example Spring Data JPA gets instantiated if a JPA implementation and Spring Data JPA is found on the classpath). Spring also relies on reflection to scan for @Configuration, @Service, @Repository and many other types of classes: simply scan the class path, inspect the annotations, and instantiate accordingly.

Early versions of Spring Native proposed replacing this way of configuration by the Spring Fu (previously functional bean initialization DSL). Spring Fu aims to reduce startup time by replacing annotation based configuration by a DSL in either Java or Kotlin describing exactly which beans to configure. If you think this sounds like going back to the days with giant Spring XML files for configuring a Spring application in every minute detail, that is exactly what it is, and nobody wanted that.

GraalVM can support reflection, but needs to know for exactly which classes this is needed. Developers can write JSON files describing which classes to generate reflection information for. For some reason (maybe size) it was decided to not just provide this for all classes. Spring Native comes with a slightly more declarative way to describe this in the form of a @TypeHint annotation. Better yet, Spring Native comes with configuration for many Spring and third-party libraries, that allows using autoconfiguration and reflective configuration for many applications without specifying when reflection is needed (at least not for library classes). This is very good and makes it possible to write even real applications using the system.

Most developers never think about when they are using proxies. Nor should they. Knowing when proxies are used requires deeper knowledge of implementation details than most developers are comfortable with. Hibernate uses proxies for lazy-loaded collections and objects, intercepting calls to getters and fetching the full objects on-demand. Spring (in its default configuration) makes use of proxies for implementing @Transactional, @Lazy beans, @Async calls, when using @Aspects, and a few other situations.

Proxies can be implemented in essentially three different ways: using Java dynamic proxies, using bytecode generation (typically using cglib), or using the AspectJ compiler. The first obviously relies on dynamic proxies and some level of reflection, the second relies on dynamic class loading, and the third relies on a separate compilation step. The first way can be made to work by enabling reflection in all the right places. The second cannot currently (and will likely never) be workable, as this would require compiling arbitrary native code on-demand. The third is semi-workable but also very annoying as it breaks a lot of tooling.

To generate proxies, Spring Native employs a code generator inspecting the code to try and infer where proxies will be needed. It calls this ahead-of-time (AOT) compilation to contrast with just-in-time compilation, while the rest of the world just calls this compilation. Hibernate also has a code-generation step that can generate its required proxies. This adds extra compilation steps, and is unfortunately not fool-proof. This means that the developer sometimes has to provide hints for which proxies to generate and for which classes refelction is necessary. These are the annotations I am using on a relatively simple project I’m doing in my spare time:

@NativeHint(options = { "-H:+ReportExceptionStackTraces", "--enable-url-protocols=https" }, //
	aotProxies = { //
		@AotProxyHint(targetClass = AuthorService.class, proxyFeatures = ProxyBits.IS_STATIC), //
		@AotProxyHint(targetClass = AuxService.class, proxyFeatures = ProxyBits.IS_STATIC), //
		@AotProxyHint(targetClass = CategoryService.class, proxyFeatures = ProxyBits.IS_STATIC), //
		@AotProxyHint(targetClass = DateService.class, proxyFeatures = ProxyBits.IS_STATIC), //
		@AotProxyHint(targetClass = KeywordService.class, proxyFeatures = ProxyBits.IS_STATIC), //
		@AotProxyHint(targetClass = KindService.class, proxyFeatures = ProxyBits.IS_STATIC), //
		@AotProxyHint(targetClass = MetadataService.class, proxyFeatures = ProxyBits.IS_STATIC), //
		@AotProxyHint(targetClass = PermissionsService.class, proxyFeatures = ProxyBits.IS_STATIC), //
		@AotProxyHint(targetClass = StatusService.class, proxyFeatures = ProxyBits.IS_STATIC), //
		@AotProxyHint(targetClass = TagService.class, proxyFeatures = ProxyBits.IS_STATIC)//
	}, //
	jdkProxies = { //
		@JdkProxyHint(types = { ConversionService.class,
			SpringProxy.class, Advised.class,
			DecoratingProxy.class })//
	}, //
	types = { //
		@TypeHint(types = { Aux.Serializer.class }) //
	}//
)

Just look at how elegant and simple that is. The best part is that you kind-of just have to guess which annotations are necessary, as tooling for this is very limited currently (thous a tracing agent provides superficial support). At the end of the day, an experienced developer should be able to do this for their own applications, but it is nowhere near as simple to use as plain Spring.

Spring Native also takes away tools that are normally very useful for development. Spring Devtools are not presently supported, so the build cycle takes a step half a decade back to when we had to rebuild and restart applications for changes. The normal way of running applications do not catch anywhere near all the issues you’ll run into with the native framework, so you may also have to go thru the AOT compilation and run using the tracing agent and special options to find spring Native-specific issues. Even so, you may have to go all the way to native to test your application, and the build cycle for this turns back time another decade or two. Building my very simple native application takes around 15 minutes on a large but older machine (running under Docker on a virtual machine with 32 GiB memory and 4/8 AMD Opteron 6180 SE CPUs/threads), making the process of getting the application initially running take literally days (starting from a working Spring Boot application set up with Native from the get-go).

The compilation time is due to how GraalVM is implemented. It relies heavily on analysis of the application and even launching the application to collection information during the launch phase. Hopefully this improves with time or I see little future for the functionality. Most of the problems with native compilation are self-created, and compilation technology is literally 50+ years old and very well-understood, so it must be possible to create something that works. Heck, even Javascript has mostly eliminated “eval” which made any meaningful analysis impossible there, so Java must be able to rethink its APIs (or just write a better GraalVM compiler) that does not require 15 minutes and a peak memory use of 10 GiB of memory to compile a very simple web-service.

Spring Native works ok (with the above caveats) when you stay within the Spring Ecosystem (and the supported libraries), but requires the above annotation shenanigans for libraries making use of any of the forbidden features. For many (including me), that writing annotations for a third-party library a no-go (more so if you need multiple libraries). Concretely, this means that I cannot Springfox Swagger as it relies on reflection. It has a ticket for Spring Native support, but very little actual apparent interest in making it work, so for now I generate my Swagger using a test (that needs to be run manually, because it also breaks during normal execution of tests) to generate my interfaces for import into Angular. Presumably, this situation will only improve with time.

“Simple” libraries that make no use of reflection are typically perfectly fine (e.g., most of the Apache Commons libraries), and my experience was in general that I was impressed with the accomplishments of the Spring Native team. I would not say it is usable just yet for everybody, as it requires deeper knowledge of implementation details than the average developer has, but it is certainly possible for simple projects (like micro-services which stand to gain the most anyway), pilot projects, and for developers with a deeper understanding of not just how to use libraries.

Conclusion

So, in conclusion, I do not think either Spring Webflux or Native are ready for wide use yet. Webflux remains useless for anything relying on databases until R2DBC is fixed, and based on the previous release cycles and enthusiasm for adding proper ORM features, I do not see this happing in the next 1-2 years.

Spring Native has come a long way since I first looked into it, and has reached a point where I’m not uncomfortable using it for my personal projects, and might even use it for simple client projects. Progress is relatively swift, and I expect many of my grievances will be ironed out within 1-2 years.

This is the result I got when searching for images for “not enterprise ready.” Much like blockhain, Spring Webflux and Native are not enterprise ready, but unlike blockchain, chances are they might be in 2 years.

The big reason for both of these technologies is to reduce start-up time and image footprint to allow Java to have a meaningful role in a world dominated by microservices running on Kubernetes. I do not have real performance tests yet, but do notice my application starts up in around 3 seconds when running native on my server (which has many but slow CPUs) vs 5-6 seconds on my desktop (which is reasonably new). I have not performed proper performance measurements or even run the application under identical conditions, though. A superficial comparison of application sizes is that my Paketo-generated image with Spring Native is 287 MiB while a similarly complex application is 187 MiB using an OpenJDK slim (i.e., a tiny Debian, not the no-longer-supported Alpine image). My application does make use of functionality that Spring Native warns currently make images huge (removing it cut start-up time down to 2 seconds, peak compilation memory use to 9 GiB and image size to 270 MiB), includes database drives for both Postgres and h2, and I have no idea how much or little space can be saved by tweaking Paketo. So, not only am I not comparing apples to apples, I have also not looked into space/time optimization yet.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.