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:
Artem Bilan
2017-03-31 12:45:02 -04:00
committed by Gary Russell
parent 36581bfed8
commit 735e82e721
6 changed files with 488 additions and 13 deletions

View File

@@ -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.

View File

@@ -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
}
}
}

View File

@@ -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();

View File

@@ -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;
}
}
}