優勢:異步推送消息只要客戶端發送異步請求就能夠,不依賴客戶端版本,不存在瀏覽器兼容問題。 javascript
1、 主要講解技術點,異步實現服務器推送消息html
2、 項目示例,聊天會話功能,主要邏輯以下:前端
由Logan向 Charles 發送消息,若是Charles在線,則直接發送,不然存儲爲離線消息。java
Charles 登陸後向服務端發請求獲取消息,首先查詢離線消息,若是有消息直接返回。沒有消息則等待。jquery
因爲長時間沒有消息推送,等待會超時,因此設置超時異常通知,超時則返回空內容到客戶端,由客戶端再次發送獲取消息請求,解決超時問題。web
建議先複製項目到本地工程,邊測試邊理解。spring
項目示例以下:數據庫
1. 新建Maven項目 async-pushapache
2. pom.xml瀏覽器
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.java</groupId> <artifactId>async-push</artifactId> <version>1.0.0</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> </parent> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.0.0.RELEASE</version> </dependency> <!-- 熱部署 --> <dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> <version>1.2.8.RELEASE</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
3. AsyncPushStarter.java
package com.java; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * 主啓動類 * * @author Logan * @createDate 2019-02-17 * @version 1.0.0 * */ @SpringBootApplication public class AsyncPushStarter { public static void main(String[] args) { SpringApplication.run(AsyncPushStarter.class, args); } }
4. SendMessageVo.java
package com.java.vo; /** * 發送消息封裝體 * * @author Logan * @createDate 2019-02-17 * @version 1.0.0 * */ public class SendMessageVo { /** * 發送目標ID */ private String targetId; /** * 發送消息內容 */ private String content; public String getTargetId() { return targetId; } public void setTargetId(String targetId) { this.targetId = targetId; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } @Override public String toString() { return "SendMessageVo [targetId=" + targetId + ", content=" + content + "]"; } }
5. PushMessageVo.java
package com.java.vo; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; /** * 推送消息封裝體 * * @author Logan * @createDate 2019-02-17 * @version 1.0.0 * */ public class PushMessageVo { /** * 發送人ID,即消息來源 */ private String srcId; /** * 發送消息內容 */ private String content; /** * 發送時間 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date sendTime; public String getSrcId() { return srcId; } public void setSrcId(String srcId) { this.srcId = srcId; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public Date getSendTime() { return sendTime; } public void setSendTime(Date sendTime) { this.sendTime = sendTime; } @Override public String toString() { return "PushMessageVo [srcId=" + srcId + ", content=" + content + ", sendTime=" + sendTime + "]"; } }
6. MessagePool.java
package com.java.pool; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.stereotype.Component; import org.springframework.web.context.request.async.DeferredResult; import com.java.vo.PushMessageVo; /** * 消息池,存放全部消息 * * @author Logan * @createDate 2019-02-17 * @version 1.0.0 * */ @Component public class MessagePool { private Map<String, DeferredResult<List<PushMessageVo>>> messagePool = new HashMap<>(); public void put(String targetId, DeferredResult<List<PushMessageVo>> result) { messagePool.put(targetId, result); } public DeferredResult<List<PushMessageVo>> get(String targetId) { return messagePool.get(targetId); } }
7. OfflineMessagePool.java
package com.java.pool; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.stereotype.Component; import com.java.vo.PushMessageVo; /** * 離線消息池 * * @author Logan * @createDate 2019-02-17 * @version 1.0.0 * */ @Component public class OfflineMessagePool { private Map<String, List<PushMessageVo>> offlineMessagePool = new HashMap<>(); /** * 增長一條待發送消息 * * @param targetId 發送目標ID * @param message 推送消息體 */ public void add(String targetId, PushMessageVo message) { List<PushMessageVo> list = offlineMessagePool.get(targetId); if (null == list) { list = new ArrayList<>(); offlineMessagePool.put(targetId, list); } list.add(message); } /** * 獲取全部待發送消息 * * @param targetId 發送目標ID * @return 發送目標對應的全部待發送消息 */ public List<PushMessageVo> get(String targetId) { List<PushMessageVo> list = offlineMessagePool.get(targetId); // 若是存在,則移除後返回 if (null != list) { offlineMessagePool.remove(targetId); } return list; } }
8. MessageController.java
package com.java.controller; import java.security.Principal; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import com.java.pool.MessagePool; import com.java.pool.OfflineMessagePool; import com.java.vo.PushMessageVo; import com.java.vo.SendMessageVo; /** * 發送接收消息接口類 * * @author Logan * @createDate 2019-02-17 * @version 1.0.0 * */ @RestController public class MessageController { private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Autowired private MessagePool messagePool; @Autowired private OfflineMessagePool offlineMessagePool; @PostMapping("/sentMessage") public Map<String, Object> sentMessage(Principal principal, SendMessageVo sendMessage) { PushMessageVo pushMessage = new PushMessageVo(); pushMessage.setSrcId(principal.getName()); pushMessage.setContent(sendMessage.getContent()); pushMessage.setSendTime(new Date()); System.out.println(sendMessage); System.out.println(pushMessage); DeferredResult<List<PushMessageVo>> deferredResult = messagePool.get(sendMessage.getTargetId()); // 若是未上線,存到離線消息池中 if (null == deferredResult) { offlineMessagePool.add(sendMessage.getTargetId(), pushMessage); } // 直接推送消息給目標ID else { List<PushMessageVo> list = new ArrayList<>(); list.add(pushMessage); deferredResult.setResult(list); } Map<String, Object> result = new HashMap<>(); result.put("success", true); result.put("sendTime", format.format(pushMessage.getSendTime())); return result; } @GetMapping("/getMessage") public DeferredResult<List<PushMessageVo>> getMessage(Principal principal) { DeferredResult<List<PushMessageVo>> result = new DeferredResult<>(); // 先取出未推送的離線消息 List<PushMessageVo> list = offlineMessagePool.get(principal.getName()); // 若是有離線消息,直接返回 if (null != list) { result.setResult(list); } // 不然等待接收新消息 else { messagePool.put(principal.getName(), result); } return result; } }
9. ControllerExceptionHandler.java
package com.java.advice; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import com.java.vo.PushMessageVo; /** * 捕獲異步超時異常,並進行處理 * * @author Logan * @createDate 2019-02-17 * @version 1.0.0 * */ @ControllerAdvice public class ControllerExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class); @ResponseBody @ExceptionHandler(AsyncRequestTimeoutException.class) public List<PushMessageVo> handleAsyncRequestTimeoutException(AsyncRequestTimeoutException e) { logger.info("處理異步超時異常"); // 異步超時返回一個空集合,由前端繼續發請求 List<PushMessageVo> list = new ArrayList<>(); return list; } }
下面是安全登陸相關配置
10. ApplicationContextConfig.java
package com.java.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * 配置文件類 * * @author Logan * @createDate 2019-02-17 * @version 1.0.0 * */ @Configuration public class ApplicationContextConfig { /** * 配置密碼編碼器,Spring Security 5.X必須配置,不然登陸時報空指針異常 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
11. LoginConfig.java
package com.java.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; /** * 登陸相關配置 * * @author Logan * @createDate 2019-02-17 * @version 1.0.0 * */ @Configuration public class LoginConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 設置不須要受權的請求 .antMatchers("/js/*", "/login.html").permitAll() // 其它任何請求都須要驗證權限 .anyRequest().authenticated() // 設置自定義表單登陸頁面 .and().formLogin().loginPage("/login.html") // 設置登陸驗證請求地址爲自定義登陸頁配置action ("/login/form") .loginProcessingUrl("/login/form") // 設置默認登陸成功跳轉頁面 .defaultSuccessUrl("/main.html") // 暫時停用csrf,不然會影響驗證 .and().csrf().disable(); } }
12. SecurityUserDetailsService.java
package com.java.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; /** * UserDetailsService實現類 * * @author Logan * @createDate 2019-02-17 * @version 1.0.0 * */ @Component public class SecurityUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 數據庫存儲密碼爲加密後的密文(明文爲123456) String password = passwordEncoder.encode("123456"); System.out.println("username: " + username); System.out.println("password: " + password); // 模擬查詢數據庫,獲取屬於Admin和Normal角色的用戶 User user = new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("Admin,Normal")); return user; } }
13. 靜態資源文件以下
static/login.html
static/main.html
static/js/jquery-3.3.1.min.js
14. login.html
<!DOCTYPE html> <html> <head> <title>登陸</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> </head> <body> <!--登陸框--> <div align="center"> <h2>用戶自定義登陸頁面</h2> <fieldset style="width: 300px;"> <legend>登陸框</legend> <form action="/login/form" method="post"> <table> <tr> <th>用戶名:</th> <td><input name="username" value="Logan" /> </td> </tr> <tr> <th>密碼:</th> <td><input type="password" name="password" value="123456" /> </td> </tr> <tr> <th></th> <td></td> </tr> <tr> <td colspan="2" align="center"><button type="submit">登陸</button></td> </tr> </table> </form> </fieldset> </div> </body> </html>
15. main.html
<!DOCTYPE html> <html> <head> <title>首頁</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <script type="text/javascript" src="js/jquery-3.3.1.min.js"></script> <style> body, div { margin: 0; padding: 0; } </style> <script> $(function() { getMessage(); $("#content").keydown(function(event) { if(event.keyCode == 13) { sendMessage(); } }); }); function getMessage() { $.get("/getMessage", function(data) { for(var i = 0; i < data.length; i++) { var msg = data[i]; /* 設置發送目標爲消息來源的人,方便回覆消息 */ $("#targetId").val(msg.srcId); showMessage(msg.srcId, msg.sendTime, msg.content); } getMessage(); }); } function sendMessage() { var targetId = $("#targetId").val().trim(); if(!targetId) { alert("未填寫消息接收人!"); $("#targetId").focus(); return; } /*消息內容不作任何處理,只要不爲空就發送*/ var content = $("#content").html(); if(!content) { $("#content").focus(); return; } /*發送消息*/ $.post("/sentMessage", { targetId: targetId, content: content }, function(data) { if(data.success) { $("#content").empty(); showMessage("我", data.sendTime, content); } }); } function showMessage(srcId, sendTime, content) { var title = '<span style="color: green;">' + srcId + ' ' + sendTime + '</span>'; var content = '<div style="padding-left: 10px;">' + content + '</div>'; $("#showMessage").append(title).append(content).append("<br />"); /* 設置滾動條自動翻滾 */ $("#showMessage").scrollTop($("#showMessage")[0].scrollHeight); } </script> </head> <body> <div align="center"> <div style="margin: 30px 0px;"> 發送給:<input id="targetId" name="targetId" value="Charles" placeholder="消息接收人" /> </div> <!--消息框--> <div style="width: 600px;height: 500px;position: relative;"> <!--消息展現框--> <div id="showMessage" style="border: cornflowerblue solid 2px;height: 300px;text-align: left;overflow: auto;"> </div> <!--隔離條--> <div style="height: 5px; background-color: darkgray;"></div> <!--消息發送框--> <div id="content" contenteditable="true" style="border: cornflowerblue solid 2px;height: 150px;text-align: left;"> </div> <!--發送按鈕--> <div style="position: absolute;bottom: 0px; right: 10px;"> <button onclick="sendMessage()">發送</button> </div> </div> </div> </body> </html>
16. js/jquery-3.3.1.min.js 可在官網下載
https://code.jquery.com/jquery-3.3.1.min.js
http://code.jquery.com/jquery-3.3.1.min.js
17. 運行 AsyncPushStarter.java , 啓動測試
瀏覽器輸入首頁 http://localhost:8080/main.html
地址欄自動跳轉到登陸頁面,以下:
輸入以下信息:
User:Logan
Password:123456
單擊【登陸】按鈕,自動跳轉到首頁。
輸入信息,發送給 Charles
換用其它瀏覽器,輸入 http://localhost:8080/main.html
自動跳轉到登陸頁面,以下:
輸入以下信息
User:Charles
Password:123456
用戶名必定要是 Charles,不然收不到來自Logen的消息
單擊【登陸】按鈕,自動跳轉到首頁。
自動接收來自Logan的離線消息。
輸入內容回覆,在Logan登陸的瀏覽器會自動收到回覆,以下所示
雙方消息顯示內容和時間徹底一直,角色互換。
功能正常運行
.