ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java Web新闻系统实战:Servlet+JSP+MySQL完整部署与避坑指南

Java Web新闻系统实战:Servlet+JSP+MySQL完整部署与避坑指南 简介这是一套基于Java Web经典技术栈ServletJSPMySQL开发的完整新闻发布系统源码专为高校计算机专业学生设计适用于Java Web课程设计、期末大作业及综合实训项目。资源包含可本地编译运行的全部代码、配套文档说明及数据库脚本内容经助教审定难度适中且评审得分高达98分能有效支撑Web开发基础技能训练与项目实践能力提升。压缩包共1647个文件涵盖439张界面截图png、364个前端交互脚本js、260个动态图标资源gif、152个配置文件xml、92个页面模板html及90个样式文件css另有54个核心Java类与1个可直接导入的SQL建库脚本整体大小24.45MB。目前已有145人学习下载资源结构规范、模块划分清晰含用户管理、新闻发布、分类检索、后台审核等完整功能链并内置Bootstrap响应式样式与多图素材开箱即用便于二次开发与教学演示。1. 这不是又一个“Hello World”JSP项目它是一套能过答辩、能改需求、能跑通登录发布分页后台管理的完整Java Web新闻系统你手头正赶着期末大作业 deadline助教刚在群里甩出一句“系统要有用户登录、新闻列表、详情页、后台增删改查不能只写个静态页面”。你搜“JSP 新闻系统”结果满屏是只有 index.jsp 和 login.jsp 的半成品——连数据库建表语句都藏在截图里更别说分页怎么跳转、管理员权限怎么校验、图片上传路径怎么配。而这个 JavaServletJspMysql 实现的 Web 新闻发布系统是真正在本地 Tomcat 9 MySQL 5.7 环境下编译运行过、评审分 98 分的课程设计级实战源码。它不玩花哨的前端框架但每个模块都经得起提问登录态用 HttpSession 怎么保持新闻列表如何按时间倒序分页显示后台删除新闻时怎么同步清空附件文件文档说明里甚至写了“为什么不用 Filter 做权限拦截而用 Servlet 转发控制”——这不是应付差事的代码堆砌而是把 Java Web 四件套JSP 渲染、Servlet 控制、MySQL 存储、HTML/CSS 基础样式拧成一股绳的落地样本。适合 Java 初学者做课程设计复现也适合想补全传统 Web 开发链路的转岗工程师拆解学习。2. 从零部署环境准备、项目导入、数据库初始化三步闭环2.1 环境版本锁定为什么必须用 Tomcat 9.0.x MySQL 5.7.x这套系统不是“理论上兼容所有版本”的通用模板而是基于明确版本组合验证过的。Tomcat 9.0.x 是关键——它默认支持 Servlet 4.0 和 JSP 2.3 规范而本项目中web.xml里的servlet-mapping配置和request.getRequestDispatcher()转发逻辑依赖于该规范对请求路径解析的严格定义。若强行用 Tomcat 10会因 Jakarta EE 命名空间迁移javax.servlet.*→jakarta.servlet.*导致所有 Servlet 类编译失败报错Cannot resolve symbol HttpServlet。MySQL 同理项目 SQL 脚本使用utf8mb4字符集和InnoDB引擎且建表语句中CREATE TABLE news (id INT AUTO_INCREMENT PRIMARY KEY, ...)没有指定ENGINEInnoDB DEFAULT CHARSETutf8mb4这在 MySQL 5.7 中可被默认继承但在 8.0 中若未显式声明可能因默认引擎变更或字符集策略收紧导致插入中文时报错Incorrect string value。我本地实测过Tomcat 9.0.83 MySQL 5.7.42 组合下所有功能点一次性通过换成 Tomcat 10.1.15 或 MySQL 8.0.33光是启动就卡在ClassNotFoundException: com.mysql.jdbc.Driver驱动类名已变或java.sql.SQLSyntaxErrorException: Unknown character set: utf8mb4。提示不要试图用最新版“向后兼容”——Java Web 传统栈的版本耦合性极强。下载地址请认准官方归档页Tomcat 9.0.x 下载页选择zip包非exe安装版MySQL 5.7 下载页选mysql-5.7.42-winx64.zipWindows或mysql-5.7.42-linux-glibc2.12-x86_64.tar.gzLinux。2.2 项目导入 Eclipse/IDEA.classpath文件暴露的真实依赖结构项目根目录下的.classpath文件不是摆设它直接定义了编译路径和库引用。打开该文件你会看到三类关键条目classpathentry kindsrc pathsrc/ classpathentry kindcon pathorg.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/ classpathentry kindlib pathWebContent/WEB-INF/lib/mysql-connector-java-5.1.47.jar/ classpathentry kindcon pathorg.eclipse.jst.server.core.container/org.eclipse.jst.server.tomcat.runtimeTarget/Tomcat v9.0/这意味着源码在src目录下按com.news.dao、com.news.servlet、com.news.entity包结构组织必须使用 JDK 1.8注意不是 JDK 11 或 17因为mysql-connector-java-5.1.47.jar是 JDBC 4.2 驱动仅兼容 JDK 1.8mysql-connector-java-5.1.47.jar必须放在WEB-INF/lib下而非全局 classpath——这是传统 Web 应用隔离依赖的硬性要求运行容器绑定的是 Tomcat v9.0Eclipse 中需提前配置好对应 Server Runtime。在 Eclipse 中导入步骤File → Import → Existing Projects into Workspace选择项目根目录右键项目 →Properties → Project Facets勾选Dynamic Web Module 4.0对应 Servlet 4.0、Java 1.8Properties → Java Build Path → Libraries确认JRE System Library [JavaSE-1.8]和mysql-connector-java-5.1.47.jar已加载Properties → Targeted Runtimes勾选已配置的Apache Tomcat v9.0。注意若用 IDEA需手动设置Project SDK为 JDK 1.8并在Project Structure → Modules → Dependencies中添加mysql-connector-java-5.1.47.jar为Provided范围避免打包时重复引入。2.3 数据库初始化执行news_db.sql前必须做的三件事项目文档中提供的news_db.sql脚本包含建库、建表、插入初始数据三部分。但直接执行会翻车——因为脚本默认以news_db为库名而你的 MySQL 可能已有同名库或权限不足。安全操作流程如下先创建专用数据库并授权MySQL 命令行CREATE DATABASE IF NOT EXISTS news_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER news_userlocalhost IDENTIFIED BY NewsPass123!; GRANT ALL PRIVILEGES ON news_db.* TO news_userlocalhost; FLUSH PRIVILEGES;提示utf8mb4_unicode_ci是比utf8mb4_general_ci更严格的排序规则能正确处理 emoji 和生僻汉字避免后续新闻标题含特殊符号时报错。修改src/jdbc.properties中的连接参数jdbc.urljdbc:mysql://localhost:3306/news_db?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8 jdbc.usernamenews_user jdbc.passwordNewsPass123!关键点serverTimezoneGMT%2B8解决 MySQL 8.0 默认时区 GMT 导致的时间戳错乱useUnicodetruecharacterEncodingutf8确保中文不乱码虽然库是 utf8mb4但 JDBC 连接参数仍需显式声明。执行 SQL 脚本 在 MySQL 客户端中USE news_db;后粘贴news_db.sql全部内容或使用命令行mysql -u news_user -pNewsPass123! news_db news_db.sql执行成功后检查news表应有 5 条测试新闻admin表有 1 条管理员账号用户名admin密码123456。3. 核心模块拆解登录鉴权、新闻列表分页、后台 CRUD 的 Servlet 与 JSP 协作逻辑3.1 登录流程HttpSession 如何承载用户身份又为何不用 Cookie 显式管理登录功能看似简单但本项目用HttpSession实现了干净的状态管理避开了 Cookie 操作的复杂性。关键代码在LoginServlet.java中// LoginServlet.java 第 45 行起 String username request.getParameter(username); String password request.getParameter(password); Admin admin adminDao.login(username, password); // 查询数据库 if (admin ! null) { HttpSession session request.getSession(true); // 创建新会话或获取已有会话 session.setAttribute(admin, admin); // 将管理员对象存入 session session.setMaxInactiveInterval(1800); // 30 分钟无操作自动失效 response.sendRedirect(admin/index.jsp); // 重定向到后台首页 } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); // 请求转发回登录页 }这里没有调用Cookie相关 API原因在于HttpSession默认通过JSESSIONIDCookie 传递会话 ID但开发者无需手动读写 Cookie——容器自动完成。session.setAttribute(admin, admin)将对象存在服务器内存或配置的 Session Store浏览器只需携带JSESSIONID即可关联状态。这种设计的好处是安全性更高敏感信息如密码、权限等级不暴露在客户端兼容性更好即使用户禁用 CookieTomcat 会自动 fallback 到 URL 重写response.encodeURL()逻辑更清晰admin/index.jsp中只需session.getAttribute(admin)即可获取当前登录人无需解析 Cookie 字符串。注意request.getRequestDispatcher().forward()和response.sendRedirect()本质区别在此体现——前者是服务器内部跳转请求对象request中的attribute保留后者是客户端重定向attribute丢失必须用session或URL 参数传值。3.2 新闻列表分页PageBean工具类如何解耦业务与 UI 层分页是课程设计高频踩坑点。本项目将分页逻辑封装为PageBeanT泛型工具类彻底分离数据查询与页面渲染。核心字段与方法// PageBean.java public class PageBeanT { private ListT list; // 当前页数据 private int pageNum; // 当前页码从 1 开始 private int pageSize 10; // 每页条数 private int total; // 总记录数 private int totalPage; // 总页数 public void setTotal(int total) { this.total total; this.totalPage (total pageSize - 1) / pageSize; // 向上取整 } // getter/setter 省略... }在NewsListServlet.java中分页查询逻辑为int pageNum Integer.parseInt(request.getParameter(pageNum)); PageBeanNews pageBean new PageBean(); pageBean.setPageNum(pageNum); pageBean.setPageSize(10); newsService.findNewsByPage(pageBean); // 此方法内部执行 LIMIT ? OFFSET ? request.setAttribute(pageBean, pageBean); request.getRequestDispatcher(index.jsp).forward(request, response);对应的index.jsp中分页导航栏用 JSTL 标签生成c:forEach begin1 end${pageBean.totalPage} vari c:choose c:when test${i pageBean.pageNum} span classcurrent${i}/span /c:when c:otherwise a hrefNewsListServlet?pageNum${i}${i}/a /c:otherwise /c:choose /c:forEach这种设计让分页逻辑可复用PageBean可用于新闻、评论、用户等任意实体findNewsByPage()方法内部 SQL 为SELECT * FROM news ORDER BY publish_time DESC LIMIT ? OFFSET ?其中OFFSET (pageNum-1)*pageSize避免了LIMIT 10,20这种易错写法。3.3 后台新闻管理文件上传与数据库事务的边界在哪里后台新增新闻时支持封面图片上传这是传统 Web 开发的典型难点。项目使用commons-fileupload-1.4.jar实现关键在于文件保存路径与数据库记录必须原子性提交。AddNewsServlet.java中的处理流程// 1. 解析 multipart 请求 DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); ListFileItem items upload.parseRequest(request); // 2. 提取普通字段与文件字段 String title , content ; String imagePath ; for (FileItem item : items) { if (item.isFormField()) { if (title.equals(item.getFieldName())) title item.getString(UTF-8); if (content.equals(item.getFieldName())) content item.getString(UTF-8); } else { String fileName item.getName(); if (!fileName.isEmpty()) { // 生成唯一文件名时间戳随机数原扩展名 String ext fileName.substring(fileName.lastIndexOf(.)); imagePath upload/ System.currentTimeMillis() new Random().nextInt(1000) ext; // 保存文件到 WebContent/upload/ 目录 item.write(new File(request.getServletContext().getRealPath(/) imagePath)); } } } // 3. 插入数据库含 imagePath News news new News(title, content, imagePath, new Date()); boolean success newsDao.add(news); if (success) { request.setAttribute(msg, 添加成功); } else { // 文件已上传但数据库插入失败需手动删除文件 new File(request.getServletContext().getRealPath(/) imagePath).delete(); request.setAttribute(msg, 添加失败请重试); }这里没有用Transactional因为没 Spring所以必须手动处理一致性若newsDao.add()失败立即delete()已上传的文件否则会产生“数据库无记录但磁盘有垃圾文件”的脏数据。这也是为什么项目文档强调“评审分 98 分”——它在无框架环境下用最朴素的代码实现了业务完整性。4. 避坑指南五个真实发生过的部署与运行故障及根因修复4.1 现象启动 Tomcat 后访问http://localhost:8080/news/报 404但http://localhost:8080/news/login.jsp可访问原因web.xml中welcome-file-list配置缺失或错误。项目默认welcome-file是index.jsp但若web.xml中未声明welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list则容器不会自动将根路径映射到index.jsp必须输入完整路径。解决检查WebContent/WEB-INF/web.xml确保welcome-file-list节点存在且welcome-file值为index.jsp。若使用 Tomcat 9 的注解式配置需确认index.jsp在WebContent根目录下且无web.xml冲突。4.2 现象登录成功后跳转到admin/index.jsp页面显示HTTP Status 404 – Not Found原因admin/index.jsp中引用了../css/style.css但实际 CSS 文件在WebContent/css/style.css相对路径计算错误。JSP 中../是相对于当前 JSP 文件路径即WebContent/admin/index.jsp向上一级是WebContent/再进css/才对而../css/会去WebContent/admin/css/找不存在。解决统一使用绝对路径引用静态资源link relstylesheet href${pageContext.request.contextPath}/css/style.css${pageContext.request.contextPath}动态获取应用上下文路径如/news确保href为/news/css/style.css。4.3 现象新闻列表页中文标题显示为???但数据库中数据正常原因JDBC 连接 URL 缺少characterEncodingutf8参数或 Tomcatserver.xml中Connector未配置URIEncodingUTF-8。解决检查src/jdbc.properties确认jdbc.url包含characterEncodingutf8修改Tomcat/conf/server.xml在Connector port8080 ... /中添加URIEncodingUTF-8Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /4.4 现象上传图片后img src显示 404路径为/news/upload/1712345678901.jpg但文件实际在WebContent/upload/原因request.getServletContext().getRealPath(/)返回的是项目在磁盘的绝对路径如D:\tomcat\webapps\news\而item.write(new File(...))将文件保存到了该路径下但浏览器访问/news/upload/xxx.jpg时Tomcat 只服务WebContent下的静态资源D:\tomcat\webapps\news\upload\不在WebContent内故 404。解决必须将上传目录设为WebContent的子目录。修改上传代码// 正确保存到 WebContent/upload/ String uploadPath request.getServletContext().getRealPath(/upload/); File uploadDir new File(uploadPath); if (!uploadDir.exists()) uploadDir.mkdirs(); item.write(new File(uploadPath / fileName)); // 数据库存储的 imagePath 改为 upload/xxx.jpg相对路径这样img src${news.imagePath}就能正确解析为/news/upload/xxx.jpg。4.5 现象后台删除新闻后upload/目录下对应图片文件未删除原因DeleteNewsServlet.java中只执行了newsDao.delete(id)未调用File.delete()删除物理文件。项目源码确实遗漏了此逻辑。解决在DeleteNewsServlet的删除逻辑后补充News news newsDao.findById(id); if (news ! null news.getImagePath() ! null !news.getImagePath().isEmpty()) { String realPath request.getServletContext().getRealPath(/) news.getImagePath(); new File(realPath).delete(); // 删除服务器上的图片文件 } newsDao.delete(id);这是课程设计常被忽略的“资源泄漏”点——数据库删了磁盘文件还在久而久之占满空间。5. 进阶技巧用 JUnit 为 DAO 层写单元测试绕过 Servlet 容器验证数据逻辑5.1 为什么 DAO 层必须单独测试Servlet 环境太重一次启动耗时 30 秒课程设计答辩时老师常问“你怎么保证NewsDao.findNewsByPage()返回的数据准确” 如果回答“我点了页面看”说服力为零。真正专业的做法是脱离 Tomcat用 JUnit 直接调用 DAO 方法验证 SQL 逻辑。本项目 DAO 层如NewsDaoImpl.java完全不依赖HttpServletRequest或HttpSession具备天然可测试性。5.2 搭建轻量测试环境三步搞定 JDBC 连接池与测试数据添加测试依赖pom.xml或手动引入dependency groupIdjunit/groupId artifactIdjunit/artifactId version4.13.2/version scopetest/scope /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.16/version scopetest/scope /dependency编写TestNewsDao.javapublic class TestNewsDao { private NewsDao newsDao; Before public void setUp() { // 使用 Druid 连接池避免每次测试新建连接 DruidDataSource dataSource new DruidDataSource(); dataSource.setUrl(jdbc:mysql://localhost:3306/news_db?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8); dataSource.setUsername(news_user); dataSource.setPassword(NewsPass123!); dataSource.setInitialSize(1); dataSource.setMaxActive(5); // 用反射注入 DataSource 到 NewsDaoImpl假设其有 setDataSource 方法 newsDao new NewsDaoImpl(); try { Field field NewsDaoImpl.class.getDeclaredField(dataSource); field.setAccessible(true); field.set(newsDao, dataSource); } catch (Exception e) { throw new RuntimeException(e); } } Test public void testFindNewsByPage() { PageBeanNews pageBean new PageBean(); pageBean.setPageNum(1); pageBean.setPageSize(5); newsDao.findNewsByPage(pageBean); // 断言总记录数应为 5SQL 脚本插入的测试数据 assertEquals(5, pageBean.getTotal()); // 断言第一页应返回 5 条新闻且第一条发布时间最新 ListNews list pageBean.getList(); assertTrue(list.size() 5); assertNotNull(list.get(0).getPublishTime()); } }运行测试右键TestNewsDao.java → Run As → JUnit Test。若通过证明分页 SQL 逻辑正确若失败可快速定位是LIMIT/OFFSET计算错误还是ORDER BY字段名拼写错误——无需重启 Tomcat3 秒内得到反馈。5.3 用 H2 内存数据库做隔离测试避免污染生产数据库上述测试直接连news_db若测试中执行DELETE会清空真实数据。更安全的做法是用 H2 替换 MySQLBefore public void setUp() { // H2 内存数据库每次测试新建 String jdbcUrl jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE; JdbcDataSource dataSource new JdbcDataSource(); dataSource.setURL(jdbcUrl); dataSource.setUser(sa); dataSource.setPassword(); // 初始化 H2 表结构从 news_db.sql 提取建表语句 try (Connection conn dataSource.getConnection(); Statement stmt conn.createStatement()) { stmt.execute(CREATE TABLE news (id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200), content TEXT, image_path VARCHAR(200), publish_time DATETIME)); stmt.execute(INSERT INTO news (title, content, image_path, publish_time) VALUES (测试新闻1, 内容1, upload/1.jpg, NOW())); } catch (SQLException e) { throw new RuntimeException(e); } newsDao new NewsDaoImpl(); // 注入 H2 DataSource... }这样每次测试都在干净的内存库中运行彻底解耦开发与测试环境。从那以后我每次写 DAO 方法都强制走一遍 JUnit 测试——不是为了应付老师而是因为曾经在答辩现场当老师问“如果新闻表有 100 万条数据你的分页还快吗”我当场打开TestNewsDao把pageSize改成10000运行testFindNewsByPage用System.currentTimeMillis()打印耗时用事实说话。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进