Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.reactive.function.server.cache

/**
* Annotation indicating that the result of invoking a *suspend method* can be cached
* for the lifespan of the underlying web request coroutine.
*
* Example:
*
* ```
* class MyServiceBean {
*
* @CoRequestCacheable(key = "#userName")
* suspend fun fetchUserAgeFromDownstreamService(userName: String, authHeader: String): Int {
* // prepare request and fetch the user info
* return userInfo.age
* }
*
* }
* ```
*
* Each time an advised suspend method is invoked, caching behavior will be applied,
* checking whether the method has been already invoked for the given arguments *within the same web request execution*.
* A sensible default simply uses the method parameters to compute the key, but
* a SpEL expression can be provided via the [key] attribute.
*
* If no value is found in the cache for the computed key, the target method
* will be invoked and the returned value will be stored in the coroutine context.
*
* Note that breaking
* [structured concurrency](https://kotlinlang.org/docs/coroutines-basics.html#coroutine-scope-and-structured-concurrency)
* by invoking the annotated method in a coroutine scope not tied to the web request, will prevent any caching behaviour.
*
* @author Angelo Bracaglia
* @since 7.0
* @see EnableCoRequestCaching
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class CoRequestCacheable(

/**
* Spring Expression Language (SpEL) expression for computing the key dynamically.
*
* The default value is `""`, meaning all method parameters are considered as a key.
*
* Method arguments can be accessed by index. For instance the second argument
* can be accessed via `#p1` or `#a1`.
* Arguments can also be accessed by name if that information is available.
*/
val key: String = ""
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.reactive.function.server.cache

import org.springframework.context.annotation.AdviceMode
import org.springframework.context.annotation.Import
import org.springframework.core.Ordered
import org.springframework.web.reactive.function.server.cache.config.CoRequestCacheConfigurationSelector

/**
* Enables request-scoped cache management capability for Spring WebFlux servers with
* [Kotlin coroutines support enabled](https://docs.spring.io/spring-framework/reference/languages/kotlin/coroutines.html#dependencies).
*
* To be used together with @[Configuration](org.springframework.context.annotation.Configuration) classes as follows:
*
* ```
* @Configuration
* @EnableCoRequestCaching
* class AppConfig {
*
* @Bean
* fun myService(): MyService {
* // configure and return a class having @CoRequestCacheable suspend methods
* return MyService()
* }
*
* }
* ```
*
* Note that the only supported advice [mode] is [AdviceMode.PROXY],
* so local calls within the same class cannot get intercepted.
*
* @author Angelo Bracaglia
* @since 7.0
* @see CoRequestCacheable
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Import(CoRequestCacheConfigurationSelector::class)
annotation class EnableCoRequestCaching(

/**
* Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
* to standard Java interface-based proxies. The default is `false`.
*/
val proxyTargetClass: Boolean = false,

/**
* Indicate the ordering of the execution of the co-request caching advisor
* when multiple advices are applied at a specific joinpoint.
*
* The default is [Ordered.LOWEST_PRECEDENCE].
*/
val order: Int = Ordered.LOWEST_PRECEDENCE,

/**
* Indicate how caching advice should be applied.
* The default and *only supported mode* is [AdviceMode.PROXY].
*/
val mode: AdviceMode = AdviceMode.PROXY
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.reactive.function.server.cache.config

import org.aopalliance.intercept.MethodInterceptor
import org.springframework.aop.Advisor
import org.springframework.beans.factory.config.BeanDefinition
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.ImportAware
import org.springframework.context.annotation.Role
import org.springframework.core.annotation.AnnotationAttributes
import org.springframework.core.type.AnnotationMetadata
import org.springframework.web.reactive.function.server.cache.EnableCoRequestCaching
import org.springframework.web.reactive.function.server.cache.context.CoRequestCacheWebFilter
import org.springframework.web.reactive.function.server.cache.interceptor.CoRequestCacheAdvisor
import org.springframework.web.reactive.function.server.cache.interceptor.CoRequestCacheInterceptor
import org.springframework.web.reactive.function.server.cache.interceptor.CoRequestCacheKeyGenerator
import org.springframework.web.reactive.function.server.cache.operation.CoRequestCacheOperationSource
import org.springframework.web.server.CoWebFilter
import kotlin.reflect.jvm.jvmName

/**
* `@Configuration` class that registers the Spring infrastructure beans necessary
* to enable proxy-based request-scoped cache for Spring WebFlux servers
* with Kotlin coroutines support enabled.
*
* @author Angelo Bracaglia
* @since 7.0
* @see CoRequestCacheConfigurationSelector
*/
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
internal class CoRequestCacheConfiguration : ImportAware {
lateinit var enableCoRequestCaching: AnnotationAttributes

override fun setImportMetadata(importMetadata: AnnotationMetadata) {
enableCoRequestCaching =
AnnotationAttributes.fromMap(
importMetadata.getAnnotationAttributes(EnableCoRequestCaching::class.jvmName)
) ?: throw IllegalArgumentException(
"@EnableCoRequestCaching is not present on importing class ${importMetadata.className}"
)
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun coRequestCacheOperationSource(): CoRequestCacheOperationSource = CoRequestCacheOperationSource()

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun coRequestCacheInterceptor(coRequestCacheOperationSource: CoRequestCacheOperationSource): MethodInterceptor =
CoRequestCacheInterceptor(
CoRequestCacheKeyGenerator(
coRequestCacheOperationSource
)
)

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun coRequestCacheAdvisor(
coRequestCacheOperationSource: CoRequestCacheOperationSource,
coRequestCacheInterceptor: MethodInterceptor
): Advisor =
CoRequestCacheAdvisor(
coRequestCacheOperationSource,
coRequestCacheInterceptor
)
.apply {
if (::enableCoRequestCaching.isInitialized) {
order = enableCoRequestCaching.getNumber("order")
}
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
fun coRequestCacheWebFilter(): CoWebFilter = CoRequestCacheWebFilter()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.reactive.function.server.cache.config

import org.springframework.context.annotation.AdviceMode
import org.springframework.context.annotation.AdviceModeImportSelector
import org.springframework.context.annotation.AutoProxyRegistrar
import org.springframework.web.reactive.function.server.cache.EnableCoRequestCaching
import kotlin.reflect.jvm.jvmName

private const val UNSUPPORTED_ADVISE_MODE_MESSAGE = "CoRequestCaching does not support aspectj advice mode"

/**
* Select which classes to import according to the value of [EnableCoRequestCaching.mode] on the importing
* `@Configuration` class.
*
* Only [AdviceMode.PROXY] is currently supported.
*
* @author Angelo Bracaglia
* @since 7.0
* @see CoRequestCacheConfiguration
*/
internal class CoRequestCacheConfigurationSelector : AdviceModeImportSelector<EnableCoRequestCaching>() {
override fun selectImports(adviceMode: AdviceMode): Array<out String> =
when (adviceMode) {
AdviceMode.PROXY -> arrayOf(AutoProxyRegistrar::class.jvmName, CoRequestCacheConfiguration::class.jvmName)
AdviceMode.ASPECTJ -> throw UnsupportedOperationException(UNSUPPORTED_ADVISE_MODE_MESSAGE)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.reactive.function.server.cache.context

import org.reactivestreams.Publisher
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext

/**
* A coroutine context element holding the [cache] values for
* [@CoRequestCacheable][org.springframework.web.reactive.function.server.cache.CoRequestCacheable]
* annotated methods.
*
* @author Angelo Bracaglia
* @since 7.0
*/
internal class CoRequestCacheContext(
val cache: ConcurrentHashMap<Any, Publisher<*>> = ConcurrentHashMap()
) : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<CoRequestCacheContext>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.reactive.function.server.cache.context

import kotlinx.coroutines.withContext
import org.springframework.web.server.CoWebFilter
import org.springframework.web.server.CoWebFilterChain
import org.springframework.web.server.ServerWebExchange

/**
* Add a [CoRequestCacheContext] element to the web request coroutine context.
*
* This [CoWebFilter] is automatically registered when
* [EnableCoRequestCaching][org.springframework.web.reactive.function.server.cache.EnableCoRequestCaching]
* is applied to the app configuration.
*
* @author Angelo Bracaglia
* @since 7.0
*/
internal class CoRequestCacheWebFilter : CoWebFilter() {
override suspend fun filter(
exchange: ServerWebExchange,
chain: CoWebFilterChain
) {
return withContext(CoRequestCacheContext()) {
chain.filter(exchange)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.web.reactive.function.server.cache.interceptor

import org.aopalliance.aop.Advice
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor
import org.springframework.web.reactive.function.server.cache.operation.CoRequestCacheOperationSource
import java.lang.reflect.Method

/**
* Advisor driven by a [coRequestCacheOperationSource], used to match suspend methods that are cacheable for the lifespan
* of the coroutine handling a web request.
*
* @author Angelo Bracaglia
* @since 7.0
*/
internal class CoRequestCacheAdvisor(
val coRequestCacheOperationSource: CoRequestCacheOperationSource,
coRequestCacheAdvice: Advice,
) : StaticMethodMatcherPointcutAdvisor(coRequestCacheAdvice) {
override fun matches(method: Method, targetClass: Class<*>): Boolean =
coRequestCacheOperationSource.hasCacheOperations(method, targetClass)
}
Loading