优点:异步推送消息只要客户端发送异步请求就可以,不依赖客户端版本,不存在浏览器兼容问题。
一、 主要讲解技术点,异步实现服务器推送消息
二、 项目示例,聊天会话功能,主要逻辑如下:
由Logan向 Charles 发送消息,如果Charles在线,则直接发送,否则存储为离线消息。
Charles 登录后向服务端发请求获取消息,首先查询离线消息,如果有消息直接返回。没有消息则等待。
由于长时间没有消息推送,等待会超时,所以设置超时异常通知,超时则返回空内容到客户端,由客户端再次发送获取消息请求,解决超时问题。
建议先复制项目到本地工程,边测试边理解。
项目示例如下:
1. 新建Maven项目 async-push
2. pom.xml
http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
com.java
async-push
1.0.0
org.springframework.boot
spring-boot-starter-parent
2.0.5.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-oauth2
2.0.0.RELEASE
org.springframework
springloaded
1.2.8.RELEASE
provided
org.springframework.boot
spring-boot-devtools
provided
${project.artifactId}
org.apache.maven.plugins
maven-compiler-plugin
1.8
1.8
UTF-8
org.springframework.boot
spring-boot-maven-plugin
repackage
3. AsyncPushStarter.java
packagecom.java;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/*** 主启动类
*
*@authorLogan
* @createDate 2019-02-17
*@version1.0.0
**/@SpringBootApplicationpublic classAsyncPushStarter {public static voidmain(String[] args) {
SpringApplication.run(AsyncPushStarter.class, args);
}
}
4. SendMessageVo.java
packagecom.java.vo;/*** 发送消息封装体
*
*@authorLogan
* @createDate 2019-02-17
*@version1.0.0
**/
public classSendMessageVo {/*** 发送目标ID*/
privateString targetId;/*** 发送消息内容*/
privateString content;publicString getTargetId() {returntargetId;
}public voidsetTargetId(String targetId) {this.targetId =targetId;
}publicString getContent() {returncontent;
}public voidsetContent(String content) {this.content =content;
}
@OverridepublicString toString() {return "SendMessageVo [targetId=" + targetId + ", content=" + content + "]";
}
}
5. PushMessageVo.java
packagecom.java.vo;importjava.util.Date;importcom.fasterxml.jackson.annotation.JsonFormat;/*** 推送消息封装体
*
*@authorLogan
* @createDate 2019-02-17
*@version1.0.0
**/
public classPushMessageVo {/*** 发送人ID,即消息来源*/
privateString srcId;/*** 发送消息内容*/
privateString content;/*** 发送时间*/@JsonFormat(pattern= "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")privateDate sendTime;publicString getSrcId() {returnsrcId;
}public voidsetSrcId(String srcId) {this.srcId =srcId;
}publicString getContent() {returncontent;
}public voidsetContent(String content) {this.content =content;
}publicDate getSendTime() {returnsendTime;
}public voidsetSendTime(Date sendTime) {this.sendTime =sendTime;
}
@OverridepublicString toString() {return "PushMessageVo [srcId=" + srcId + ", content=" + content + ", sendTime=" + sendTime + "]";
}
}
6. MessagePool.java
packagecom.java.pool;importjava.util.HashMap;importjava.util.List;importjava.util.Map;importorg.springframework.stereotype.Component;importorg.springframework.web.context.request.async.DeferredResult;importcom.java.vo.PushMessageVo;/*** 消息池,存放所有消息
*
*@authorLogan
* @createDate 2019-02-17
*@version1.0.0
**/@Componentpublic classMessagePool {private Map>> messagePool = new HashMap<>();public void put(String targetId, DeferredResult>result) {
messagePool.put(targetId, result);
}public DeferredResult>get(String targetId) {returnmessagePool.get(targetId);
}
}
7. OfflineMessagePool.java
packagecom.java.pool;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.List;importjava.util.Map;importorg.springframework.stereotype.Component;importcom.java.vo.PushMessageVo;/*** 离线消息池
*
*@authorLogan
* @createDate 2019-02-17
*@version1.0.0
**/@Componentpublic classOfflineMessagePool {private Map> offlineMessagePool = new HashMap<>();/*** 增加一条待发送消息
*
*@paramtargetId 发送目标ID
*@parammessage 推送消息体*/
public voidadd(String targetId, PushMessageVo message) {
List list =offlineMessagePool.get(targetId);if (null ==list) {
list= new ArrayList<>();
offlineMessagePool.put(targetId, list);
}
list.add(message);
}/*** 获取所有待发送消息
*
*@paramtargetId 发送目标ID
*@return发送目标对应的所有待发送消息*/
public Listget(String targetId) {
List list =offlineMessagePool.get(targetId);//如果存在,则移除后返回
if (null !=list) {
offlineMessagePool.remove(targetId);
}returnlist;
}
}
8. MessageController.java
packagecom.java.controller;importjava.security.Principal;importjava.text.SimpleDateFormat;importjava.util.ArrayList;importjava.util.Date;importjava.util.HashMap;importjava.util.List;importjava.util.Map;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RestController;importorg.springframework.web.context.request.async.DeferredResult;importcom.java.pool.MessagePool;importcom.java.pool.OfflineMessagePool;importcom.java.vo.PushMessageVo;importcom.java.vo.SendMessageVo;/*** 发送接收消息接口类
*
*@authorLogan
* @createDate 2019-02-17
*@version1.0.0
**/@RestControllerpublic classMessageController {private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@AutowiredprivateMessagePool messagePool;
@AutowiredprivateOfflineMessagePool offlineMessagePool;
@PostMapping("/sentMessage")public MapsentMessage(Principal principal, SendMessageVo sendMessage) {
PushMessageVo pushMessage= newPushMessageVo();
pushMessage.setSrcId(principal.getName());
pushMessage.setContent(sendMessage.getContent());
pushMessage.setSendTime(newDate());
System.out.println(sendMessage);
System.out.println(pushMessage);
DeferredResult> deferredResult =messagePool.get(sendMessage.getTargetId());//如果未上线,存到离线消息池中
if (null ==deferredResult) {
offlineMessagePool.add(sendMessage.getTargetId(), pushMessage);
}//直接推送消息给目标ID
else{
List list = new ArrayList<>();
list.add(pushMessage);
deferredResult.setResult(list);
}
Map result = new HashMap<>();
result.put("success", true);
result.put("sendTime", format.format(pushMessage.getSendTime()));returnresult;
}
@GetMapping("/getMessage")public DeferredResult>getMessage(Principal principal) {
DeferredResult> result = new DeferredResult<>();//先取出未推送的离线消息
List list =offlineMessagePool.get(principal.getName());//如果有离线消息,直接返回
if (null !=list) {
result.setResult(list);
}//否则等待接收新消息
else{
messagePool.put(principal.getName(), result);
}returnresult;
}
}
9. ControllerExceptionHandler.java
packagecom.java.advice;importjava.util.ArrayList;importjava.util.List;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.web.bind.annotation.ControllerAdvice;importorg.springframework.web.bind.annotation.ExceptionHandler;importorg.springframework.web.bind.annotation.ResponseBody;importorg.springframework.web.context.request.async.AsyncRequestTimeoutException;importcom.java.vo.PushMessageVo;/*** 捕获异步超时异常,并进行处理
*
*@authorLogan
* @createDate 2019-02-17
*@version1.0.0
**/@ControllerAdvicepublic classControllerExceptionHandler {private static final Logger logger = LoggerFactory.getLogger(ControllerExceptionHandler.class);
@ResponseBody
@ExceptionHandler(AsyncRequestTimeoutException.class)public ListhandleAsyncRequestTimeoutException(AsyncRequestTimeoutException e) {
logger.info("处理异步超时异常");//异步超时返回一个空集合,由前端继续发请求
List list = new ArrayList<>();returnlist;
}
}
下面是安全登录相关配置
10. ApplicationContextConfig.java
packagecom.java.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;/*** 配置文件类
*
*@authorLogan
* @createDate 2019-02-17
*@version1.0.0
**/@Configurationpublic classApplicationContextConfig {/*** 配置密码编码器,Spring Security 5.X必须配置,否则登录时报空指针异常*/@BeanpublicPasswordEncoder passwordEncoder() {return newBCryptPasswordEncoder();
}
}
11. LoginConfig.java
packagecom.java.config;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;/*** 登录相关配置
*
*@authorLogan
* @createDate 2019-02-17
*@version1.0.0
**/@Configurationpublic class LoginConfig extendsWebSecurityConfigurerAdapter {
@Overrideprotected void configure(HttpSecurity http) throwsException {
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
packagecom.java.service;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.core.authority.AuthorityUtils;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.stereotype.Component;/*** UserDetailsService实现类
*
*@authorLogan
* @createDate 2019-02-17
*@version1.0.0
**/@Componentpublic class SecurityUserDetailsService implementsUserDetailsService {
@AutowiredprivatePasswordEncoder passwordEncoder;
@Overridepublic UserDetails loadUserByUsername(String username) throwsUsernameNotFoundException {//数据库存储密码为加密后的密文(明文为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"));returnuser;
}
}
13. 静态资源文件如下
static/login.html
static/main.html
static/js/jquery-3.3.1.min.js
14. login.html
登录用户自定义登录页面
登录框
用户名: | |
---|---|
密码: | |
登录