Hybrid spring-boot : webflux and webmvc

I know some of you think about getting to speead with your api.
How can one have a java api that can scale up without affecting more costs and machines ?

You may have looked into reactive apis and WebFlux recently,
…but you wonder how you can successfully convert your apis with a reversible approach.

Suppose you have a simple config

package com.mycompany.myservice.configuration

import com.mycompany.myservice.service.MyBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
internal class MyConfiguration {
    @Bean
    fun myBean() = MyBean()
}

If MyBean is server-type dependent, you may want to rename it into MyServletBean and then fork it as MyReactiveBean, if it is a simple bean (for example if that bean stores an attribute in the request or the exchange)

package com.mycompany.myservice.configuration

import com.mycompany.myservice.service.reactive.MyReactiveBean
import com.mycompany.myservice.service.servlet.MyServletBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.REACTIVE
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type.SERVLET
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
internal class MyConfiguration {

    @ConditionalOnWebApplication(type = REACTIVE)
    @Configuration
    internal class Reactive {
        @Bean
        fun myReactiveBean() = MyReactiveBean()
    }

    @ConditionalOnWebApplication(type = SERVLET)
    @Configuration
    internal class Servlet {
        @Bean
        fun myServletBean() = MyServletBean()
    }

}

What if you don’t want to clone your entire app ? I guess it is the right thing to do for adapters, but you might not want to clone your intrinsic domain logic just because you have to read server-type sensitive data.
Even if you try to combine the two implementations in the same method, the only fact of declaring variables that depend on different server types might lead to NoClassDefFoundError.

Let me put an example in picture :
You are asked to extract the authentication object from the request to pass it through a downstream service.
If you do the following

val bearerToken = if (reactive) {
        it.getContext()?.let {
            extractFromContext<SecurityContext>(it)
        }?.authentication?.let { BearerTokenAuthentication::class.safeCast(it) }
} else {
    SecurityContextHolder.getContext()
        ?.authentication?.let { BearerTokenAuthentication::class.safeCast(it) }
}

Your app will fail to start… why ? Either
* your app is reactive and SecurityContextHolder is not in the classpath
* your app is servlet based and the reactor context is not in the classpath.

And you can try running with the two libraries … to no avail.

But there is one way to avoid the bytecode to be type-verified : lambdas.
With lambdas, you can define functions that use libraries outside of your classpath.

So if you can execute conditionally functions, the last blocker that we need to address is :
“how do I know programmatically what is the server type”.
The answer is quite simple : just read it at startup time and you will be fine.
Here is my solution to determine the server type at runtime and to use it :

package com.mycompany.myservice.configuration

import org.springframework.boot.SpringApplication
import org.springframework.boot.WebApplicationType
import org.springframework.boot.WebApplicationType.REACTIVE
import org.springframework.boot.WebApplicationType.SERVLET
import org.springframework.boot.env.EnvironmentPostProcessor
import org.springframework.core.env.ConfigurableEnvironment

class WhichServerTypeIsThis : EnvironmentPostProcessor {

    override fun postProcessEnvironment(environment: ConfigurableEnvironment, application: SpringApplication) {
        currentType = application.webApplicationType
    }

    companion object {
        @JvmStatic
        private lateinit var currentType: WebApplicationType

        @JvmStatic
        fun <T> doConditionally(forReactive: (() -> T)?, forServlet: (() -> T)?): T? =
            when (currentType) {
                REACTIVE -> forReactive?.invoke()
                SERVLET -> forServlet?.invoke()
                else -> throw IllegalStateException(🤔 Hmm… This is probably not a spring server, is it ?”)
            }

        @JvmStatic
        val actualType get() = currentType
    }
}

Now, you can successfully mix the two server types in the same method, with the use of that doConditionally shortcut.
Let’s go back to the bearerTokenAuthentication example :

val bearerToken = doConditionally(
    forReactive = {
        it.getContext()?.let {
            extractFromContext<SecurityContext>(it)
        }?.authentication?.let { BearerTokenAuthentication::class.safeCast(it) }
    },
    forServlet = {
        SecurityContextHolder.getContext()
            ?.authentication?.let { BearerTokenAuthentication::class.safeCast(it) }
    }
)

Yes, this will work and does not require any classpath mangling,
so you can write code that can work for both server types.

And hopefully migrate your code to WebFlux painlessly.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

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