Merge pull request #94 from davidbilge/AMQP-308
* AMQP-308c: AMQP-308 Add Spring Remoting over AMQP Add classes to expose and use a spring-remoting-service via amqp
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.remoting.client;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter;
|
||||
import org.springframework.remoting.RemoteProxyFailureException;
|
||||
import org.springframework.remoting.support.DefaultRemoteInvocationFactory;
|
||||
import org.springframework.remoting.support.RemoteAccessor;
|
||||
import org.springframework.remoting.support.RemoteInvocation;
|
||||
import org.springframework.remoting.support.RemoteInvocationFactory;
|
||||
import org.springframework.remoting.support.RemoteInvocationResult;
|
||||
|
||||
/**
|
||||
* {@link org.aopalliance.intercept.MethodInterceptor} for accessing RMI-style AMQP services.
|
||||
*
|
||||
* @author David Bilge
|
||||
* @author Gary Russell
|
||||
* @since 1.2
|
||||
* @see AmqpInvokerServiceExporter
|
||||
* @see AmqpProxyFactoryBean
|
||||
* @see org.springframework.remoting.RemoteAccessException
|
||||
*/
|
||||
public class AmqpClientInterceptor extends RemoteAccessor implements MethodInterceptor {
|
||||
|
||||
private AmqpTemplate amqpTemplate;
|
||||
|
||||
private String routingKey = null;
|
||||
|
||||
private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory();
|
||||
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
RemoteInvocation remoteInvocation = getRemoteInvocationFactory().createRemoteInvocation(invocation);
|
||||
|
||||
Object rawResult;
|
||||
if (getRoutingKey() == null) {
|
||||
// Use the template's default routing key
|
||||
rawResult = amqpTemplate.convertSendAndReceive(remoteInvocation);
|
||||
}
|
||||
else {
|
||||
rawResult = amqpTemplate.convertSendAndReceive(routingKey, remoteInvocation);
|
||||
}
|
||||
|
||||
if (rawResult == null) {
|
||||
throw new RemoteProxyFailureException("No reply received - perhaps a timeout in the template?", null);
|
||||
}
|
||||
else if (!(rawResult instanceof RemoteInvocationResult)) {
|
||||
throw new RemoteProxyFailureException("Expected a result of type "
|
||||
+ RemoteInvocationResult.class.getCanonicalName() + " but found "
|
||||
+ rawResult.getClass().getCanonicalName(), null);
|
||||
}
|
||||
|
||||
RemoteInvocationResult result = (RemoteInvocationResult) rawResult;
|
||||
return result.recreate();
|
||||
}
|
||||
|
||||
public AmqpTemplate getAmqpTemplate() {
|
||||
return amqpTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* The AMQP template to be used for sending messages and receiving results. This class is using "Request/Reply" for
|
||||
* sending messages as described <a href=
|
||||
* "http://static.springsource.org/spring-amqp/reference/html/amqp.html#request-reply" >in the Spring-AMQP
|
||||
* documentation</a>.
|
||||
*/
|
||||
public void setAmqpTemplate(AmqpTemplate amqpTemplate) {
|
||||
this.amqpTemplate = amqpTemplate;
|
||||
}
|
||||
|
||||
public String getRoutingKey() {
|
||||
return routingKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* The routing key to send calls to the service with. Use this to route the messages to a specific queue on the
|
||||
* broker. If not set, the {@link AmqpTemplate}'s default routing key will be used.
|
||||
* <p>
|
||||
* This property is useful if you want to use the same AmqpTemplate to talk to multiple services.
|
||||
*/
|
||||
public void setRoutingKey(String routingKey) {
|
||||
this.routingKey = routingKey;
|
||||
}
|
||||
|
||||
public RemoteInvocationFactory getRemoteInvocationFactory() {
|
||||
return remoteInvocationFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the RemoteInvocationFactory to use for this accessor. Default is a {@link DefaultRemoteInvocationFactory}.
|
||||
* <p>
|
||||
* A custom invocation factory can add further context information to the invocation, for example user credentials.
|
||||
*/
|
||||
public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) {
|
||||
this.remoteInvocationFactory = remoteInvocationFactory;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.remoting.client;
|
||||
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.remoting.rmi.RmiServiceExporter;
|
||||
|
||||
/**
|
||||
* {@link FactoryBean} for AMQP proxies. Exposes the proxied service for use as a bean reference, using the specified
|
||||
* service interface. Proxies will throw Spring's unchecked RemoteAccessException on remote invocation failure.
|
||||
*
|
||||
* <p>
|
||||
* This is intended for an "RMI-style" (i.e. synchroneous) usage of the AMQP protocol. Obviously, AMQP allows for a much
|
||||
* broader scope of execution styles, which are not the scope of the mechanism at hand.
|
||||
* <p>
|
||||
* Calling a method on the proxy will cause an AMQP message being sent according to the configured {@link AmqpTemplate}.
|
||||
* This can be received and answered by an {@link AmqpInvokerServiceExporter}.
|
||||
*
|
||||
* @author David Bilge
|
||||
* @since 1.2
|
||||
* @see #setServiceInterface
|
||||
* @see AmqpClientInterceptor
|
||||
* @see RmiServiceExporter
|
||||
* @see org.springframework.remoting.RemoteAccessException
|
||||
*/
|
||||
public class AmqpProxyFactoryBean extends AmqpClientInterceptor implements FactoryBean<Object>, BeanClassLoaderAware,
|
||||
InitializingBean {
|
||||
|
||||
private Object serviceProxy;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
if (getServiceInterface() == null) {
|
||||
throw new IllegalArgumentException("Property 'serviceInterface' is required");
|
||||
}
|
||||
this.serviceProxy = new ProxyFactory(getServiceInterface(), this).getProxy(getBeanClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getObject() throws Exception {
|
||||
return this.serviceProxy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
return getServiceInterface();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.remoting.service;
|
||||
|
||||
import org.springframework.amqp.AmqpRejectAndDontRequeueException;
|
||||
import org.springframework.amqp.core.Address;
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.core.MessageListener;
|
||||
import org.springframework.amqp.core.MessageProperties;
|
||||
import org.springframework.amqp.remoting.client.AmqpProxyFactoryBean;
|
||||
import org.springframework.amqp.support.converter.MessageConverter;
|
||||
import org.springframework.amqp.support.converter.SimpleMessageConverter;
|
||||
import org.springframework.remoting.support.RemoteInvocation;
|
||||
import org.springframework.remoting.support.RemoteInvocationBasedExporter;
|
||||
import org.springframework.remoting.support.RemoteInvocationResult;
|
||||
|
||||
/**
|
||||
* This message listener exposes a plain java service via AMQP. Such services can be accessed via plain AMQP or via
|
||||
* {@link AmqpProxyFactoryBean}.
|
||||
*
|
||||
* To configure this message listener so that it actually receives method calls via AMQP, it needs to be put into a
|
||||
* listener container. See {@link MessageListener}.
|
||||
*
|
||||
* <p>
|
||||
* When receiving a message, a service method is called according to the contained {@link RemoteInvocation}. The result
|
||||
* of that invocation is returned as a {@link RemoteInvocationResult} contained in a message that is sent according to
|
||||
* the <code>ReplyToAddress</code> of the received message.
|
||||
*
|
||||
* <p>
|
||||
* Please note that this exporter does not use the {@link MessageConverter} of the injected {@link AmqpTemplate} to
|
||||
* convert incoming calls and their results. Instead you have to directly inject the <code>MessageConverter</code> into
|
||||
* this class.
|
||||
*
|
||||
* <p>
|
||||
* This listener responds to "Request/Reply"-style messages as described <a href=
|
||||
* "http://static.springsource.org/spring-amqp/reference/html/amqp.html#request-reply" >here</a>.
|
||||
*
|
||||
* @author David Bilge
|
||||
* @since 1.2
|
||||
*/
|
||||
public class AmqpInvokerServiceExporter extends RemoteInvocationBasedExporter implements MessageListener {
|
||||
|
||||
private AmqpTemplate amqpTemplate;
|
||||
|
||||
private MessageConverter messageConverter = new SimpleMessageConverter();
|
||||
|
||||
@Override
|
||||
public void onMessage(Message message) {
|
||||
Address replyToAddress = message.getMessageProperties().getReplyToAddress();
|
||||
if (replyToAddress == null) {
|
||||
throw new AmqpRejectAndDontRequeueException("No replyToAddress in inbound AMQP Message");
|
||||
}
|
||||
|
||||
Object invocationRaw = messageConverter.fromMessage(message);
|
||||
if (invocationRaw == null || !(invocationRaw instanceof RemoteInvocation)) {
|
||||
send(new RuntimeException("The message does not contain a RemoteInvocation payload"), replyToAddress);
|
||||
return;
|
||||
}
|
||||
RemoteInvocation invocation = (RemoteInvocation) invocationRaw;
|
||||
|
||||
RemoteInvocationResult remoteInvocationResult = invokeAndCreateResult(invocation, getService());
|
||||
send(remoteInvocationResult, replyToAddress);
|
||||
}
|
||||
|
||||
private void send(Object object, Address replyToAddress) {
|
||||
Message message = messageConverter.toMessage(object, new MessageProperties());
|
||||
|
||||
getAmqpTemplate().send(replyToAddress.getExchangeName(), replyToAddress.getRoutingKey(), message);
|
||||
}
|
||||
|
||||
public AmqpTemplate getAmqpTemplate() {
|
||||
return amqpTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* The AMQP template to use for sending the return value.
|
||||
*
|
||||
* <p>
|
||||
* Note that the exchange and routing key parameters on this template are ignored for these return messages. Instead
|
||||
* of those the respective parameters from the original message's <code>returnAddress</code> are being used.
|
||||
* <p>
|
||||
* Also, the templates {@link MessageConverter} is not used for the reply.
|
||||
* @see {@link AmqpInvokerServiceExporter#setMessageConverter(MessageConverter)}
|
||||
*/
|
||||
public void setAmqpTemplate(AmqpTemplate amqpTemplate) {
|
||||
this.amqpTemplate = amqpTemplate;
|
||||
}
|
||||
|
||||
public MessageConverter getMessageConverter() {
|
||||
return messageConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the message converter for this remote service. Used to deserialize remote method calls and to serialize their
|
||||
* return values.
|
||||
* <p>
|
||||
* The default converter is a SimpleMessageConverter, which is able to handle byte arrays, Strings, and Serializable
|
||||
* Objects depending on the message content type header.
|
||||
* <p>
|
||||
* Note that this class never uses the message converter of the underlying {@link AmqpTemplate}!
|
||||
*
|
||||
* @see org.springframework.amqp.support.converter.SimpleMessageConverter
|
||||
*/
|
||||
public void setMessageConverter(MessageConverter messageConverter) {
|
||||
this.messageConverter = messageConverter;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import org.springframework.amqp.core.MessageProperties;
|
||||
import org.springframework.amqp.utils.SerializationUtils;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.remoting.rmi.CodebaseAwareObjectInputStream;
|
||||
import java.rmi.server.RMIClassLoader;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
@@ -78,6 +77,7 @@ public class SimpleMessageConverter extends AbstractMessageConverter implements
|
||||
/**
|
||||
* Converts from a AMQP Message to an Object.
|
||||
*/
|
||||
@Override
|
||||
public Object fromMessage(Message message) throws MessageConversionException {
|
||||
Object content = null;
|
||||
MessageProperties properties = message.getMessageProperties();
|
||||
@@ -118,6 +118,7 @@ public class SimpleMessageConverter extends AbstractMessageConverter implements
|
||||
/**
|
||||
* Creates an AMQP Message from the provided Object.
|
||||
*/
|
||||
@Override
|
||||
protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
|
||||
byte[] bytes = null;
|
||||
if (object instanceof byte[]) {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.remoting;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.amqp.AmqpException;
|
||||
import org.springframework.amqp.core.Address;
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.core.MessageProperties;
|
||||
import org.springframework.amqp.remoting.client.AmqpProxyFactoryBean;
|
||||
import org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter;
|
||||
import org.springframework.amqp.remoting.testhelper.AbstractAmqpTemplate;
|
||||
import org.springframework.amqp.remoting.testhelper.SentSavingTemplate;
|
||||
import org.springframework.amqp.remoting.testservice.GeneralException;
|
||||
import org.springframework.amqp.remoting.testservice.SpecialException;
|
||||
import org.springframework.amqp.remoting.testservice.TestServiceImpl;
|
||||
import org.springframework.amqp.remoting.testservice.TestServiceInterface;
|
||||
import org.springframework.amqp.support.converter.MessageConverter;
|
||||
|
||||
/**
|
||||
* @author David Bilge
|
||||
* @since 1.2
|
||||
*/
|
||||
public class RemotingTest {
|
||||
|
||||
private TestServiceInterface riggedProxy;
|
||||
|
||||
/**
|
||||
* Set up a rig of directly wired-up proxy and service listener so that both can be tested together without needing
|
||||
* a running rabbit.
|
||||
*/
|
||||
@Before
|
||||
public void initializeTestRig() throws Exception {
|
||||
// Set up the service
|
||||
TestServiceInterface testService = new TestServiceImpl();
|
||||
final AmqpInvokerServiceExporter serviceExporter = new AmqpInvokerServiceExporter();
|
||||
final SentSavingTemplate sentSavingTemplate = new SentSavingTemplate();
|
||||
serviceExporter.setAmqpTemplate(sentSavingTemplate);
|
||||
serviceExporter.setService(testService);
|
||||
serviceExporter.setServiceInterface(TestServiceInterface.class);
|
||||
|
||||
// Set up the client
|
||||
AmqpProxyFactoryBean amqpProxyFactoryBean = new AmqpProxyFactoryBean();
|
||||
amqpProxyFactoryBean.setServiceInterface(TestServiceInterface.class);
|
||||
AmqpTemplate directForwardingTemplate = new AbstractAmqpTemplate() {
|
||||
@Override
|
||||
public Object convertSendAndReceive(Object payload) throws AmqpException {
|
||||
MessageConverter messageConverter = serviceExporter.getMessageConverter();
|
||||
|
||||
Address replyTo = new Address("fakeExchange", "fakeExchangeName", "fakeRoutingKey");
|
||||
MessageProperties messageProperties = new MessageProperties();
|
||||
messageProperties.setReplyToAddress(replyTo);
|
||||
Message message = messageConverter.toMessage(payload, messageProperties);
|
||||
|
||||
serviceExporter.onMessage(message);
|
||||
|
||||
Message resultMessage = sentSavingTemplate.getLastMessage();
|
||||
return messageConverter.fromMessage(resultMessage);
|
||||
}
|
||||
};
|
||||
amqpProxyFactoryBean.setAmqpTemplate(directForwardingTemplate);
|
||||
amqpProxyFactoryBean.afterPropertiesSet();
|
||||
Object rawProxy = amqpProxyFactoryBean.getObject();
|
||||
riggedProxy = (TestServiceInterface) rawProxy;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEcho() {
|
||||
Assert.assertEquals("Echo Test", riggedProxy.simpleStringReturningTestMethod("Test"));
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void testExceptionPropagation() {
|
||||
riggedProxy.exceptionThrowingMethod();
|
||||
}
|
||||
|
||||
@Test(expected = GeneralException.class)
|
||||
public void testExceptionReturningMethod() {
|
||||
riggedProxy.notReallyExceptionReturningMethod();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testActuallyExceptionReturningMethod() {
|
||||
SpecialException returnedException = riggedProxy.actuallyExceptionReturningMethod();
|
||||
|
||||
Assert.assertNotNull(returnedException);
|
||||
Assert.assertTrue(returnedException instanceof SpecialException);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.remoting.testhelper;
|
||||
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
import org.springframework.amqp.AmqpException;
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import org.springframework.amqp.core.Message;
|
||||
import org.springframework.amqp.core.MessagePostProcessor;
|
||||
|
||||
/**
|
||||
* @author David Bilge
|
||||
* @since 1.2
|
||||
*/
|
||||
public abstract class AbstractAmqpTemplate implements AmqpTemplate {
|
||||
|
||||
@Override
|
||||
public void send(Message message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String routingKey, Message message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String exchange, String routingKey, Message message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void convertAndSend(Object message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void convertAndSend(String routingKey, Object message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void convertAndSend(String exchange, String routingKey, Object message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void convertAndSend(Object message, MessagePostProcessor messagePostProcessor) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void convertAndSend(String routingKey, Object message, MessagePostProcessor messagePostProcessor)
|
||||
throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void convertAndSend(String exchange, String routingKey, Object message,
|
||||
MessagePostProcessor messagePostProcessor) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive() throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message receive(String queueName) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receiveAndConvert() throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object receiveAndConvert(String queueName) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendAndReceive(Message message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendAndReceive(String routingKey, Message message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message sendAndReceive(String exchange, String routingKey, Message message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(Object message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(String routingKey, Object message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(String exchange, String routingKey, Object message) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(Object message, MessagePostProcessor messagePostProcessor) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(String routingKey, Object message, MessagePostProcessor messagePostProcessor)
|
||||
throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertSendAndReceive(String exchange, String routingKey, Object message,
|
||||
MessagePostProcessor messagePostProcessor) throws AmqpException {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.remoting.testhelper;
|
||||
|
||||
import org.springframework.amqp.AmqpException;
|
||||
import org.springframework.amqp.core.Message;
|
||||
|
||||
/**
|
||||
* @author David Bilge
|
||||
* @since 1.2
|
||||
*/
|
||||
public class SentSavingTemplate extends AbstractAmqpTemplate {
|
||||
private Message lastMessage = null;
|
||||
private String lastExchange = null;
|
||||
private String lastRoutingKey = null;
|
||||
|
||||
@Override
|
||||
public void send(String exchange, String routingKey, Message message) throws AmqpException {
|
||||
this.lastExchange = exchange;
|
||||
this.lastRoutingKey = routingKey;
|
||||
this.lastMessage = message;
|
||||
}
|
||||
|
||||
public Message getLastMessage() {
|
||||
return lastMessage;
|
||||
}
|
||||
|
||||
public String getLastExchange() {
|
||||
return lastExchange;
|
||||
}
|
||||
|
||||
public String getLastRoutingKey() {
|
||||
return lastRoutingKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.remoting.testservice;
|
||||
|
||||
/**
|
||||
* @author David Bilge
|
||||
* @since 1.2
|
||||
*/
|
||||
public class GeneralException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1763252570120227426L;
|
||||
|
||||
public GeneralException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public GeneralException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.remoting.testservice;
|
||||
|
||||
/**
|
||||
* @author David Bilge
|
||||
* @since 1.2
|
||||
*/
|
||||
public class SpecialException extends RuntimeException {
|
||||
private static final long serialVersionUID = 7254934411128057730L;
|
||||
|
||||
public SpecialException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public SpecialException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.remoting.testservice;
|
||||
|
||||
/**
|
||||
* @author David Bilge
|
||||
* @since 1.2
|
||||
*/
|
||||
public class TestServiceImpl implements TestServiceInterface {
|
||||
@Override
|
||||
public void simpleTestMethod() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public String simpleStringReturningTestMethod(String string) {
|
||||
return "Echo " + string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionThrowingMethod() {
|
||||
throw new RuntimeException("This is an exception");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object echo(Object o) {
|
||||
return o;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpecialException notReallyExceptionReturningMethod() {
|
||||
throw new GeneralException("This exception should not be interpreted as a return type but be thrown instead.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpecialException actuallyExceptionReturningMethod() {
|
||||
return new SpecialException("This exception should not be thrown on the client side but just be returned!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.remoting.testservice;
|
||||
|
||||
/**
|
||||
* @author David Bilge
|
||||
* @since 1.2
|
||||
*/
|
||||
public interface TestServiceInterface {
|
||||
void simpleTestMethod();
|
||||
|
||||
String simpleStringReturningTestMethod(String string);
|
||||
|
||||
void exceptionThrowingMethod();
|
||||
|
||||
Object echo(Object o);
|
||||
|
||||
SpecialException notReallyExceptionReturningMethod();
|
||||
|
||||
SpecialException actuallyExceptionReturningMethod();
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.amqp.rabbit.remoting;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.amqp.rabbit.test.BrokerRunning;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.remoting.RemoteProxyFailureException;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 1.2
|
||||
*
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class RemotingTests {
|
||||
|
||||
@Rule
|
||||
public BrokerRunning brokerRunning = BrokerRunning.isRunning();
|
||||
|
||||
@Autowired
|
||||
private ServiceInterface client;
|
||||
|
||||
@Autowired
|
||||
private RabbitTemplate template;
|
||||
|
||||
private static CountDownLatch latch;
|
||||
|
||||
private static String receivedMessage;
|
||||
|
||||
@Test
|
||||
public void testEcho() throws Exception {
|
||||
String reply = client.echo("foo");
|
||||
assertEquals("echo:foo", reply);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoAnswer() throws Exception {
|
||||
latch = new CountDownLatch(1);
|
||||
client.noAnswer("foo");
|
||||
assertTrue(latch.await(5, TimeUnit.SECONDS));
|
||||
assertEquals("received:foo", receivedMessage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTimeout() {
|
||||
try {
|
||||
client.suspend();
|
||||
fail("Exception expected");
|
||||
}
|
||||
catch (RemoteProxyFailureException e) {
|
||||
assertTrue("No reply received - perhaps a timeout in the template?".equals(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
public interface ServiceInterface {
|
||||
|
||||
String echo(String message);
|
||||
|
||||
void noAnswer(String message);
|
||||
|
||||
void suspend();
|
||||
|
||||
}
|
||||
|
||||
public static class ServiceImpl implements ServiceInterface {
|
||||
|
||||
@Override
|
||||
public String echo(String message) {
|
||||
return "echo:" + message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void noAnswer(String message) {
|
||||
receivedMessage = "received:" + message;
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void suspend() {
|
||||
try {
|
||||
Thread.sleep(3000);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit-1.2.xsd
|
||||
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
|
||||
|
||||
<bean id="client" class="org.springframework.amqp.remoting.client.AmqpProxyFactoryBean">
|
||||
<property name="amqpTemplate" ref="template" />
|
||||
<property name="serviceInterface" value="org.springframework.amqp.rabbit.remoting.RemotingTests$ServiceInterface" />
|
||||
</bean>
|
||||
|
||||
<bean id="listener" class="org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter">
|
||||
<property name="serviceInterface" value="org.springframework.amqp.rabbit.remoting.RemotingTests$ServiceInterface" />
|
||||
<property name="service" ref="service" />
|
||||
<property name="amqpTemplate" ref="template" />
|
||||
</bean>
|
||||
|
||||
<bean id="service" class="org.springframework.amqp.rabbit.remoting.RemotingTests$ServiceImpl" />
|
||||
|
||||
<rabbit:connection-factory id="connectionFactory" />
|
||||
|
||||
<rabbit:template id="template" connection-factory="connectionFactory" reply-timeout="2000"
|
||||
routing-key="remoting.test.binding" exchange="remoting.test.exchange" />
|
||||
|
||||
<rabbit:admin connection-factory="connectionFactory" />
|
||||
|
||||
<rabbit:queue name="remoting.test.queue" />
|
||||
|
||||
<rabbit:direct-exchange name="remoting.test.exchange">
|
||||
<rabbit:bindings>
|
||||
<rabbit:binding queue="remoting.test.queue" key="remoting.test.binding" />
|
||||
</rabbit:bindings>
|
||||
</rabbit:direct-exchange>
|
||||
|
||||
<rabbit:listener-container connection-factory="connectionFactory">
|
||||
<rabbit:listener ref="listener" queue-names="remoting.test.queue" />
|
||||
</rabbit:listener-container>
|
||||
</beans>
|
||||
@@ -873,6 +873,87 @@ Object receiveAndConvert(String queueName) throws AmqpException;]]></programlist
|
||||
application using 1.1, you must set the attribute to <code>spring_reply_correlation</code>.
|
||||
</note>
|
||||
</section>
|
||||
<section id="remoting">
|
||||
<title>Spring Remoting with AMQP</title>
|
||||
<para>
|
||||
The Spring Framework has a general remoting capability, allowing
|
||||
<ulink url="http://static.springsource.org/spring/docs/current/spring-framework-reference/html/remoting.html">
|
||||
Remote Procedure Calls (RPC)</ulink> using various transports.
|
||||
Spring-AMQP supports a similar mechanism with a <classname>AmqpProxyFactoryBean</classname> on the client
|
||||
and a <classname>AmqpInvokerServiceExporter</classname> on the server. This provides RPC over AMQP.
|
||||
On the client side, a <classname>RabbitTemplate</classname> is used as described above; on the server side,
|
||||
the invoker (configured as a <interfacename>MessageListener</interfacename>) receives the message, invokes
|
||||
the configured service, and returns the reply using the inbound message's <code>replyTo</code> information.
|
||||
</para>
|
||||
<para>
|
||||
The client factory bean can be injected into any bean (using its <code>serviceInterface</code>); the client
|
||||
can then invoke methods on the proxy, resulting in remote execution over AMQP.
|
||||
</para>
|
||||
<note>
|
||||
<para>
|
||||
With the default <interfacename>MessageConverter</interfacename>s, the method paramters and returned
|
||||
value must be instances of <interfacename>Serializable</interfacename>.
|
||||
</para>
|
||||
<para>
|
||||
On the server side, the <classname>AmqpInvokerServiceExporter</classname> has
|
||||
both <interfacename>AmqpTemplate</interfacename> and <interfacename>MessageConverter</interfacename>
|
||||
properties. Currently, the template's <interfacename>MessageConverter</interfacename> is not
|
||||
used. If you need to supply a custom message converter, then you should provide it using
|
||||
the <code>messageConverter</code> property. On the client side, a custom message converter
|
||||
can be added to the <interfacename>AmqpTemplate</interfacename> which is provided to the
|
||||
<classname>AmqpProxyFactoryBean</classname> using its <code>amqpTemplate</code> property.
|
||||
</para>
|
||||
</note>
|
||||
<para>
|
||||
Sample client and server configurations are shown below.
|
||||
</para>
|
||||
<programlisting language="xml"><![CDATA[<bean id="client"
|
||||
class="org.springframework.amqp.remoting.client.AmqpProxyFactoryBean">
|
||||
<property name="amqpTemplate" ref="template" />
|
||||
<property name="serviceInterface" value="foo.ServiceInterface" />
|
||||
</bean>
|
||||
|
||||
<rabbit:connection-factory id="connectionFactory" />
|
||||
|
||||
<rabbit:template id="template" connection-factory="connectionFactory" reply-timeout="2000"
|
||||
routing-key="remoting.binding" exchange="remoting.exchange" />
|
||||
|
||||
<rabbit:admin connection-factory="connectionFactory" />
|
||||
|
||||
<rabbit:queue name="remoting.queue" />
|
||||
|
||||
<rabbit:direct-exchange name="remoting.exchange">
|
||||
<rabbit:bindings>
|
||||
<rabbit:binding queue="remoting.queue" key="remoting.binding" />
|
||||
</rabbit:bindings>
|
||||
</rabbit:direct-exchange>]]></programlisting>
|
||||
<programlisting language="xml"><![CDATA[<bean id="listener"
|
||||
class="org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter">
|
||||
<property name="serviceInterface" value="foo.ServiceInterface" />
|
||||
<property name="service" ref="service" />
|
||||
<property name="amqpTemplate" ref="template" />
|
||||
</bean>
|
||||
|
||||
<bean id="service" class="foo.ServiceImpl" />
|
||||
|
||||
<rabbit:connection-factory id="connectionFactory" />
|
||||
|
||||
<rabbit:template id="template" connection-factory="connectionFactory" />
|
||||
|
||||
<rabbit:queue name="remoting.queue" />
|
||||
|
||||
<rabbit:listener-container connection-factory="connectionFactory">
|
||||
<rabbit:listener ref="listener" queue-names="remoting.queue" />
|
||||
</rabbit:listener-container>]]></programlisting>
|
||||
<important>
|
||||
The <classname>AmqpInvokerServiceExporter</classname> can only process properly
|
||||
formed messages, such as those sent from the <classname>AmqpProxyFactoryBean</classname>.
|
||||
If it receives a message that it cannot interpret, a serialized
|
||||
<classname>RuntimeException</classname> will be sent as a reply. If the message has
|
||||
no <code>replyToAddress</code> property, the message will be rejected and permanently lost if no
|
||||
Dead Letter Exchange has been configured.
|
||||
</important>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="broker-configuration">
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
with the existing converter that uses Jackson 1.x.
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>AMQP Remoting</title>
|
||||
<para>
|
||||
Facilities are now provided for using Spring Remoting techniques, using AMQP
|
||||
as the transport for the RPC calls. For more information see <xref linkend="remoting"/>
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
Reference in New Issue
Block a user