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:
Gary Russell
2013-04-29 14:09:00 -04:00
15 changed files with 983 additions and 1 deletions

View File

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

View File

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

View File

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

View File

@@ -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[]) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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!");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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