【SOLUTION】Spring Boot 集成 WebSocket

1. Maven 依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2. 注册ServerEndpointExporter、WebSocketConfigProperties

WebSocketConfigProperties:

  1. 建议代码中的第三方插件配置属性都通过@ConfigurationProperties注解类实现配置属性的定义,方便Spring管理和维护
  2. 建议第三方插件资源都设置一个enabel属性控制资源的加载,可以节省内存开销和代码安全
java 复制代码
package xx.xx.xx.xx.xx;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties("spring.socket")
public class WebSocketConfigProperties {

    //是否开启WebSocket
    private boolean enabel;
}

ServerEndpointExporter:

  1. WebSocket 需要创建一个ServerEndpointExporter的实例,ServerEndpointExporter负责注册ServerEndpoint和管理ServerContainer
  2. 配置类继承BeanFactoryAware 是因为WebSocket在每次连接时都会重新创建ServerEndpoint类,所以导致在ServerEndpoint类中无法通过@Autowrite自动注入IOC中的Bean,需要借助BeanFactory来获取对应的类
java 复制代码
package xx.xx.xx.xx.xx;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@ConditionalOnProperty(prefix = "spring.socket", name = "enabel", havingValue = "true")
@Configuration
@EnableConfigurationProperties(WebSocketConfigProperties.class)
public class WebSocketAutoConfig implements BeanFactoryAware {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        SocketBaseService.setBeanFactory(beanFactory);
    }
}

配置SocketBaseService

这里配置SocketBaseService类的原因有以下几点:

  1. 每个Socket端点都需要创建一个ServerEndpoint类,所以在代码上可以节省时间成本且规范ServerEndpoint
  2. ServerEndpoint类有4个声明周期@OnOpen@OnMessage@OnError@OnClose,而每次连接在四个周期方法内都需要统一处理一些逻辑,例如在@OnOpen时需要储存Session,在@OnClose时需要删除Session@OnError时需要记录错误日志等;所以如果不写SocketBaseService类,则需要在每个ServerEndpoint中写重复的逻辑,导致代码可读性低,维护难等问题
  3. 子级只需要通过继承SocketBaseService 抽象类,实现内部4个生命周期的抽象方法,即可实现对应的业务逻辑,由于子级的4个生命周期方法是父级抽象出来的,所以子级方法上就无需在标注@OnOpen@OnMessage@OnError@OnClose注解
java 复制代码
package xx.xx.xx.xx.xx;

import cn.hutool.core.util.*;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;

import javax.websocket.*;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;


public abstract class SocketBaseService {


    protected String randomId;

    protected Session session;

    protected static BeanFactory beanFactory;

    /**
     * 会话池
     */
    private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>();

    /**
     * 解析自动装配属性
     */
    private void processAutowriteFields() {
        Field[] fields = this.getClass().getDeclaredFields();
        Arrays.stream(fields).forEach(field -> {
            Autowired annotation = field.getAnnotation(Autowired.class);
            if (ObjectUtil.isEmpty(annotation)) {
                return;
            }
            Object bean = SocketBaseService.beanFactory.getBean(field.getType());
            if (ObjectUtil.isNotEmpty(bean)) {
                try {
                    field.setAccessible(true);
                    field.set(this, bean);
                } catch (IllegalAccessException e) {
                }
            }
        });
    }


    @OnOpen
    public void onConnect(Session session) {
        this.processAutowriteFields();

        this.randomId = IdUtil.nanoId();
        this.session = session;
        sessionPool.put(randomId, session);
        this.afterConnect(session);
    }

    /**
     * 连接回调
     *
     * @param session
     */
    public abstract void afterConnect(Session session);

    /**
     * 消息
     *
     * @param message
     */
    @OnMessage
    public void onMessage(String message) {
        this.afterMessage(message);
    }

    /**
     * 接受消息回调
     *
     * @param message
     */
    public abstract void afterMessage(String message);

    /**
     * 连接错误
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        this.afterError(error);
    }

    /**
     * 连接错误回调
     *
     * @param error
     */
    public abstract void afterError(Throwable error);

    /**
     * 关闭连接
     */
    @OnClose
    public void onClose() {
        this.beforeClose();
        sessionPool.remove(randomId);
    }

    /**
     * 关联连接回调
     */
    public abstract void beforeClose();
}

案例:文件上传进度监听

在面对大文件上传时通常遇到无法即时获取上传进度的问题,在没有WebSocket之前都是异步上传,然后通过轮询的方式实时获取上传进度

而在有了WebSocket后就更方便了,可以实现持续连接,实现实时获取上传进度,步骤如下:

第一步:异步上传

由于userFileService.asynceUpload是异步方法,所以这里不会等待userFileService.asynceUpload方法执行,而是立即返回任务id

Controller
java 复制代码
    @PostMapping("/asynceUpload")
    @Transactional
    public Object asynceUpload(FileInfo info) {
    	//获取一个异步上传任务id,方便后续跟踪文件上传进度
        String taskId = userFileService.getAsynceUploadTaskId(info);
        userFileService.asynceUpload(info, taskId);
        return taskId;
    }
Service

Spring如果想实现异步方法,需要在方法上添加@Async注解,并且在启动类或配置类上添加@EnabelAsync注解开启异步任务

java 复制代码
@Async
@Override
public String asynceUpload(FileInfo info, String taskId) {
//处理文件
//更新进度
}

第二步:监听上传进度

监听步骤:

  1. 继承上文的SocketBaseService 抽象类,实现4个生命周期抽象方法

  2. afterConnect方法中注册定时任务,实现定时获取任务进度

  3. afterMessage中接受接口传的任务id

  4. beforeClose中关闭定时任务

  5. exce方法中编写获取任务进度的逻辑,并通过Session返回获取到的进度信息
    注意:

  6. 子类中的teamDataService属性通过@Autowite注入成功是因为,在SocketBaseService onConnect方法中执行了processAutowriteFields方法,而processAutowriteFields通过获取当前类中标注了@Autowite注解的字段,再根据字段的类型通过BeanFactoryIOC获取对应的实例的方式注册

java 复制代码
package xx.xx.xx.xx.xx.xx;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.cron.CronUtil;
import cn.hutool.cron.task.Task;
import cn.hutool.json.JSONUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xx.common.web.socket.SocketBaseService;
import xx.xx.client.server.domain.TeamData;
import xx.xx.client.server.service.TeamDataService;

import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
@ServerEndpoint("/socket/getSyncFileTask")
public class UserFileSocketController extends SocketBaseService {

    private String scheduleId;

    private List<String> tasks = new ArrayList<>();

    @Autowired
    private TeamDataService teamDataService;

    @Override
    public void afterConnect(Session session) {

        scheduleId = CronUtil.schedule("*/2 * * * * *", new Task() {
            @Override
            public void execute() {
                exce();
            }
        });
    }

    @Override
    public void afterMessage(String message) {
        this.tasks = JSONUtil.toList(message, String.class);
    }

    @Override
    public void afterError(Throwable error) {

    }

    @Override
    public void beforeClose() {
        if (ObjectUtil.isNotEmpty(scheduleId)) {
            CronUtil.remove(scheduleId);
        }
    }

    private void exce() {
        if (session.isOpen()) {
        	//获取任务进度
            List<TeamData> taskList = teamDataService.findByIds(tasks);
            //返回进度信息
            session.getAsyncRemote().sendText(JSONUtil.toJsonStr(taskList.stream().filter(el -> StrUtil.startWith(el.getKey(), "SYNCE_UPLOAD_")).collect(Collectors.toList())));
        }
    }
}
相关推荐
why1516 小时前
腾讯(QQ浏览器)后端开发
开发语言·后端·golang
浪裡遊7 小时前
跨域问题(Cross-Origin Problem)
linux·前端·vue.js·后端·https·sprint
声声codeGrandMaster7 小时前
django之优化分页功能(利用参数共存及封装来实现)
数据库·后端·python·django
呼Lu噜7 小时前
WPF-遵循MVVM框架创建图表的显示【保姆级】
前端·后端·wpf
bing_1587 小时前
为什么选择 Spring Boot? 它是如何简化单个微服务的创建、配置和部署的?
spring boot·后端·微服务
学c真好玩7 小时前
Django创建的应用目录详细解释以及如何操作数据库自动创建表
后端·python·django
Asthenia04127 小时前
GenericObjectPool——重用你的对象
后端
Piper蛋窝8 小时前
Go 1.18 相比 Go 1.17 有哪些值得注意的改动?
后端
excel8 小时前
招幕技术人员
前端·javascript·后端
盖世英雄酱581368 小时前
什么是MCP
后端·程序员