本文整合的意义在于,后台处理websocket请求能像Controller处理请求一样简便,同样能在Controller层定义接口,返回数据,使用Spring注解对象等。
1. pom引用websocket
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. websocket配置
@Configuration
@EnableWebSocket
publicclass WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServerEndpointExporter serverEndpointExporter(ApplicationContextcontext){
return new ServerEndpointExporter();
}
//注册websocket处理类(TWSHandler),以及访问路径(/xxx/main)
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistryregistry) {
registry.addHandler(new TWSHandler(),"/xxx/main").addInterceptors(newWebSocketInterceptor()).setAllowedOrigins("*");
}
}
3. websocket消息统一处理
@Component
publicclass TWSHandler extends TextWebSocketHandler {
private final String charset ="UTF-8";
@Override
public void handleTextMessage(WebSocketSessionsession,
TextMessage message) throwsException {
// TODO Auto-generated method stub
String msg = message.getPayload();
System.out.println("handlerText===========>"+ msg);
JSONObject result = null;
Object stamp = null; //时间戳,用来标识返回结果
Pattern pattern =Pattern.compile("^\\{(\"\\w+\":\\S+,{0,1})+\\}$");
if(pattern.matcher(msg).matches()){
JSONObject json = JSONObject.fromObject(message.getPayload());
stamp =json.get("stamp");
result =WSDispatcher.dispatch(json, session);
}
String response = "";
if(result == null) response = "404";
else{
result.put("stamp",stamp);
response = String.valueOf(result);
}
session.sendMessage(newTextMessage(response.getBytes(charset)));
}
@Override
public voidafterConnectionClosed(WebSocketSession session,
CloseStatus status) throwsException {
// TODO Auto-generated method stub
super.afterConnectionClosed(session,status);
WSServer.instance().disconnect(session);
}
@Override
public voidhandleTransportError(WebSocketSession session, Throwable exception) throwsException {
super.handleTransportError(session,exception);
WSServer.instance().disconnect(session);
}
}
这里用到了两个关键类:
WSDispatcher用于请求转发,将访问路径和参数传递给WSController方法;
WSServer保存了所有websocket session信息,并且封装了相关处理方法。
需要注意的是:
前后端交互的数据格式采用json;
stamp参数有很多用途,需要前端配合,比如前端多个请求共用同一个websocket连接时(节省资源),可以标识返回结果属于哪个请求,实现类似ajax的功能;也可以用于标识广播消息的类型,前端根据不同stamp状态进行不同处理。
本项目配套的前端插件ws_client.js就实现了前端的消息统一处理。
4. WSDispatcher请求转发
@Component
publicclass WSDispatcher {
public static Map<Object, HandlerMethod>webSocketMapping = new HashMap<Object, HandlerMethod>();
public static voidinit(RequestMappingHandlerMapping requestMappingHandlerMapping){
webSocketMapping.clear();
Map<RequestMappingInfo,HandlerMethod> map = requestMappingHandlerMapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo,HandlerMethod> m : map.entrySet()) {
RequestMappingInfo info =m.getKey();
HandlerMethod method = m.getValue();
//无WSController注解的过滤掉
if(method.getBeanType().getDeclaredAnnotationsByType(WSController.class).length== 0)
continue;
Set<String> patterns =info.getPatternsCondition().getPatterns();
if(patterns.size()>0){
System.out.println(patterns.toArray()[0]);
webSocketMapping.put(patterns.toArray()[0],method);
}
}
}
/*
* 接口分发
*/
public static Object dispatch(String url,Object parameter, WebSocketSession session){
HandlerMethod method =webSocketMapping.get(url);
if(method != null){
try{
Class<?> cls= method.getMethod().getDeclaringClass();
ObjectcontrollerObj = SpringContextUtil.getBean(cls);
Object[] args = newObject[method.getMethod().getParameterCount()];
Class<?>[]argTypes = method.getMethod().getParameterTypes();
for(int i=0;i<args.length; i++){
if(argTypes[i].equals(Map.class)) args[i] = parameter;
elseif(argTypes[i].equals(WebSocketSession.class)) args[i]= session;
}
if(args.length ==0)
returnmethod.getMethod().invoke(controllerObj);
else
returnmethod.getMethod().invoke(controllerObj, args);
}catch(Exception e){
e.printStackTrace();
}
}
return null;
}
/*
* 接口分发
* 注意:json参数必须包含url和params
*/
public static JSONObject dispatch(JSONObjectjson, WebSocketSession session){
Object response =dispatch(String.valueOf(json.get("url")),json.get("params"), session);
JSONObject jsonObj = newJSONObject();
jsonObj.put("data",response);
return jsonObj;
}
}
请求转发首先需要获取所有WSController注解的接口,再调用init方法初始化,本文是在springboot启动时进行初始化。
@SpringBootApplication
@EnableAutoConfiguration
publicclass AppStarter {
@Autowired
private RequestMappingHandlerConfigrequestMappingHandlerConfig;
public static void main(String[] args) {
SpringApplication springApplication=new SpringApplication(AppStarter.class);
springApplication.run(args);
}
@PostConstruct
public void detectHandlerMethods(){
WSDispatcher.init(requestMappingHandlerConfig.requestMappingHandlerMapping());
System.out.println(WSDispatcher.webSocketMapping.size());
}
}
接下来就是构造参数,委托调用访问接口对应的方法。
注意:
参数匹配没有springmvc做的那么完美,只能匹配Map和WebSocketSession,个人觉得有点Low,有待改善。
获取WSController实例对象不能使用反射的方法,必须要用Spring容器的方法SpringContextUtil.getBean。这是因为Spring容器能实例化WSController组件内部的@Autowired对象,而反射不能。如果WSController组件内部没有Spring容器对象,两种方法都行。
5. websocket登录
为了结合用户操作,文本单独做了session注册
@Component
@WSController()
@RequestMapping("/log")
publicclass LoginController {
@RequestMapping("/login")
public String login(Map map, WebSocketSessionsession){
if(map.get("uid") == null)
return "parameter withoutuserId";
WSServer.instance().connect(String.valueOf(map.get("uid")),session);
return "login success!";
}
}
6. WSController方法
@Component
@WSController()
@RequestMapping("/msg")
publicclass MessageController {
@RequestMapping("/hello")
public String hello(Mapmap, WebSocketSession session){
System.out.println(session.getId());
System.out.println("msg-----hello---------"+ map);
return"from hello";
}
}
这个跟Controller没啥区别,返回类型任意,最终都会JSON化,不需要@ResponseBody注解。参数只能匹配Map和WebSocketSession,够用了。
7. 总结一下
上篇博客有人要源码,过了N久才看到,尴尬
这次先留个联系方式QQ:1733073247
源码上传到gitee,https://gitee.com/wangbaishi_libi/web-msg
源码项目是SpringBoot项目,将其它项目写到Redis的消息推送到前端,这部分凑合着能用吧。
本来只是想混个积分下东西,发现好多东西要整,瞬间没动力了
下篇有空把ws_client.js插件的用法说明一下(唉~,下次不知道什么时候)
懒惰是原罪,我就想静静的躺着