简介:这是一套基于JavaWeb的新闻发布系统毕业设计完整资料包,面向计算机专业毕业生、Java Web学习者及需要实战项目的开发者。资源围绕可运行的源代码、数据库SQL脚本与毕业论文展开,覆盖新闻管理、栏目分类、评论互动、用户登录等核心功能;代码采用Servlet+JSP+DAO分层,帮助理解请求处理、业务逻辑与数据访问的完整链路。压缩包内共556个文件,大小11.65MB,包含26个Java核心类、21个JSP视图、12个Jar依赖库,以及224个JS、80个PNG、30个CSS等前端页面资源,后端逻辑与前端界面一一对应;同时附有1个SQL数据库脚本和doc格式论文,可直接导入数据库并参考写作。开发环境说明与部署常见问题解答也已包含,可帮助规避Tomcat、数据库连接等典型问题。目前已有123人学习下载,适合作为毕业设计蓝本、框架实战练习或求职项目经验积累。
1. 把 JavaWeb 新闻发布系统拆开看:这套 Servlet + JSP 源码到底能练出什么
聊到 JavaWeb 新闻发布系统,很多人的第一反应是“又是毕设老三样”。但真把这份源码的类文件摊开——NewsServlet、NewsDao、CommentDao、IndexServlet、UserServlet、ImageUploadServlet——你会发现它是一套非常典型的 Servlet + JSP + DAO 三层结构。没有 Spring 全家桶的包装,反而把请求分发、JDBC 操作、分页查询、文件上传这些 JavaWeb 核心考点全暴露在明面上。对于做毕业设计、准备校招面试基础轮、或者需要一个干净模板做二次开发的人来说,这种“裸奔”结构的项目比框架整合项目更有拆解价值。它能让你在一两天内把 JavaWeb 的完整请求链路跑通,也知道哪些地方最容易翻车。
2. 请求分发链路:从 NewsServlet 到 DAO 的三层结构与分页参数
2.1 项目结构与请求流转的底层逻辑
这套项目里没有 Spring MVC 那套注解路由,而是用最原始的 Servlet 映射 + 业务分发来驱动整个系统。拿到源码包后,第一件事不是急着在 IDEA 里跑起来,而是先把类名和职责对上:
- InitServlet:应用启动时初始化,通常用来加载分类列表到内存或配置文件;
- IndexServlet:首页入口,负责组装新闻列表和分类数据;
- NewsServlet:新闻操作的统一入口,列表、详情、新增、删除、修改都走它;
- CommentServlet:评论提交与列表查询;
- UserServlet:登录、注册、登出;
- NewsTypeServlet:新闻分类维护;
- LinkServlet:友情链接维护;
- ImageUploadServlet:图片上传处理。
这套命名非常直白,对应的就是后台管理菜单和前台页面模块。请求流转是一条单向链路:浏览器发起 URL 请求 → 容器根据 web.xml 映射找到对应的 Servlet → Servlet 调用业务方法 → 业务方法调 DAO → DAO 拼 JDBC 执行 SQL → 结果逐层返回 → Servlet 转发或重定向到 JSP 页面。
看源码时建议先从 NewsServlet 下手,它是整个系统里业务分支最多的类。常见的做法是 doGet 和 doPost 共用同一个 dispatcher,然后通过 action 参数区分具体操作:
protected void service(HttpServletRequest request, HttpServletResponse response) { String action = request.getParameter("action"); if ("list".equals(action)) { // 分页查询新闻列表 } else if ("detail".equals(action)) { // 查看新闻详情 } else if ("add".equals(action)) { // 跳转到新增页面 } else if ("save".equals(action)) { // 保存新闻,含插入正文 } else if ("delete".equals(action)) { // 删除新闻 } }action 参数的值必须和 JSP 页面表单里提交的 action 完全一致,否则会直接掉进 else 分支返回空页面。这个参数是整套请求分发机制的唯一约定,改错一个字符就是请求 404 或者页面空白。
源码里各个 Servlet 之间没有继承统一的 BaseServlet,每个类单独实现 service 或 doGet/doPost 方法。好处是逻辑直观,坏处是代码重复率高。读代码时重点看两个地方:RequestDispatcher 的 forward 跳转和 sendRedirect 的重定向,前者用于带着 request 域中的数据到 JSP 渲染,后者用于表单提交后防止重复提交。
2.2 分页参数设计:page 与 pageSize 的计算规则
新闻列表是用户访问最频繁的页面,分页查询的实现质量直接决定你能不能在答辩时讲清楚。这套项目里分页走的是传统 limit 方式,传 page 和 pageSize 两个参数给 DAO,SQL 动态拼 limit 偏移量。
看 NewsDao 的分页方法时注意这个核心逻辑:
public List<News> findPage(int page, int pageSize) { int offset = (page - 1) * pageSize; String sql = "SELECT id, title, category_id, author, publish_time " + "FROM news ORDER BY publish_time DESC LIMIT ?, ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, offset); ps.setInt(2, pageSize); // 执行查询并封装结果 }offset = (page - 1) * pageSize 这条公式是整个分页的命根子。page 为 1 时偏移量是 0,查第一页;page 为 2 时偏移量是 pageSize,跳过第一页的数据。前端页面上显示“上一页 / 下一页”时,也要在 URL 里带上 page 参数,否则每次点击都会回到第一页。
总页数的计算在 Service 层,通常用 ceil 处理:
int totalCount = newsDao.countAll(); int totalPage = (int) Math.ceil((double) totalCount / pageSize);countAll 对应的是 SELECT COUNT(*) FROM news,这条统计 SQL 和列表 SQL 必须保持同样的过滤条件,否则列表页数和实际数据对不上。如果源码里加了分类筛选,那么 count 和 findPage 都要同时带 categoryId 条件,这就是一个容易踩的隐藏坑。
3. 数据库设计与首页渲染:news_category 与 news 表结构的取舍
3.1 表结构设计与外键约束的关键点
这套系统 SQL 文件里最核心的表是 news(新闻表)、news_category(分类表)、t_comment(评论表,也有项目叫 comment)和 t_user(用户表)。SQL 文件导入后,先用 Navicat 或 DBeaver 把表关系理清再动代码。
新闻表是一张信息密度很高的表,设计时大部分字段是必填的:
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | int | 主键自增 | 新闻唯一标识 |
| title | varchar(100) | 非空 | 新闻标题 |
| content | text | 非空 | 正文内容 |
| category_id | int | 外键 | 关联 news_category.id |
| author | varchar(50) | 可空 | 作者 |
| image | varchar(255) | 可空 | 封面图路径 |
| publish_time | datetime | 默认当前时间 | 发布时间 |
| click_count | int | 默认 0 | 点击量 |
category 表就简单得多:id 自增主键 + name 分类名。两个表通过 category_id 建立一对多关系,一张分类对应多条新闻。创建表时建议加上外键约束并在 category_id 上建普通索引,查询按分类筛选时会快不少:
CREATE TABLE news_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE news ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, content TEXT NOT NULL, category_id INT NOT NULL, author VARCHAR(50), image VARCHAR(255), publish_time DATETIME DEFAULT CURRENT_TIMESTAMP, click_count INT DEFAULT 0, INDEX idx_category (category_id), CONSTRAINT fk_news_category FOREIGN KEY (category_id) REFERENCES news_category(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字符集用 utf8mb4 而不是 utf8,这点很多毕设项目都忽略。utf8 在 MySQL 里最多存 3 字节,遇到表情符号会直接报 Incorrect string value 错误,utf8mb4 是完整的 4 字节支持。另外注意到外键策略了吗?这里没有指定 ON DELETE 规则,默认是 RESTRICT。也就是说后台分类管理中想删除一个下面还有新闻的分类,数据库会直接拒绝。
3.2 IndexServlet 首页数据组装与 JSP 渲染的传值方式
首页 IS 上展示的内容通常是:顶部导航栏分类列表、中间最新新闻列表、侧边栏热门新闻。这里涉及一个知识点:一次请求如何组装多个数据集。IndexServlet 的做法是循环向分类列表里填充每类新闻的数据,属于经典的手动组装:
public class IndexServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) { // 1. 查询所有分类 List<NewsType> typeList = newsTypeDao.findAll(); // 2. 查询最新新闻列表 List<News> newestList = newsDao.findTopN(10); // 3. 查询点击量最高的新闻 List<News> hotList = newsDao.findByClickCount(8); req.setAttribute("typeList", typeList); req.setAttribute("newestList", newestList); req.setAttribute("hotList", hotList); req.getRequestDispatcher("/index.jsp").forward(req, resp); } }findTopN 在 DAO 层对应的 SQL 是 ORDER BY publish_time DESC LIMIT 10,findByClickCount 对应 ORDER BY click_count DESC LIMIT 8。两个方法都需要在 SQL 层面控制返回条数,而不是查全表后在 Java 里截取。
JSP 页面里用 JSTL 标签循环输出,注意页面顶部要引入标签库:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <ul> <c:forEach items="${newestList}" var="news"> <li> <a href="news?action=detail&id=${news.id}">${news.title}</a> <span>${news.publishTime}</span> </li> </c:forEach> </ul>itemList 的 key 必须和 request.setAttribute 时的 key 完全一致。新闻标题如果是用户输入的富文本,在列表页输出前建议做 HTML 转义,否则标题里插一段 script 就是存储型 XSS。这是新闻类系统面试时高频被问到的安全问题,源码里如果没做防御,你可以自己加一个简单的 HtmlUtils 处理。
4. 后台管理闭环:NewsTypeServlet 与 ImageUploadServlet 的增删改查和文件上传
4.1 NewsTypeServlet 分类维护的完整 CRUD 流程
后台管理里新闻分类维护是最典型的 CRUD 场景。NewsTypeServlet 走的还是 action 分发模式,没有用独立的方法路由。新增分类和编辑分类都走 save action,通过是否携带 id 参数判断是插入还是更新:
protected void doPost(HttpServletRequest req, HttpServletResponse resp) { request.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); if ("add".equals(action)) { // 把表单数据封装成 NewsType 对象,调用 DAO 插入 NewsType type = new NewsType(); type.setName(req.getParameter("name")); newsTypeDao.save(type); resp.sendRedirect("type?action=list"); } else if ("update".equals(action)) { // 必须有 id,否则无法定位记录 int id = Integer.parseInt(req.getParameter("id")); NewsType type = newsTypeDao.findById(id); type.setName(req.getParameter("name")); newsTypeDao.update(type); resp.sendRedirect("type?action=list"); } }这里有一个非常重要的细节:doPost 里第一行必须设置 request 编码,否则表单提交的中文分类名到后端全是问号。还有 update 操作必须先按 id 查出原对象,再把新名字 set 上去,避免把其他字段误置为 null。源码里如果直接 new 对象只填 id 和 name,那么旧记录的备注、排序字段就会丢。
删除分类时要注意,外键约束默认 RESTRICT 会导致删除失败。源码里如果没有先删除该分类下所有新闻再删分类的逻辑,那后台点击删除按钮就会报 SQLIntegrityConstraintViolationException。建议改成逻辑删除——给 category 表加一个 status 字段,删除时 UPDATE status = 0,新闻列表查询时只展示 status = 1 的分类。这样既不破坏外键关系,也不丢历史新闻。
4.2 ImageUploadServlet 文件上传与存储路径的决定性细节
图片上传是新闻发布系统里的高频功能,稿件封面、正文插图都依赖它。这套项目里的 ImageUploadServlet 用的是 Servlet 3.0 的 Part 接口,没有引入 commons-fileupload 外部库,所以 web.xml 里对应 Servlet 声明需要配置 multipart 支持:
@MultipartConfig( maxFileSize = 5 * 1024 * 1024, // 单个文件最大 5MB maxRequestSize = 10 * 1024 * 1024, // 整个请求最大 10MB fileSizeThreshold = 1024 * 1024 // 超过 1MB 写入临时文件 ) public class ImageUploadServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) { Part part = req.getPart("file"); // 表单里文件域的 name String fileName = extractFileName(part); // 保存到指定目录 String uploadPath = getServletContext().getRealPath("/upload"); part.write(uploadPath + File.separator + fileName); // 返回可访问的 URL 给前端 resp.getWriter().write("upload/" + fileName); } }extractFileName 是封装的工具方法,核心逻辑是截取 Part 头里 filename= 的值。这里最大的坑是 getRealPath("/upload") 拿到的其实是项目部署目录,在 IDEA 里跑的时候是 target 目录下的临时路径,重启 Tomcat 后上传的图片就没了。我一般会改成配置文件里指定绝对路径,比如 D:/news_upload/,然后把这个路径映射成虚拟目录给前端访问。另一种做法是在 Tomcat 的 server.xml 里配 Context 的虚拟路径映射,但那样会让项目换个环境就起不来,不利于答辩演示。
上传文件名的处理上,源码里如果直接把用户原始文件名写到磁盘,两个用户传同名图片就会互相覆盖。要在服务端做唯一化处理:
String ext = fileName.substring(fileName.lastIndexOf(".")); String uuidName = UUID.randomUUID().toString().replace("-", "") + ext;用 UUID 重新命名后存盘,数据库里只存 uuidName,前端回显时拼上访问前缀就能显示。注意 ext 要控制后缀类型,只允许 jpg、png、gif,防止有人传 jsp 文件伪装成图片,配合 Tomcat 目录执行权限问题就是拿站姿势。
5. 避坑记录:Tomcat 版本、字符集、SQL 与上传路径的四个坑
5.1 Servlet 类编译报错或页面 404:Tomcat 10 的 jakarta 命名空间
现象:项目导入 IDEA 后一堆 Servlet 相关的 import 全部飘红,编译都过不去;或者能编译但启动后访问页面直接 404,Tomcat 日志里提示 ClassNotFoundException: javax.servlet.ServletException。
原因:Tomcat 10 开始把 Servlet API 的包名从 javax.servlet 迁移到了 jakarta.servlet,传统 JavaWeb 项目用的是 javax.servlet.http.HttpServlet,在 Tomcat 10 里根本找不到类。现在很多教程默认配 Tomcat 9 以下,新手如果自己下载了最新版 Tomcat 10,就会撞上这个坑。
解决:下载 Tomcat 9 重新配置 IDEA 的 Run Configuration,把 Server 指向 Tomcat 9 的目录。如果想在 Tomcat 10 上跑,就得把所有 import 批量替换成 jakarta.servlet,工作量大且容易漏。
5.2 中文全部变成问号:请求编码与连接串编码双重缺失
现象:列表页标题和内容正常,但后台新增新闻填了中文,保存后数据库里存的是 ??,前台显示也全是问号。
原因:两层问题叠加。第一层是请求没设置编码,Tomcat 默认用 ISO-8859-1 解码 POST 表单,中文天然乱码;第二层是 JDBC 连接串没加 characterEncoding=UTF-8,即使请求编码对了,数据插入 MySQL 时还是按错误字符集转换。
解决:写一个全局 Filter 设置 request 和 response 的编码:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); chain.doFilter(request, response); }JDBC 连接串完整写法是:
jdbc:mysql://localhost:3306/news?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false数据库连接串里没带 serverTimezone 的还会在 MySQL 8.0 下报时区错误,这也是常见附加坑。
5.3 分页第二页开始空白:limit 偏移量写死
现象:首页第一页新闻正常显示,点击“下一页”后 URL 变成了 ?action=list&page=2,但页面还是第一页的数据,或者直接空白。
原因:DAO 层的分页 SQL 把偏移量写死了,比如 LIMIT 0, 5,page 参数传进来完全没用;另一种是页面上的上一页/下一页链接没有拼接 page 参数,每次点击都从默认的 page=1 开始。
解决:把页码当前值从 request 参数读取,偏移量用 (page-1) * pageSize 计算,且每一条分页链接上都要带当前页参数。调试时在控制台打印最终执行的 SQL,一眼能看出 LIMIT 后面的值对不对。
5.4 上传图片重启后丢失或裂图:保存到了部署临时目录
现象:后台提交新闻带封面图,当时显示正常,但重启 Tomcat 后图片全部 404;或者图片上传后立即回显,刷新页面就裂图。
原因:getServletContext().getRealPath() 在 IDEA 开发模式下解析到的是 target/项目名/ 目录,每次重新构建会清空,图片自然被连带删除。
解决:把保存路径拎到项目外部,比如 Windows 下固定为 D:/newsUpload/,数据库里存相对路径,前端使用独立的文件访问 URL。部署到生产环境时再根据服务器磁盘重新规划路径,但代码里不要出现相对路径拼接。
6. 部署与验证:在 IDEA 里把整套 JavaWeb 新闻系统跑起来的关键三步
拿到源码后最快的验证路径是:先导数据库,再配 Tomcat,最后过一遍三个核心页面。先说数据库部分。新建一个 news 库,导入项目里的 SQL 文件,然后确认连接配置——看 db.properties 或 JDBC 工具类里的用户名密码是否和本地 MySQL 一致。这里要确认 MySQL 服务真的在运行,用 Navicat 能连上不代表 IDEA 里能连上,要看连接串的端口和编码参数。
然后配置 IDEA 的 Run Configuration。Project Structure 里把 Artifact 设置为 Web Application Exploded,点击右侧的坑位图标——Deployment 里选这个 Artifact,Application context 填 /news。Server 选项卡里配好 Tomcat 9 的路径。系统里已经装 Tomcat 10 的记得先换掉,这是复现成本最低的版本选择。
启动后在浏览器里依次验证三个页面:
| 验证点 | 访问地址 | 通过标准 |
|---|---|---|
| 前台首页 | http://localhost:8080/news/ | 分类导航显示、新闻列表有数据、图片正常 |
| 新闻详情 | 首页点击任意新闻标题 | 正文完整、评论列表加载 |
| 后台管理 | /admin/login.jsp 或对应登录路径 | 用户名密码登录后能进后台,能新增新闻 |
登录后第一件事是新增一条带中文标题和图片的新闻。如果这条新闻存进去是乱码、图片裂图、或者保存后列表看不到——恭喜你,把第 5 章的坑全部踩了一遍。
从那以后我接手任何 JavaWeb 项目,第一件事永远是看它的 web.xml 版本和 Tomcat 版本,第二件事是确认数据库连接串里的字符集参数,这两个字段没问题,项目基本就能跑通了。这个小习惯帮我避免过至少十次“代码明明没问题但就是跑不起来”的排查,希望帮到你。
本文还有配套的精品资源,点击获取