在咱們以往的軟件或者網站使用中,都有遇到過這種狀況,莫名的彈出廣告或者通知!而在咱們的業務系統中,有的時候也須要羣發通知公告的方式去告知網站用戶一些信息,那麼這種功能是怎麼實現的呢,本文將使用springboot+webSocket來實現這類功能,固然也有其餘方式來實現 長鏈接/websocket/SSE等主流服務器推送技術比較javascript
使用Intellij IDEA 快速建立一個springboot + webSocket項目css
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
複製代碼
@ServerEndpoint
這個註解。這個註解是Javaee標準裏的註解,tomcat7以上已經對其進行了實現,若是是用傳統方法使用tomcat發佈的項目,只要在pom文件中引入javaee標準便可使用。<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
複製代碼
首先要注入ServerEndpointExporter類,這個bean會自動註冊使用了@ServerEndpoint
註解聲明的Websocket endpoint。要注意,若是使用獨立的servlet容器,而不是直接使用springboot的內置容器,就不要注入ServerEndpointExporter,由於 它(ServerEndpointExporter) 將由容器本身提供和管理。html
WebSocketConfig.java前端
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
複製代碼
接下來就是寫websocket的具體實現類,很簡單,直接上代碼:java
BulletinWebSocket.javamysql
package com.example.websocket.controller;
import com.example.websocket.service.BulletinService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @ServerEndpoint 該註解用來指定一個URI,客戶端能夠經過這個URI來鏈接到WebSocket。
* 相似Servlet的註解mapping。無需在web.xml中配置。
* configurator = SpringConfigurator.class是爲了使該類能夠經過Spring注入。
* @Author jiangpeng
*/
@ServerEndpoint(value = "/webSocket/bulletin")
@Component
public class BulletinWebSocket {
private static final Logger LOGGER = LoggerFactory.getLogger(BulletinWebSocket.class);
private static ApplicationContext applicationContext;
public static void setApplicationContext(ApplicationContext context) {
applicationContext = context;
}
public BulletinWebSocket() {
LOGGER.info("BulletinWebSocket init ");
}
// concurrent包的線程安全Set,用來存放每一個客戶端對應的MyWebSocket對象。
private static CopyOnWriteArraySet<BulletinWebSocket> BULLETIN_WEBSOCKETS = new CopyOnWriteArraySet<BulletinWebSocket>();
// 與某個客戶端的鏈接會話,須要經過它來給客戶端發送數據
private Session session;
/**
* 鏈接創建成功調用的方法
* */
@OnOpen
public void onOpen(Session session) throws IOException {
this.session = session;
// 加入set中
BULLETIN_WEBSOCKETS.add(this);
// 新登陸用戶廣播通知
this.session.getBasicRemote().sendText(applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date());
LOGGER.info("有新鏈接加入{}!當前在線人數爲{}", session, getOnlineCount());
}
@OnClose
public void onClose() {
BULLETIN_WEBSOCKETS.remove(this);
LOGGER.info("有一鏈接關閉!當前在線人數爲{}", getOnlineCount());
}
/**
* 收到客戶端消息後調用的方法
*
* @param message 客戶端發送過來的消息
* @param session 可選的參數
*/
@OnMessage
public void onMessage(String message, Session session) {
LOGGER.info("來自客戶端的信息:{}", message);
}
@OnError
public void onError(Session session, Throwable error) {
LOGGER.error("發生錯誤:{}", session.toString());
error.printStackTrace();
}
/**
* 這個方法與上面幾個方法不同。沒有用註解,是根據本身須要添加的方法。
* 由於使用了Scheduled定時任務,因此方法不是有參數
* @throws Exception
*/
@Scheduled(cron = "0/2 * * * * ?")
public void sendMessage() throws IOException {
// 全部在線用戶廣播通知
BULLETIN_WEBSOCKETS.forEach(socket -> {
try {
socket.session.getBasicRemote().sendText("定時:"+applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date());
} catch (IOException e) {
e.printStackTrace();
}
});
}
public static synchronized int getOnlineCount() {
return BULLETIN_WEBSOCKETS.size();
}
}
複製代碼
使用springboot的惟一區別是要添加@Component
註解,而使用獨立容器不用,是由於容器本身管理websocket的,但在springboot中連容器都是spring管理的。jquery
雖然@Component
默認是單例模式的,但springboot仍是會爲每一個websocket鏈接初始化一個bean,因此能夠用一個靜態set保存起來private static CopyOnWriteArraySet<BulletinWebSocket> BULLETIN_WEBSOCKETS = new CopyOnWriteArraySet<BulletinWebSocket>();
。git
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>static</h1>
<div id="msg" class="panel-body">
</div>
<input id="text" type="text"/>
<button onclick="send()">發送</button>
</body>
<script src="https://cdn.bootcss.com/web-socket-js/1.0.0/web_socket.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script type="text/javascript">
var websocket = null;
//判斷當前瀏覽器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://127.0.0.1:8080/webSocket/bulletin");
}
else {
alert("對不起!你的瀏覽器不支持webSocket")
}
//鏈接發生錯誤的回調方法
websocket.onerror = function () {
setMessageInnerHTML("error");
};
//鏈接成功創建的回調方法
websocket.onopen = function (event) {
setMessageInnerHTML("加入鏈接");
};
//接收到消息的回調方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
};
//鏈接關閉的回調方法
websocket.onclose = function () {
setMessageInnerHTML("斷開鏈接");
};
//監聽窗口關閉事件,當窗口關閉時,主動去關閉websocket鏈接,
// 防止鏈接還沒斷開就關閉窗口,server端會拋異常。
window.onbeforeunload = function () {
var is = confirm("肯定關閉窗口?");
if (is) {
websocket.close();
}
};
//將消息顯示在網頁上
function setMessageInnerHTML(innerHTML) {
$("#msg").append(innerHTML + "<br/>")
};
//關閉鏈接
function closeWebSocket() {
websocket.close();
}
![](https://user-gold-cdn.xitu.io/2019/2/21/1690f655083376d7?w=721&h=457&f=gif&s=31053)
//發送消息
function send() {
var message = $("#text").val();
websocket.send(message);
$("#text").val("");
}
</script>
</html>
複製代碼
GITHUB源碼地址《===github
CREATE TABLE `bulletin` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '編號id',
`title` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '標題',
`content` varchar(1000) COLLATE utf8_bin NOT NULL COMMENT '內容',
`user_type` tinyint(1) NOT NULL COMMENT '通告對象類型 1:單個用戶 2:多個用戶 3:所有用戶',
`user_roles` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '通告對象角色',
`user_depts` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '通告對象部門',
`type` tinyint(1) DEFAULT NULL COMMENT '通告類型 1:系統升級',
`publish_time` datetime DEFAULT NULL COMMENT '發佈時間',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '狀態 0:待發布 1:已發佈 2:撤銷 ',
`created_at` datetime NOT NULL COMMENT '建立時間',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
`created_by` int(11) NOT NULL COMMENT '建立人',
`updated_by` int(11) NOT NULL COMMENT '修改人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='通告表';
複製代碼
CREATE TABLE `bulletin_user` (
`bulletin_id` int(11) NOT NULL COMMENT '通告編號id',
`user_id` int(11) NOT NULL COMMENT '用戶id',
`is_read` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否閱讀 0否 1是',
`created_at` datetime NOT NULL COMMENT '建立時間',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
PRIMARY KEY (`bulletin_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='用戶通告標記表';
複製代碼
以上的功能實現竟然能夠參考上面 BulletinWebSocket.java 中的這幾塊代碼web
/**
* 鏈接創建成功調用的方法
* */
@OnOpen
public void onOpen(Session session) throws IOException {
this.session = session;
// 加入set中
BULLETIN_WEBSOCKETS.add(this);
// 新登陸用戶廣播通知
this.session.getBasicRemote().sendText(applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date());
LOGGER.info("有新鏈接加入{}!當前在線人數爲{}", session, getOnlineCount());
}
複製代碼
public void sendMessage() throws IOException {
// 全部在線用戶廣播通知
BULLETIN_WEBSOCKETS.forEach(socket -> {
try {
socket.session.getBasicRemote().sendText("定時:"+applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date());
} catch (IOException e) {
e.printStackTrace();
}
});
}
複製代碼
SpringBoot 部署與Spring部署都有一些差異,但如今用Srpingboot的公司多,SpringBoot建立項目快,因此使用該方式來說解,有一個問題就是開發WebSocket時發現沒法經過@Autowired注入bean,一直爲空。怎麼解決呢?
其實不是不能注入,是已經注入了,可是客戶端每創建一個連接就會建立一個對象,這個對象沒有任何的bean注入操做,下面貼下實踐
applicationContext.getBean(BulletinService.class)