#RabbitListener for multiple object types - spring-boot

In SpringBoot I use RabbitTemplate and #RabbitListener to produce and receive Messages.
I have an abstract class to send and receive RabbitMQ message of a specific type and send those messages to a STOMP web socket.
I use one RabbitMQ topic exchange which is bind to one RabbitMQ queue. In the topic-exchange I send messages (Java objects) of 2 different types and I am using 2 #RabbitListener to consume these messages.
#Configuration
public class WsRabbitMqConfiguration {
public static final String WS_EXCHANGE_NAME = "websocket.replication";
#Getter
#Value(value = "${spring.application.name}")
private String routingKey;
#Bean
TopicExchange wsReplicationExchange() {
return new TopicExchange(
WS_EXCHANGE_NAME,
true,
false,
null);
}
#Bean
public Queue wsReplicationQueue() {
return new AnonymousQueue();
}
#Bean
public Binding bindingWsAttributeValueQueue(TopicExchange wsReplicationExchange,
Queue wsReplicationQueue) {
return BindingBuilder
.bind(wsReplicationQueue)
.to(wsReplicationExchange)
.with(routingKey);
}
}
#RequiredArgsConstructor
public abstract class AbstractWebsocketService<T> implements WebSocketService {
public static final String HEADER_WS_DESTINATION = "x-Websocket-Destination";
private final RabbitTemplate rabbitTemplate;
private final WsRabbitMqConfiguration wsRabbitMqConfiguration;
#Override
public final <T> void send(String destination, T payload) {
rabbitTemplate.convertAndSend(WsRabbitMqConfiguration.WS_EXCHANGE_NAME, wsRabbitMqConfiguration.getRoutingKey(), payload,
message -> putDestination(message, destination));
}
protected abstract void handleWebsocketSending(T payload, String destination);
#Service
public class FooWebSocketService extends AbstractWebsocketService<Foo> {
public FooWebSocketService(RabbitTemplate rabbitTemplate,
WsRabbitMqConfiguration rabbitMqConfiguration) {
super(rabbitTemplate, rabbitMqConfiguration);
}
#RabbitListener(queues = "#{wsReplicationQueue.name}", ackMode = "NONE")
protected void handleWebsocketSending(#Payload Foo payload, #Header(HEADER_WS_DESTINATION) String destination) {
// logic here
}
}
#Service
public class BarWebSocketService extends AbstractWebsocketService<Bar> {
public BarWebSocketService(RabbitTemplate rabbitTemplate,
WsRabbitMqConfiguration rabbitMqConfiguration) {
super(rabbitTemplate, rabbitMqConfiguration);
}
#RabbitListener(queues = "#{wsReplicationQueue.name}", ackMode = "NONE")
protected void handleWebsocketSending(#Payload Bar payload, #Header(HEADER_WS_DESTINATION) String destination) {
// logic here
}
}
From another service class I want to send RMQ messages, but randomly wrong #RabbitListener is activated.
Eg.
#Service
#RequiredArgsConstructor
public class TestService {
private final BarWebSocketService barWebSocketService;
public void sendMessage(Bar bar) {
// 1st message
barWebSocketService.send("bar-destination", bar);
// 2nd message
barWebSocketService.send("bar-destination2", bar);
}
}
For the 1st message #RabbitListener in FooWebSocketService is activated (which is wrong) and for the 2nd message #RabbitListener in BarWebSocketService (which is right).
Any suggestions what I am doing wrong? Thank you!

Related

What is idea of bindings in spring boot rabbitmq?

I need to bind several exchanges with several routing keys to one single queue and be able to send messages by exchange and routing key and receive it by listening to queue by queue-name.
my code:
#Configuration
#RequiredArgsConstructor
#EnableConfigurationProperties(ExchangeConfig.class)
public class RabbitConfig {
private final ExchangeConfig exchangeConfig;
#Bean
public List<Binding> bindings() {
List<Binding> bindings = new ArrayList<>();
exchangeConfig.getExchangesWithKeys()
.forEach(exchangeWithKeys -> exchangeWithKeys.getRoutingKeys()
.forEach(key -> {
Exchange exchange = ExchangeBuilder.directExchange(exchangeWithKeys.getExchange()).build();
Queue queue = QueueBuilder.durable(exchangeConfig.getLogsQueue()).build();
Binding binding = BindingBuilder.bind(queue).to(exchange)
.with(key).noargs();
bindings.add(binding);
}));
return bindings;
}
}
config:
spring:
rabbitmq:
host: localhost
port: 5672
rabbitmq:
exchanges-with-keys:
- exchange: exchange1
routing-keys: exchange1.live, exchange1.after
- exchange: exchange2
routing-keys: exchange2.live, exchange2.after
- exchange: exchange3
routing-keys: exchange3.live, exchange3.after
logs-queue: log-messages_q
props:
#Data
#Component
#ConfigurationProperties(prefix = "rabbitmq")
public class ExchangeConfig {
private String logsQueue;
private List<ExchangeWithKeys> exchangesWithKeys;
#Data
public static class ExchangeWithKeys {
private String exchange;
private List<String> routingKeys;
}
}
listener:
#Component
#Slf4j
#RequiredArgsConstructor
public class LogsListener {
private final LogMessageEventProcessor logMessageEventProcessor;
#RabbitListener(queues = "${rabbitmq.logs-queue}")
public void onLiveEvent(LogMessageEvent event) {
log.info("Received log event message [{}]", event.getBody());
logMessageEventProcessor.processLogMessageEvent(event);
}
}
test:
#SpringBootTest
#ContextConfiguration(initializers = LogsListenerTest.Initializer.class)
class LogsListenerTest {
#Autowired
private RabbitTemplate template;
#ClassRule
private static final RabbitMQContainer container = new RabbitMQContainer("rabbitmq:3.7.25-management-alpine")
.withExposedPorts(5672, 15672).withQueue("log-messages_q");
#BeforeAll
private static void startRabbit() {container.start();}
#AfterAll
private static void stopRabbit() {
container.stop();
}
#Test
public void test() {
template.convertAndSend("exchange1", "exchange1.live", new LogMessageEvent());
template.receiveAndConvert("log-messages_q");
}
public static class Initializer implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(#NotNull ConfigurableApplicationContext configurableApplicationContext) {
val values = TestPropertyValues.of(
"spring.rabbitmq.host=" + container.getContainerIpAddress(),
"spring.rabbitmq.port=" + container.getMappedPort(5672)
);
values.applyTo(configurableApplicationContext);
}
}
}
Everything above does not working.
So where should i put these bindings to make it work? Thanks.
What version are you using? The use of List<Binding> has been replaced by Declarables.
See https://docs.spring.io/spring-amqp/docs/current/reference/html/#collection-declaration
The documentation is a bit out of date, the admin declareCollections property was removed in 2.2.

How to pass message to controller by #MessageMapping with specified user url?

I have such problem. When i try to send message from client side to server, it doesn't match with my #MessageMapping methods. I don't know how to intercept messages on controller layer.
Client side
sends message (it's react-stomp that uses sockjs):
move = (move) => {
this.clientRef.sendMessage("/user/${this.state.opponentId}/queue/move", JSON.stringify(move))
};
Server side. WebSocketConfig:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/handler")
.setHandshakeHandler(new CustomHandshakeHandler())
.setAllowedOrigins("http://localhost:3000")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry brokerRegistry) {
brokerRegistry.setApplicationDestinationPrefixes("/app");
brokerRegistry.enableSimpleBroker("/topic", "/queue", "/user");
}
#EventListener
void handleSessionConnectedEvent(SessionConnectedEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new MyChannelInterceptor());
}
}
I also added interceptor class to check path of incomming message:
public class MyChannelInterceptor implements ChannelInterceptor {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
return message;
}
}
On debugging of MyChannelInterceptor i see message with payload and headers. There is simpDestination header with such value:
/user/baedde36-0f9e-4fa5-b8d7-687db1dbcd67/queue/move
What #MessageMapping value should i write to handle messages from specified users? This message succesfully gets to frontside by subscription on this topic but doesn't stay on any controller:
`/user/${message}/queue/move`
I just want to handle messages on server side but i can't catch it there.
Okay. As i understood there is 3 ways to handle websocket messages:
/app - handles with controller
/user - handles with broker, sends messages to specific users
/topic - broadcast to topic for all subscribers
In my situation i just need to create json object with userId, receiverId and message. On server side add DTO class and get it as attribute in my controller method.
Solution:
move = (move) => {
let moveDto = {move: move, userId: this.state.userId, opponentId: this.state.opponentId}
this.clientRef.sendMessage(`/app/move`, JSON.stringify(moveDto))
};
Server side:
#Data
#NoArgsConstructor
#AllArgsConstructor
public class MoveDto {
private String userId;
private String opponentId;
private int move;
}
Controller class:
#RestController
public class GameController {
#Autowired
private SimpMessagingTemplate simpMessagingTemplate;
...//some code here
#MessageMapping("/move")
public void message(MoveDto moveDto) {
String userMessage= "foo";
String opponentMessage = "bar";
simpMessagingTemplate.convertAndSendToUser(
moveDto.getUserId(), "/queue/message", userMessage);
simpMessagingTemplate.convertAndSendToUser(
moveDto.getOpponentId(), "/queue/message", opponentMessage );
}

SpringBoot - How can I call a controller from a MQ message listener

I have a JMS listener configured which receives messages from IBM MQ. Once there is a message in the MQ, I want to post the message to my rest controller.
MQ JMS Listener
public class MqMessageConsumer {
private Logger log = LoggerFactory.getLogger(MqMessageConsumer.class);
#JmsListener(destination = "${ibm.mq.queue-name}")
public void receiveMessage(String message) {
log.info("There is a message in the queue");
//invoke rest end point
log.info(message);
//invoke rest controller in case of MQ call.
}
}
Below is my RestController
#RequestMapping("/users")
public class UsersController {
Logger logger = LoggerFactory.getLogger(UsersController.class);
#PostMapping("/status/checkUser")
public String getStatus() {
return "details";
}
}
Any idea how the rest controller can be called from the MQ consumer class?
You can add a UsersService and use it in both the consumer and controller. In general we keep controller as thin as possible and put business logic in a service
Code can be something like
#Service
public class UsersService {
public String getStatus() {
return "details";
}
}
#Component
public class MqMessageConsumer {
#Autowired UsersService usersService;
#JmsListener(destination = "${ibm.mq.queue-name}")
public void receiveMessage(String message) {
usersService.getStatus();
}
}
#RequestMapping("/users")
public class UsersController {
#Autowired UsersService service;
#PostMapping("/status/checkUser")
public String getStatus() {
return service.getStatus();
}
}

Maybe not public or not valid? Using Spring's Websocket and Kafka

As I am trying to consume data from a topic (the topic name is based on user) and during runtime I am trying to consume message from the topic but I am getting the following error.
Caused by:
org.springframework.expression.spel.SpelEvaluationException: EL1008E:
Property or field 'consumerProperties' cannot be found on object of
type 'org.springframework.beans.factory.config.BeanExpressionContext'
- maybe not public or not valid?
Here is my code
#Service
public class kafkaConsumerService {
private SimpMessagingTemplate template;
KafkaConsumerProperties consumerProperties;
#Autowired
public kafkaConsumerService(KafkaConsumerProperties consumerProperties, SimpMessagingTemplate template) {
this.consumerProperties=consumerProperties;
this.template=template;
}
#KafkaListener(topics = {"#{consumerProperties.getTopic()}"})
// #KafkaListener(topics="Chandan3706")
public void consume(#Payload Message message) {
System.out.println("from kafka topic::" + message);
template.convertAndSend("/chat/getMessage", message);
}
}
My KafkaConsumerProperties.class
#Component
#ConfigurationProperties(prefix="kafka.consumer")
public class KafkaConsumerProperties {
private String bootStrap;
private String group;
private String topic;
public String getBootStrap() {
return bootStrap;
}
public void setBootStrap(String bootStrap) {
this.bootStrap = bootStrap;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
#Override
public String toString() {
return "KafkaConsumerProperties [bootStrap=" + bootStrap + ", group=" + group + ", topic=" + topic + "]";
}
}
Thanks in advance
Since you don’t provide any bean name for your KafkaConsumerProperties component, the default one is de-capitalized class name. That’s one.
The expression you use in the #KafkaListener is regular bean definition phase expression, therefore a root object is some BeanExpressionContext , but not your listener bean as you try to get access through the property.
Not sure if you need that KafkaConsumerProperties property in this listener, but expression must ask for the kafkaConsumerProperties bean:
#Service
public class kafkaConsumerService {
private SimpMessagingTemplate template;
#Autowired
public kafkaConsumerService(SimpMessagingTemplate template) {
this.template=template;
}
#KafkaListener(topics = {"#{kafkaConsumerProperties.topic}"})
// #KafkaListener(topics="Chandan3706")
public void consume(#Payload Message message) {
System.out.println("from kafka topic::" + message);
template.convertAndSend("/chat/getMessage", message);
}
}
The following code worked for me, notice to #DependsOn("KafkaConsumerProperties") and #Component("KafkaConsumerProperties") annotations.
KafkaConsumerService class:
#Service
#DependsOn("KafkaConsumerProperties")
public class KafkaConsumerService {
#KafkaListener(topics = "#{#KafkaConsumerProperties.getTopic()}")
public void consume(#Payload Message message) {
System.out.println("from kafka topic::" + message);
template.convertAndSend("/chat/getMessage", message);
}
}
KafkaConsumerProperties class:
#Component("KafkaConsumerProperties")
#ConfigurationProperties(prefix="kafka.consumer")
public class KafkaConsumerProperties {
private String topic;
public String getTopic() {
return topic;
}
}

Send notification to offline user with persistent queue STOMP

I have this code: Client-side with javascript:
socket = new SockJS(context.backend + '/myWebSocketEndPoint');
stompClient = Stomp.over(socket);
stompClient.connect({},function (frame) {
stompClient.subscribe('/queue/'+clientId+'/notification', function(response){
alert(angular.fromJson(response.body));
});
});
In this code, a client when connects, subscribe to receive notification using '/queue/'+ his client id + '/notification/. So i have a queue for every client. I use stomp with sockjs
In my server (Java + spring boot) i have a notification listener which when an event is published, it send a notification to all clients. So i have:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/myWebSocketEndPoint")
.setAllowedOrigins("*")
.withSockJS();
}
}
the class MenuItemNotificationChannel who call MenuItemNotificationSender to send the notification to the users.
#Component
public class MenuItemNotificationChannel extends AbstractNotificationChannel {
#Autowired
private MenuItemNotificationSender menuItemNotificationSender;
#Autowired
private UserRepository userRepository;
#Override
public void sendNotification(KitaiEvent<?> event, Map<String, Object> notificationConfiguration) throws Exception {
String menuItem = Optional.ofNullable((String) notificationConfiguration.get(MENU_ENTRY_KEY)).orElseThrow(IllegalArgumentException::new);
List<User> userList = userRepository.findAll();
for(User u: userList){
menuItemNotificationSender.sendNotification(new MenuItemDto(menuItem),u.getId());
}
MenuItemNotificationSender class is:
#Component
public class MenuItemNotificationSender {
#Autowired
private SimpMessagingTemplate messagingTemplate;
#Autowired
public MenuItemNotificationSender(SimpMessagingTemplate messagingTemplate){
this.messagingTemplate = messagingTemplate;
}
public void sendNotification(MenuItemDto menuItem,Long id) {
String address = "/queue/"+id+"/notification";
messagingTemplate.convertAndSend(address, menuItem);
}
}
This code works perfectly: notifications are sent to every user. But if a user is not online, notifications are losts. My questions are:
How can i verify whit stomp what subscriptions are active and what are not?? (If i can verify if a subscription is active, i solve my problem because i save notification for users offline and then send them when they do login)
Can i use persistent queues? (i read something about it, but i have not understand if i can use it only with stomp and sockjs)
Sorry for my english! :D
You can put a spring event listener on the session connected event and the session disconnect event
I tested this one with spring 4.3.4
#Component
public class WebSocketSessionListener
{
private static final Logger logger = LoggerFactory.getLogger(WebSocketSessionListener.class.getName());
private List<String> connectedClientId = new ArrayList<String>();
#EventListener
public void connectionEstablished(SessionConnectedEvent sce)
{
MessageHeaders msgHeaders = sce.getMessage().getHeaders();
Principal princ = (Principal) msgHeaders.get("simpUser");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sce.getMessage());
List<String> nativeHeaders = sha.getNativeHeader("userId");
if( nativeHeaders != null )
{
String userId = nativeHeaders.get(0);
connectedClientId.add(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
else
{
String userId = princ.getName();
connectedClientId.add(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
}
#EventListener
public void webSockectDisconnect(SessionDisconnectEvent sde)
{
MessageHeaders msgHeaders = sde.getMessage().getHeaders();
Principal princ = (Principal) msgHeaders.get("simpUser");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sde.getMessage());
List<String> nativeHeaders = sha.getNativeHeader("userId");
if( nativeHeaders != null )
{
String userId = nativeHeaders.get(0);
connectedClientId.remove(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Disconnessione websocket. ID Utente "+userId);
}
}
else
{
String userId = princ.getName();
connectedClientId.remove(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Disconnessione websocket. ID Utente "+userId);
}
}
}
public List<String> getConnectedClientId()
{
return connectedClientId;
}
public void setConnectedClientId(List<String> connectedClientId)
{
this.connectedClientId = connectedClientId;
}
}
When a client is connected you add in the List of clients id the client id; when it disconnects you remove it
Then you can inject this bean or its List where you want to check if the client is active or less and then you can check if the client id is between the connected clients ID you can send the message, otherwise you must save it and resend later
On client side you can do something like this:
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({userId:"customUserId"}, function (frame) {
});
Angelo
why not using some events like below, you can export classes to differents files and use SessionConnectedEvent and SessionDisconnectEvent OR SessionSubscribeEvent and SessionUnsubscribeEvent.
see doc here http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-appplication-context-events
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;
#Component
public class SessionConnectedListener extends SessionsListener implements ApplicationListener<SessionConnectedEvent> {
#Override
public void onApplicationEvent(SessionConnectedEvent event) {
users.add(event.getUser().getName());
}
}
#Component
class SessionDisconnectListener extends SessionsListener implements ApplicationListener<SessionDisconnectEvent> {
#Override
public void onApplicationEvent(SessionDisconnectEvent event) {
users.remove(event.getUser().getName());
}
}
#Component
class SessionSubscribeListener extends SessionsListener implements ApplicationListener<SessionSubscribeEvent> {
#Override
public void onApplicationEvent(SessionSubscribeEvent event) {
users.add(event.getUser().getName());
}
}
#Component
class SessionUnsubscribeListener extends SessionsListener implements ApplicationListener<SessionUnsubscribeEvent> {
#Override
public void onApplicationEvent(SessionUnsubscribeEvent event) {
users.remove(event.getUser().getName());
}
}
class SessionsListener {
protected List<String> users = Collections.synchronizedList(new LinkedList<String>());
public List<String> getUsers() {
return users;
}
}
and change your code :
#Autowired
private SessionsListener sessionsListener;
#Override
public void sendNotification(KitaiEvent<?> event, Map<String, Object> notificationConfiguration) throws Exception {
String menuItem = Optional.ofNullable((String) notificationConfiguration.get(MENU_ENTRY_KEY)).orElseThrow(IllegalArgumentException::new);
List<String> userList = sessionsListener.getUsers();
for(String u: userList){
menuItemNotificationSender.sendNotification(new MenuItemDto(menuItem),u);
}

Resources