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:
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 "";
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user