赞
踩
正在做的项目,有一个需求。大屏展示后台数据,当时同事大佬推荐使用WebSocket来做,需求细分的话,主要有两部分,一部分是后台主动推送数据给前台,还有一部分是大屏通过点击筛选条件,实现数据的变更。
因为刚开始大佬就给了模块化的设计理念,所以后台设计的第一版本就是页面分块进行数据的推送。就有了我最初的代码实现:每个模块对应后台的一个类,同时我使用了一个Service来放所有的获取数据的代码实现。推送的数据是一个Map,key就是一个枚举,value就是数据,前端接收到这个Map之后,对数据进行渲染。
优点:脑瘫如我都能写出来,并且实现最终的功能(虽然这样会很麻烦)
缺点:
1.只有一个Service,这个类里面的代码量太大,后期维护费劲,修改费劲
2.由于给前端传的是一个Map,所以并不能限定Map的key就是对应的枚举,对于我自己来说还好,如果回头有同事加入这个项目,可能会导致,传给前端的,并不是标准的 枚举 -> 数据 的Map格式
3.正如上述的需求所说,页面还需要点击不同的筛选条件,展示的数据不同,由于筛选条件和具体改变的数据,其实并不在一个模块内,所以第一版本的设计,可能根本就实现不了这个需求(我也没想出来怎么实现)
4.对于每个模块来说,每个模块的实现类,都需要写一边WebSocket的推送数据的逻辑,很麻烦
5.整体代码的观赏性极差,也很不优雅
****(肯定还有,只不过还没发现)
偶然间大佬同事看见了我的代码,指出了我这样写的不足,同时告诉我,类也可以是key-value格式,你可以把推送数据给抽象出来,每一个模块对应一个类,对应一个service实现,使用一个公共的推送数据的接口,每一个模块的实现,只用去关心怎么取出当前模块的数据,把推送数据的方法,给公共的接口去实现,只用去暴露这个推送数据的方法,同时使用泛型来约束推送数据,直接上代码
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.cloud.top.tower.common.utils.SpringUtils; import com.cloud.top.tower.enums.LargeIdentificationEnum; import com.cloud.top.tower.model.largeScreen.ReceiveData; import com.cloud.top.tower.model.largeScreen.first.accept.ConnectionDataAccept; import com.cloud.top.tower.model.largeScreen.second.accept.MapQueryAccept; import com.cloud.top.tower.model.largeScreen.second.accept.NetConnectionSelectAccept; import com.cloud.top.tower.service.pushLogin.IPushService; import com.google.common.collect.Maps; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.yeauty.annotation.*; import org.yeauty.pojo.Session; import java.io.IOException; import java.net.SocketAddress; import java.util.List; import java.util.Map; /** * @author YanYan * @description: webSocket * @date 2022/12/30 13:59 */ @Slf4j @ServerEndpoint(path = "/ws", port = "8189") @Component public class WebSocketServer { private static Map<String, Session> AllSession = Maps.newConcurrentMap(); /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session) throws IOException { SocketAddress socketAddress = session.remoteAddress(); String clientIp = socketAddress != null ? socketAddress.toString().replace("/", "").split(":")[0] : ""; log.info("WebSocket提示信息,IP为:{}", clientIp + "连接成功"); AllSession.put(clientIp, session); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(Session session) { SocketAddress socketAddress = session.remoteAddress(); String clientIp = socketAddress != null ? socketAddress.toString().replace("/", "").split(":")[0] : ""; if (StringUtils.isNoneBlank(clientIp)) { AllSession.remove(clientIp); } log.info("WebSocket提示信息,一个连接关闭,ip为:{}", clientIp); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 * @param session 发送消息的客户端 * @return */ @OnMessage public String onMessage(String message, Session session) { if (!"HeartBeat".equals(message)) { this.handleSend(message, session); } else { sendMessage(message); } return "servet 发送:" + message; } /** * webSocket报错后执行的方法 */ @OnError public void onError(Session session, Throwable error) { log.error("Websocket出错,错误原因为:{}", error.getMessage()); } /** * 实现服务器主动推送 */ public void sendMessage(Object object) { for (String key : AllSession.keySet()) { AllSession.get(key).sendText(JSON.toJSONString(object)); } } /** * 实现服务器根据session推送数据 */ public void sendMessage(Object object, Session session) { session.sendText(JSON.toJSONString(object)); } /** * 处理前端传输的指令数据,并进行指定数据推送 */ private void handleSend(String message, Session session) { List<IPushService> pushBySendServiceList = SpringUtils.getPushServiceList(); JSONObject jsonObject = JSONObject.parseObject(message); LargeIdentificationEnum key = LargeIdentificationEnum.getByStr(jsonObject.getString("key")); if (ObjectUtils.isEmpty(key)) { log.info("WebSocket提示:前端通信消息{}", jsonObject); return; } if (key.equals(LargeIdentificationEnum.ConnectionDataAll)) { if (StringUtils.isNotEmpty(jsonObject.getString("emission")) && StringUtils.isNotEmpty(jsonObject.getString("platform"))) { ConnectionDataAccept connectionData = new ConnectionDataAccept(jsonObject.getString("platform"), jsonObject.getString("emission")); ReceiveData<ConnectionDataAccept> receiveData = new ReceiveData<>(LargeIdentificationEnum.CustomerRunDataOil, connectionData); ReceiveData<ConnectionDataAccept> receiveDataAttend = new ReceiveData<>(LargeIdentificationEnum.CustomerRunDataAttend, connectionData); ReceiveData<ConnectionDataAccept> receiveDataMileage = new ReceiveData<>(LargeIdentificationEnum.CustomerRunDataMileage, connectionData); pushBySendServiceList.forEach(v -> v.pushBySend(receiveData, session)); pushBySendServiceList.forEach(v -> v.pushBySend(receiveDataAttend, session)); pushBySendServiceList.forEach(v -> v.pushBySend(receiveDataMileage, session)); } } else if (key.equals(LargeIdentificationEnum.NetConnectionDataIsSecond)) { //第二屏-入网连接量数据 NetConnectionSelectAccept netConnectionSelectAccept = new NetConnectionSelectAccept(); netConnectionSelectAccept.setPlatformId(StringUtils.isNotEmpty(jsonObject.getString("platformId")) ? Integer.parseInt(jsonObject.getString("platformId")) : null); netConnectionSelectAccept.setBrandId(StringUtils.isNotEmpty(jsonObject.getString("brandId")) ? Integer.parseInt(jsonObject.getString("brandId")) : null); netConnectionSelectAccept.setTypeId(StringUtils.isNotEmpty(jsonObject.getString("typeId")) ? Integer.parseInt(jsonObject.getString("typeId")) : null); netConnectionSelectAccept.setEmissionId(StringUtils.isNotEmpty(jsonObject.getString("emissionId")) ? Integer.parseInt(jsonObject.getString("emissionId")) : null); ReceiveData<NetConnectionSelectAccept> acceptData = new ReceiveData<>(LargeIdentificationEnum.NetConnectionDataIsSecond, netConnectionSelectAccept); pushBySendServiceList.forEach(v -> v.pushBySend(acceptData, session)); } else if (key.equals(LargeIdentificationEnum.MapData)) { //第二屏-地图数据 MapQueryAccept mapQueryAccept = new MapQueryAccept(); mapQueryAccept.setType(StringUtils.isNotEmpty(jsonObject.getString("type")) ? jsonObject.getString("type") : null); mapQueryAccept.setProvinceStr(StringUtils.isNotEmpty(jsonObject.getString("provinceStr")) ? jsonObject.getString("provinceStr") : null); ReceiveData<MapQueryAccept> receiveData = new ReceiveData<>(LargeIdentificationEnum.MapData, mapQueryAccept); pushBySendServiceList.forEach(v -> v.pushBySend(receiveData, session)); } else { pushBySendServiceList.forEach(v -> v.push(key)); } log.info("WebSocket提示:前端通信消息{}", jsonObject); } }
此处可以先不看handleSend
方法,等后面所有的逻辑都理清之后,再看
import com.cloud.top.tower.enums.LargeIdentificationEnum; import lombok.Getter; import lombok.Setter; /** * @author YanYan * @description: 推送数据对象 * @date 2023/2/7 10:13 */ @Setter @Getter public class PushData<T> { /** * 推送数据的key */ private LargeIdentificationEnum key; /** * 推送数据的value */ private T value; public PushData(LargeIdentificationEnum key, T value) { this.key = key; this.value = value; } }
推送数据的公共类中,用构造函数来约束key必须为大屏展示的每个模块对应的枚举,此处只要和前端约束好枚举,就能实现分模块推送,同时泛型是每个模块对应的具体数据的实现类,这样就完全把推送数据对象给抽象出来了,对于业务来说,推送数据就是这个对象,具体实现就看程序员怎么写。
package com.cloud.top.tower.model.largeScreen; import com.cloud.top.tower.enums.LargeIdentificationEnum; import lombok.Getter; import lombok.Setter; /** * @author YanYan * @description: 接收数据对象 * @date 2023/2/13 14:30 */ @Setter @Getter public class ReceiveData<T> { /** * 接收数据的key */ private LargeIdentificationEnum key; /** * 接收数据的value */ private T value; public ReceiveData(LargeIdentificationEnum key, T value) { this.key = key; this.value = value; } }
此处是通过大佬的想法,我把接收数据的类,也抽象为一个公共类,和推送数据公共类原理是一样的,此类的主要功能就是,接收前端传给后端的消息,通过此消息,推送指定的数据
(如:进入页面的时候,可以向后端发送一个推送全部数据的指令,后端通过此指令,推送指定的数据)
import com.cloud.top.tower.common.utils.SpringUtils; import com.cloud.top.tower.common.webSocket.WebSocketServer; import com.cloud.top.tower.enums.LargeIdentificationEnum; import com.cloud.top.tower.model.largeScreen.PushData; import com.cloud.top.tower.model.largeScreen.ReceiveData; import org.yeauty.pojo.Session; /** * @author YanYan * @description: 推送接口(包括后端主动推送数据给前端;前端通过给后端发送指令,后端推送数据) * @date 2023/2/7 11:04 */ public interface IPushService<T, S> { WebSocketServer webSocketService = SpringUtils.getBean(WebSocketServer.class); /** * 获取推送数据的方法 * * @param receiveData 接收数据对象 * @return 推送数据类 */ PushData<S> getPushData(ReceiveData<T> receiveData); /** * 主动推送数据的方法 */ default PushData<S> getPushData(LargeIdentificationEnum largeIdentificationEnum) { ReceiveData<T> receiveData = new ReceiveData<>(largeIdentificationEnum, null); return getPushData(receiveData); } /** * 推送数据,需要前端传送指令 */ default void pushBySend(ReceiveData<T> receiveData, Session session) { PushData<S> pushData = getPushData(receiveData); if (receiveData.getKey().equals(pushData.getKey())) { webSocketService.sendMessage(pushData, session); } } /** * 推送数据,根据枚举常量即可 */ default void push(LargeIdentificationEnum largeIdentificationEnum) { PushData<S> pushData = getPushData(largeIdentificationEnum); /* * 判断是否获取数据 写在每一个service实现里面了,只需要判断value是否为null 就可以来判断是否推送 * */ if (pushData.getValue() != null) { webSocketService.sendMessage(pushData); } } /** * 批量推送 */ default void push(LargeIdentificationEnum... largeIdentificationEnums) { for (LargeIdentificationEnum largeIdentificationEnum : largeIdentificationEnums) { this.push(largeIdentificationEnum); } } }
推送数据的公共Service是整个设计的核心。
getPushData方法的入参是接收数据对象,此处主要用来实现业务中前端选择筛选条件后,后端可以接收筛选条件,然后通过处理筛选条件,进行指定数据的推送,同时此处还不收模块化的影响,可以根据筛选条件,推送不同模块的数据。
getPushData方法的返回值,就是推送给前端的数据,此返回值必定包括模块的枚举key,数据可有可无,同时数据的获取逻辑是每一个实现推送数据公共Serivce的实现类里面。
push方法,实现了后端主动向前端推送数据,push方法的入参就是推送指定模块的枚举,只需要调用default PushData<S> getPushData(LargeIdentificationEnum largeIdentificationEnum)
此方法,组装接收数据对象,不需要填写筛选条件,填写指定模块的key即可实现,同时每一个公共Service的实现类里面,都会判断传输过来的枚举,是否为当前模块的枚举,如果不是,直接返回PushData的value为空,push方法通过判断,来找出真正需要推送的模块,调用webSocket的推送数据的方法,完成推送
其他的方法,都是在此的扩展之下,如果读懂了上述逻辑,那么是非常好理解的。
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; import com.cloud.top.tower.common.cache.DictCache; import com.cloud.top.tower.common.utils.LargeIdentificationJudgeUtil; import com.cloud.top.tower.entity.bu.BuFleetConfigInformation; import com.cloud.top.tower.enums.DictBelongEnum; import com.cloud.top.tower.enums.LargeIdentificationEnum; import com.cloud.top.tower.enums.ServiceEnum; import com.cloud.top.tower.model.base.DictVO; import com.cloud.top.tower.model.largeScreen.PushData; import com.cloud.top.tower.model.largeScreen.ReceiveData; import com.cloud.top.tower.model.largeScreen.second.NumberOfIncomingConnections; import com.cloud.top.tower.model.largeScreen.second.accept.NetConnectionSelectAccept; import com.cloud.top.tower.service.bu.IFleetConfigInformationService; import com.cloud.top.tower.service.pushLogin.IPushService; import lombok.Setter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * @author YanYan * @description: 第二屏-入网连接量数据推送实现 * @date 2023/2/14 10:36 */ @Service @Setter(onMethod = @__(@Autowired)) public class NumberOfIncomingConnectionsServiceImpl implements IPushService<NetConnectionSelectAccept, NumberOfIncomingConnections> { private IFleetConfigInformationService fleetConfigInformationService; /** * 获取推送数据的方法 * * @param receiveData 接收数据对象 * @return 推送数据类 */ @Override public PushData<NumberOfIncomingConnections> getPushData(ReceiveData<NetConnectionSelectAccept> receiveData) { if (LargeIdentificationJudgeUtil.judge(LargeIdentificationEnum.NetConnectionDataIsSecond, receiveData)) { return new PushData<>(LargeIdentificationEnum.NetConnectionDataIsSecond, null); } NumberOfIncomingConnections numberOfIncomingConnections = new NumberOfIncomingConnections(); //获取需要查询逻辑 NetConnectionSelectAccept accept = receiveData.getValue(); numberOfIncomingConnections.setIsAllData(ObjectUtils.isEmpty(accept) ? ServiceEnum.yes.getStateCode() : ServiceEnum.no.getStateCode()); //根据条件获取车辆 List<BuFleetConfigInformation> fleetConfigInformationList = fleetConfigInformationService.getByNetConnectionAccept(accept); numberOfIncomingConnections.setCarCount(fleetConfigInformationList.size()); //获取所有车辆 List<BuFleetConfigInformation> fleetConfigInformationListAll = fleetConfigInformationService.list(); //获取字典数据缓存中 需要展示的 动力类型的数据集合 List<DictVO> powerTypes = DictCache.DictVoMap.values().stream().filter(v -> v.getIdentification().equals(DictBelongEnum.powerType.getCode()) && v.getBigLarger().equals(ServiceEnum.yes.getStateCode())).collect(Collectors.toList()); List<NumberOfIncomingConnections.KeyValue> connectionList = new ArrayList<>(); powerTypes.forEach(v -> { NumberOfIncomingConnections.KeyValue connections = new NumberOfIncomingConnections.KeyValue(); connections.setName(v.getModule() + "连接量:"); connections.setCount(fleetConfigInformationListAll.stream().filter(s -> s.getPowerType().equals(v.getId())).count()); connectionList.add(connections); }); numberOfIncomingConnections.setConnections(connectionList); return new PushData<>(LargeIdentificationEnum.NetConnectionDataIsSecond, numberOfIncomingConnections); } }
此实现类,就是本项目的集大成者,抽象出来的最终形态,此模块既有后端主动给前端推送数据,也有前端发送指令,后端通过指令筛选条件,然后推送数据。中间的,都是获取数据的逻辑,不用在意,主要还是学习思想
还有一个注意的点,由于公共推送数据Service具有很多实现,所以通过自动注入是不行的,推送指定模块数据的代码大概是这个样子的,就两行
//获取推送数据的所有Service实现
List<IPushService> pushBySendServiceList = SpringUtils.getPushServiceList();
//此处的key就是 每个模块对应的枚举
pushBySendServiceList.forEach(v -> v.push(key));
最后,各位,思想无价,这个设计,可能还有很多不足之处,不过对我来说,受益终身,现在做的项目,学到这个,很值。
同时也欢迎各位在下面讨论,我肯定还有很多说的不够严谨的地方,大家可以指出,同时有不理解的地方,也欢迎大家进行讨论和提问,我看见一定会回答。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。