Skip to content

Commit

Permalink
patch: add multiple conditionals to prevent Bean clashes #91 (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
rspiegl authored Feb 7, 2024
1 parent 361a708 commit 516b7d9
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 27 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,39 @@ class QueryDslArtifactRepositoryImpl(

### Caching

TBD
The module `platform-spring-caching` autoconfigures Redis Caches + Sessions for you. This happens automatically
if it can find Redis on the classpath. You can do this with

```kotlin
dependencies {
implementation("io.cloudflight.platform.spring:platform-spring-caching")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
// for sessions
implementation("org.springframework.session:spring-session-data-redis")
}
```

The [CachingAutoConfiguration](platform-spring-bom/src/main/kotlin/io/cloudflight/platform/spring/autoconfigure/CachingAutoConfiguration.kt)
then registers a CacheErrorHandler that evicts keys when they can't be serialized.
It also creates a default RedisCacheConfiguration that supports cache properties like `timeToLive`, `keyPrefix`
and `isUseKeyPrefix`.
It will create a default RedisTemplate with a JSON de/serializer if it cannot find a RedisTemplate.

#### Session Handling

If you have the `spring-session-core` and `spring-session-data-redis` dependencies on the classpath of
your `WebApplication`,
the [SessionAutoConfiguration](platform-spring-bom/src/main/kotlin/io/cloudflight/platform/spring/autoconfigure/SessionAutoConfiguration.kt)
will automatically configure a RedisSessionRepository for you if you haven't already defined one. In the same manner it
will create a SafeRedisSessionSerializer that warns you of non-deserializable sessions.

> **_NOTE:_** The property spring.session.store-type was removed in Spring Boot 3, and so we have removed it as well.
> The `SessionRepository` is identified by the presence of dependencies on the classpath in the following order:
> 1. Redis (Our implementation)
> 2. JDBC (from Spring Boot)
> 3. Hazelcast (from Spring Boot)
> 4. Mongo (from Spring Boot)
> 5. If none of the above are available, no SessionRepository is configured.
### Scheduling

The module `platform-spring-scheduling` integrates [Shedlock](https://github.com/lukas-krecan/ShedLock) with Spring-Boot by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package io.cloudflight.platform.spring.caching.autoconfigure
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.ObjectMapper
import io.cloudflight.platform.spring.caching.serializer.SafeRedisSessionSerializer
import io.cloudflight.platform.spring.json.ObjectMapperFactory
import mu.KotlinLogging
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.cache.CacheProperties
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
import org.springframework.cache.annotation.CachingConfigurer
import org.springframework.cache.annotation.EnableCaching
Expand All @@ -26,18 +26,16 @@ import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair.fromSerializer
import org.springframework.data.redis.serializer.RedisSerializer
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession
import java.time.Duration

@AutoConfiguration(before = [RedisAutoConfiguration::class])
@EnableCaching(order = 1000)
@Import(CachingAutoConfiguration.RedisConfiguration::class, CachingAutoConfiguration.RedisSessionConfig::class)
@Import(CachingAutoConfiguration.RedisConfiguration::class)
class CachingAutoConfiguration {
/**
* Same condition as in RedisAutoConfiguration
*/

@Configuration
@ConditionalOnClass(RedisOperations::class)
@ConditionalOnBean(RedisConnectionFactory::class)
class RedisConfiguration : CachingConfigurer {
@Bean
override fun errorHandler(): CacheErrorHandler {
Expand Down Expand Up @@ -71,14 +69,14 @@ class CachingAutoConfiguration {
}

@Bean
fun platformRedisCacheManagerBuilderCustomizer(cacheConfiguration: RedisCacheConfiguration):
RedisCacheManagerBuilderCustomizer {
fun platformRedisCacheManagerBuilderCustomizer(): RedisCacheManagerBuilderCustomizer {
return RedisCacheManagerBuilderCustomizer { builder ->
builder.transactionAware()
}
}

@Bean
@ConditionalOnMissingBean(RedisTemplate::class)
@Lazy
fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<Any, Any> {
return RedisTemplate<Any, Any>().apply {
Expand All @@ -96,16 +94,6 @@ class CachingAutoConfiguration {
}
}

@Configuration
@ConditionalOnProperty(value = ["spring.session.store-type"], havingValue = "redis")
@EnableRedisHttpSession
class RedisSessionConfig {
@Bean
fun springSessionDefaultRedisSerializer(): RedisSerializer<*> {
return SafeRedisSessionSerializer(RedisSerializer.java())
}
}

companion object {
private val JSON = createJsonSerializer()
private val LOG = KotlinLogging.logger { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.cloudflight.platform.spring.caching.autoconfigure

import io.cloudflight.platform.spring.caching.serializer.SafeRedisSessionSerializer
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisOperations
import org.springframework.data.redis.serializer.RedisSerializer
import org.springframework.session.Session
import org.springframework.session.SessionRepository
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession

@AutoConfiguration(
after = [CachingAutoConfiguration::class, RedisAutoConfiguration::class],
before = [org.springframework.boot.autoconfigure.session.SessionAutoConfiguration::class]
)
@ConditionalOnClass(Session::class)
@ConditionalOnWebApplication
@Import(SessionAutoConfiguration.RedisSessionConfig::class)
class SessionAutoConfiguration {

@Configuration
@ConditionalOnClass(RedisOperations::class)
@ConditionalOnBean(RedisConnectionFactory::class)
@ConditionalOnMissingBean(SessionRepository::class)
@EnableRedisHttpSession
class RedisSessionConfig

@Bean
@ConditionalOnClass(RedisOperations::class)
@ConditionalOnBean(SessionRepository::class)
@ConditionalOnMissingBean(RedisSerializer::class)
fun springSessionDefaultRedisSerializer(): RedisSerializer<*> {
return SafeRedisSessionSerializer(RedisSerializer.java())
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
io.cloudflight.platform.spring.caching.autoconfigure.CachingAutoConfiguration
io.cloudflight.platform.spring.caching.autoconfigure.CachingAutoConfiguration
io.cloudflight.platform.spring.caching.autoconfigure.SessionAutoConfiguration
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
package io.cloudflight.platform.spring.caching

import io.cloudflight.platform.spring.caching.autoconfigure.CachingAutoConfiguration
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.springframework.boot.autoconfigure.AutoConfigurations
import org.springframework.boot.autoconfigure.cache.CacheProperties
import org.springframework.boot.test.context.runner.ApplicationContextRunner
import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.data.redis.cache.FixedDurationTtlFunction
import org.springframework.data.redis.cache.RedisCacheConfiguration
import org.springframework.data.redis.cache.RedisCacheManager
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import java.time.Duration


class CachingAutoConfigurationTest {

private val contextRunner = ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CachingAutoConfiguration::class.java))

@Test
fun testRedisTypeInfoSerialization() {
val serializer = CachingAutoConfiguration.createJsonSerializer()
Expand All @@ -12,8 +33,74 @@ class CachingAutoConfigurationTest {
val bytes = serializer.serialize(data)
val obj = serializer.deserialize(bytes, java.lang.Object::class.java) as DataDto

assert(data.foo == obj.foo)
assertThat(obj.foo).isEqualTo(data.foo)
}
}

data class DataDto(var foo: String)
private data class DataDto(var foo: String)

@Test
fun redisTemplateBackOff() {
this.contextRunner.withUserConfiguration(CustomRedisTemplateConfiguration::class.java)
.run { context ->
assertThat(context).hasSingleBean(RedisTemplate::class.java)
assertThat(context).getBean("myCustomRedisTemplate")
.isSameAs(context.getBean(RedisTemplate::class.java))
}
}

@Test
fun redisCacheConfigurationWithCacheProperties() {
this.contextRunner.withUserConfiguration(CustomCachePropertiesConfiguration::class.java)
.run { context ->
assertThat(context).hasSingleBean(RedisCacheConfiguration::class.java)
val redisCacheConfiguration = context.getBean(RedisCacheConfiguration::class.java)
assertThat(redisCacheConfiguration).extracting(RedisCacheConfiguration::getTtlFunction)
.isInstanceOf(FixedDurationTtlFunction::class.java)
.extracting("duration")
.isEqualTo(Duration.ofSeconds(15))
assertThat(redisCacheConfiguration.allowCacheNullValues).isFalse()
assertThat(redisCacheConfiguration.usePrefix()).isTrue()
assertThat(redisCacheConfiguration.getKeyPrefixFor("")).isEqualTo("foo::")
}
}

@Configuration(proxyBeanMethods = false)
@EnableCaching
private class BasicRedisConfiguration {
@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
return mock(RedisConnectionFactory::class.java)
}

@Bean
fun cacheManager(connectionFactory: RedisConnectionFactory): CacheManager {
return RedisCacheManager.create(connectionFactory)
}
}

@Configuration(proxyBeanMethods = false)
@Import(BasicRedisConfiguration::class)
private class CustomRedisTemplateConfiguration {
@Bean
fun cacheProperties(): CacheProperties {
return CacheProperties()
}

@Bean
fun myCustomRedisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<*, *>? {
return mock(RedisTemplate::class.java)
}
}

@Configuration(proxyBeanMethods = false)
@Import(BasicRedisConfiguration::class)
private class CustomCachePropertiesConfiguration {
@Bean
fun cacheProperties(): CacheProperties {
return CacheProperties().apply {
redis.timeToLive = Duration.ofSeconds(15)
redis.keyPrefix = "foo"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package io.cloudflight.platform.spring.caching

import io.cloudflight.platform.spring.caching.autoconfigure.SessionAutoConfiguration
import io.cloudflight.platform.spring.caching.serializer.SafeRedisSessionSerializer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.springframework.boot.autoconfigure.AutoConfigurations
import org.springframework.boot.test.context.FilteredClassLoader
import org.springframework.boot.test.context.runner.WebApplicationContextRunner
import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.EnableCaching
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Import
import org.springframework.data.redis.cache.RedisCacheManager
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.RedisSerializer
import org.springframework.session.Session
import org.springframework.session.SessionRepository
import org.springframework.session.data.redis.RedisIndexedSessionRepository
import org.springframework.session.data.redis.RedisSessionRepository

class SessionAutoConfigurationTest {

private val contextRunner = WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SessionAutoConfiguration::class.java))

@Test
fun autoConfigurationDisabledIfNoClassMatches() {
this.contextRunner.withClassLoader(FilteredClassLoader(Session::class.java))
.withUserConfiguration(BasicRedisConfiguration::class.java)
.run { context ->
assertThat(context).doesNotHaveBean(SessionRepository::class.java)
}
}

@Test
fun defaultRedisHttpSession() {
this.contextRunner.withUserConfiguration(BasicRedisConfiguration::class.java)
.run { context ->
assertThat(context).hasSingleBean(SessionRepository::class.java)
assertThat(context).getBean("sessionRepository")
.isInstanceOf(RedisSessionRepository::class.java)
}
}

@Test
fun redisHttpSessionBackOff() {
this.contextRunner.withUserConfiguration(CustomRedisIndexedHttpSessionConfiguration::class.java)
.run { context ->
assertThat(context).hasSingleBean(SessionRepository::class.java)
assertThat(context).getBean("sessionRepository")
.isInstanceOf(RedisIndexedSessionRepository::class.java)
}
}

@Test
fun safeRedisSessionSerializerBackoff() {
this.contextRunner.withUserConfiguration(CustomRedisSerializerConfiguration::class.java)
.run { context ->
assertThat(context).hasSingleBean(RedisSerializer::class.java)
assertThat(context).getBean("customRedisSerializer")
.isInstanceOf(SafeRedisSessionSerializer::class.java)
}
}

@Test
fun defaultSafeRedisSessionSerializer() {
this.contextRunner.withUserConfiguration(CustomRedisIndexedHttpSessionConfiguration::class.java)
.run { context ->
assertThat(context).hasSingleBean(RedisSerializer::class.java)
assertThat(context).getBean("springSessionDefaultRedisSerializer")
.isInstanceOf(SafeRedisSessionSerializer::class.java)
}
}

@Configuration(proxyBeanMethods = false)
@EnableCaching
private class BasicRedisConfiguration {
@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
return mock(RedisConnectionFactory::class.java)
}

@Bean
fun cacheManager(connectionFactory: RedisConnectionFactory): CacheManager {
return RedisCacheManager.create(connectionFactory)
}
}


@Configuration(proxyBeanMethods = false)
@Import(BasicRedisConfiguration::class)
private class CustomRedisIndexedHttpSessionConfiguration {

@Bean
fun redisTemplate(): RedisTemplate<*, *>? {
return mock(RedisTemplate::class.java)
}

@Bean
fun sessionRepository(): RedisIndexedSessionRepository {
return mock(RedisIndexedSessionRepository::class.java)
}
}

@Configuration(proxyBeanMethods = false)
@Import(BasicRedisConfiguration::class)
private class CustomRedisSerializerConfiguration {

@Bean
fun customRedisSerializer(): RedisSerializer<*> {
return SafeRedisSessionSerializer(RedisSerializer.java())
}
}
}
Loading

0 comments on commit 516b7d9

Please sign in to comment.