INT-28 Initial commit of (new in 2.0) MessagePublishingInterceptor. An AOP interceptor that uses EL to construct a Message payload and then publishes it.

This commit is contained in:
Mark Fisher
2009-08-25 19:26:37 +00:00
parent 1d07bbc3a5
commit 42a1cf0e13
9 changed files with 812 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2002-2009 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
*
* http://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.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation that provides the variable names to use when constructing the
* evaluation context for a MessagePublishingInterceptor.
*
* @author Mark Fisher
* @since 2.0
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExpressionBinding {
/**
* Names of the arguments as a comma-separated list. If not provided, the
* names will be discovered automatically if enabled by the compiler settings.
* These names will be used as the keys in the argument Map.
*/
String argNames() default "";
/**
* Name of the variable in the context that refers to the Map of arguments.
* <p>The default is "args".
*/
String argumentMapName() default ExpressionSource.DEFAULT_ARGUMENT_MAP_NAME;
/**
* Name of the variable in the context that refers to the return value, if any.
* <p>The default is "return".
*/
String returnValueName() default ExpressionSource.DEFAULT_RETURN_VALUE_NAME;
/**
* Name of the variable in the context that refers to any exception thrown
* by the method invocation that is being intercepted.
* <p>The default is "exception".
*/
String exceptionName() default ExpressionSource.DEFAULT_EXCEPTION_NAME;
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2002-2009 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
*
* http://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.aop;
import java.lang.reflect.Method;
/**
* Strategy for determining the expression string and evaluation context
* variable names from a Method.
*
* @author Mark Fisher
* @since 2.0
*/
interface ExpressionSource {
static final String DEFAULT_ARGUMENT_MAP_NAME = "args";
static final String DEFAULT_RETURN_VALUE_NAME = "return";
static final String DEFAULT_EXCEPTION_NAME = "exception";
/**
* Returns the expression string to be evaluated.
*/
String getExpressionString(Method method);
/**
* Returns the variable names to be associated with the intercepted method
* invocation's argument array.
*/
String[] getArgumentNames(Method method);
/**
* Returns the variable name to use in the evaluation context for the Map
* of arguments. The keys in this map will be determined by the result of
* the {@link #getArgumentNames(Method)} method.
*/
String getArgumentMapName(Method method);
/**
* Returns the variable name to use in the evaluation context for any
* return value resulting from the method invocation.
*/
String getReturnValueName(Method method);
/**
* Returns the variable name to use in the evaluation context for any
* exception thrown from the method invocation.
*/
String getExceptionName(Method method);
/**
* Returns the channel name to which Messages should be published
* for this particular method invocation.
*/
String getChannelName(Method method);
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2002-2009 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
*
* http://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.aop;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.SpelExpressionParserFactory;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.channel.ChannelResolver;
import org.springframework.integration.channel.MessageChannelTemplate;
import org.springframework.integration.core.Message;
import org.springframework.integration.core.MessageChannel;
import org.springframework.integration.gateway.SimpleMessageMapper;
import org.springframework.integration.message.InboundMessageMapper;
import org.springframework.util.Assert;
/**
* A {@link MethodInterceptor} that publishes Messages to a channel. The
* payload of the published Message can be derived from arguments or any return
* value or exception resulting from the method invocation. That mapping is the
* responsibility of the EL expression provided by the ExpressionSource.
*
* @author Mark Fisher
* @since 2.0
*/
public class MessagePublishingInterceptor implements MethodInterceptor {
private final MessageChannelTemplate channelTemplate = new MessageChannelTemplate();
private final ExpressionSource expressionSource;
private final ExpressionParser parser = SpelExpressionParserFactory.getParser();
private final InboundMessageMapper<Object> messageMapper = new SimpleMessageMapper();
private volatile ChannelResolver channelResolver;
public MessagePublishingInterceptor(ExpressionSource expressionSource) {
Assert.notNull(expressionSource, "expressionSource must not be null");
this.expressionSource = expressionSource;
}
public void setDefaultChannel(MessageChannel defaultChannel) {
this.channelTemplate.setDefaultChannel(defaultChannel);
}
public void setChannelResolver(ChannelResolver channelResolver) {
this.channelResolver = channelResolver;
}
public final Object invoke(final MethodInvocation invocation) throws Throwable {
StandardEvaluationContext context = new StandardEvaluationContext();
context.addPropertyAccessor(new MapAccessor());
Class<?> targetClass = AopUtils.getTargetClass(invocation.getThis());
Method method = AopUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
String[] argumentNames = this.expressionSource.getArgumentNames(method);
if (invocation.getArguments().length > 0 && argumentNames != null) {
int index = 0;
Map<String, Object> argumentMap = new HashMap<String, Object>();
for (String argumentName : argumentNames) {
if (invocation.getArguments().length <= ++index) {
break;
}
argumentMap.put(argumentName, invocation.getArguments()[index]);
}
context.setVariable(this.expressionSource.getArgumentMapName(method), argumentMap);
}
try {
Object returnValue = invocation.proceed();
context.setVariable(this.expressionSource.getReturnValueName(method), returnValue);
return returnValue;
}
catch (Throwable t) {
context.setVariable(this.expressionSource.getExceptionName(method), t);
throw t;
}
finally {
publishMessage(method, context);
}
}
private void publishMessage(Method method, EvaluationContext context) throws Exception {
String expressionString = this.expressionSource.getExpressionString(method);
if (expressionString != null) {
Expression expression = this.parser.parseExpression(expressionString);
Object result = expression.getValue(context);
if (result != null) {
Message<?> message = this.messageMapper.toMessage(result);
String channelName = this.expressionSource.getChannelName(method);
MessageChannel channel = null;
if (channelName != null) {
Assert.state(this.channelResolver != null, "ChannelResolver is required to resolve channel names.");
channel = this.channelResolver.resolveChannelName(channelName);
}
if (channel != null) {
this.channelTemplate.send(message, channel);
}
else {
this.channelTemplate.send(message);
}
}
}
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2002-2009 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
*
* http://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.aop;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Set;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* An {@link ExpressionSource} implementation that retrieves the expression
* string and evaluation context variable names from an annotation.
*
* @author Mark Fisher
* @since 2.0
*/
public class MethodAnnotationExpressionSource implements ExpressionSource {
private final Set<Class<? extends Annotation>> annotationTypes;
private volatile String channelAttributeName = "channel";
private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
public MethodAnnotationExpressionSource() {
this(Collections.<Class<? extends Annotation>>singleton(Publisher.class));
}
public MethodAnnotationExpressionSource(Set<Class<? extends Annotation>> annotationTypes) {
Assert.notEmpty(annotationTypes, "annotationTypes must not be empty");
this.annotationTypes = annotationTypes;
}
public void setChannelAttributeName(String channelAttributeName) {
Assert.hasText(channelAttributeName, "channelAttributeName must not be empty");
this.channelAttributeName = channelAttributeName;
}
public String getExpressionString(Method method) {
return this.getAnnotationValue(method, null, String.class);
}
public String[] getArgumentNames(Method method) {
ExpressionBinding annotation = AnnotationUtils.findAnnotation(method, ExpressionBinding.class);
if (annotation != null) {
String name = annotation.argNames();
if (StringUtils.hasText(name)) {
return StringUtils.tokenizeToStringArray(name, ",");
}
}
return this.parameterNameDiscoverer.getParameterNames(method);
}
public String getArgumentMapName(Method method) {
ExpressionBinding annotation = AnnotationUtils.findAnnotation(method, ExpressionBinding.class);
if (annotation != null) {
return annotation.argumentMapName();
}
return ExpressionSource.DEFAULT_ARGUMENT_MAP_NAME;
}
public String getReturnValueName(Method method) {
ExpressionBinding annotation = AnnotationUtils.findAnnotation(method, ExpressionBinding.class);
if (annotation != null) {
return annotation.returnValueName();
}
return ExpressionSource.DEFAULT_RETURN_VALUE_NAME;
}
public String getExceptionName(Method method) {
ExpressionBinding annotation = AnnotationUtils.findAnnotation(method, ExpressionBinding.class);
if (annotation != null) {
return annotation.exceptionName();
}
return ExpressionSource.DEFAULT_EXCEPTION_NAME;
}
public String getChannelName(Method method) {
String channelName = this.getAnnotationValue(method, this.channelAttributeName, String.class);
return (StringUtils.hasText(channelName) ? channelName : null);
}
@SuppressWarnings("unchecked")
private <T> T getAnnotationValue(Method method, String attributeName, Class<T> expectedType) {
T value = null;
for (Class<? extends Annotation> annotationType : this.annotationTypes) {
Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType);
if (annotation != null) {
if (value != null) {
throw new IllegalStateException(
"method [" + method + "] contains more than one publisher annotation");
}
Object valueAsObject = (attributeName == null) ? AnnotationUtils.getValue(annotation)
: AnnotationUtils.getValue(annotation, attributeName);
if (valueAsObject != null) {
if (expectedType.isAssignableFrom(valueAsObject.getClass())) {
value = (T) valueAsObject;
}
else {
throw new IllegalArgumentException("expected type [" + expectedType.getName() +
"] for attribute '" + attributeName + "' on publisher annotation [" +
annotationType + "], but actual type was [" + valueAsObject.getClass() + "]");
}
}
}
}
return value;
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2002-2009 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
*
* http://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.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to indicate that a method, or all public methods if applied at
* class-level, should publish Messages whose payloads will be determined by
* the provided EL expression.
*
* @author Mark Fisher
* @since 2.0
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Publisher {
/**
* String representation of a Spel Expression. Required.
*/
String value();
/**
* Name of the Message Channel to which Messages will be published.
*/
String channel() default "";
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2002-2009 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
*
* http://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.aop;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.integration.channel.BeanFactoryChannelResolver;
import org.springframework.integration.core.MessageChannel;
/**
* An advisor that will apply the {@link MessagePublishingInterceptor} to any
* methods containing the provided annotations. If no annotations are provided,
* the default will be {@link Publisher @Publisher}.
*
* @author Mark Fisher
* @since 2.0
*/
@SuppressWarnings("serial")
public class PublisherAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
private final Set<Class<? extends Annotation>> publisherAnnotationTypes;
private final MessagePublishingInterceptor interceptor;
public PublisherAnnotationAdvisor(Class<? extends Annotation> ... publisherAnnotationTypes) {
this.publisherAnnotationTypes = new HashSet<Class<? extends Annotation>>(Arrays.asList(publisherAnnotationTypes));
ExpressionSource source = new MethodAnnotationExpressionSource(this.publisherAnnotationTypes);
this.interceptor = new MessagePublishingInterceptor(source);
}
@SuppressWarnings("unchecked")
public PublisherAnnotationAdvisor() {
this(Publisher.class);
}
public void setDefaultChannel(MessageChannel defaultChannel) {
this.interceptor.setDefaultChannel(defaultChannel);
}
public void setBeanFactory(BeanFactory beanFactory) {
this.interceptor.setChannelResolver(new BeanFactoryChannelResolver(beanFactory));
}
public Advice getAdvice() {
return this.interceptor;
}
public Pointcut getPointcut() {
return this.buildPointcut();
}
private Pointcut buildPointcut() {
ComposablePointcut result = null;
for (Class<? extends Annotation> publisherAnnotationType : this.publisherAnnotationTypes) {
Pointcut cpc = new AnnotationMatchingPointcut(publisherAnnotationType, true);
Pointcut mpc = new AnnotationMatchingPointcut(null, publisherAnnotationType);
if (result == null) {
result = new ComposablePointcut(cpc).union(mpc);
}
else {
result.union(cpc).union(mpc);
}
}
return result;
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2002-2009 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
*
* http://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.aop;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.lang.reflect.Method;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.integration.channel.MapBasedChannelResolver;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.core.Message;
/**
* @author Mark Fisher
* @since 2.0
*/
public class MessagePublishingInterceptorTests {
private final ExpressionSource source = new TestExpressionSource();
private final MapBasedChannelResolver channelResolver = new MapBasedChannelResolver();
private final QueueChannel testChannel = new QueueChannel();
@Before
public void setup() {
channelResolver.setChannelMap(Collections.singletonMap("c", testChannel));
}
@Test
public void returnValue() {
MessagePublishingInterceptor interceptor = new MessagePublishingInterceptor(source);
interceptor.setChannelResolver(channelResolver);
ProxyFactory pf = new ProxyFactory(new TestBeanImpl());
pf.addAdvice(interceptor);
TestBean proxy = (TestBean) pf.getProxy();
proxy.test();
Message<?> message = testChannel.receive(0);
assertNotNull(message);
assertEquals("foo", message.getPayload());
}
static interface TestBean {
String test();
}
static class TestBeanImpl implements TestBean {
public String test() {
return "foo";
}
}
private static class TestExpressionSource implements ExpressionSource {
public String getArgumentMapName(Method method) {
return "m";
}
public String[] getArgumentNames(Method method) {
return new String[] { "a1", "a2" };
}
public String getChannelName(Method method) {
return "c";
}
public String getExceptionName(Method method) {
return "x";
}
public String getExpressionString(Method method) {
return "#r";
}
public String getReturnValueName(Method method) {
return "r";
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2002-2009 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
*
* http://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.aop;
import static org.junit.Assert.assertEquals;
import java.lang.reflect.Method;
import org.junit.Test;
/**
* @author Mark Fisher
* @since 2.0
*/
public class MethodAnnotationExpressionSourceTests {
private final MethodAnnotationExpressionSource source = new MethodAnnotationExpressionSource();
@Test
public void defaultBindings() {
Method method = getMethod("methodWithExpressionAnnotationOnly", String.class, int.class);
String expressionString = source.getExpressionString(method);
assertEquals("testExpression1", expressionString);
assertEquals(2, source.getArgumentNames(method).length);
assertEquals("arg1", source.getArgumentNames(method)[0]);
assertEquals("arg2", source.getArgumentNames(method)[1]);
assertEquals(ExpressionSource.DEFAULT_ARGUMENT_MAP_NAME, source.getArgumentMapName(method));
assertEquals(ExpressionSource.DEFAULT_EXCEPTION_NAME, source.getExceptionName(method));
assertEquals(ExpressionSource.DEFAULT_RETURN_VALUE_NAME, source.getReturnValueName(method));
}
@Test
public void annotationBindings() {
Method method = getMethod("methodWithExpressionBinding", String.class, int.class);
String expressionString = source.getExpressionString(method);
assertEquals("testExpression2", expressionString);
assertEquals(2, source.getArgumentNames(method).length);
assertEquals("s", source.getArgumentNames(method)[0]);
assertEquals("i", source.getArgumentNames(method)[1]);
assertEquals("argz", source.getArgumentMapName(method));
assertEquals("x", source.getExceptionName(method));
assertEquals("result", source.getReturnValueName(method));
}
@Test
public void channelName() {
Method method = getMethod("methodWithChannelAndReturnAsPayload");
String channelName = source.getChannelName(method);
assertEquals("foo", channelName);
}
private static Method getMethod(String name, Class<?> ... params) {
try {
return MethodAnnotationExpressionSourceTests.class.getMethod(name, params);
}
catch (Exception e) {
throw new RuntimeException("failed to resolve method", e);
}
}
@Publisher("testExpression1")
public void methodWithExpressionAnnotationOnly(String arg1, int arg2) {
}
@Publisher(value="#return", channel="foo")
public void methodWithChannelAndReturnAsPayload() {
}
@Publisher("testExpression2")
@ExpressionBinding(argNames="s, i", argumentMapName="argz", exceptionName="x", returnValueName="result")
public void methodWithExpressionBinding(String arg1, int arg2) {
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2002-2009 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
*
* http://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.aop;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.core.Message;
/**
* @author Mark Fisher
* @since 2.0
*/
public class PublisherAnnotationAdvisorTests {
private final StaticApplicationContext context = new StaticApplicationContext();
@Before
public void setup() {
context.registerSingleton("testChannel", QueueChannel.class);
}
@Test
public void returnValue() {
PublisherAnnotationAdvisor advisor = new PublisherAnnotationAdvisor();
advisor.setBeanFactory(context);
QueueChannel testChannel = context.getBean("testChannel", QueueChannel.class);
advisor.setDefaultChannel(testChannel);
ProxyFactory pf = new ProxyFactory(new TestBeanImpl());
pf.addAdvisor(advisor);
TestBean proxy = (TestBean) pf.getProxy();
proxy.test();
Message<?> message = testChannel.receive(0);
assertNotNull(message);
assertEquals("foo", message.getPayload());
}
static interface TestBean {
String test();
}
static class TestBeanImpl implements TestBean {
@Publisher("#return")
public String test() {
return "foo";
}
}
}