0

I have several tutorials working with Spring Boot and RPC through RabbitMQ. However, as soon as I attempt to add a Jackson JSON message converter, it all falls to pieces.

The remote invocation is successfully received by the server, so I feel pretty confident it's not the client configuration.

Exchange    DATAFLOW_EXCHANGE
Routing Key     dataflowRunner
Redelivered     ○
Properties  
reply_to:   amq.rabbitmq.reply-to.g2dkABZyYWJiaXRAdXNoeWRnbmFkaXBhbHZ4AAAr0wAAAAAB.MmIZ6Htejtc1qB11G7BBQw==
priority:   0
delivery_mode:  2
headers:    
__TypeId__: org.springframework.remoting.support.RemoteInvocation
content_encoding:   UTF-8
content_type:   application/json
Payload
675 bytes
Encoding: string


{"methodName":"run","parameterTypes":["dw.dataflow.Dataflow"],"arguments":[{ Valid Dataflow JSON Removed for Brevity } ]}

However, the following exception is output:

Caused by: org.springframework.messaging.converter.MessageConversionException: 
No converter found to convert to class dw.dataflow.Dataflow, message=GenericMessage 
[payload=RemoteInvocation: method name 'run'; parameter types [dw.dataflow.Dataflow], headers={amqp_receivedExchange=DATAFLOW_EXCHANGE, amqp_deliveryTag=1, amqp_replyTo=amq.rabbitmq.reply-to.g2dkABZyYWJiaXRAdXNoeWRnbmFkaXBhbHZ4AAArRAAAAAQC.PA/bJ6lcUfaP3csAP5v5NA==, amqp_consumerQueue=DATAFLOW_QUEUE, amqp_redelivered=false, amqp_receivedRoutingKey=dataflowRunner, amqp_contentEncoding=UTF-8, amqp_deliveryMode=PERSISTENT, id=adb37c77-c0da-16bd-8df4-b739cfddf89f, amqp_consumerTag=amq.ctag-N_tFCc_Hp9UtQkiXl7FZ8g, contentType=application/json, __TypeId__=org.springframework.remoting.support.RemoteInvocation, timestamp=1462560945203}]
at org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver.resolveArgument(PayloadArgumentResolver.java:118)
at org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:98)
at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:138)
at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:107)
at org.springframework.amqp.rabbit.listener.adapter.HandlerAdapter.invoke(HandlerAdapter.java:48)
at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:112)
... 12 common frames omitted

So, on delivery, it KNOWS it should be a dw.dataflow.Dataflow object, it just can't find a converter. However, I have my converter defined EVERYWHERE.

Server Configuration

@Configuration
@EnableRabbit
public class RabbitListenerConfiguration {
    @Autowired
    ConnectionFactory connectionFactory;
    @Autowired
    ObjectMapper      jacksonObjectMapper;

@Bean
public TopicExchange exchange() {
    return new TopicExchange("DATAFLOW_EXCHANGE", true, false);
}

@Bean
public Queue queue() {
    return new Queue("DATAFLOW_QUEUE", true);
}

@Bean
public AmqpInvokerServiceExporter amqpInvokerServiceExporter() {
    AmqpInvokerServiceExporter exporter = new AmqpInvokerServiceExporter() ;
    exporter.setAmqpTemplate(rabbitTemplate());
    exporter.setMessageConverter(jackson2JsonMessageConverter());
    exporter.setServiceInterface(DataflowRunner.class);
    exporter.setService(dataflowRunner());
    return exporter ;
}

@Bean
public DataflowRunner dataflowRunner() {
    return new DataflowRunnerServerImpl();
}

@Bean
public MessageConverter jackson2JsonMessageConverter() {
    Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
    converter.setJsonObjectMapper(jacksonObjectMapper);
    return converter;
}

@Bean
public RabbitTemplate rabbitTemplate() {
    RabbitTemplate template = new RabbitTemplate(connectionFactory);
    template.setMessageConverter(jackson2JsonMessageConverter());
    return template;
}


@Bean(name="rabbitListenerContainerFactory")
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(jackson2JsonMessageConverter());
    factory.setDefaultRequeueRejected(false); 
    return factory;
}

Here is the Service interface:

public interface DataflowRunner {
    String run(Dataflow dataflow) throws Exception;
}

And concrete implementation:

public class DataflowRunnerServerImpl implements DataflowRunner {
@RabbitListener(containerFactory = "rabbitListenerContainerFactory", queues="DATAFLOW_QUEUE")
public String run(Dataflow dataflow) throws Exception {
    // SNIP
}

For grins and giggles, I also attempted to configure the server implementation class with the following annotations, but it has the same error:

@RabbitHandler
@RabbitListener(
        bindings = @QueueBinding(key = "dataflowRunner",
                value = @Queue(value = "DATAFLOW_QUEUE", durable = "true", autoDelete = "false", exclusive = "false"),
                exchange = @Exchange(value = "DATAFLOW_EXCHANGE", durable = "true", autoDelete = "false", type = "topic")) )
public String run(Dataflow dataflow) throws Exception {

Client Configuration

    @Bean
public ConnectionFactory connectionFactory() {
    CachingConnectionFactory connectionFactory = new CachingConnectionFactory(rabbitHost, rabbitPort);
    connectionFactory.setUsername(rabbitUser);
    connectionFactory.setPassword(rabbitPassword);
    connectionFactory.setAddresses(rabbitAddresses);
    return connectionFactory;
}

@Bean
public AmqpAdmin amqpAdmin() {
    return new RabbitAdmin(connectionFactory());
}

@Bean
public RabbitTemplate rabbitTemplate() {
    RabbitTemplate template = new RabbitTemplate(connectionFactory());
    template.setMessageConverter(jackson2MessageConverter());
    return template;
}

Does anything seem incorrectly configured? What am I missing? I have the converter set on the service exporter, and the listener container factory.

Any help and/or thoughts appreciated.

Lukas Bradley
  • 410
  • 3
  • 10
  • Please, share those tutorials. There is no need to provide any `impl`. The `AmqpProxyFactoryBean` does the stuff for you. You have just mixed concerns a bit: http://docs.spring.io/spring-amqp/reference/html/_reference.html#remoting – Artem Bilan May 06 '16 at 20:46

3 Answers3

5

@RabbitListener is not intended to be used with the service exporter - just a plain Java class.

For Spring Remoting over RPC, the service exporter is the MessageListener for a SimpleMessageListenerContainer.

With @RabbitListener, there's a special listener adapter that wraps the pojo method.

So you seem to be mixing two different paradigms.

The ServiceExporter (Spring remoting) is expected to be paired with a AmqpProxyFactoryBean on the client side with the service exporter as the listener on the server side.

For simple POJO RPC (which is much newer than using Spring Remoting over RabbitMQ), use @RabbitListener and RabbitTemplate.convertSendAndReceive() on the client side. Get rid of the PFB and SE.

Can you explain what led you down this path, in case we need to add some clarification to the documentation.

EDIT

If you do want to use Spring Remoting (inject an interface on the client side and have it "magically" invoke a service on the server side), you need to get rid of all the container factory stuff and simply wire up a SimpleMessageListenerContainer and inject the service exporter as the MessageListener.

The reference manual has an XML example but you can wire up the SMLC as a @Bean.

EDIT2

I have run some tests and Spring Remoting over AMQP doesn't work with JSON because the top level object is a RemoteInvocation - while the message converter can re-create that object, it has no type information about the actual arguments so leaves it as a linked hash map.

For now, if you must use JSON, the template convertSendAndReceive in conjunction with the @RabbitListener is the way to go here. I will open a JIRA issue to see if we can address using Spring Remoting RPC with JSON, but it was really designed for Java Serialization.

Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • Just what I was looking for. I ran into this same problem and I have like an hour reading Spring AMQP and Spring Remoting and code and I had reached the exact same conclusion, that JSON is not supported because it does not convert the actual value, but only the wrapper object. This is unfortunate, I was hoping to expose an existing service that produces JSON using this feature. My current DTOs are not serializable, so I couldn't just use the regular converter. – Edwin Dalorzo Jan 23 '17 at 05:52
0

I spent a few minutes on this and I manage to solve the problem with a horrible hack that seems to work.

I basically extended the classes involved in the invocation at both sides to make sure the internal arguments and value are converted to/from JSON strings.

With a little bit more of love this could be improve to work with other data types using other converters, but I did't have time for that. I leave it to you if are brave enough to give it a try :-)

On the Server Side

First, I subclassed the AmqpInvokerServiceExporter to be able to add support for conversion to/from JSON objects. The first step is convert method arguments from JSON to there corresponding types. The second step is convert the returned value from an object to its corresponding JSON string to send it back.

public class JSONAmqpInvokerServiceExporter extends AmqpInvokerServiceExporter {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @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 = getMessageConverter().fromMessage(message);

        RemoteInvocationResult remoteInvocationResult;
        if (invocationRaw == null || !(invocationRaw instanceof RemoteInvocation)) {
            remoteInvocationResult = new RemoteInvocationResult(
                new IllegalArgumentException("The message does not contain a RemoteInvocation payload"));
        }
        else {
            RemoteInvocation invocation = (RemoteInvocation) invocationRaw;
            int argCount = invocation.getArguments().length;
            if (argCount > 0) {
                Object[] arguments = invocation.getArguments();
                Class<?>[] parameterTypes = invocation.getParameterTypes();
                for (int i = 0; i < argCount; i++) {
                    try {
                        //convert arguments from JSON strings to objects
                        arguments[i] = objectMapper.readValue(arguments[i].toString(), parameterTypes[i]);
                    }
                    catch (IOException cause) {
                        throw new MessageConversionException(
                            "Failed to convert JSON to value: " + arguments[i] + " of type" + parameterTypes[i], cause);
                    }
                }
            }

            remoteInvocationResult = invokeAndCreateResult(invocation, getService());
        }
        send(remoteInvocationResult, replyToAddress);
    }

    private void send(RemoteInvocationResult result, Address replyToAddress) {
        Object value = result.getValue();
        if (value != null) {
            try {
                //convert the returning value from a model to a JSON string
                //before we send it back
                Object json = objectMapper.writeValueAsString(value);
                result.setValue(json);
            }
            catch (JsonProcessingException cause) {
                throw new MessageConversionException("Failed to convert value to JSON: " + value, cause);
            }
        }
        Message message = getMessageConverter().toMessage(result, new MessageProperties());

        getAmqpTemplate().send(replyToAddress.getExchangeName(), replyToAddress.getRoutingKey(), message);
    }

}

Now, with this class defined I changed the definition of my service listener to something like this:

<bean id="toteServiceListener" class="amqphack.FFDAmqpInvokerServiceExporter">
    <property name="serviceInterface" value="ampqphack.ToteService"/>
    <property name="service" ref="defaultToteService"/>
    <property name="amqpTemplate" ref="rabbitTemplate"/>
</bean>

<rabbit:listener-container connection-factory="connectionFactory">
    <rabbit:listener ref="toteServiceListener" queue-names="tote-service"/>
</rabbit:listener-container>

I used the regular AmqTemplate in this case, since I know the ResultInvocationValue will be always converted to a JSON string anyways, so I don't mind if the InvocationResult is serialized using traditional Java serialization.

On the Client Side

In the client I had to change to things. First, I need that any arguments we send to in a invocation be converted to JSON strings before we do, but we still keep their parameter types. Fortunately, the existing AmqpProxyFactoryBean accepts a remoteInvocationFactory parameter where we can intercept the invocation and change it. So I first defined and new RemoteInvocationFactory:

public class JSONRemoteInvocationFactory implements RemoteInvocationFactory {

    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    public RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) {
        RemoteInvocation invocation = new RemoteInvocation(methodInvocation);
        if (invocation.getParameterTypes() != null) {
            int paramCount = invocation.getParameterTypes().length;
            Object[] arguments = new Object[paramCount];
            try {
                for (int i = 0; i < paramCount; i++) {
                    arguments[i] = mapper.writeValueAsString(invocation.getArguments()[i]);
                }
                invocation.setArguments(arguments);
            }
            catch (JsonProcessingException cause) {
                throw new RuntimeException(
                    "Failed converting arguments to json: " + Arrays.toString(invocation.getArguments()), cause);
            }
        }
        return invocation;
    }
}

But that is not enough. When we get the result back, we will need turn its result back again into a Java object. For this we can use the service interface expected return type. And for this I extended the exist AmqpProxyFactoryBean to simply convert its result, which I know will always be a String, into a Java model.

public class JSONAmqpProxyFactoryBean extends AmqpProxyFactoryBean {

    private final ObjectMapper mapper = DefaultObjectMapper.createDefaultObjectMapper();

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object ret = super.invoke(invocation);
        return mapper.readValue(ret.toString(), invocation.getMethod().getReturnType());
    }

}

And with this, I was able to define my client side somewhat like this:

<bean id="toteService" class="amqphack.JSONAmqpProxyFactoryBean">
    <property name="amqpTemplate" ref="rabbitTemplate"/>
    <property name="serviceInterface" value="amqphack.ToteService"/>
    <property name="routingKey" value="tote-service"/>
    <property name="remoteInvocationFactory" ref="remoteInvocationFactory"/>
</bean>

And after this it all worked like a charm:

ToteService toteService = context.getBean("toteService", ToteService.class);
ToteModel tote = toteService.findTote("18251", "ABCD");

Since I don change the traditional converter, it means the exceptions are still properly serialized in the InvocationResult.

Edwin Dalorzo
  • 76,803
  • 25
  • 144
  • 205
0

Don't know if its still needed, but this is how I solved the problem for using JSON with AmqpProxyFactoryBean / AmqpInvokerServiceExporter. On the client side I use the Jackson2JsonMessageConverter converter and on the server side a RemoteInvocationAwareMessageConverterAdapter which wraps the Jackson2JsonMessageConverter converter.

ClientConfig.java:

import com.stayfriends.commons.services.interfaces.GameService;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.remoting.client.AmqpProxyFactoryBean;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ClientConfig {

    @Bean
    public RabbitTemplate gameServiceTemplate(ConnectionFactory connectionFactory,
                                              Jackson2JsonMessageConverter messageConverter) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setExchange("rpc");
        template.setMessageConverter(messageConverter);
        return template;
    }

    @Bean
    public ServiceAmqpProxyFactoryBean gameServiceProxy2(@Qualifier("gameServiceTemplate") RabbitTemplate template) {
        return new ServiceAmqpProxyFactoryBean(template);
    }


    public static class ServiceAmqpProxyFactoryBean implements FactoryBean<Service>, InitializingBean {
        private final AmqpProxyFactoryBean proxy;

        ServiceAmqpProxyFactoryBean(RabbitTemplate template) {
            proxy = new AmqpProxyFactoryBean();
            proxy.setAmqpTemplate(template);
            proxy.setServiceInterface(GameService.class);
            proxy.setRoutingKey(GameService.class.getSimpleName());
        }

        @Override
        public void afterPropertiesSet() {
            proxy.afterPropertiesSet();
        }

        @Override
        public Service getObject() throws Exception {
            return (Service) proxy.getObject();
        }

        @Override
        public Class<?> getObjectType() {
            return Service.class;
        }

        @Override
        public boolean isSingleton() {
            return proxy.isSingleton();
        }
    }

}

ServerConfig.java

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.MessageListenerContainer;
import org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter;
import org.springframework.amqp.support.converter.RemoteInvocationAwareMessageConverterAdapter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ServerConfig {

    @Bean
    public DirectExchange serviceExchange() {
        return new DirectExchange("rpc");
    }

    @Bean
    public Queue serviceQueue() {
        return new Queue(Service.class.getSimpleName());
    }

    @Bean
    public Binding binding(@Qualifier("serviceQueue") Queue queue, @Qualifier("serviceExchange") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(Service.class.getSimpleName()).noargs();
    }

    @Bean("remoteInvocationAwareMessageConverter")
    @Primary
    public RemoteInvocationAwareMessageConverterAdapter remoteInvocationAwareMessageConverterAdapter(
        Jackson2JsonMessageConverter jsonMessageConverter) {
        return new RemoteInvocationAwareMessageConverterAdapter(jsonMessageConverter);
    }

    @Bean
    public AmqpInvokerServiceExporter exporter(RabbitTemplate template, ServiceImpl service,
                                               RemoteInvocationAwareMessageConverterAdapter messageConverter) {
        AmqpInvokerServiceExporter exporter = new AmqpInvokerServiceExporter();
        exporter.setAmqpTemplate(template);
        exporter.setService(service);
        exporter.setServiceInterface(Service.class);
        exporter.setMessageConverter(messageConverter);
        return exporter;
    }

    @Bean
    public MessageListenerContainer container(ConnectionFactory connectionFactory,
                                              @Qualifier("serviceQueue") Queue queue,
                                              AmqpInvokerServiceExporter exporter) {
        DirectMessageListenerContainer container = new DirectMessageListenerContainer(connectionFactory);
        container.setQueues(queue);
        container.setMessageListener(exporter);
        container.setConsumersPerQueue(5);
        return container;
    }
}