尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于J2EE的实时新闻推送系统设计与WebSocket实战

基于J2EE的实时新闻推送系统设计与WebSocket实战 简介在Web开发中实时数据交互一直是工程实践的重点与难点。从早期的短轮询、长轮询到如今主流的WebSocket不同技术方案在实时性、服务器负载与实现复杂度上各有取舍。WebSocket作为HTML5提供的全双工通信协议能够在客户端与服务器之间建立持久连接为服务端主动推送提供了高效通道。在J2EE规范体系下结合Servlet、JSP与JMS等核心技术可以构建出稳定可靠的企业级应用。实时新闻推送系统正是这一技术的典型应用场景新闻发布后系统需将内容瞬间送达在线用户涉及会话管理、并发控制、订阅关系与消息格式等多层设计。本文围绕基于J2EE的实时新闻推送网站系统从架构分层、数据库设计、核心代码实现到兼容性降级方案系统解析了如何利用WebSocket实现真正的服务端推送并兼顾工程实践的稳定性与答辩展示的说服力为毕业设计或类似项目提供完整参考。1. 为什么是J2EE一个新闻推送毕业设计背后的真实考点1.1 表面是新闻网站实际考的是这四件事“基于J2EE的实时新闻推送网站系统”这个题目在毕业设计里出现的频率非常高。很多同学第一眼看到会以为又是一个新闻增删改查但实际上这类题目的考查点并不在新闻本身而是四个容易被忽视的地方会话管理、并发控制、消息机制、实时通信。我接过不少类似的项目咨询发现一个共同规律凡是只把新闻CRUD做完就去写论文的答辩时几乎都会被问到同一个问题——“你的实时推送是怎么实现的”如果回答不上来整个系统的含金量直接打对折。但如果能把推送机制讲清楚哪怕界面朴素一点评委也会认为你对J2EE的核心规范有真实理解。所以这篇博文把话放在前面这个项目的重心不在于“新闻”而在于“实时推送”。你在设计和实现时所有技术选型都应该围绕“如何把一条新闻在发布瞬间推送到在线用户面前”来展开。1.2 技术栈选型J2EE规范、SSH、SSM到底选哪条路先说一个很多刚接触J2EE的读者容易混淆的点。J2EE本身是一套规范集合包含Servlet、JSP、EJB、JMS、JTA、JNDI等。但在真实项目中很少有人把EJB全家桶全部用上尤其是在毕业设计这种体量下。常见的路线有三条路线组成优点缺点适用场景规范路线Servlet JSP JDBC WebSocket贴近J2EE原旨答辩时规范性强代码量大开发效率低论文需要突出“J2EE规范”经典框架路线Struts2 Spring Hibernate分层清晰资料多配置繁琐已趋于老旧老牌选题模板轻量整合路线Spring MVC MyBatis WebSocket开发效率高维护方便偏离“J2EE”字面实际工程常用我给这个项目的建议是走规范路线为主、框架辅助为辅。也就是说核心业务用Servlet Service DAO的传统分层来实现Session管理用HttpSession实时推送用WebSocket的Java API数据库访问用JDBC封装或轻量工具。这样在论文里你可以理直气壮地写“基于J2EE规范体系”同时在代码实现上又不会陷入EJB部署的泥潭。可能有同学会担心不用Spring事务和依赖注入怎么办这一点完全不用担心。对于毕业设计体量的系统手动管理连接、事务和依赖已经足够而且更能体现你对底层原理的理解。Spring是个好东西但在答辩时“我用手写的连接池管理类实现了数据库连接的获取与释放通过ThreadLocal保证同一事务内使用同一个Connection”比“我用Spring的Transactional”更有说服力。1.3 实时推送的三条技术路线轮询、长轮询、WebSocket“实时”这个词在Web开发里没有绝对含义它取决于你选择的技术方案能容忍多大的延迟。我梳理一下这个项目里最常用的三条路线短轮询Polling前端每隔几秒发一次AJAX请求查询有没有新新闻。实现最简单但服务器压力大且实时性受轮询间隔限制。如果间隔3秒用户最多等3秒这已经能算“准实时”。长轮询Long Polling前端发起请求后服务器不立即返回而是把请求挂起等有新新闻时才响应。响应后前端立刻再发起下一次请求。这种方式实时性好很多但服务器需要维护大量挂起的请求对并发能力有要求。WebSocket客户端和服务器建立一条TCP长连接服务器可以主动往客户端推数据。这是真正的“服务端推送”实时性最好也是J2EE 7规范中正式纳入的Java WebSocket APIJSR 356所支持的方案。我的建议是主推WebSocket同时保留一个轮询降级开关。原因很简单WebSocket在某些老旧浏览器或特殊网络环境下可能连接失败比如公司内网代理就会拦截WebSocket握手。降级方案不是摆设它会在你做现场演示时救你一命。2. 架构分层与数据库设计推送系统最先要定的三件事2.1 包结构与分层设计这个项目的代码结构我推荐按下面这个方式组织src/main/java ├── com.newsportal │ ├── common // 工具类、常量、统一响应类 │ ├── dao // 数据访问层 │ ├── entity // 实体类 │ ├── service // 业务逻辑层接口实现 │ ├── servlet // Servlet控制器 │ ├── websocket // WebSocket端点 │ └── listener // 监听器用于启动时初始化分层原则很朴素Servlet只做参数接收、调用服务和结果转发不写SQLService只做业务逻辑不出现HttpServletRequestDAO只做数据访问不处理业务判断。这种约束可以让你的代码在论文中画分层架构图时非常清晰也方便后续扩展。有一点容易被忽略WebSocket端点和Servlet是两套不同的组件。WebSocket端点不是Servlet它不受web.xml中Servlet映射的管理而是由容器在启动时扫描ServerEndpoint注解来注册。这就带来一个问题在WebSocket端点里无法像Servlet那样直接通过request.getSession()拿到HttpSession。后面我会专门讲怎么处理登录状态和用户身份绑定。2.2 数据库表设计五张核心表新闻推送系统的数据库设计除了常规的用户表、新闻表还必须考虑“订阅关系”和“推送记录”。我给出一个经过实践调整的表结构用户表t_userCREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), category_pref VARCHAR(255), -- 偏好分类逗号分隔如 1,2,3 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );新闻分类表t_categoryCREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, code VARCHAR(20) NOT NULL UNIQUE );新闻表t_newsCREATE TABLE t_news ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, summary VARCHAR(500), content TEXT, category_id INT, publisher_id INT, status INT DEFAULT 1, -- 1上线 0下线 publish_time DATETIME, FOREIGN KEY (category_id) REFERENCES t_category(id), FOREIGN KEY (publisher_id) REFERENCES t_user(id) );订阅表t_subscribeCREATE TABLE t_subscribe ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, category_id INT NOT NULL, subscribe_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_category (user_id, category_id) );推送记录表t_push_logCREATE TABLE t_push_log ( id INT PRIMARY KEY AUTO_INCREMENT, news_id INT NOT NULL, user_id INT NOT NULL, push_type VARCHAR(10), -- WS 或 POLLING push_status INT, -- 1成功 0失败 push_time DATETIME DEFAULT CURRENT_TIMESTAMP );推送记录表看起来不起眼但它在论文里作用很大。你可以在答辩时展示“系统对每位用户的推送行为有完整记录包括推送方式、推送结果、推送时间便于统计推送成功率和延迟。”这是一句非常有分量的介绍因为它证明你不是只做了功能还做了可观测性设计。2.3 “实时”的粒度推送时机如何定义在设计推送机制之前必须先明确一个问题什么时刻算“需要推送”的实时节点我的定义是新闻表中insert一条status1的记录之后系统立即把这条新闻推送给所有订阅了对应分类的在线用户。这个定义包含两个关键点推送的对象是“订阅了对应分类的用户”而不是全站所有人。这样设计既符合新闻客户端的真实场景也能体现出“个性化推荐”的思想。推送的触发点是“发布动作”而不是定时扫描。我的实现方式是新闻发布Servlet在完成数据库insert后调用一个推送服务把新闻对象和关联的分类ID传递给WebSocket推送管理器。WEB容器收到推送请求后根据订阅关系找到在线用户再通过各用户的WebSocket Session发送消息。这里有个细节值得注意推送动作和发布动作应该在同一个业务请求里完成但不一定在同一个事务里。如果新闻插入成功但推送失败不能回滚新闻插入否则新闻就发不出去了。正确的做法是新闻插入独立事务推送失败只记日志不影响新闻发布。这个取舍思路在答辩时也是加分项。3. 后端核心实现新闻模块、订阅关系与推送引擎怎么写3.1 新闻发布模块的实现新闻发布的入口是PublishNewsServlet。这个Servlet接收管理员表单提交校验参数后调用NewsService.publish()方法。WebServlet(/admin/publishNews) public class PublishNewsServlet extends HttpServlet { private NewsService newsService new NewsServiceImpl(); private PushService pushService new PushServiceImpl(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String title request.getParameter(title); String summary request.getParameter(summary); String content request.getParameter(content); int categoryId Integer.parseInt(request.getParameter(categoryId)); int publisherId (Integer) request.getSession().getAttribute(userId); News news new News(); news.setTitle(title); news.setSummary(summary); news.setContent(content); news.setCategoryId(categoryId); news.setPublisherId(publisherId); news.setStatus(1); news.setPublishTime(new Date()); // 第一步入库这一步是独立事务 int newsId newsService.publish(news); if (newsId 0) { response.getWriter().write({\code\:500,\msg\:\发布失败\}); return; } news.setId(newsId); // 第二步触发推送推送失败不影响发布结果 try { pushService.pushNewsToSubscribers(news); response.getWriter().write({\code\:200,\msg\:\发布成功已推送\}); } catch (Exception e) { response.getWriter().write({\code\:200,\msg\:\发布成功但推送异常\}); } } }这里有一个代码层面的细节需要注意news.setId(newsId)这行看起来多余实际上非常关键。因为news对象在插入数据库之前是没有ID的而后续推送逻辑需要携带新闻ID用于生成推送消息体、写入推送日志。我见过很多人在这一步漏掉ID回填导致推送消息里没有新闻ID前端点击推送消息无法跳转到新闻详情页。3.2 订阅模块的核心逻辑订阅模块的价值在于支撑“按分类推送”。用户访问某个新闻分类页面时可以选择订阅或取消订阅。核心SQL很简单public boolean subscribe(int userId, int categoryId) { String sql INSERT IGNORE INTO t_subscribe(user_id, category_id) VALUES(?, ?); // 执行即可INSERT IGNORE 保证重复订阅不报错 } public boolean unsubscribe(int userId, int categoryId) { String sql DELETE FROM t_subscribe WHERE user_id ? AND category_id ?; // 执行即可 } public ListInteger findSubscriberIdsByCategory(int categoryId) { String sql SELECT user_id FROM t_subscribe WHERE category_id ?; // 返回订阅用户ID列表 }为什么推荐用INSERT IGNORE因为在实际操作中用户可能因为网络原因连续点击两次订阅按钮第一次请求还没返回第二次又发过来了。如果没有唯一键约束和INSERT IGNORE就会插入两条重复的订阅记录导致用户收到两次同样的推送。这在论文测试时是一个很容易被发现的低级Bug提前用唯一索引堵住是最好的方式。3.3 推送引擎WebSocket Session的注册与管理实现服务端主动推送核心是管理好所有在线用户的WebSocket Session。我的做法是写一个WebSocketSessionManager类专门维护“用户ID → Session”的映射关系。public class WebSocketSessionManager { private static final ConcurrentHashMapInteger, Session onlineSessions new ConcurrentHashMap(); public static void addSession(Integer userId, Session session) { onlineSessions.put(userId, session); } public static void removeSession(Session session) { onlineSessions.entrySet().removeIf(entry - entry.getValue().equals(session)); } public static Session getSession(Integer userId) { return onlineSessions.get(userId); } public static boolean isOnline(Integer userId) { return onlineSessions.containsKey(userId); } }用ConcurrentHashMap而不是普通的HashMap是因为WebSocket的事件回调是多线程环境下触发的。多个客户端同时建立连接或断开连接时如果使用非线程安全的Map扩容时可能出现死循环或数据丢失。这一点在并发量稍微一上来就会暴露也是很多同学在并发测试时遇到“诡异卡死”的根源之一。3.4 WebSocket端点中的用户身份绑定前面提到过WebSocket端点拿不到HttpSession那怎么知道当前连接的用户是谁我的方案是在连接建立时通过URL参数携带用户ID。ServerEndpoint(/newsPush/{userId}) public class NewsPushEndpoint { OnOpen public void onOpen(Session session, PathParam(userId) Integer userId) { WebSocketSessionManager.addSession(userId, session); System.out.println(用户 userId 已建立连接); } OnClose public void onClose(Session session) { WebSocketSessionManager.removeSession(session); } OnError public void onError(Session session, Throwable error) { WebSocketSessionManager.removeSession(session); } }前端连接时这样写var ws new WebSocket(ws://localhost:8080/newsportal/newsPush/ userId);这个方案的优点是简单直接但有一个安全隐患用户ID暴露在URL里如果被人恶意拼接就能以别人的身份建立连接从而收到别人的推送消息。对毕业设计来说这个方案能跑通但如果想做得更健壮可以在建立连接前先通过HTTP接口获取一个短期有效的token再用token建立WebSocket连接。不过说实话在毕设阶段token方案的复杂度会明显上升我建议先把功能跑通在论文的“进一步工作”里提一句“后续可通过token机制增强安全性”就够了。3.5 推送消息的组装与发送当新闻发布后PushServiceImpl要做三件事查询订阅用户、组装消息、逐个发送。public void pushNewsToSubscribers(News news) { ListInteger subscriberIds subscribeDao.findSubscriberIdsByCategory(news.getCategoryId()); // 组装推送消息体用JSON PushMessage message new PushMessage(); message.setType(NEWS_PUSH); message.setNewsId(news.getId()); message.setTitle(news.getTitle()); message.setSummary(news.getSummary()); message.setCategoryId(news.getCategoryId()); message.setPublishTime(news.getPublishTime().getTime()); String json new Gson().toJson(message); // 逐个尝试推送 for (Integer userId : subscriberIds) { boolean success sendToUser(userId, json); pushLogDao.insert(news.getId(), userId, WS, success ? 1 : 0); } } private boolean sendToUser(Integer userId, String message) { Session session WebSocketSessionManager.getSession(userId); if (session null) { return false; // 用户离线直接记录失败 } try { synchronized (session) { session.getBasicRemote().sendText(message); } return true; } catch (Exception e) { WebSocketSessionManager.removeSession(session); return false; } }这里有一个我在实际测试中发现的细节多个线程同时往同一个WebSocket Session发送消息时会抛出IllegalStateException。虽然getBasicRemote().sendText()在API文档里没有明确说必须加锁但Session内部状态比如当前正在发送的消息缓冲区不是线程安全的。我实测下来在高并发推送场景下不加锁的确会出现偶发的消息中断。所以我在sendToUser方法里加了synchronized (session)。这个锁的成本极低但对推送稳定性的提升是决定性的。如果消息量特别大更好的方案是每个Session配一个消息队列发送线程从队列中逐个取消息发送但这个复杂度对毕设来说就超标了。4. 前端的“实时”是怎么来的WebSocket接入与降级方案4.1 前端页面结构与推送消息的展现前端我采用的是JSP JavaScript的组合。页面布局分为三大块顶部导航栏、左侧新闻分类列表、右侧新闻内容列表。用户的登录状态和用户ID在登录后写入Session并在页面初始化时通过${sessionScope.userId}输出到JavaScript变量中。新闻列表的渲染是通过AJAX从/news/list接口加载的每次加载一页每页10条。新推送来的新闻则插入到列表最顶部并显示一个浅黄色背景的“新”标签持续几秒后背景色渐变消失。这个UI效果实现起来很简单但能让评委一眼看出“推送确实生效了”。4.2 核心JavaScript接收推送并动态渲染var userId ${sessionScope.userId}; var ws; function connectWebSocket() { ws new WebSocket(ws:// window.location.host /newsportal/newsPush/ userId); ws.onopen function() { console.log(WebSocket连接已建立); // 可以发送一个心跳消息 ws.send(JSON.stringify({type: PING})); }; ws.onmessage function(event) { var msg JSON.parse(event.data); if (msg.type NEWS_PUSH) { prependNews(msg); showToast(收到新新闻 msg.title); } }; ws.onclose function() { console.log(WebSocket连接已关闭); // 3秒后重连 setTimeout(connectWebSocket, 3000); }; ws.onerror function(err) { console.error(WebSocket发生错误, err); ws.close(); }; } function prependNews(msg) { var newsHtml div classnews-item h3a href/newsportal/news/detail?id msg.newsId msg.title /a/h3 p msg.summary /p span classnews-time刚刚/span /div; $(#newsList).prepend(newsHtml); } connectWebSocket();这个代码看起来简单但有三个细节值得说明第一心跳机制。过了一段时间如果没有数据往来某些网络设备比如NAT路由器会认为连接空闲而回收连接。这种情况的表现是客户端和服务器都觉得连接还活着但实际上数据已经传不过去了。解决办法就是定时发送一个极小的PING消息让它“没事也吱一声”。我采用的是在onopen后每30秒发送一次心跳。第二断线重连。WebSocket连接不可能永远稳定。服务器重启、网络抖动都可能导致连接断开。我的策略是onclose之后延迟3秒重连。这里注意重连延迟不宜太短否则服务器重启的时候多用户客户端会以极短间隔疯狂重连形成“重连风暴”把刚启动的服务器打趴。3秒到5秒是一个比较合适的区间。第三消息类型的约定。服务器推送给客户端的消息不止一种类型除了NEWS_PUSH新闻推送还有可能是系统通知。所以消息体里必须有type字段前端通过switch分支处理不同类型。这个设计在答辩时也能体现你要把协议设计做规范化的意识。4.3 兼容性降级WebSocket不可用时的轮询方案尽管WebSocket已经是主流但作为一个完整的工程系统我还是建议加一个降级开关。我在项目中是这样做的在ApplicationListener中读取一个配置项push.mode启动时把它写入ServletContext的attribute中。前端页面加载时先尝试建立WebSocket连接如果3秒内未能建立成功就自动切换到长轮询模式。var useWebSocket true; var timer setTimeout(function() { if (!ws || ws.readyState ! WebSocket.OPEN) { console.log(WebSocket连接超时切换到长轮询); useWebSocket false; startLongPolling(); } }, 3000); function startLongPolling() { function poll() { $.ajax({ url: /newsportal/news/poll?lastTime lastPushTime, timeout: 25000, success: function(data) { if (data data.list.length 0) { data.list.forEach(prependNews); lastPushTime data.lastTime; } poll(); // 立即发起下一次 }, error: function() { setTimeout(poll, 3000); } }); } poll(); }这个降级方案的核心逻辑是客户端把当前已收到新闻的最新时间戳lastPushTime传给服务器服务器在这个接口上挂起最多20秒如果期间有比lastPushTime更新的新闻就立即返回否则超时返回空列表客户端再发起下一次请求。长轮询的服务端实现也比较直接WebServlet(/news/poll) public class NewsPollServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) { long lastTime Long.parseLong(request.getParameter(lastTime)); // 循环等待最多20秒期间每2秒查一次新新闻 long deadline System.currentTimeMillis() 20000; ListNews newList Collections.emptyList(); while (System.currentTimeMillis() deadline) { newList newsDao.findNewsAfter(lastTime, 10); if (!newList.isEmpty()) { break; } try { Thread.sleep(2000); } catch (InterruptedException e) { break; } } // 返回JSON } }这个降级方案写在论文的“系统健壮性设计”一节里非常加分。它说明你不只会用新技术还考虑了技术可能失效的边界情况。5. 项目配置与运行指南从零跑通到答辩演示准备的细节5.1 开发环境与版本选择这个项目的开发环境我推荐用下面的组合都是经过验证的稳定搭配组件版本建议说明JDKJDK 8J2EE相关规范兼容性最好不要用太高版本应用服务器Tomcat 8.5 或 9.0支持WebSocket 1.1配置简单数据库MySQL 5.7 或 8.0两种都可以构建工具Maven 3.6统一管理依赖IDEEclipse 或 IntelliJ IDEA皆可在版本选择上我特别强调JDK不要用太高。有人图新鲜装了JDK 17结果发现某些旧版本的库或者Tomcat版本不兼容折腾半天才跑通。毕业设计的核心是稳定不是追新。Tomcat 9 JDK 8是当前最稳的组合。5.2 web.xml关键配置项虽然现在的Servlet可以用WebServlet注解代替web.xml中的映射但有些配置还是必须写在web.xml里。我用到的关键配置如下web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 display-nameNewsPortal/display-name welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list !-- 字符编码过滤器 -- filter filter-nameencodingFilter/filter-name filter-classcom.newsportal.common.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- 会话超时时间30分钟 -- session-config session-timeout30/session-timeout /session-config /web-app字符编码过滤器是中文乱码的克星。我在做项目时发现如果只设置JSP的pageEncoding而忘了在Servlet端使用request.setCharacterEncoding(UTF-8)那么AJAX提交的中文标题就会变成乱码。用过滤器统一处理所有请求的编码是最省心的做法。5.3 数据库初始化脚本项目附带的SQL初始化脚本要包含建库、建表、插入初始数据三部分。我一般命名为init.sql放在项目的doc目录下。下面是一段示例CREATE DATABASE IF NOT EXISTS newsportal DEFAULT CHARACTER SET utf8mb4; USE newsportal; -- 建表语句略见第2节 INSERT INTO t_category (id, name, code) VALUES (1, 要闻, TOP), (2, 科技, TECH), (3, 体育, SPORTS), (4, 财经, FINANCE); INSERT INTO t_user (id, username, password, nickname) VALUES (1, admin, 123456, 管理员), (2, zhangsan, 123456, 张三), (3, lisi, 123456, 李四);注意数据库字符集要用utf8mb4而不是utf8。因为utf8在MySQL里最多支持3字节的字符存储部分生僻字或特殊符号会报错而utf8mb4是完整的4字节UTF-8编码。这个坑我踩过用utf8存某些带emoji的新闻标题时插入直接报Incorrect string value错误改成utf8mb4后问题消失。5.4 从零到跑通的完整步骤整个项目从拿到源码到可以在浏览器里演示我建议按以下顺序操作安装JDK 8配置JAVA_HOME和PATH安装Maven配置MAVEN_HOME和PATH安装Tomcat 9解压到指定目录安装MySQL启动服务用init.sql初始化数据库修改db.properties中的数据库连接信息用户名、密码、URL在项目根目录执行mvn clean package生成war包将war包复制到Tomcat的webapps目录启动Tomcat访问http://localhost:8080/newsportal/用管理员账号登录在后台发布一条新闻用另一个浏览器或隐身窗口登录普通用户账号在新闻列表页观察是否有新新闻实时弹出第10步是演示的关键。我建议演示时准备两个浏览器窗口并排放在屏幕上一个窗口登录管理员另一个窗口登录普通用户。管理员发布新闻后普通用户窗口能实时弹出新闻提示。这个画面比任何PPT截图都有说服力。5.5 答辩演示时的几个准备细节演示环节有几个容易被忽略的细节直接影响评委体验提前关闭无线网络用有线网络或本机访问。如果现场WiFi不稳定WebSocket连接可能反复断开演示效果大打折扣。准备好测试账号。在init.sql里预置好管理账号和测试用户账号避免现场注册浪费时间。准备一条已经在草稿箱的新闻。如果现场网络或服务器出现意外你可以快速走一次“发新闻”流程降低故障概率。演示前先访问一次系统让Tomcat完成JSP编译。第一次访问JSP通常比较慢因为容器要把它编译成Servlet如果在正式演示时第一次访问卡住几秒会显得系统很慢。6. 我踩过的坑和最终做掉的优化6.1 坑一WebSocket连接只能建立一次第二次页面刷新就失败这是我在项目联调时遇到的第一个大坑。现象是第一次访问新闻首页WebSocket能正常连接并收到推送刷新页面后WebSocket一直报错无法建立。排查后发现原因在Tomcat的Session管理。当页面刷新时旧连接还没完全关闭新连接又请求建立服务器端的旧Session和新Session产生冲突。我的解决方案是在前端beforeunload事件中主动关闭WebSocket连接window.addEventListener(beforeunload, function() { if (ws) { ws.close(); } });同时在服务端OnClose回调中不仅要移除Session映射还要检查会话状态确保旧连接资源被释放。两步配合后刷新页面就能稳定重连了。这类问题的排查思路值得复盘一下先看浏览器控制台的报错信息再打开Tomcat的localhost日志重点观察WebSocket握手阶段的日志。我当时就是通过日志发现“A session has already been registered”的提示才定位到Session冲突的问题。6.2 坑二推送消息丢失日志显示已发送但前端没收到另一个让我头疼的问题是部分用户收不到推送但查看推送日志状态明明是“成功”。跟踪到最后发现原因出在WebSocket消息的异步发送上。我用的是session.getAsyncRemote().sendText()这个方法是异步的方法返回不代表消息已经真正发出。如果连续发送多条消息后一条消息可能会覆盖前一条未发送完的消息导致最后只有最后一条消息到达客户端。解决办法有两个方向一是改用getBasicRemote().sendText()同步发送二是为每个Session建立消息队列。考虑到项目体量我选择了同步发送并加锁配合心跳机制最终消息丢失没有再出现。这件事给我的启示是在写论文时不要只写“用了WebSocket实现实时推送”最好能提一句“经过对比同步与异步发送方式的优劣最终采用同步发送以保证消息有序不丢失”这在答辩时会让评委觉得你有工程判断力。6.3 坑三Tomcat重启后推送失效要重启浏览器才行这个问题本质上还是Session管理的问题Tomcat重启后服务器端所有WebSocket连接都断了但浏览器的onclose事件可能没有立刻触发。客户端并不知情还以为连接正常于是后续推送全部落空。解决方案是前面4.2节提到的“心跳 自动重连”机制。心跳能及时发现假死连接重连能让客户端在服务器重启后自动恢复。我特意把心跳间隔设为30秒重连延迟设为3秒目的就是既能尽快发现异常又不会在服务器刚启动时对其造成过大的连接压力。6.4 性能优化把线程池和数据库连接池用起来项目基本成型后我做了两处性能优化。第一处是引入数据库连接池。在没有连接池的情况下每次DAO操作都DriverManager.getConnection()在模拟1000个并发用户测试时数据库连接创建和释放的开销非常可观接口响应时间接近3秒。换成Druid连接池后连接复用让响应时间降到200毫秒以内。Druid配置的核心内容jdbc.driverClassNamecom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/newsportal?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456 # 连接池初始化和扩容配置 jdbc.initialSize5 jdbc.maxActive50 jdbc.maxWait60000 jdbc.minIdle5第二处是给推送模块加了一个简单的线程池。新闻发布后的推送操作涉及“查询订阅用户 逐个发送 写日志”如果直接在发布Servlet里同步执行管理员的发布请求会阻塞在推送环节当订阅用户达到上千人时发布接口的响应时间会不可接受。我用一个固定大小的ExecutorService来异步执行推送任务让发布请求立即返回。private static final ExecutorService PUSH_POOL Executors.newFixedThreadPool(10); // 在发布成功后调用 PUSH_POOL.submit(() - { pushService.pushNewsToSubscribers(news); });不过线程池也要小心使用如果任务提交速度超过线程池处理速度队列会积压积压的消息会造成推送延迟。所以在性能压测时我特意统计了推送任务的平均执行时间确保线程池大小和队列容量是匹配的。6.5 一点实用的优化用Redis缓存热点新闻列表进阶方向如果想让系统有更多优化空间可以引入Redis来缓存首页的热点新闻列表。思路是首页默认展示最近10条新闻这些新闻从Redis缓存中读取当新新闻发布时除了推送给用户还更新Redis中的缓存列表。这样能显著减少数据库查询压力。这个优化在毕业设计里属于“锦上添花”的加分项但要注意它引入的复杂度需要安装Redis、引入Jedis依赖、处理缓存和数据库的一致性。我的建议是如果时间和精力允许可以做一版并写进论文的“系统优化”章节如果时间紧可以在论文的“进一步工作”中提出来并不会扣分。最后再分享两个小技巧这个项目做完之后我自己有几个体会比较深的点简单分享出来第一架构图和表结构图一定要画得足够细。答辩时评委可能不会逐行看代码但他们一定会看架构图、流程图、E-R图。图里的每个模块、每条线都要能说出设计理由。我论文里的架构图就画到了“新闻管理模块、用户模块、订阅模块、推送模块、数据访问模块”五个层次每层之间的调用关系标注清楚评委扫一眼就能理解系统的全貌。第二把演示脚本提前写下来。包括点击哪几个按钮、期望看到什么效果、如果发生异常怎么解释。这不是背书而是确保演示过程流畅有序。我的演示脚本里甚至写到了“如果WebSocket连接失败就打开控制台Network标签展示长轮询的请求在正常发送”这样任何意外情况都能转变成展示系统健壮性的机会。如果你也是选了这个题目希望这篇内容能帮你少走一些弯路。推送这块的核心代码量其实不大真正的难点在于理解“实时”背后的通信机制以及把各种异常情况考虑周全。把这个过程完整地走一遍你的收获绝对不止是一个毕业设计。本文还有配套的精品资源点击获取
返回列表