spring 使用WebSocket 和 STOMP 實現訊息功能
1)本文旨在 介紹如何 利用 WebSocket 和 STOMP 實現訊息功能;
2)要知道, WebSocket 是傳送和接收訊息的 底層API,而SockJS 是在 WebSocket 之上的 API;最後 STOMP(面向訊息的簡單文字協議)是基於 SockJS 的高階API
(乾貨——簡而言之,WebSocket 是底層協議,SockJS 是WebSocket 的備選方案,也是 底層協議,而 STOMP 是基於 WebSocket(SockJS) 的上層協議)
3)broker==經紀人,代理;
4)當然,你可以直接跳轉到 STOMP 知識(章節【3】);
【1】WebSocket
1)intro:WebSocket 協議提供了 通過一個套接字實現全雙工通訊的功能。也能夠實現 web 瀏覽器 和 server 間的 非同步通訊, 全雙工意味著 server 與 瀏覽器間 可以傳送和接收訊息。
【1.1】使用 spring 的低層級 WebSocket API
1)intro:為了在 spring 中 使用較低層級的 API 來處理訊息。有如下方案:
scheme1)我們必須編寫一個實現 WebSocketHandler:
public interface WebSocketHandler { void afterConnectionEstablished(WebSocketSession session) throws Exception; void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception; void handleTransportError(WebSocketSession session, Throwable exception) throws Exception; void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception; boolean supportsPartialMessages(); }
scheme2)當然,我們也可以擴充套件 AbstractWebSocketHandler(更加簡單一點);
// you can also extends TextWebSocketHandler public class ChatTextHandler extends AbstractWebSocketHandler { // handle text msg. @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { session.sendMessage(new TextMessage("hello world.")); } }
對以上程式碼的分析(Analysis): 當然了,我們還可以過載其他三個方法:
handleBinaryMessage() handlePongMessage() handleTextMessage()
scheme3)也可以擴充套件 TextWebSocketHandler(文字 WebSocket 處理器), 不在擴充套件AbstractWebSocketHandler , TextWebSocketHandler 繼承 AbstractWebSocketHandler ;
2)你可能會關係建立和關閉連線感興趣。可以過載 afterConnectionEstablished() and afterConnectionClosed():
// 當新連線建立的時候,被呼叫;
public void afterConnectionEstablished(WebSocketSession session)
throws Exception {
logger.info("Connection established");
}
// 當連線關閉時被呼叫;
@Override
public void afterConnectionClosed(
WebSocketSession session, CloseStatus status) throws Exception {
logger.info("Connection closed. Status: " + status);
}
3)現在已經有了 message handler 類了,下面對其進行配置,配置到 springmvc 的執行環境中。
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(getTextHandler(), "/websocket/p2ptext");
} // 將 ChatTextHandler 處理器 對映到 /websocket/p2ptext 路徑下.
@Bean
public ChatTextHandler getTextHandler() {
return new ChatTextHandler();
}
}
對上述程式碼的分析(Analysis):registerWebSocketHandlers方法 是註冊訊息處理器的關鍵: 通過 呼叫 WebSocketHandlerRegistry .addHandler() 方法 來註冊資訊處理器;
Attention)server 端的 WebSocket 配置完畢,下面配置客戶端;
4)WebSocket 客戶端配置
4.1)client 傳送 一個文字到 server,他監聽來自 server 的文字訊息。下面程式碼 展示了 利用 js 開啟一個原始的 WebSocket 並使用它來發送訊息給server;
4.2)程式碼如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 上述3個meta標籤*必須*放在最前面,任何其他內容都*必須*跟隨其後! --> <title>web socket</title> <link href="<c:url value="/"/>bootstrap/css/bootstrap.min.css" rel="stylesheet"> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <script src="<c:url value="/"/>bootstrap/jquery/jquery.min.js"></script> <!-- Include all compiled plugins (below), or include individual files as needed --> <script src="<c:url value="/"/>bootstrap/js/bootstrap.min.js"></script> <script type="text/javascript"> $(document).ready(function() { websocket_client(); }); function websocket_client() { var hostaddr = window.location.host + "<c:url value='/websocket/p2ptext' />"; var url = 'ws://' + hostaddr; var sock = new WebSocket(url); // 以下的 open(), onmessage(), onclose() // 對應到 ChatTextHandler 的 // afterConnectionEstablished(), handleTextMessage(), afterConnectionClosed(); sock.open = function() { alert("open successfully."); sayMarco(); }; sock.onmessage = function(e) { alert("onmessage"); alert(e); }; sock.onclose = function() { alert("close"); }; function sayMarco() { sock.send("this is the websocket client."); } } </script> </head> <body> <div id="websocket"> websocket div. </div> </body> </html>
error)這樣配置後, WebSocket 無法正常執行;
【2】應對不支援 WebSocket 的場景(引入 SockJS)
1)problem+solutions:
1.1)problem:許多瀏覽器不支援 WebSocket 協議;
1.2)solutions: SockJS 是 WebSocket 技術的一種模擬。SockJS 會 儘可能對應 WebSocket API,但如果 WebSocket 技術 不可用的話,就會選擇另外的 通訊方式協議;
2)SockJS 會優先選擇 WebSocket 協議,但是如果 WebSocket協議不可用的話,他就會從如下 方案中挑選最優可行方案:
-
XHR streaming
-
XDR streaming
-
iFrame event source
-
iFrame HTML file
-
XHR polling
-
XDR polling
-
iFrame XHR polling
-
JSONP polling
3)如何在 server 端配置 SockJS :新增 withSockJS() 方法;
// 將 ChatTextHandler 對映到 /chat/text 路徑下.
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(getTextHandler(), "/websocket/p2ptext").withSockJS();
// withSockJS() 方法宣告我們想要使用 SockJS 功能,如果WebSocket不可用的話,會使用 SockJS;
}
4)客戶端配置 SockJS, 想要確保 載入了 SockJS 客戶端;
4.1)具體做法是 依賴於 JavaScript 模組載入器(如 require.js or curl.js) 還是簡單使用 <script> 標籤載入 JavaScript 庫。最簡單的方法是 使用 <script> 標籤從 SockJS CDN 中進行載入,如下所示:
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
Attention)用 WebJars 解析 Web資源(可選,有興趣的童鞋可以嘗試下)
A1)在springmvc 配置中搭建一個 資源處理器,讓它負責解析路徑以 "webjars/**" 開頭的請求,這也是 WebJars 的標準路徑:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); }
A2)在這個資源處理器 準備就緒後,我們可以在 web 頁面中使用 如下的 <script> 標籤載入 SockJS 庫;
<script src="sockjs.min.js}"> </script>
5)處理載入 SockJS 客戶端庫以外,還要修改 兩行程式碼:
var url = 'p2ptext';
var sock = new SockJS(url);
對以上程式碼的分析(Analysis):
A1)SockJS 所處理的URL 是 "http://" 或 "https://" 模式,而不是 "ws://" or "wss://" ;
A2)其他的函式如 onopen, onmessage, and onclose ,SockJS 客戶端與 WebSocket 一樣;
6)SockJS 為 WebSocket 提供了 備選方案。但無論哪種場景,對於實際應用來說,這種通訊形式層級過低。下面看一下如何 在 WebSocket 之上使用 STOMP協議,來為瀏覽器 和 server間的 通訊增加適當的訊息語義;(乾貨——引入 STOMP—— Simple Text Oriented Message Protocol——面向訊息的簡單文字協議)
【3】使用 STOMP訊息
1)intro: 如何理解 STOMP 與 WebSocket 的關係:
1.1)假設 HTTP 協議 並不存在,只能使用 TCP 套接字來 編寫 web 應用,你可能認為這是一件瘋狂的 事情;
1.2)不過 幸好,我們有 HTTP協議,它解決了 web 瀏覽器發起請求以及 web 伺服器響應請求的細節;
1.3)直接使用 WebSocket(SockJS) 就很類似於 使用 TCP 套接字來編寫 web 應用;因為沒有高層協議,因此就需要我們定義應用間所傳送訊息的語義,還需要確保 連線的兩端都能遵循這些語義;
1.4)同 HTTP 在 TCP 套接字上新增 請求-響應 模型層一樣,STOMP 在 WebSocket 之上提供了一個基於 幀的線路格式層,用來定義訊息語義;(乾貨——STOMP 在 WebSocket 之上提供了一個基於 幀的線路格式層,用來定義訊息語義)
2)STOMP 幀:該幀由命令,一個或多個 頭資訊 以及 負載所組成。如下就是傳送 資料的一個 STOMP幀:(乾貨——引入了 STOMP幀格式)
SEND
destination:/app/marco
content-length:20
{\"message\":\"Marco!\"}
對以上程式碼的分析(Analysis):
A1)SEND:STOMP命令,表明會發送一些內容;
A2)destination:頭資訊,用來表示訊息傳送到哪裡;
A3)content-length:頭資訊,用來表示 負載內容的 大小;
A4)空行:
A5)幀內容(負載)內容:
3)STOMP幀 資訊 最有意思的是 destination頭資訊了: 它表明 STOMP 是一個訊息協議,類似於 JMS 或 AMQP。訊息會發送到 某個 目的地,這個 目的地實際上可能真的 有訊息代理作為 支撐。另一方面,訊息處理器 也可以監聽這些目的地,接收所傳送過來的訊息;
【3.1】啟用STOMP 訊息功能
1)intro:spring 的訊息功能是基於訊息代理構建的,因此我們必須要配置一個 訊息代理 和 其他的一些訊息目的地;(乾貨——spring 的訊息功能是基於訊息代理構建的)
2)如下程式碼展現了 如何通過 java配置 啟用基於代理的的web 訊息功能;
(乾貨——@EnableWebSocketMessageBroker 註解的作用: 能夠在 WebSocket 上啟用 STOMP)
package com.spring.spittr.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
// 應用程式以 /app 為字首,而 代理目的地以 /topic 為字首.
// js.url = "/spring13/app/hello" -> @MessageMapping("/hello") 註釋的方法.
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/hello").withSockJS();
// 在網頁上我們就可以通過這個連結 /server/hello ==<c:url value='/hello'></span> 來和伺服器的WebSocket連線
}
}
對以上程式碼的分析(Analysis):
A1)EnableWebSocketMessageBroker註解表明: 這個配置類不僅配置了 WebSocket,還配置了 基於代理的 STOMP 訊息;
A2)它過載了 registerStompEndpoints() 方法:將 "/hello" 路徑 註冊為 STOMP 端點。這個路徑與之前傳送和接收訊息的目的路徑有所不同, 這是一個端點,客戶端在訂閱或釋出訊息 到目的地址前,要連線該端點,即 使用者傳送請求 url='/server/hello' 與 STOMP server 進行連線,之後再轉發到 訂閱url;(server== name of your springmvc project )(乾貨——端點的作用——客戶端在訂閱或釋出訊息 到目的地址前,要連線該端點)
A3)它過載了 configureMessageBroker() 方法:配置了一個 簡單的訊息代理。如果不過載,預設case下,會自動配置一個簡單的 記憶體訊息代理,用來處理 "/topic" 為字首的訊息。但經過過載後,訊息代理將會處理字首為 "/topic" and "/queue" 訊息。
A4)之外:傳送應用程式的訊息將會帶有 "/app" 字首,下圖展現了 這個配置中的 訊息流;
對上述處理step的分析(Analysis):
A1)應用程式的目的地 以 "/app" 為字首,而代理的目的地以 "/topic" 和 "/queue" 作為字首;
A2)以應用程式為目的地的訊息將會直接路由到 帶有 @MessageMapping 註解的控制器方法中;(乾貨—— @MessageMapping的作用)
A3)而傳送到 代理上的訊息,包括 @MessageMapping註解方法的返回值所形成的訊息,將會路由到 代理上,並最終傳送到 訂閱這些目的地客戶端;
(乾貨——client 連線地址和 傳送地址是不同的,以本例為例,前者是/server/hello, 後者是/server/app/XX,先連線後傳送)
【3.1.1】啟用 STOMP 代理中繼
1)intro:在生成環境下,可能會希望使用 真正支援 STOMP 的代理來支援 WebSocket 訊息,如RabbitMQ 或 ActiveMQ。這樣的代理提供了可擴充套件性和健壯性更好的訊息功能,當然,他們也支援 STOMP 命令;
2)如何 使用 STOMP 代理來替換記憶體代理,程式碼如下:
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 啟用了 STOMP 代理中繼功能,並將其代理目的地字首設定為 /topic and /queue .
registry.enableStompBrokerRelay("/queue", "/topic")
.setRelayPort(62623);
registry.setApplicationDestinationPrefixes("/app"); // 應用程式目的地.
}
對以上程式碼的分析(Analysis):(乾貨——STOMP代理字首和 應用程式字首的意義)
A1)方法第一行啟用了 STOMP 代理中繼功能: 並將其目的地字首設定為 "/topic" or "/queue" ;spring就能知道 所有目的地字首為 "/topic" or "/queue" 的訊息都會發送到 STOMP 代理中;
A2)方法第二行設定了 應用的字首為 "app":所有目的地以 "/app" 打頭的訊息(傳送訊息url not 連線url)都會路由到 帶有 @MessageMapping 註解的方法中,而不會發布到 代理佇列或主題中;
3)下圖闡述了 代理中繼如何 應用於 spring 的 STOMP 訊息處理之中。與 上圖的 關鍵區別在於: 這裡不再模擬STOMP 代理的功能,而是由 代理中繼將訊息傳送到一個 真正的訊息代理來進行處理;
Attention)
A1)enableStompBrokerRelay() and setApplicationDestinationPrefixes() 方法都可以接收變長 引數;
A2)預設情況下: STOMP 代理中繼會假設 代理監聽 localhost 的61613 埠,並且 client 的 username 和password 均為 guest。當然你也可以自行定義;
@Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/topic", "/queue") .setRelayHost("rabbit.someotherserver") .setRelayPort(62623) .setClientLogin("marcopolo") .setClientPasscode("letmein01"); registry.setApplicationDestinationPrefixes("/app", "/foo"); } // setXXX()方法 是可選的
【3.2】 處理來自客戶端的 STOMP 訊息
1)藉助 @MessageMapping 註解能夠 在 控制器中處理 STOMP 訊息
package com.spring.spittr.web;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import com.spring.pojo.Greeting;
import com.spring.pojo.HelloMessage;
@Controller
public class GreetingController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
System.out.println("receiving " + message.getName());
System.out.println("connecting successfully.");
return new Greeting("Hello, " + message.getName() + "!");
}
}
對以上程式碼的分析(Analysis):
A1)@MessageMapping註解:表示 handleShout()方法能夠處理 指定目的地上到達的訊息;
A2)這個目的地(訊息傳送目的地url)就是 "/server/app/hello",其中 "/app" 是 隱含的 ,"/server" 是 springmvc 專案名稱;
2)因為我們現在處理的 不是 HTTP,所以無法使用 spring 的 HttpMessageConverter 實現 將負載轉換為Shout 物件。Spring 4.0 提供了幾個訊息轉換器如下:(Attention, 如果是傳輸json資料的話,定要新增 Jackson jar 包到你的springmvc 專案中,不然連線不會成功的)
【3.2.1】處理訂閱(@SubscribeMapping註解)
1)@SubscribeMapping註解 的方法:當收到 STOMP 訂閱訊息的時候,帶有 @SubscribeMapping 註解 的方法將會觸發;其也是通過 AnnotationMethodMessageHandler 來接收訊息的;
2)@SubscribeMapping註解的應用場景:實現 請求-迴應模式。在請求-迴應模式中,客戶端訂閱一個目的地,然後預期在這個目的地上 獲得一個一次性的 響應;(乾貨——引入了@SubsribeMapping註解實現 請求-迴應模式)
2.1)看個荔枝:
@SubscribeMapping({"/marco"})
public Shout handleSubscription() {
Shout outgoing = new Shout();
outgoing.setMessage("Polo!");
return outgoing;
}
對以上程式碼的分析(Analysis):
A1)@SubscribeMapping註解 的方法來處理 對 "/app/macro" 目的地訂閱(與 @MessageMapping類似,"/app" 是隱含的 );
A2)請求-迴應模式與 HTTP GET 的全球-響應模式差不多: 關鍵區別在於, HTTP GET 請求是同步的,而訂閱的全球-迴應模式是非同步的,這樣客戶端能夠在迴應可用時再去處理,而不必等待;(乾貨——HTTP GET 請求是同步的,而訂閱的請求-迴應模式是非同步的)
【3.2.2】編寫 JavaScript 客戶端
1)intro:藉助 STOMP 庫,通過 JavaScript傳送訊息
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
document.getElementById('response').innerHTML = '';
}
function connect() {
var socket = new SockJS("<c:url value='/hello'/>");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function(greeting){
showGreeting(JSON.parse(greeting.body).content);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
var name = document.getElementById('name').value;
stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
}
function showGreeting(message) {
var response = document.getElementById('response');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(message));
response.appendChild(p);
}
</script>
對以上程式碼的 分析(Analysis): 以上程式碼連線“/hello” 端點併發送 ”name“;
2)stompClient.send("/app/hello", {}, JSON.stringify({'name':name})): 第一個引數:json 負載訊息傳送的 目的地; 第二個引數:是一個頭資訊的Map,它會包含在 STOMP 幀中;第三個引數:負載訊息;
(乾貨—— stomp client 連線地址 和 傳送地址不一樣的,連線地址為 <c:url value='/hello'/> ==localhost:8080/springmvc_project_name/hello , 而 傳送地址為 '/app/hello',這裡要當心)
<script src="<c:url value="/resources/sockjs-1.1.1.js" />"></script>
<script src="<c:url value="/resources/stomp.js" />"></script>
//this line.
function connect() {
var socket = new SockJS("<c:url value='/hello'/>");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function(greeting){
showGreeting(JSON.parse(greeting.body).content);
});
stompClient.subscribe('/app/macro',function(greeting){
alert(JSON.parse(greeting.body).content);
showGreeting(JSON.parse(greeting.body).content);
});
});
}
function sendName() {
var name = document.getElementById('name').value;
stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
}
-
package com.spring.spittr.web; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.annotation.SubscribeMapping; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import com.spring.pojo.Greeting; import com.spring.pojo.HelloMessage; @Controller public class GreetingController { // @MessageMapping defines the sending addr for client. // 訊息傳送地址: /server/app/hello @MessageMapping("/hello") @SendTo("/topic/greetings") public Greeting greeting(HelloMessage message) throws Exception { System.out.println("receiving " + message.getName()); System.out.println("connecting successfully."); return new Greeting("Hello, " + message.getName() + "!"); } @SubscribeMapping("/macro") public Greeting handleSubscription() { System.out.println("this is the @SubscribeMapping('/marco')"); Greeting greeting = new Greeting("i am a msg from SubscribeMapping('/macro')."); return greeting; } /*@MessageMapping("/feed") @SendTo("/topic/feed") public Greeting greetingForFeed(HelloMessage message) throws Exception { System.out.println("receiving " + message.getName()); System.out.println("connecting successfully."); return new Greeting("i am /topic/feed, hello " + message.getName() + "!"); }*/ // private SimpMessagingTemplate template; // SimpMessagingTemplate implements SimpMessageSendingOperations. private SimpMessageSendingOperations template; @Autowired public GreetingController(SimpMessageSendingOperations template) { this.template = template; } @RequestMapping(path="/feed", method=RequestMethod.POST) public void greet( @RequestParam String greeting) { String text = "you said just now " + greeting; this.template.convertAndSend("/topic/feed", text); } } package com.spring.spittr.web; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic", "/queue"); config.setApplicationDestinationPrefixes("/app"); // 應用程式以 /app 為字首,而 代理目的地以 /topic 為字首. // js.url = "/spring13/app/hello" -> @MessageMapping("/hello") 註釋的方法. } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/hello").withSockJS(); // 在網頁上我們就可以通過這個連結 /server/hello 來和伺服器的WebSocket連線 } } package com.spring.spittr.web; import java.io.IOException; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.core.io.FileSystemResource; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.commons.CommonsMultipartResolver; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.springframework.web.servlet.view.tiles3.TilesConfigurer; import org.springframework.web.servlet.view.tiles3.TilesViewResolver; @Configuration @ComponentScan(basePackages = { "com.spring.spittr.web" }) @EnableWebMvc @Import({WebSocketConfig.class}) public class WebConfig extends WebMvcConfigurerAdapter { @Bean public TilesConfigurer tilesConfigurer() { TilesConfigurer tiles = new TilesConfigurer(); tiles.setDefinitions(new String[] { "/WEB-INF/layout/tiles.xml" }); tiles.setCheckRefresh(true); return tiles; } // config processing for static resources. @Override public void configureDefaultServletHandling( DefaultServletHandlerConfigurer configurer) { configurer.enable(); } // InternalResourceViewResolver @Bean public ViewResolver viewResolver1() { TilesViewResolver resolver = new TilesViewResolver(); return resolver; } @Bean public ViewResolver viewResolver2() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); resolver.setExposeContextBeansAsAttributes(true); resolver.setViewClass(org.springframework.web.servlet.view.JstlView.class); return resolver; } @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); messageSource.setBasename("messages"); return messageSource; } @Bean public MultipartResolver multipartResolver() throws IOException { CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); multipartResolver.setUploadTempDir(new FileSystemResource("/WEB-INF/tmp/spittr/uploads")); multipartResolver.setMaxUploadSize(2097152); multipartResolver.setMaxInMemorySize(0); return multipartResolver; } } 【3.3】傳送訊息到客戶端 1)intro:spring提供了兩種 傳送資料到 client 的方法: method1)作為處理訊息 或處理訂閱的附帶結果; method2)使用訊息模板; 【3.3.1】在處理訊息後,傳送訊息(server 對 client 請求的 響應訊息) 1)intro:如果你想要在接收訊息的時候,在響應中傳送一條訊息,修改方法簽名 不是void 型別即可, 如下: @MessageMapping("/hello") @SendTo("/topic/greetings") //highlight line. public Greeting greeting(HelloMessage message) throws Exception { System.out.println("receiving " + message.getName()); System.out.println("connecting successfully."); return new Greeting("Hello, " + message.getName() + "!"); }
對以上程式碼的分析(Analysis):返回的物件將會進行轉換(通過訊息轉換器) 並放到 STOMP 幀的負載中,然後傳送給訊息代理(訊息代理分為 STOMP代理中繼 和 記憶體訊息代理);
2)預設情況下:幀所發往的目的地會與 觸發 處理器方法的目的地相同。所以返回的物件 會寫入到 STOMP 幀的負載中,併發布到 "/topic/stomp" 目的地。不過,可以通過 @SendTo 註解,過載目的地;(乾貨——註解 @SendTo 註解的作用)
程式碼同上。
對以上程式碼的分析(Analysis):訊息將會發布到 /topic/hello, 所有訂閱這個主題的應用都會收到這條訊息;
3)@SubscriptionMapping 註解標註的方式也能傳送一條訊息,作為訂閱的迴應。
3.1)看個荔枝: 通過為 控制器新增如下的方法,當客戶端訂閱的時候,將會發送一條 shout 資訊:
@SubscribeMapping("/macro") // defined in Controller. attention for addr '/macro' in server.
public Greeting handleSubscription() {
System.out.println("this is the @SubscribeMapping('/marco')");
Greeting greeting = new Greeting("i am a msg from SubscribeMapping('/macro').");
return greeting;
}
function connect() {
var socket = new SockJS("<c:url value='/hello'/>");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function(greeting){
showGreeting(JSON.parse(greeting.body).content);
});
// starting line.
stompClient.subscribe('/app/macro',function(greeting){
alert(JSON.parse(greeting.body).content);
showGreeting(JSON.parse(greeting.body).content);
}); // ending line. attention for addr '/app/macro' in client.
});
}
對以上程式碼的分析(Analysis):
A0)這個SubscribeMapping annotation標記的方法,是在訂閱的時候呼叫的,也就是說,基本是隻執行一次的方法,client 呼叫定義在server 的 該 Annotation 標註的方法,它就會返回結果,不過經過代理。
A1)這裡的 @SubscribeMapping 註解表明當 客戶端訂閱 "/app/macro" 主題的時候("/app"是應用目的地的字首,注意,這裡沒有加springmvc 專案名稱字首), 將會呼叫 handleSubscription 方法。它所返回的shout 物件 將會進行轉換 併發送回client;
A2)SubscribeMapping註解的區別在於: 這裡的 Shout 訊息將會直接傳送給 client,不用經過 訊息代理;但,如果為方法新增 @SendTo 註解的話,那麼 訊息將會發送到指定的目的地,這樣就會經過代理;(乾貨——SubscribeMapping註解返回的訊息直接傳送到 client,不經過代理,而 @SendTo 註解的路徑,就會經過代理,然後再發送到 目的地)
【3.3.2】 在應用的任意地方傳送訊息
1)intro:spring 的 SimpMessagingTemplate 能夠在應用的任何地方傳送訊息,不必以接收一條訊息為 前提;
2)看個荔枝: 讓首頁訂閱一個 STOMP主題,在 Spittle 建立的時候,該主題能夠收到 Spittle 更新時的 feed;
2.1)JavaScript 程式碼:
<script>
var sock = new SockJS('spittr');
var stomp = Stomp.over(sock);
stomp.connect('guest', 'guest', function(frame) {
console.log('Connected');
stomp.subscribe("/topic/spittlefeed", handleSpittle); // highlight.
});
function handleSpittle(incoming) {
var spittle = JSON.parse(incoming.body);
console.log('Received: ', spittle);
var source = $("#spittle-template").html();
var template = Handlebars.compile(source);
var spittleHtml = template(spittle);
$('.spittleList').prepend(spittleHtml);
}
</script>
對以上程式碼的分析(Analysis): 在連線到 STMOP 代理後,我們訂閱了 "/topic/spittlefeed" 主題,並指定當訊息到達的是,由 handleSpittle()函式來處理 Spittle 更新。
2.2) server 端程式碼:使用 SimpMessagingTemplate 將所有新建立的 Spittle 以訊息的形式釋出到 "/topic/feed" 主題上;
@Service
public class SpittleFeedServiceImpl implements SpittleFeedService {
private SimpMessageSendingOperations messaging;
@Autowired
public SpittleFeedServiceImpl(
SimpMessageSendingOperations messaging) { // 注入訊息模板.
this.messaging = messaging;
}
public void broadcastSpittle(Spittle spittle) {
messaging.convertAndSend("/topic/spittlefeed", spittle); // 傳送訊息.
}
}
對以上程式碼的分析(Analysis):
A1)配置 spring 支援 stomp 的一個附帶功能是 在spring應用上下文中已經包含了 Simple
A2)在釋出訊息給 STOMP 主題的時候,所有訂閱該主題的客戶端都會收到訊息。但有的時候,我們希望將訊息傳送給指定使用者;
【4】 為目標使用者傳送訊息
1)intro:在使用 srping 和 STOMP 訊息功能的時候,有三種方式來利用認證使用者:
way1)@MessageMapping and @SubscribeMapping 註解標註的方法 能夠使用 Principal 來獲取認證使用者;
way2)@MessageMapping, @SubscribeMapping, and @MessageException 方法返回的值能夠以 訊息的形式傳送給 認證使用者;
way3)SimpMessagingTemplate 能夠傳送訊息給特定使用者;
【4.1】在控制器中處理使用者的 訊息
1)看個荔枝: 編寫一個控制器方法,根據傳入的訊息建立新的Spittle 物件,併發送一個迴應,表明 物件建立成功;(這種 REST也可以實現,不過它是同步的,而這裡是非同步的);
1.1)程式碼如下:它會處理傳入的訊息並將其儲存我 Spittle:
@MessageMapping("/spittle") @SendToUser("/queue/notifications") public Notification handleSpittle( Principal principal, SpittleForm form) { Spittle spittle = new Spittle( principal.getName(), form.getText(), new Date()); spittleRepo.save(spittle); return new Notification("Saved Spittle"); }
1.2)該方法最後返回一個 新的 Notificatino,表明物件儲存成功;
1.3)該方法使用了 @MessageMapping("/spittle") 註解,所以當有發往 "/app/spittle" 目的地的訊息 到達時,該方法就會觸發;如果使用者已經認證的話,將會根據 STOMP 幀上的頭資訊得到 Principal 物件;
1.4)@SendToUser註解: 指定了 Notification 要傳送的 目的地 "/queue/notifications";
1.5)表明上, "/queue/notifications" 並不會與 特定使用者相關聯,但因為 這裡使用的是 @SendToUser註解, 而不是 @SendTo,所以 就會發生更多的事情了;
2)看一下針對 控制器方法釋出的 Notificatino 物件的目的地,客戶端該如何進行訂閱。
2.1)看個荔枝:考慮如下的 JavaScript程式碼,它訂閱了一個 使用者特定的 目的地:
stomp.subscribe("/user/queue/notifications", handleNotifications);
對以上程式碼的分析(Analysis):這個目的地使用了 "/user" 作為字首,在內部,以"/user" 為字首的訊息將會通過 UserDestinationMessageHandler 進行處理,而不是 AnnotationMethodMessageHandler 或 SimpleBrokerMessageHandler or StompBrokerRelayMessageHandler,如下圖所示:
Attention)UserDestinationMessageHandler 的主要任務: 是 將使用者訊息重新路由到 某個使用者獨有的目的地上。 在處理訂閱的時候,它會將目標地址中的 "/user" 字首去掉,並基於使用者 的會話新增一個字尾。如,對 "/user/queue/notifications" 的訂閱最後可能路由到 名為 "/queue/notifacations-user65a4sdfa" 目的地上;
【4.2】為指定使用者傳送訊息
1)intro:SimpMessagingTemplate還提供了 convertAndSendToUser() 方法,該方法能夠讓 我們給特定使用者傳送訊息;
2)我們在 web 應用上新增一個特性: 當其他使用者提交的 Spittle 提到某個使用者時,將會提醒該使用者(乾貨——這難道不是 微博的 @ 功能嗎)
2.1)看個荔枝:如果Spittle 文字中包含 "@tangrong",那麼我們就應該傳送一條訊息給 使用 tangrong 使用者名稱登入的client,程式碼例項如下:
@Service
public class SpittleFeedServiceImpl implements SpittleFeedService {
private SimpMessagingTemplate messaging;
// 實現使用者提及功能的正則表示式
private Pattern pattern = Pattern.compile("\\@(\\S+)");
@Autowired
public SpittleFeedServiceImpl(SimpMessagingTemplate messaging) {
this.messaging = messaging;
}
public void broadcastSpittle(Spittle spittle) {
messaging.convertAndSend("/topic/spittlefeed", spittle);
Matcher matcher = pattern.matcher(spittle.getMessage());
if (matcher.find()) {
String username = matcher.group(1);
// 傳送提醒給使用者.
messaging.convertAndSendToUser(
username, "/queue/notifications",
new Notification("You just got mentioned!"));
}
}
}
【5】處理訊息異常
1)intro:我們也可以在 控制器方法上新增 @MessageExceptionHandler 註解,讓它來處理 @MessageMapping 方法所丟擲的異常;
2)看個荔枝:它會處理 訊息方法所丟擲的異常;
@MessageExceptionHandler
public void handleExceptions(Throwable t) {
logger.error("Error handling message: " + t.getMessage());
}
3)我們也可以以 引數的形式宣告它所能處理的異常;
@MessageExceptionHandler(SpittleException.class) // highlight line.
public void handleExceptions(Throwable t) {
logger.error("Error handling message: " + t.getMessage());
}
// 或者:
@MessageExceptionHandler( {SpittleException.class, DatabaseException.class}) // highlight line.
public void handleExceptions(Throwable t) {
logger.error("Error handling message: " + t.getMessage());
}
4)該方法還可以迴應一個錯誤:
@MessageExceptionHandler(SpittleException.class)
@SendToUser("/queue/errors")
public SpittleException handleExceptions(SpittleException e) {
logger.error("Error handling message: " + e.getMessage());
return e;
}
// 如果丟擲 SpittleException 的話,將會記錄這個異常,並將其返回.
// 而 UserDestinationMessageHandler 會重新路由這個訊息到特定使用者所對應的 唯一路徑;