diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/HeaderEnricherSpec.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/HeaderEnricherSpec.java index 839e7db12c..a206579977 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/HeaderEnricherSpec.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/HeaderEnricherSpec.java @@ -688,7 +688,7 @@ public class HeaderEnricherSpec extends ConsumerEndpointSpec * If the header exists, it will not be overwritten unless {@link #defaultOverwrite(boolean)} is true. * @param routingSlipPath the header value for {@link IntegrationMessageHeaderAccessor#ROUTING_SLIP}. * @return the header enricher spec. @@ -709,7 +709,7 @@ public class HeaderEnricherSpec extends ConsumerEndpointSpec * @param overwrite true to overwrite an existing header. * @param routingSlipPath the header value for {@link IntegrationMessageHeaderAccessor#ROUTING_SLIP}. * @return the header enricher spec. diff --git a/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/CacheRequestHandlerAdvice.java b/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/CacheRequestHandlerAdvice.java new file mode 100644 index 0000000000..577f48352f --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/handler/advice/CacheRequestHandlerAdvice.java @@ -0,0 +1,265 @@ +/* + * Copyright 2019 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.integration.handler.advice; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheAspectSupport; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.interceptor.CacheEvictOperation; +import org.springframework.cache.interceptor.CacheOperation; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.interceptor.CachePutOperation; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.CacheableOperation; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.integration.expression.ExpressionUtils; +import org.springframework.integration.expression.FunctionExpression; +import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; +import org.springframework.integration.support.AbstractIntegrationMessageBuilder; +import org.springframework.lang.Nullable; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * The {@link AbstractRequestHandlerAdvice} implementation for caching + * {@link AbstractReplyProducingMessageHandler.RequestHandler#handleRequestMessage(Message)} results. + * Supports all the cache operations - cacheable, put, evict. + * By default only cacheable is applied for the provided {@code cacheNames}. + * The default cache {@code key} is {@code payload} of the request message. + * + * @author Artem Bilan + * + * @since 5.2 + * + * @see AbstractReplyProducingMessageHandler.RequestHandler + * @see CacheAspectSupport + * @see CacheOperation + */ +public class CacheRequestHandlerAdvice extends AbstractRequestHandlerAdvice + implements SmartInitializingSingleton { + + private static final Method HANDLE_REQUEST_METHOD = + ReflectionUtils.findMethod(AbstractReplyProducingMessageHandler.RequestHandler.class, + "handleRequestMessage", Message.class); + + private final IntegrationCacheAspect delegate = new IntegrationCacheAspect(); + + private final String[] cacheNames; + + private final List cacheOperations = new ArrayList<>(); + + private Expression keyExpression = new FunctionExpression>(Message::getPayload); + + /** + * Create a {@link CacheRequestHandlerAdvice} instance based on the provided name of caches + * and {@link CacheableOperation} as default one. + * This can be overridden by the {@link #setCacheOperations}. + * @param cacheNames the name of caches to use in the advice. + * @see #setCacheOperations + */ + public CacheRequestHandlerAdvice(String... cacheNames) { + this.cacheNames = cacheNames; + CacheableOperation.Builder builder = new CacheableOperation.Builder(); + builder.setName(toString()); + this.cacheOperations.add(builder.build()); + } + + /** + * Configure a set of {@link CacheOperation} which are going to be applied to the + * {@link AbstractReplyProducingMessageHandler.RequestHandler#handleRequestMessage(Message)} + * method via {@link IntegrationCacheAspect}. + * This is similar to the technique provided by the + * {@link org.springframework.cache.annotation.Caching} annotation. + * @param cacheOperations the array of {@link CacheOperation} to use. + * @see org.springframework.cache.annotation.Caching + */ + public void setCacheOperations(CacheOperation... cacheOperations) { + Assert.notEmpty(cacheOperations, "'cacheOperations' must not be empty"); + Assert.notNull(cacheOperations, "'cacheOperations' must not be null"); + this.cacheOperations.clear(); + this.cacheOperations.addAll(Arrays.asList(cacheOperations)); + } + + /** + * Configure a common {@link CacheManager} if some {@link CacheOperation} comes without it. + * See {@link org.springframework.cache.annotation.CacheConfig} annotation for similar approach. + * @param cacheManager the {@link CacheManager} to use. + * @see org.springframework.cache.annotation.CacheConfig + */ + public void setCacheManager(CacheManager cacheManager) { + this.delegate.setCacheManager(cacheManager); + } + + /** + * Configure a common {@link CacheResolver} if some {@link CacheOperation} comes without it. + * See {@link org.springframework.cache.annotation.CacheConfig} for similar approach. + * @param cacheResolver the {@link CacheResolver} to use. + * @see org.springframework.cache.annotation.CacheConfig + */ + public void setCacheResolver(CacheResolver cacheResolver) { + this.delegate.setCacheResolver(cacheResolver); + } + + /** + * Set the {@link CacheErrorHandler} instance to use to handle errors + * thrown by the cache provider. + * @param errorHandler the {@link CacheErrorHandler} to use. + * @see CacheAspectSupport#setErrorHandler(CacheErrorHandler) + */ + public void setErrorHandler(CacheErrorHandler errorHandler) { + Assert.notNull(errorHandler, "'errorHandler' must not be null"); + this.delegate.setErrorHandler(errorHandler); + } + + /** + * Configure an expression in SpEL style to evaluate a cache key at runtime + * against a request message. + * @param keyExpression the expression to use for cache key generation. + */ + public void setKeyExpressionString(String keyExpression) { + Assert.hasText(keyExpression, "'keyExpression' must not be empty"); + setKeyExpression(EXPRESSION_PARSER.parseExpression(keyExpression)); + } + + /** + * Configure a {@link Function} to evaluate a cache key at runtime + * against a request message. + * @param keyFunction the {@link Function} to use for cache key generation. + */ + public void setKeyFunction(Function, ?> keyFunction) { + Assert.notNull(keyFunction, "'keyFunction' must not be null"); + setKeyExpression(new FunctionExpression<>(keyFunction)); + } + + /** + * Configure a SpEL expression to evaluate a cache key at runtime + * against a request message. + * @param keyExpression the expression to use for cache key generation. + */ + public void setKeyExpression(Expression keyExpression) { + Assert.notNull(keyExpression, "'keyExpression' must not be null"); + this.keyExpression = keyExpression; + } + + + @Override + public void afterSingletonsInstantiated() { + this.delegate.afterSingletonsInstantiated(); + } + + @Override + protected void onInit() { + List cacheOperations; + if (!ObjectUtils.isEmpty(this.cacheNames)) { + cacheOperations = this.cacheOperations.stream() + .filter((operation) -> ObjectUtils.isEmpty(operation.getCacheNames())) + .map((operation) -> { + CacheOperation.Builder builder; + if (operation instanceof CacheableOperation) { + CacheableOperation cacheableOperation = (CacheableOperation) operation; + CacheableOperation.Builder cacheableBuilder = new CacheableOperation.Builder(); + cacheableBuilder.setSync(cacheableOperation.isSync()); + String unless = cacheableOperation.getUnless(); + if (unless != null) { + cacheableBuilder.setUnless(unless); + } + builder = cacheableBuilder; + } + else if (operation instanceof CacheEvictOperation) { + CacheEvictOperation.Builder cacheEvictBuilder = new CacheEvictOperation.Builder(); + CacheEvictOperation cacheEvictOperation = (CacheEvictOperation) operation; + cacheEvictBuilder.setBeforeInvocation(cacheEvictOperation.isBeforeInvocation()); + cacheEvictBuilder.setCacheWide(cacheEvictOperation.isCacheWide()); + builder = cacheEvictBuilder; + } + else { + CachePutOperation cachePutOperation = (CachePutOperation) operation; + CachePutOperation.Builder cachePutBuilder = new CachePutOperation.Builder(); + String unless = cachePutOperation.getUnless(); + if (unless != null) { + cachePutBuilder.setUnless(unless); + } + builder = cachePutBuilder; + } + + builder.setName(operation.getName()); + builder.setCacheManager(operation.getCacheManager()); + builder.setCacheNames(this.cacheNames); + builder.setCacheResolver(operation.getCacheResolver()); + builder.setCondition(operation.getCondition()); + builder.setKey(operation.getKey()); + builder.setKeyGenerator(operation.getKeyGenerator()); + return builder.build(); + }) + .collect(Collectors.toList()); + } + else { + cacheOperations = this.cacheOperations; + } + + this.delegate.setBeanFactory(getBeanFactory()); + EvaluationContext evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory()); + this.delegate.setKeyGenerator((target, method, params) -> + this.keyExpression.getValue(evaluationContext, params[0])); // NOSONAR + this.delegate.setCacheOperationSources((method, targetClass) -> cacheOperations); + this.delegate.afterPropertiesSet(); + + } + + @Nullable + @Override + protected Object doInvoke(ExecutionCallback callback, Object target, Message message) { + CacheOperationInvoker operationInvoker = + () -> { + Object result = callback.execute(); + // Drop MessageBuilder optimization in favor of Serializable support in cache implementation. + if (result instanceof AbstractIntegrationMessageBuilder) { + return ((AbstractIntegrationMessageBuilder) result).build(); + } + else { + return result; + } + + }; + + return this.delegate.invoke(operationInvoker, target, message); + } + + private static class IntegrationCacheAspect extends CacheAspectSupport { + + IntegrationCacheAspect() { + } + + @Nullable + Object invoke(CacheOperationInvoker invoker, Object target, Message message) { + return super.execute(invoker, target, HANDLE_REQUEST_METHOD, new Object[] { message }); // NOSONAR + } + + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/transformer/AbstractMessageProcessingTransformer.java b/spring-integration-core/src/main/java/org/springframework/integration/transformer/AbstractMessageProcessingTransformer.java index de96f7a853..bf6ad29833 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/transformer/AbstractMessageProcessingTransformer.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/transformer/AbstractMessageProcessingTransformer.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.context.Lifecycle; import org.springframework.integration.handler.MessageProcessor; +import org.springframework.integration.support.AbstractIntegrationMessageBuilder; import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.integration.support.MessageBuilderFactory; import org.springframework.integration.support.utils.IntegrationUtils; @@ -118,10 +119,18 @@ public abstract class AbstractMessageProcessingTransformer return (Message) result; } + AbstractIntegrationMessageBuilder messageBuilder; + + if (result instanceof AbstractIntegrationMessageBuilder) { + messageBuilder = (AbstractIntegrationMessageBuilder) result; + } + else { + messageBuilder = getMessageBuilderFactory().withPayload(result); + } + MessageHeaders requestHeaders = message.getHeaders(); - return getMessageBuilderFactory() - .withPayload(result) + return messageBuilder .filterAndCopyHeadersIfAbsent(requestHeaders, this.selectiveHeaderPropagation ? this.notPropagatedHeaders : null) .build(); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/CacheRequestHandlerAdviceTests.java b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/CacheRequestHandlerAdviceTests.java new file mode 100644 index 0000000000..eca81514cc --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/handler/advice/CacheRequestHandlerAdviceTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2019 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.integration.handler.advice; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.interceptor.CacheEvictOperation; +import org.springframework.cache.interceptor.CachePutOperation; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.annotation.Transformer; +import org.springframework.integration.config.EnableIntegration; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * @author Artem Bilan + * + * @since 5.2 + */ +@SpringJUnitConfig +@DirtiesContext +public class CacheRequestHandlerAdviceTests { + + private static final String TEST_CACHE = "testCache"; + + private static final String TEST_PUT_CACHE = "testPutCache"; + + @Autowired + private AtomicInteger cachedMethodCounter; + + @Autowired + private MessageChannel transformerChannel; + + @Autowired + private MessageChannel serviceChannel; + + @Autowired + private CacheManager cacheManager; + + @Test + @SuppressWarnings("unchecked") + void testCacheRequestHandlerAdvice() { + GenericMessage testMessage1 = new GenericMessage<>("foo"); + this.transformerChannel.send(testMessage1); + GenericMessage testMessage2 = new GenericMessage<>("foo"); + this.transformerChannel.send(testMessage2); + this.transformerChannel.send(new GenericMessage<>("foo")); + + assertThat(this.cachedMethodCounter.get()).isEqualTo(1); + Cache testCache = cacheManager.getCache(TEST_CACHE); + assertThat(testCache).isNotNull(); + + ConcurrentMap nativeCache = (ConcurrentMap) testCache.getNativeCache(); + assertThat(nativeCache).hasSize(1); + assertThat(nativeCache.values()).element(0).isSameAs(testMessage1); + + this.serviceChannel.send(testMessage1); + this.serviceChannel.send(testMessage2); + + assertThat(nativeCache).hasSize(0); + + testCache = cacheManager.getCache(TEST_PUT_CACHE); + assertThat(testCache).isNotNull(); + + nativeCache = (ConcurrentMap) testCache.getNativeCache(); + assertThat(nativeCache).hasSize(1); + assertThat(nativeCache.values()).element(0).isSameAs(testMessage2); + } + + @Configuration + @EnableIntegration + public static class Config { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } + + @Bean + public CacheRequestHandlerAdvice cacheAdvice() { + CacheRequestHandlerAdvice cacheRequestHandlerAdvice = new CacheRequestHandlerAdvice(TEST_CACHE); + cacheRequestHandlerAdvice.setKeyExpressionString("payload"); + return cacheRequestHandlerAdvice; + } + + @Bean + public AtomicInteger cachedMethodCounter() { + return new AtomicInteger(); + } + + @Transformer(inputChannel = "transformerChannel", outputChannel = "nullChannel", adviceChain = "cacheAdvice") + public Object transform(Message message) { + cachedMethodCounter().getAndIncrement(); + return MessageBuilder.fromMessage(message); + } + + @Bean + public CacheRequestHandlerAdvice cachePutAndEvictAdvice() { + CacheRequestHandlerAdvice cacheRequestHandlerAdvice = new CacheRequestHandlerAdvice(); + cacheRequestHandlerAdvice.setKeyExpressionString("payload"); + CachePutOperation.Builder cachePutBuilder = new CachePutOperation.Builder(); + cachePutBuilder.setCacheName(TEST_PUT_CACHE); + CacheEvictOperation.Builder cacheEvictBuilder = new CacheEvictOperation.Builder(); + cacheEvictBuilder.setCacheName(TEST_CACHE); + cacheRequestHandlerAdvice.setCacheOperations(cachePutBuilder.build(), cacheEvictBuilder.build()); + return cacheRequestHandlerAdvice; + } + + @ServiceActivator(inputChannel = "serviceChannel", outputChannel = "nullChannel", + adviceChain = "cachePutAndEvictAdvice") + public Message service(Message message) { + return message; + } + + } + +} diff --git a/src/reference/asciidoc/handler-advice.adoc b/src/reference/asciidoc/handler-advice.adoc index c02ada0c0c..75a9ae172c 100644 --- a/src/reference/asciidoc/handler-advice.adoc +++ b/src/reference/asciidoc/handler-advice.adoc @@ -55,6 +55,7 @@ In addition to providing the general mechanism to apply AOP advice classes, Spri * `RequestHandlerCircuitBreakerAdvice` (described in <>) * `ExpressionEvaluatingRequestHandlerAdvice` (described in <>) * `RateLimiterRequestHandlerAdvice` (described in <>) +* `CacheRequestHandlerAdvice` (described in <>) [[retry-advice]] ===== Retry Advice @@ -498,6 +499,56 @@ public String handleRequest(String payload) { ---- ==== +[[cache-advice]] +===== Caching Advice + +Starting with version 5.2, the `CacheRequestHandlerAdvice` has been introduced. +It is based on the caching abstraction in https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache[Spring Framework] and aligned with the concepts and functionality provided by the `@Caching` annotation family. +The logic internally is based on the `CacheAspectSupport` extension, where proxying for caching operations is done around the `AbstractReplyProducingMessageHandler.RequestHandler.handleRequestMessage` method with the request `Message` as the argument. +This advice can be configured with a SpEL expression or a `Function` to evaluate a cache key. +The request `Message` is available as the root object for the SpEL evaluation context, or as the `Function` input argument. +By default, the `payload` of the request message is used for the cache key. +The `CacheRequestHandlerAdvice` must be configured with `cacheNames`, when a default cache operation is a `CacheableOperation`, or with a set of any arbitrary `CacheOperation` s. +Every `CacheOperation` can be configured separately or have shared options, like a `CacheManager`, `CacheResolver` and `CacheErrorHandler`, can be reused from the `CacheRequestHandlerAdvice` configuration. +This configuration functionality is similar to Spring Framework's `@CacheConfig` and `@Caching` annotation combination. +If a `CacheManager` is not provided, a single bean is resolved by default from the `BeanFactory` in the `CacheAspectSupport`. + +The following example configures two advices with different set of caching operations: +==== +[source, java] +---- +@Bean +public CacheRequestHandlerAdvice cacheAdvice() { + CacheRequestHandlerAdvice cacheRequestHandlerAdvice = new CacheRequestHandlerAdvice(TEST_CACHE); + cacheRequestHandlerAdvice.setKeyExpressionString("payload"); + return cacheRequestHandlerAdvice; +} + +@Transformer(inputChannel = "transformerChannel", outputChannel = "nullChannel", adviceChain = "cacheAdvice") +public Object transform(Message message) { + ... +} + +@Bean +public CacheRequestHandlerAdvice cachePutAndEvictAdvice() { + CacheRequestHandlerAdvice cacheRequestHandlerAdvice = new CacheRequestHandlerAdvice(); + cacheRequestHandlerAdvice.setKeyExpressionString("payload"); + CachePutOperation.Builder cachePutBuilder = new CachePutOperation.Builder(); + cachePutBuilder.setCacheName(TEST_PUT_CACHE); + CacheEvictOperation.Builder cacheEvictBuilder = new CacheEvictOperation.Builder(); + cacheEvictBuilder.setCacheName(TEST_CACHE); + cacheRequestHandlerAdvice.setCacheOperations(cachePutBuilder.build(), cacheEvictBuilder.build()); + return cacheRequestHandlerAdvice; +} + +@ServiceActivator(inputChannel = "serviceChannel", outputChannel = "nullChannel", + adviceChain = "cachePutAndEvictAdvice") +public Message service(Message message) { + ... +} +---- +==== + [[custom-advice]] ==== Custom Advice Classes @@ -505,7 +556,7 @@ In addition to the provided advice classes <>, While you can provide any implementation of `org.aopalliance.aop.Advice` (usually `org.aopalliance.intercept.MethodInterceptor`), we generally recommend that you subclass `o.s.i.handler.advice.AbstractRequestHandlerAdvice`. This has the benefit of avoiding the writing of low-level aspect-oriented programming code as well as providing a starting point that is specifically tailored for use in this environment. -Subclasses need to implement the `doInvoke()`` method, the definition of which follows: +Subclasses need to implement the `doInvoke()` method, the definition of which follows: ==== [source,java] @@ -737,8 +788,7 @@ On the other hand, if you want all the attempts and any recovery operations (in Sometimes, it is useful to access handler properties from within the advice. For example, most handlers implement `NamedComponent` to let you access the component name. -The target object can be accessed through the `target` argument (when subclassing `AbstractRequestHandlerAdvice`) or -`invocation.getThis()` (when implementing `org.aopalliance.intercept.MethodInterceptor`). +The target object can be accessed through the `target` argument (when subclassing `AbstractRequestHandlerAdvice`) or `invocation.getThis()` (when implementing `org.aopalliance.intercept.MethodInterceptor`). When the entire handler is advised (such as when the handler does not produce replies or the advice implements `HandleMessageAdvice`), you can cast the target object to an interface, such as `NamedComponent`, as shown in the following example: @@ -758,8 +808,7 @@ String componentName = ((NamedComponent) invocation.getThis()).getComponentName( ---- ==== -When only the `handleRequestMessage()` method is advised (in a reply-producing handler), you need to access the -full handler, which is an `AbstractReplyProducingMessageHandler`. +When only the `handleRequestMessage()` method is advised (in a reply-producing handler), you need to access the full handler, which is an `AbstractReplyProducingMessageHandler`. The following example shows how to do so: ==== @@ -892,9 +941,8 @@ public IntegrationFlow flow() { ... } ---- +==== NOTE: The `IdempotentReceiverInterceptor` is designed only for the `MessageHandler.handleMessage(Message)` method. Starting with version 4.3.1, it implements `HandleMessageAdvice`, with the `AbstractHandleMessageAdvice` as a base class, for better dissociation. See <> for more information. - -==== diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 7c072506c8..24ea2db9ec 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -13,6 +13,13 @@ If you are interested in more details, see the Issue Tracker tickets that were r The `RateLimiterRequestHandlerAdvice` is now available for limiting requests rate on handlers. See <> for more information. +[[x5.2-cacheAdvice]] +=== Caching Advice Support + +The `CacheRequestHandlerAdvice` is now available for caching request results on handlers. +See <> for more information. + + [[x5.2-general]] === General Changes