Add support for CacheRequestHandlerAdvice
* Fix `AbstractMessageProcessingTransformer` to react for the `AbstractIntegrationMessageBuilder` invocation result and don't wrap it into the `Message` * Demonstrate functionality in the `CacheRequestHandlerAdviceTests` * Polishing and Docs * Fix JavaDocs warnings Doc polishing.
This commit is contained in:
committed by
Gary Russell
parent
36581bfed8
commit
735e82e721
@@ -688,7 +688,7 @@ public class HeaderEnricherSpec extends ConsumerEndpointSpec<HeaderEnricherSpec,
|
||||
/**
|
||||
* Add a {@link IntegrationMessageHeaderAccessor#ROUTING_SLIP} header.
|
||||
* The possible values are:
|
||||
* <p><ul>
|
||||
* <ul>
|
||||
* <li>A {@link org.springframework.messaging.MessageChannel} instance.
|
||||
* <li>A {@link org.springframework.messaging.MessageChannel} bean name.
|
||||
* <li>A {@link org.springframework.integration.routingslip.RoutingSlipRouteStrategy} instance.
|
||||
@@ -696,7 +696,7 @@ public class HeaderEnricherSpec extends ConsumerEndpointSpec<HeaderEnricherSpec,
|
||||
* <li>A {@code String} for SpEL expression which has to be evaluated to the
|
||||
* {@link org.springframework.messaging.MessageChannel} or
|
||||
* {@link org.springframework.integration.routingslip.RoutingSlipRouteStrategy}.
|
||||
* </ul><p>
|
||||
* </ul>
|
||||
* If the header exists, it will <b>not</b> 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<HeaderEnricherSpec,
|
||||
/**
|
||||
* Add a {@link IntegrationMessageHeaderAccessor#ROUTING_SLIP} header.
|
||||
* The possible values are:
|
||||
* <p><ul>
|
||||
* <ul>
|
||||
* <li>A {@link org.springframework.messaging.MessageChannel} instance.
|
||||
* <li>A {@link org.springframework.messaging.MessageChannel} bean name.
|
||||
* <li>A {@link org.springframework.integration.routingslip.RoutingSlipRouteStrategy} instance.
|
||||
@@ -717,7 +717,7 @@ public class HeaderEnricherSpec extends ConsumerEndpointSpec<HeaderEnricherSpec,
|
||||
* <li>A {@code String} for SpEL expression which has to be evaluated to the
|
||||
* {@link org.springframework.messaging.MessageChannel} or
|
||||
* {@link org.springframework.integration.routingslip.RoutingSlipRouteStrategy}.
|
||||
* </ul><p>
|
||||
* </ul>
|
||||
* @param overwrite true to overwrite an existing header.
|
||||
* @param routingSlipPath the header value for {@link IntegrationMessageHeaderAccessor#ROUTING_SLIP}.
|
||||
* @return the header enricher spec.
|
||||
|
||||
@@ -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<CacheOperation> cacheOperations = new ArrayList<>();
|
||||
|
||||
private Expression keyExpression = new FunctionExpression<Message<?>>(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<Message<?>, ?> 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<CacheOperation> 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String> testMessage1 = new GenericMessage<>("foo");
|
||||
this.transformerChannel.send(testMessage1);
|
||||
GenericMessage<String> 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<Object, Object> nativeCache = (ConcurrentMap<Object, Object>) 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<Object, Object>) 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user