Springboot+WebSocket+RabbitMQ

Springboot+WebSocket+RabbitMQ

一、起因摘要

为什么我要把这三个技术绑到一块儿来为大家介绍呢?

因为前期自己的程序起初需求要求有一些业务逻辑要求前台异步请求并将消息推送rabbitmq的其中一个队列中,接下来需要监听rabbitmq的另一个消费队列获取监听结果。本人实现此功能后,新的需求来了,要求把监听到的结果返回给前台并作出响应提示,这下坑就来了,接下来我就按步骤从坑出现、最容易踩坑的地方、坑解决、最后到整个业务需要的实现代码实例展示给大家,希望对大家有帮助,避坑而行…

二、RabbitMQ的介绍

RabbitMQ使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现(AMQP的主要特征是面向消息、队列、路由、可靠性、安全)。支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现很出色。

相关概念

消息队列通常有三个概念:发送消息(生产者)、队列、接收消息(消费者)。RabbitMQ在这个基本概念之上,多做了一层抽象,在发送消息和队列之间,加入了交换机。这样发送消息和队列就没有直接关系,而是通过交换机来做转发,交换机会根据分发策略把消息转给队列。

RabbitMQ模型

P为发送消息(生产者)、X为交换机、Q为消息队列、C为接收消息(消费者)

RabbitMQ比较重要的几个概念:

虚拟主机:RabbitMQ支持权限控制,但是最小控制粒度为虚拟主机。一个虚拟主机可以包含多个交换机、队列、绑定。

交换机:RabbitMQ分发器,根据不同的策略将消息分发到相关的队列。

队列:缓存消息的容器。

绑定:设置交换机与队列的关系。

++注:具体如何配置与安装这里不做过多介绍(详情参考https://blog.csdn.net/zhuzhezhuzhe1/article/details/80454956),本人主要从往队列推送开始演示++


代码展示前在此先声明一下:Springboot框架很好的集成了rabbitmq,只需要几个简单的注解就可以实现推送消息、监听队列


1.集成rabbitmq,添加maven依赖

1.1)坐标:

			org.springframework.boot
			spring-boot-starter-amqp


1.2)添加rabbitmq服务配置(application.properties)

#rabbitmq相关配置

spring.rabbitmq.host=192.168.15.131
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456

import org.springframework.amqp.core.AmqpTemplat
@Autowired

private AmqpTemplate amqpTemplate;

依赖注入AmqpTemplate(小心不要导错包

推送消息:


注:推送的msg,必须严格按照自己的业务需求的队列格式进行推送,这样子推送就算完事儿了。

监听消息

接下来就准备监听队列,本人推送消息的队列和监听的队列不是同一个队列,这点大家注意,这时候我所说的 就出现了

先给大家分析说明一下 @RabbitListener@RabbitHandler这两个注解的含义及用法

@RabbitListener 用法:
使用 @RabbitListener 注解标记方法,当监听到队列 debug 中有消息时则会进行接收并处理

@RabbitListener 和 @RabbitHandler 搭配使用
@RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用
@RabbitListener 标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler 的方法处理,具体使用哪个方法处理,根据 MessageConverter 转换后的参数类型
(代码示范如下图)

注:在监听队列获取监听结果要注意接受结果消息的参数类型,一般监听到的结果是字节数组,所以要注意进行转换如下

@Component
@RabbitListener(queues = "consumer_queue")
public class Receiver {

    @RabbitHandler
    public void processMessage1(String message) {
        System.out.println(message);
    }

    @RabbitHandler
    public void processMessage2(byte[] message) {
        System.out.println(new String(message));
    }
    
}
Message 内容对象序列化与反序列化

使用 Java 序列化与反序列化
默认的 SimpleMessageConverter 在发送消息时会将对象序列化成字节数组,若要反序列化对象,需要自定义 MessageConverter

@Configuration
public class RabbitMQConfig {

    @Bean
    public RabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new MessageConverter() {
            @Override
            public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
                return null;
            }

            @Override
            public Object fromMessage(Message message) throws MessageConversionException {
                try(ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(message.getBody()))){
                    return (User)ois.readObject();
                }catch (Exception e){
                    e.printStackTrace();
                    return null;
                }
            }
        });

        return factory;
    }

}
@Component
@RabbitListener(queues = "consumer_queue")
public class Receiver {

    @RabbitHandler
    public void processMessage1(User user) {
        System.out.println(user.getName());
    }

}

此时我们的推送消息与监听消息就算完成了(rabbitmq相对于其他消息中间件来说还是比较偏受欢迎的,简洁好用)。

三、WebSocket简单介绍,坑出现及跨坑

随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务端的通信功能,使服务端也能主动向客户端发送数据.

我们知道,传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;这种客户端是主动方,服务端是被动方的传统Web模式 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,如带有即时通信、实时数据、订阅推送等功能的应用。伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。WebSocket的工作流程是这样的:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过TCP连接传输数据。

集成WebSocket,添加maven依赖

        
            org.springframework.boot
            spring-boot-starter-websocket
            1.3.5.RELEASE
        

随着需求不断地增加本人就遇到了一个难题,就是上面的监听rabbitmq队列后要将监听结果返回给前端,坑出现了,springboot项目中集成rabbitmq而使用此中间件监听队列时就要@RabbitListener 和 @RabbitHandler 搭配使用,最一开始不知道,加了@RabbitHandler注解的方法的返回值类型是String,结果报了一个诡异异常,这是一个大坑如下:

> 大坑在此

org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException: Listener threw exception
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.wrapToListenerExecutionFailedExceptionIfNeeded(AbstractMessageListenerContainer.java:915)
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:825)
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:745)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$001(SimpleMessageListenerContainer.java:97)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$1.invokeListener(SimpleMessageListenerContainer.java:189)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.invokeListener(SimpleMessageListenerContainer.java:1276)
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:726)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:1219)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:1189)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$1500(SimpleMessageListenerContainer.java:97)
	at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1421)
	at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.amqp.rabbit.listener.adapter.ReplyFailureException: Failed to send reply with payload 'OK'
	at org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener.handleResult(AbstractAdaptableMessageListener.java:285)
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.onMessage(MessagingMessageListenerAdapter.java:108)
	at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:822)
	... 10 common frames omitted
Caused by: org.springframework.amqp.AmqpException: Cannot determine ReplyTo message property value: Request message does not contain reply-to property, and no default response Exchange was set.
	at org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener.getReplyToAddress(AbstractAdaptableMessageListener.java:373)
	at org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener.handleResult(AbstractAdaptableMessageListener.java:281)
	... 12 common frames omitted

终于在这里找到了答案。原来,使用

>跨坑

@RabbitListener(queues = Constants.POST_PUSH_QUEUE_THIRD_REDIS)
    @RabbitHandler

修饰的方法,即队列的监听函数,不能返回任何值!否则会导致一个rabbit reply message 回复异常,该异常是由于此方法返回的消息没有设置目的地,查看源码可以看出:

protected void handleResult(Object resultArg, Message request, Channel channel, Object source) throws Exception {
		if (channel != null) {
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Listener method returned result [" + resultArg
						+ "] - generating response message for it");
			}
			try {
				Object result = resultArg instanceof ResultHolder ? ((ResultHolder) resultArg).result : resultArg;
				Message response = buildMessage(channel, result);
				postProcessResponse(request, response);
				Address replyTo = getReplyToAddress(request, source, resultArg);
				sendResponse(channel, replyTo, response);
			}
			catch (Exception ex) {
				throw new ReplyFailureException("Failed to send reply with payload '" + resultArg + "'", ex);
			}
		}
		else if (this.logger.isWarnEnabled()) {
			this.logger.warn("Listener method returned result [" + resultArg
					+ "]: not generating response message for it because no Rabbit Channel given");
		}
	}

其中getReplyToAddress()方法:

protected Address getReplyToAddress(Message request, Object source, Object result) throws Exception {
		Address replyTo = request.getMessageProperties().getReplyToAddress();
		if (replyTo == null) {
			if (this.responseAddress == null && this.responseExchange != null) {
				this.responseAddress = new Address(this.responseExchange, this.responseRoutingKey);
			}
			if (result instanceof ResultHolder) {
				replyTo = evaluateReplyTo(request, source, result, ((ResultHolder) result).sendTo);
			}
			else if (this.responseExpression != null) {
				replyTo = evaluateReplyTo(request, source, result, this.responseExpression);
			}
			else if (this.responseAddress == null) {
				throw new AmqpException(
						"Cannot determine ReplyTo message property value: " +
								"Request message does not contain reply-to property, " +
								"and no default response Exchange was set.");
			}
			else {
				replyTo = this.responseAddress;
			}
		}
		return replyTo;
	}

而我的代码为什么会出现这个异常呢?
因为Groovy的方法会自动将最后一句的结果返回,所有不能使用String定义方法,而应该使用void!
而使用此两注解方法的返回值类型只能是void,而业务又要求必须将监听到的结果返回给前端,所以就想到使用WebSocket前后端建立连接,由后台主动给前台发消息,具体实现代码如下:

后台contriller层及WebSocket具体实现类
package com.iecas.controller;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
/**
 * Created by qs5 on 2018/10/11.
 * @author wangzhen
 * @version 1.0
 */
@Slf4j
@ServerEndpoint(value = "/")
@Component
@RabbitListener(queues = "task_REPqueue")
public class MsgReceiverController {
    public static String msg;
    private static Logger logger = LoggerFactory.getLogger(MsgReceiverController.class);
    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;
    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    private static CopyOnWriteArraySet wsClientMap = new CopyOnWriteArraySet<>();
    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;
    //右键监听结果
   @RabbitHandler
   @OnMessage
    public void receiver(String body) throws IOException {
        logger.info("<=============监听到task_REPqueued队列消息============>"+body);
        msg=body;
        if("成功".equals(msg)){
            sendMessage(msg);
        }else{
            sendMessage(msg);
        }
    }
/*    @RequestMapping("/getMessage")
    public String receiver(String body,String taskId){
        return msg;
    }*/
    /**
     * 连接建立成功调用的方法
     * @param session 当前会话session
     */
    @OnOpen
    public void onOpen (Session session){
        this.session = session;
        wsClientMap.add(this);
        addOnlineCount();
        logger.info(session.getId()+"有新链接加入,当前链接数为:" + wsClientMap.size());
    }
    /**
     * 连接关闭
     */
    @OnClose
    public void onClose (){
        wsClientMap.remove(this);
        subOnlineCount();
        logger.info("有一链接关闭,当前链接数为:" + wsClientMap.size());
    }
    /**
     * 收到客户端消息
     * @param message 客户端发送过来的消息
     * @param session 当前会话session
     * @throws IOException
     */
   /* @OnMessage
    public void onMessage (String message, Session session) throws IOException {
        logger.info("客户端发送过来的消息:" + message);
//        String message1="你吃饭了吗?";
//        sendMessage(message1);
    }*/
    /**
     * 发生错误
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.info("wsClientMap发生错误!");
        error.printStackTrace();
    }

    /**
     * 给所有客户端群发消息
     * @param message 消息内容
     * @throws IOException
     */
    public void sendMsgToAll(String message) throws IOException {
        for ( MsgReceiverController item : wsClientMap ){
            item.session.getBasicRemote().sendText(message);
        }
        logger.info("成功群送一条消息:" + wsClientMap.size());
    }
    //实现服务器主动推送
    public void sendMessage (String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
        logger.info("成功发送一条消息:" + message);
    }

    public static synchronized  int getOnlineCount (){
        return MsgReceiverController.onlineCount;
    }

    public static synchronized void addOnlineCount (){
        MsgReceiverController.onlineCount++;
    }

    public static synchronized void subOnlineCount (){
        MsgReceiverController.onlineCount--;
    }
}

前台页面



    My WebSocket


Welcome

四 接下来就给大家分析介绍一下

1>页面要注意首先要判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
        websocket = new WebSocket("ws://localhost:8081/web");
    }

要切记"ws://localhost:8081/web"此路径8081/后的部分必须和后台方法处理器即controller层类上的注解@ServerEndpoint(value = “/”),/后的路径保持一致,不然前后台的WebSocket建立不起连接,就无法实现后台消息向前台的实时推送;

2>核心是@ServerEndpoint这个注解

这个注解是Javaee标准里的注解,tomcat7以上已经对其进行了实现,如果是用传统方法使用tomcat发布项目,只要在pom文件中引入javaee标准即可使用。但使用springboot的内置tomcat时,就不需要引入javaee-api了,spring-boot已经包含了。使用springboot的websocket功能首先引入springboot组件。springboot的高级组件会自动引用基础的组件,像spring-boot-starter-websocket就引入了spring-boot-starter-web和spring-boot-starter,所以不要重复引入。
使用@ServerEndpoint创立websocket endpoint
首先要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。要注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
3>Controller层中的sendMessage方法,此方法很重要,它就是实现后台服务器主动推送消息给前台,这样就可以解决那个坑带来的麻烦不能通过Springboot框架视图渲染将结果返给前台,只有通过WebSocket后台主动推送消息给前台,这个坑就这样跨过去了…

    public void sendMessage (String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
        logger.info("成功发送一条消息:" + message);
    }
3>本人的业务需是将监听类与websocket的实现类整合到了一起

要想实现将加了 @RabbitHandler注解的无返回值类型的方法(注:该方法是监听消息队列的方法),必须还得有值返回给前台,此时就必须要在这个方法上再加上WebSocket中的@OnMessage这个注解,然后调用sendMessage(String message)方法就可以实现主动推送消息给前台了

五 结语

初次编写技术文档,只因此次查看了各种资料,发现这种业务需求也是比较常见的,因为在做的过程前期是比较复杂繁琐的,自己也是费了一些时间和精力,为此编写了这个文档,总结整理出自己的一些认知。希望能帮到各位同僚,少走弯路,以此共勉,避坑而行。此文仅是个人的一些理解,不是很深,但是有料,如有改进之处还望各位留言共同探讨指正。

最后一句微语送给大家:

乞丐不一定会妒忌百万富翁,但可能会妒忌收入更高的乞丐。没有更高的视野,你会纠结于现在的圈子;有了更高的视野,你会把身边的人与事看淡!
Springboot+WebSocket+RabbitMQ_第1张图片

你可能感兴趣的:(经验小记)