Spring Boot

Spring Boot is an open source Java-based framework made to ease the bootstrapping and development of the Spring applications. It is developed by Pivotal, and is a part of the Spring Framework. It gives you easy access to rich Spring ecosystem, with many enterprise-grade libraries and technologies. Spring Boot applications can be written in Kotlin and the language is officially supported by the framework.

Build configuration

The integration with Spring Boot is contained in the kvision-server-spring-boot module. It has to be added as the dependency in the common target. This module depends on the spring-boot-starter, spring-boot-starter-webflux, spring-boot-starter-security and spring-data-relational. Any other dependencies can be added to build.gradle.kts and then be used in your application.

build.gradle.kts
dependencies {
    implementation(kotlin("stdlib-jdk8"))
    implementation(kotlin("reflect"))
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.springframework.boot:spring-boot-devtools")
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot.experimental:spring-boot-actuator-autoconfigure-r2dbc:$springAutoconfigureR2dbcVersion")
    implementation("org.springframework.data:spring-data-r2dbc:$springDataR2dbcVersion")
    implementation("io.r2dbc:r2dbc-postgresql:$r2dbcPostgresqlVersion")
    implementation("io.r2dbc:r2dbc-h2:$r2dbcH2Version")
}

Application configuration

The standard way to configure Spring Boot application is src/jvmMain/resources/application.yml file. It contains options needed for optional dependencies. It can be empty if they are not used.

Implementation

Service class

The implementation of the service class comes down to implementing required interface methods and making it a Spring component by using a @Service annotation with a "prototype" scope.

@Service
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Suppress("ACTUAL_WITHOUT_EXPECT")
actual class AddressService : IAddressService {
    override suspend fun getAddressList(search: String?, sort: Sort) {
        return listOf()
    }
    override suspend fun addAddress(address: Address) {
        return Address()
    }
    override suspend fun updateAddress(id: Int, address: Address) {
        return Address()
    }
    override suspend fun deleteAddress(id: Int) {
        return false
    }
}

Spring IoC (Inversion of Control) allows you to inject resources and other Spring components into your service class. You can use standard Spring @Autowired annotation or constructor based injection.

@Service
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Suppress("ACTUAL_WITHOUT_EXPECT")
actual class AddressService : IAddressService {
    @Autowired
    lateinit var env: Environment

    override suspend fun getAddressList(search: String?, sort: Sort) {
        println(env.getProperty("option1", "default"))
        return listOf()
    }
    // ...
}

You can also inject custom Spring components, defined throughout your application.

KVision allows you to inject ServerRequest object, which gives you access to the user session as well.

@Service
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Suppress("ACTUAL_WITHOUT_EXPECT")
actual class AddressService : IAddressService {

    @Autowired
    lateinit var serverRequest: ServerRequest

    override suspend fun getAddressList(search: String?, sort: Sort) {
        println(serverRequest.uri())
        println(serverRequest.session().awaitSingle().id)
        return listOf()
    }
    // ...
}

Note: The new instance of the service class will be created by Spring for every server request. Use session or request objects to store your state with appropriate scope.

Blocking code

Since Spring WebFlux architecture is asynchronous and non-blocking, you should not block threads in your application code. If you have to use some blocking code (e.g. blocking I/O, JDBC) use the dedicated coroutine dispatcher.

@Service
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Suppress("ACTUAL_WITHOUT_EXPECT")
actual class AddressService : IAddressService {
    override suspend fun getAddressList(search: String?, sort: Sort) {
        return withContext(Dispatchers.IO) {
            retrieveAddressesFromDatabase(search, sort)
        }
    }
}

Application class

To allow KVision work with Spring Boot you have to pass all instances of the KVServiceManager objects (defined in common code) to the Spring environment. You do this by defining a provider method for the List<KVServiceManager<Any>> instance in the main application class.

Use @EnableAutoConfiguration annotation to disable security if your app doesn't need it.

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean

@SpringBootApplication
@EnableAutoConfiguration(
    exclude = [
        org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration::class,
        org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration::class
    ]
)
class KVApplication {
    @Bean
    fun getManagers() = listOf(getServiceManager<IAddressService>())
}

fun main(args: Array<String>) {
    runApplication<KVApplication>(*args)
}

Authentication

You can use standard Spring WebFlux Security configuration, and with a help of serviceMatchers extension function, you can automatically select endpoints that should be secured.

@EnableWebFluxSecurity
@Configuration
class SecurityConfiguration {

    @Bean
    fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        return http.authorizeExchange {
            it.serviceMatchers(getServiceManager<IAddressService>(), getServiceManager<IProfileService>())
                .authenticated().pathMatchers("/**").permitAll()
        }.csrf {
            it.disable()
        }.exceptionHandling {
            it.authenticationEntryPoint { exchange, _ ->
                val response = exchange.response
                response.statusCode = HttpStatus.UNAUTHORIZED
                exchange.mutate().response(response)
                Mono.empty()
            }
        }.formLogin {
            it.loginPage("/login")
                .authenticationSuccessHandler(RedirectServerAuthenticationSuccessHandler().apply {
                    this.setRedirectStrategy { exchange, _ ->
                        Mono.fromRunnable {
                            val response = exchange.response
                            response.statusCode = HttpStatus.OK
                        }
                    }
                }).authenticationFailureHandler(RedirectServerAuthenticationFailureHandler("/login").apply {
                    this.setRedirectStrategy { exchange, _ ->
                        Mono.fromRunnable {
                            val response = exchange.response
                            response.statusCode = HttpStatus.UNAUTHORIZED
                        }
                    }
                })
        }.logout {
            it.logoutUrl("/logout")
                .requiresLogout(ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout"))
                .logoutSuccessHandler(RedirectServerLogoutSuccessHandler().apply {
                    setLogoutSuccessUrl(URI.create("/"))
                })
        }.build()
    }
}

Last updated