在搭建Websocket服務時,Spring的官方文檔讓人感受凌亂,Google了一把,找到了一篇不錯的教程,翻譯一下供你們參考。javascript
原文地址:http://g00glen00b.be/spring-angular-sockjs/css
------------------------------------------------原文------------------------------------------------html
剛纔我寫了一篇關於如何使用Spring、AngularJS和Websockets搭建一個Web應用的教程。然而,那篇教程僅僅使用了Websockets可以作的一小部分,所以在這篇教程裏我將解釋怎樣使用相同的框架:Spring,AngularJS,Stomp.js以及SockJS來編寫一個聊天應用。整個應用將會使用JavaConfig編寫,甚至web.xml(我仍將在先前的教程中保留)也會被WebAppInitializer替代。前端
咱們將要編寫的應用看上去會像這樣:java
曾經,某人決定寫一個郵件列表應用。一開始,他編寫了一個每分鐘檢查是否有新郵件的客戶端。大多數狀況下是沒有新郵件的,但客戶端還老是發送新請求,致使服務器巨大的負擔。這個技術很流行,被稱爲輪詢(polling)。git
過了一下子,他們使用了一項新技術,客戶端檢查是否有新郵件,服務器一有新郵件就返回響應。這項技術比輪詢好一點,但你仍然須要發送請求,致使許多沒必要要的(阻塞)傳輸,咱們稱之爲長輪詢(long polling)。github
當你開始想,你能得出的惟一結論就是服務器應該一有郵件就向客戶端發送消息。客戶端不該當初始化請求,但服務器須要作。好久以來不可能這麼作,但Websockets引入以後成爲了可能。web
Websocket是一個協議以及Javascript API,該協議是很底層的、全雙工協議,意味着消息可以同時雙向發送。這使得服務器發送數據至客戶端,而不是反過來,成爲可能。輪詢和長輪詢不再須要了,它們在之前快樂地生活着。spring
由於Websockets提供了雙向通訊的方式,它一般用於實時應用。好比,某人打開了你的應用並修改了一些數據,你可以使用Websocket直接更新可視化的數據來通知全部用戶。json
這裏你將須要幾個庫,主要是用於創建Web應用的Spring Web MVC框架以及用於創建Websocket部分應用的Spring messaging + Websocket。咱們也須要一個像Jackson同樣的JSON序列化器,由於Stomp須要JSON序列化/反序列化,所以我也將會把那些加入到咱們的應用中。
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.1.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>4.1.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> <version>4.1.1.RELEASE</version> </dependency> <dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.jaxrs</groupId> <artifactId>jackson-jaxrs-json-provider</artifactId> <version>2.3.3</version> </dependency>
在前端,我也將須要一些庫,我將使用Bower創建。若是你不打算使用Bower,你總能夠本身下載下來。
{ "name": "spring-ng-chat", "version": "0.0.1-SNAPSHOT", "dependencies": { "sockjs": "0.3.4", "stomp-websocket": "2.3.4", "angular": "1.3.8", "lodash": "2.4.1" } }
我將使用的庫是:SockJS+Stomp.js用於經過Websocket通訊,AngularJS將用於創建客戶端的應用,Lo-Dash是將要使用的工具庫(Underscore.js的一個分支)
什麼是STOMP?就像我以前所說的,Websocket協議是個漂亮的底層協議,然而,一些高層協議能夠在Websocket的上層,如MQTT和STOMP。好比STOMP爲Websocket添加了另外的可能性,好比對於主題的發佈和訂閱。
與使用XML配置咱們的應用相反,我將向你展現如何配置相同的應用而不須要任何XML。咱們須要的第一個類是web.xml的替代品,用於啓動咱們的web應用。在這個類中咱們能夠定義咱們的應用上下文,咱們的web應用上下文和一些與servlet相關的配置。
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected void customizeRegistration(ServletRegistration.Dynamic registration) { registration.setInitParameter("dispatchOptionsRequest", "true"); registration.setAsyncSupported(true); } @Override protected Class< ?>[] getRootConfigClasses() { return new Class< ?>[] { AppConfig.class, WebSocketConfig.class }; } @Override protected Class< ?>[] getServletConfigClasses() { return new Class< ?>[] { WebConfig.class }; } @Override protected String[] getServletMappings() { return new String[] { "/" }; } @Override protected Filter[] getServletFilters() { CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); characterEncodingFilter.setEncoding(StandardCharsets.UTF_8.name()); return new Filter[] { characterEncodingFilter }; } }
這個類的大部分都很清楚。首先咱們用getRootConfigClasses和getServletConfigClasses()來定義咱們的bean配置類。getServletMappings()和getServletFilters()跟servlet配置相關。在這裏我將應用映射到上下文root而且添加了一個Filter來確保全部數據都是UTF-8的。
而後來到這裏最後的方法customizeRegistrion。若是你在Tomcat容器中運行應用的話,這可能會很重要。它表示容許異步通訊來防止鏈接不用被直接關閉。
就像你可能注意到的,獲得三個類沒法找到的編譯錯誤。我就如今定義那些類,所以讓咱們從AppConfig開始:
@Configuration @ComponentScan(basePackages = "be.g00glen00b", excludeFilters = { @ComponentScan.Filter(value = Controller.class, type = FilterType.ANNOTATION), @ComponentScan.Filter(value = Configuration.class, type = FilterType.ANNOTATION) }) public class AppConfig { }
這裏很空也很沒用,它表示掃描哪些包,但排除全部的配置和控制器類(配置類被咱們的WebAppInitializer啓動,而控制器類綁定在咱們的WebConfig類上)。既然咱們只須要一個控制器,這個類將不作特別的事情,但若是你有特殊的服務,若是它們正確註解的話將成爲Spring的bean。
下一個類是WebConfig:
@Configuration @EnableWebMvc @ComponentScan(basePackages = "be.g00glen00b.controller") public class WebConfig extends WebMvcConfigurerAdapter { @Bean public InternalResourceViewResolver getInternalResourceViewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/views/"); resolver.setSuffix(".jsp"); return resolver; } @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } @Bean public WebContentInterceptor webContentInterceptor() { WebContentInterceptor interceptor = new WebContentInterceptor(); interceptor.setCacheSeconds(0); interceptor.setUseExpiresHeader(true); interceptor.setUseCacheControlHeader(true); interceptor.setUseCacheControlNoStore(true); return interceptor; } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/libs/**").addResourceLocations("/libs/"); registry.addResourceHandler("/app/**").addResourceLocations("/app/"); registry.addResourceHandler("/assets/**").addResourceLocations("/assets/"); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(webContentInterceptor()); } }
這個配置類啓動咱們的web上下文。它告訴咱們哪些靜態資源能被服務(使用addResourceHandlers)。它添加無緩衝的攔截器(webContentInterceptor()和addInterceptors())並經過getInternalResourceViewResolver() bean告訴咱們動態資源的路徑(JSP文件)。
最後是Websocket的配置:
@Configuration @EnableWebSocketMessageBroker @ComponentScan(basePackages = "be.g00glen00b.controller") public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/chat").withSockJS(); } }
就像WebConfig同樣,它也會掃描控制器包,由於咱們將咱們的Websocket通訊映射到咱們的控制器。而後咱們將使用configureMessageBroker來配置消息經紀人(通訊進入和離開的地方),而且使用registerStompEndpoints來配置咱們的節點。
WebSocket尚未在全部的瀏覽器上都能工做起來。許多WebSocket庫(好比SockJS和Socket.io)提供了使用長輪詢和輪詢等的回退選項。Spring也容許這些回退,而且與SockJS兼容。這也是爲何選擇SockJS做爲客戶端是一個好主意的緣由。
咱們主要的通訊會經過WebSocket。爲了通訊,咱們會發送一個特定的載荷並相應到一個指定的Stomp.js主體。咱們須要兩個類,Message和OutputMessage。
首先,Message會包含聊天消息自身以及一個產生的ID,好比:
public class Message { private String message; private int id; public Message() { } public Message(int id, String message) { this.id = id; this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public int getId() { return id; } public void setId(int id) { this.id = id; } } OutputMessage將擴展Message,但也會加入一個時戳(當前日期): public class OutputMessage extends Message { private Date time; public OutputMessage(Message original, Date time) { super(original.getId(), original.getMessage()); this.time = time; } public Date getTime() { return time; } public void setTime(Date time) { this.time = time; } }
咱們應用的Java部分的最後一步是帶兩個映射的控制器本身;一個用於包含咱們應用的HTML/JSP頁面,另外一個用於WebSocket傳輸:
@Controller @RequestMapping("/") public class ChatController { @RequestMapping(method = RequestMethod.GET) public String viewApplication() { return "index"; } @MessageMapping("/chat") @SendTo("/topic/message") public OutputMessage sendMessage(Message message) { return new OutputMessage(message, new Date()); } }
這裏很容易,當咱們運行到root上下文時,viewApplication被映射到那裏,從而index.jsp做爲視圖。另外一個方法,sendMessage容許咱們在一個消息進入消息經紀人 /app/chat時廣播一個消息到/topic/message(不要忘記咱們在WebSocketConfig定義了前綴/app)。
如今整個Java代碼已經編寫完畢,讓咱們經過定義JSP頁面開始。這個頁面將包含兩個主要的組件;添加新消息的表單,以及消息列表自身。
<!DOCTYPE HTML> <html> <head> <link href="http://fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" /> <link href="assets/style.css" rel="stylesheet" type="text/css" /> </head> <body ng-app="chatApp"> <div ng-controller="ChatCtrl"> <form ng-submit="addMessage()" name="messageForm"> <input type="text" placeholder="Compose a new message..." ng-model="message" /> <div> <span ng-bind="max - message.length" ng-class="{danger: message.length > max}">140</span> <button ng-disabled="message.length > max || message.length === 0">Send</button> </div> </form> <hr /> <p ng-repeat="message in messages | orderBy:'time':true"> <time>{{message.time | date:'HH:mm'}}</time> <span ng-class="{self: message.self}">{{message.message}}</span> </p> </div> <script src="libs/sockjs/sockjs.min.js" type="text/javascript"></script> <script src="libs/stomp-websocket/lib/stomp.min.js" type="text/javascript"></script> <script src="libs/angular/angular.min.js"></script> <script src="libs/lodash/dist/lodash.min.js"></script> <script src="app/app.js" type="text/javascript"></script> <script src="app/controllers.js" type="text/javascript"></script> <script src="app/services.js" type="text/javascript"></script> </body> </html>
首先咱們添加了Open Sans字體以及咱們本身樣式(咱們將在這個教程後面定義)。而後咱們開始body和啓動咱們稱爲chatApp的AngularJS應用。在這個應用裏將有一個AngularJS控制器,ChatCtrl。不要將這個與咱們的Spring控制器混淆!
咱們要作的第一件事是建立一個包含文本域的表單。咱們將這個文本域綁定在一個稱爲message的model上。當咱們的表單提交時,咱們控制器的addMessage()函數將被調用,用於經過WebSocket發送消息。
爲了將表單變得奇特一點,咱們也添加了跟Twitter運行相似的計數器。當你鍵入太多字符(超過最大值)時,因爲ng-deisabled指令它將變紅而且你不能提交表單。
在表單之下,咱們依次處理消息,而且對於每一個消息打印時間和消息內容。若是消息是經過用戶本身發送的,經過ng-class指令,它將會有一個本身特別的分類。消息經過日期排序,最近的排在列表的最前面。
在咱們頁面的最後咱們載入全部的須要的庫,以及咱們應用的Javascript文件。(譯者注:就像教程以前所說的,做者使用bower來管理工程裏用到的Javascript庫文件,若是讀者若是不使用bower,能夠本身下載,或者使用免費的CDN,好比https://cdnjs.com/)
Our first JavaScript file is app.js. This file will define all module packages, in this case:
咱們的第一個Javascritp文件是app.js。這個文件將定義全部模塊包,以下:
angular.module("chatApp", [ "chatApp.controllers", "chatApp.services" ]); angular.module("chatApp.controllers", []); angular.module("chatApp.services", []);
AngularJS控制器也很簡單,由於它將一切都傳遞給咱們在教程後面要編寫的service裏。控制器包含三個跟model關聯的域——包含文本框內鍵入信息的message,包含全部接收到消息的messages數組以及用於跟Twitter外觀相似計數器的最大容許字符數量max。
angular.module("chatApp.controllers").controller("ChatCtrl", function($scope, ChatService) { $scope.messages = []; $scope.message = ""; $scope.max = 140; $scope.addMessage = function() { ChatService.send($scope.message); $scope.message = ""; }; ChatService.receive().then(null, null, function(message) { $scope.messages.push(message); }); });
咱們已經說明了當表單提交時,addMessage被調用,將消息傳遞給service,而後經過重置message model爲空字符串來清空文本域。
咱們也調用service來接收消息。這部分的服務將返回一個每次收到消息時更新進展部分指令的deferred。控制器會將其加入messages數組做爲迴應。
咱們基於AngularJS的客戶端應用的最後一部分是service。該service更復雜一點,由於他包含了全部的WebSocket傳輸處理代碼。service的代碼以下:
angular.module("chatApp.services").service("ChatService", function($q, $timeout) { var service = {}, listener = $q.defer(), socket = { client: null, stomp: null }, messageIds = []; service.RECONNECT_TIMEOUT = 30000; service.SOCKET_URL = "/spring-ng-chat/chat"; service.CHAT_TOPIC = "/topic/message"; service.CHAT_BROKER = "/app/chat"; service.receive = function() { return listener.promise; }; service.send = function(message) { var id = Math.floor(Math.random() * 1000000); socket.stomp.send(service.CHAT_BROKER, { priority: 9 }, JSON.stringify({ message: message, id: id })); messageIds.push(id); }; var reconnect = function() { $timeout(function() { initialize(); }, this.RECONNECT_TIMEOUT); }; var getMessage = function(data) { var message = JSON.parse(data), out = {}; out.message = message.message; out.time = new Date(message.time); if (_.contains(messageIds, message.id)) { out.self = true; messageIds = _.remove(messageIds, message.id); } return out; }; var startListener = function() { socket.stomp.subscribe(service.CHAT_TOPIC, function(data) { listener.notify(getMessage(data.body)); }); }; var initialize = function() { socket.client = new SockJS(service.SOCKET_URL); socket.stomp = Stomp.over(socket.client); socket.stomp.connect({}, startListener); socket.stomp.onclose = reconnect; }; initialize(); return service; });
讓咱們從最底部開始。在代碼的底部你能夠看到咱們執行了initialize函數用於創建service。這隻會執行一次,由於AngularJS服務是單例的,意味着每次都會返回同一個實例。
initialize()函數會創建SockJS Websocket客戶端而且將其用於Stomp.js的websocket客戶端。Stomp.js是Websocket協議的附件,用於容許對於主題的訂閱和通知以及JSON載荷。
當客戶端鏈接到WebSocket服務器時,startListener()函數被調用,它將監聽到全部/topic/message主題的消息接收。隨後它將數據發送到會被控制器使用的deferred。
startListener函數調用getMessage函數來將Websocket數據體(=載荷)翻譯成控制器須要的model。在這裏它將JSON字符串解析爲一個對象,並將時間設置爲一個Date對象。
若是消息ID在messageIds數組內列出,這意味着這個消息源自這個客戶端,所以它講self屬性設置爲true。
隨後它將消息ID從列表中移除使ID在消息ID池中可用。
當與服務器的Websocket斷開時,它將在30秒後調用reconnect()函數來嘗試從新初始化鏈接。
最後,咱們有兩個公共的service函數,receive()和send()。然咱們開始編寫receive()函數由於這是兩個中最簡單的。這個函數作的惟一一件事是返回用於發送消息的deferred。
另外一方面send()函數將消息做爲JSON對象發送(字符串化的)而且使用一個新產生的ID。這個ID被加入messageIds數組以使其可以被getMessage()函數使用來檢查該消息是被這個客戶端仍是另外一個添加的。
以上是咱們須要全部的Java和Javascript代碼,讓咱們用一些酷酷的樣式來結束咱們的應用吧!我使用以下的CSS代碼:
body, * { font-family: 'Open Sans', sans-serif; box-sizing: border-box; } .container { max-width: 1000px; margin: 0 auto; width: 80%; } input[type=text] { width: 100%; border: solid 1px #D4D4D1; transition: .7s; font-size: 1.1em; padding: 0.3em; margin: 0.2em 0; } input[type=text]:focus { -webkit-box-shadow: 0 0 5px 0 rgba(69, 155, 231, .75); -moz-box-shadow: 0 0 5px 0 rgba(69, 155, 231, .75); box-shadow: 0 0 5px 0 rgba(69, 155, 231, .75); border-color: #459be7; outline: none; } .info { float: right; } form:after { display: block; content: ''; clear: both; } button { background: #459be7; color: #FFF; font-weight: 600; padding: .3em 1.9em; border: none; font-size: 1.2em; margin: 0; text-shadow: 0 0 5px rgba(0, 0, 0, .3); cursor: pointer; transition: .7s; } button:focus { outline: none; } button:hover { background: #1c82dd; } button:disabled { background-color: #90BFE8; cursor: not-allowed; } .count { font-weight: 300; font-size: 1.35em; color: #CCC; transition: .7s; } .count.danger { color: #a94442; font-weight: 600; } .message time { width: 80px; color: #999; display: block; float: left; } .message { margin: 0; } .message .self { font-weight: 600; } .message span { width: calc(100% - 80px); display: block; float: left; padding-left: 20px; border-left: solid 1px #F1F1F1; padding-bottom: .5em; } hr { display: block; height: 1px; border: 0; border-top: solid 1px #F1F1F1; margin: 1em 0; padding: 0; }
在web服務器上運行咱們的應用以前,先檢查一些東西。首先,確保你設置你的根上下文到/spring-ng-chat/。若是你沒有設置,你的AngularJS服務在鏈接到Websocket服務器會遇到麻煩,鏈接到/spring-ng-chat/chat也同樣。若是你不想這樣,你須要在AngularJS service內修改SOCKET_URL屬性。
第二,若是你在使用Eclipse內嵌的Tomcat運行這個應用,你須要將Maven依賴添加到你的部署集成中去。你能夠經過到你的project properties內點擊Deployment assembly並添加庫來完成。(譯者注:譯者本身是直接使用tomcat7-maven-plugin熱部署到Tomcat上的,具體方法請谷歌)
最後,確認你使用的web容器支持WebSocket Java API。若是不是這樣,你可能須要升級你的web容器。
若是以上都準備好了,你能夠啓動你的應用,看上去應該像這樣:
若是你開始寫消息,你會看到按鈕如今是可用的,而且計數器在運行:
若是你鍵入太多,你會看到如今又不可用了,而且計數器如今用紅色顯示了一個負值:
當你輸入了一條信息併發送後,你會看到它以黑體顯示在消息列表上(由於是你發送的)。你你也會看到你的當前消息在文本框內被重置爲空字符串。
若是你在新的窗口中打開應用,你應該看到它如今是空的。WebSocket是實時的,所以在給定時間收到的消息纔會被列出來,沒有歷史。
若是你在其餘窗口發消息,你會看到消息在全部屏幕中顯示。一個使用黑體,另外一個是普通字體。
能夠看到,WebSocket在正常工做,你會看到消息實時顯示,由於客戶端發送消息到服務器,而後服務器將消息發送到全部客戶端。
感謝WebSocket,使得這個服務器-客戶端消息模型成爲可能。
成就:使用Spring,AngularJS和SockJS編寫了一個聊天應用。
看到這個意味着你完成這個使用Spring,AngularJS和SockJS編寫的簡單WebSocket聊天應用。若是你對於完整代碼示例感興趣,你能夠在Github上週到。若是你想本身嘗試代碼,你能夠從Github上下載檔案包。