基于J2EE的实时新闻推送系统设计与WebSocket实战
2026/9/7 9:42:17 网站建设 项目流程

简介:在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 API(JSR 356)所支持的方案。

我的建议是:主推WebSocket,同时保留一个轮询降级开关。原因很简单,WebSocket在某些老旧浏览器或特殊网络环境下可能连接失败,比如公司内网代理就会拦截WebSocket握手。降级方案不是摆设,它会在你做现场演示时救你一命。

2. 架构分层与数据库设计:推送系统最先要定的三件事

2.1 包结构与分层设计

这个项目的代码结构,我推荐按下面这个方式组织:

src/main/java ├── com.newsportal │ ├── common // 工具类、常量、统一响应类 │ ├── dao // 数据访问层 │ ├── entity // 实体类 │ ├── service // 业务逻辑层,接口+实现 │ ├── servlet // Servlet控制器 │ ├── websocket // WebSocket端点 │ └── listener // 监听器,用于启动时初始化

分层原则很朴素:Servlet只做参数接收、调用服务和结果转发,不写SQL;Service只做业务逻辑,不出现HttpServletRequest;DAO只做数据访问,不处理业务判断。这种约束可以让你的代码在论文中画分层架构图时非常清晰,也方便后续扩展。

有一点容易被忽略:WebSocket端点和Servlet是两套不同的组件。WebSocket端点不是Servlet,它不受web.xml中Servlet映射的管理,而是由容器在启动时扫描@ServerEndpoint注解来注册。这就带来一个问题:在WebSocket端点里无法像Servlet那样直接通过request.getSession()拿到HttpSession。后面我会专门讲怎么处理登录状态和用户身份绑定。

2.2 数据库表设计:五张核心表

新闻推送系统的数据库设计,除了常规的用户表、新闻表,还必须考虑“订阅关系”和“推送记录”。我给出一个经过实践调整的表结构:

用户表(t_user)

CREATE 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_category)

CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, code VARCHAR(20) NOT NULL UNIQUE );

新闻表(t_news)

CREATE 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_subscribe)

CREATE 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_log)

CREATE 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一条status=1的记录之后,系统立即把这条新闻推送给所有订阅了对应分类的在线用户。这个定义包含两个关键点:

  • 推送的对象是“订阅了对应分类的用户”,而不是全站所有人。这样设计既符合新闻客户端的真实场景,也能体现出“个性化推荐”的思想。
  • 推送的触发点是“发布动作”,而不是定时扫描。我的实现方式是:新闻发布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 List<Integer> 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 ConcurrentHashMap<Integer, 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) { List<Integer> 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 class="news-item">' + '<h3><a href="/newsportal/news/detail?id=' + msg.newsId + '">' + msg.title + '</a></h3>' + '<p>' + msg.summary + '</p>' + '<span class="news-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; List<News> 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 xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="3.1"> <display-name>NewsPortal</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <!-- 字符编码过滤器 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>com.newsportal.common.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 会话超时时间:30分钟 --> <session-config> <session-timeout>30</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 从零到跑通的完整步骤

整个项目从拿到源码到可以在浏览器里演示,我建议按以下顺序操作:

  1. 安装JDK 8,配置JAVA_HOMEPATH
  2. 安装Maven,配置MAVEN_HOMEPATH
  3. 安装Tomcat 9,解压到指定目录
  4. 安装MySQL,启动服务,用init.sql初始化数据库
  5. 修改db.properties中的数据库连接信息(用户名、密码、URL)
  6. 在项目根目录执行mvn clean package,生成war包
  7. 将war包复制到Tomcat的webapps目录
  8. 启动Tomcat,访问http://localhost:8080/newsportal/
  9. 用管理员账号登录,在后台发布一条新闻
  10. 用另一个浏览器(或隐身窗口)登录普通用户账号,在新闻列表页观察是否有新新闻实时弹出

第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.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/newsportal?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456 # 连接池初始化和扩容配置 jdbc.initialSize=5 jdbc.maxActive=50 jdbc.maxWait=60000 jdbc.minIdle=5

第二处是给推送模块加了一个简单的线程池。新闻发布后的推送操作涉及“查询订阅用户 + 逐个发送 + 写日志”,如果直接在发布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标签,展示长轮询的请求在正常发送”,这样任何意外情况都能转变成展示系统健壮性的机会。

如果你也是选了这个题目,希望这篇内容能帮你少走一些弯路。推送这块的核心代码量其实不大,真正的难点在于理解“实时”背后的通信机制,以及把各种异常情况考虑周全。把这个过程完整地走一遍,你的收获绝对不止是一个毕业设计。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询