Java+Servlet+JSP+MySQL新闻发布系统:从架构到实现全解析
发布时间:2026/9/24 22:34:15来源:尧图网络
简介JavaServletJSPMySQL实现的Web新闻发布系统是一份完整的项目源码与部署素材包面向Java Web初学者及有课程设计需求的在校生帮助理解基于MVC架构的新闻管理流程涵盖用户登录、新闻发布、编辑展示和数据持久化等核心环节。压缩包共269个文件包含56个Java源码、50个JSP页面、112个编译后的class文件以及数据库SQL脚本、配置文件、JAR依赖和少量图片样式资源整体约6.13MB结构清晰便于导入IDE运行调试。资源还提供了数据库建表语句和部署配置说明读者可借此学习JDBC连接MySQL、使用Servlet处理请求、通过JSP渲染动态页面的完整开发链路。目前已有3432人学习下载适合用来对照练习项目搭建、熟悉前后端交互与常见安全问题如SQL注入防护也可作为毕业设计或课设的参考基础。1. JavaServletJSPMySQL 新闻发布系统先说清楚这份项目能做什么做 Java Web 的实训和课程设计绕不开 Servlet 与 JSP 这组老搭档。本次拆解的这份「JavaServletJSPMySQL 实现 Web 新闻发布系统」看起来只是新闻增删改查实际把登录、列表、发布、图片上传收进了一条完整链路正好覆盖 Servlet 生命周期、JSP 与 Servlet 分工、JDBC 防注入、MVC 分层这些高频考点。它能做两件事给你一套可运行的新闻管理脚手架同时让「请求哪里进、数据哪里存、页面谁渲染」一目了然。适合正在做课程设计或毕业设计的学生也适合想系统回顾 JSPServlet 原理的初级 Java 开发者。下文把这套系统从架构到实现到排错逐层拆清楚。2. 请求流转与 MVC 分层Servlet、JSP、JDBC 各管哪一段2.1 一次新闻请求的完整流转打开系统首页浏览器地址栏输入http://localhost:8080/news/index.jsp这一步背后并不只是读一个页面。Tomcat 收到请求后先判断这个 URL 对应的是静态资源还是一个已注册的 Servlet如果映射到了 Servlet容器会找到对应类并调用它的doGet或doPost方法。以新闻详情为例请求通常长这样/news?id12它命中 NewsSerlvet注意原项目类名就是 Serlvet不是 Servlet后面统一按这个拼写说。WebServlet(/news) public class NewsSerlvet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String id req.getParameter(id); // 省略参数校验与异常处理常见做法是交给 Service/DAO 层 News news new CommonDAO().findNewsById(Integer.parseInt(id)); req.setAttribute(news, news); req.getRequestDispatcher(/news_detail.jsp).forward(req, resp); } }这段代码展示了从参数接收到页面转发的完整动作。getParameter拿查询串里的idCommonDAO().findNewsById()负责把那条新闻从 MySQL 里捞出来setAttribute(news, news)把结果塞进 request 作用域最后forward到 JSP。注意这里用的是forward而不是sendRedirect转发是服务器内部跳转浏览器地址栏不变request 里放的数据还能继续读到重定向是浏览器重新发起一次请求request 里的东西就白放了。整个链路串起来就是我常说的「黑匣子思维」——浏览器只看到最终 HTML中间的 Servlet、DAO、JSP 对它是不可见的。排错时要沿着这条链逐层查URL 有没有命中映射、Servlet 有没有报错、DAO 查到没查到、JSP 渲染有没有抛异常每一层都能用一个断点或一行日志确认。2.2 为什么控制器必须用 Servlet而不是在 JSP 里写 Java早期 JSP 项目有一种很流行的写法直接在 JSP 里用% ... %查数据库、写for循环拼新闻列表。页面跑起来没问题但维护超过三个页面就开始难受。Java 代码和 HTML 混在一起改一个页面样式都可能碰到数据库逻辑出了问题很难定位面试官看到这种代码也会直接摇头。MVC 的思路就是把这件事拆开Servlet 做控制器只负责收请求、调逻辑、决定下一步去哪JSP 做视图只负责把数据渲染成 HTML模型层由 POJO 加 DAO 构成负责业务规则和数据访问。这样的好处很实际——前端调样式不动 Java 代码换数据库只动 DAO加一个页面只需新增一个 Servlet 映射。本项目里Index.class管首页与列表展示NewsSerlvet.class管新闻详情OperatorServlet.class管所有写操作CommonDAO.class管全部数据库访问正是典型的 MVC 拆分。另一个容易在面试里被问到的点是doGet和doPost到底怎么选。GET 适合无副作用的查询参数会出现在 URL 上POST 适合提交数据参数在请求体里。Servlet 容器收到请求后按 method 分发到对应方法。如果要在doPost里复用doGet的逻辑可以直接调doGet(req, resp)反过来则不建议因为 POST 天然比 GET 更适合承载写操作。项目里但凡涉及发布、删除、上传表单一律写methodpost这是底线。2.3 项目里的类划分从五个 Class 文件反推架构拿到的项目包里是编译后的 class 文件文件名已经暴露了作者的设计意图。用一张表把职责对应清楚类名定位典型职责Index首页控制器查询新闻列表转发给 index.jspNewsSerlvet详情控制器按 id 查询单条新闻渲染详情页OperatorServlet操作控制器统一处理发布、编辑、删除等写操作CommonDAO数据访问层封装 JDBC 的增删改查隐藏 SQL 细节PhotoUpload上传处理接收图片、保存文件、返回图片路径这个拆法有两个值得注意的点。第一读操作和写操作分家了两个展示类的 Servlet 负责页面一个 OperatorServlet 像网关一样收敛所有写请求。第二所有数据库操作被 CommonDAO 包住上层 Servlet 不直接碰 Connection 和 SQL将来要换数据库或接连接池改动被限制在一个类里。这跟我自己动手的习惯一致宁可 DAO 大一点也别让每个 Servlet 都写一遍DriverManager.getConnection。对应的 URL 映射通常落在web.xml或WebServlet注解里。如果用 web.xml结构类似这样servlet servlet-namenewsServlet/servlet-name servlet-classcom.news.controller.NewsSerlvet/servlet-class /servlet servlet-mapping servlet-namenewsServlet/servlet-name url-pattern/news/url-pattern /servlet-mapping一个 servlet 节点配一个 mapping 节点前者告诉容器「这个类归 web 应用管」后者告诉容器「哪个 URL 进来找它」。新手最容易漏的是只写了 servlet 没写 mapping或者映射路径和表单提交地址对不上结果就是页面点按钮报 404。看到这里可以把这几个类对照自己的项目画一遍请求路径图后面排错会快很多。3. 建表与 JDBC 层把新闻数据存进 MySQL 的关键代码3.1 新闻表与用户表怎么设计新闻系统的核心数据就两大部分新闻本身和管理员/用户。建表我会用 utf8mb4 字符集原因很直接utf8mb4 兼容中文、生僻字和 emoji而且数据迁移时基本不会出乱子建表语句里顺手把排序规则设好避免后面查出来中文排序奇怪。表结构不用复杂够用就行。CREATE DATABASE IF NOT EXISTS news_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE news_db; CREATE TABLE news ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 新闻标题, content TEXT NOT NULL COMMENT 新闻正文, author VARCHAR(50) DEFAULT admin COMMENT 作者, image_path VARCHAR(255) DEFAULT NULL COMMENT 封面图路径, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 发布时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码建议存密文, role VARCHAR(20) DEFAULT admin COMMENT 角色admin/user ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个字段设置值得说明。title用 VARCHAR(200) 而不是 TEXT因为标题不需要那么长而且索引友好content必须用 TEXT新闻正文几百字很正常。created_at用 TIMESTAMP 默认当前时间插入时就不用再手动塞时间。image_path只存一个相对路径不要直接把图片二进制写进数据库不然后面查询会越来越慢。password字段我建议至少存 MD5/SHA-256 后的值生产环境再用加盐方案文本里直接写明文密码是妥妥的隐患。另外如果登录用户很多sys_user.username的唯一索引很关键如果新闻量大news.created_at上加普通索引能明显加速按时间倒序的列表查询。对课设级别的项目来说这些够了。3.2 CommonDAO把 JDBC 连接收敛到一个类JDBC 写法的坑不在 SQL而在连接管理。比较稳妥的写法是把连接 URL、用户名、密码、驱动类名都放在 CommonDAO 里上层 Servlet 不感知 MySQL 的存在。下面是简化版的 DAO 骨架package com.news.dao; import java.sql.*; import java.util.ArrayList; import java.util.List; public class CommonDAO { private static final String URL jdbc:mysql://localhost:3306/news_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } private Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public ListNews findAllNews() { ListNews list new ArrayList(); String sql SELECT id, title, content, author, image_path, created_at FROM news ORDER BY created_at DESC; try (Connection conn getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { News n new News(); n.setId(rs.getInt(id)); n.setTitle(rs.getString(title)); n.setContent(rs.getString(content)); n.setAuthor(rs.getString(author)); n.setImagePath(rs.getString(image_path)); n.setCreatedAt(rs.getTimestamp(created_at)); list.add(n); } } catch (SQLException e) { throw new RuntimeException(查询新闻列表失败, e); } return list; } }这里的静态代码块负责注册驱动保证类第一次被加载时驱动只初始化一次。URL串里的参数分别解决三件事useUnicodetruecharacterEncodingutf8让中文从数据库到 Java 不乱码serverTimezoneAsia/Shanghai解决 MySQL 8.x 的时区报错。try(...)是 Java 7 的 try-with-resourcesConnection、Statement、ResultSet 会自动关闭比在 finally 里一个个 close 省心也不会漏关连接。生产环境我不会直接DriverManager.getConnection而是换成连接池但课设和入门这个写法足够清晰。3.3 用 PreparedStatement 防止 SQL 注入如果查询带用户输入比如按关键字搜新闻、按 id 看详情就不该用Statement拼接 SQL 了。拼字符串的写法形如SELECT * FROM news WHERE id id一旦 id 传进来的是1 OR 11整张表都会被查出来。项目里应该用PreparedStatement参数用?占位再通过setXxx绑定。public News findNewsById(int id) { String sql SELECT * FROM news WHERE id ?; try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { News n new News(); n.setId(rs.getInt(id)); n.setTitle(rs.getString(title)); n.setContent(rs.getString(content)); n.setAuthor(rs.getString(author)); return n; } } } catch (SQLException e) { throw new RuntimeException(按 id 查询新闻失败, e); } return null; }ps.setInt(1, id)会把整数按数据库类型安全传过去驱动层自动处理转义恶意字符串就不会被当成 SQL 执行。另一个看不出来的好处是性能同一 SQL 结构多次执行时数据库可以复用执行计划比每次重新解析整条 SQL 快。真正的教训是养成习惯——凡是方法里有参数要进 SQL一律走PreparedStatement不要问能不能拼先默认不能。提示MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.DriverMySQL 5.x 才是com.mysql.jdbc.Driver两者写错都会在启动时直接抛ClassNotFoundException。4. 发布、编辑、删除与图片上传Servlet 侧的功能落地4.1 OperatorServlet 的 action 分发模式读操作用两个 Servlet 就够了写操作会越来越多发布新闻、编辑新闻、删除新闻、处理登录。如果每个操作都建一个 Servletweb.xml 会变得又长又乱。常见做法是共用一个OperatorServlet用表单里的action字段区分要做的事这个模式在大部分课设和实训项目里都能见到。WebServlet(/operator) public class OperatorServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String action req.getParameter(action); if (add.equals(action)) { String title req.getParameter(title); String content req.getParameter(content); String author req.getParameter(author); new CommonDAO().insertNews(title, content, author); } else if (update.equals(action)) { int id Integer.parseInt(req.getParameter(id)); String title req.getParameter(title); String content req.getParameter(content); new CommonDAO().updateNews(id, title, content); } else if (delete.equals(action)) { int id Integer.parseInt(req.getParameter(id)); new CommonDAO().deleteNews(id); } resp.sendRedirect(req.getContextPath() /index.jsp); } }action参数是个不值钱但很有效的设计一个 Servlet 的方法里做分发比新建 N 个类容易维护。注意req.setCharacterEncoding(UTF-8)必须放在读取任何参数之前放晚了就无效resp.sendRedirect的路径要带req.getContextPath()否则项目部署在带上下文的路径下会跳错。对应的表单得带一个隐藏域form action${pageContext.request.contextPath}/operator methodpost input typehidden nameaction valueadd label标题/label input typetext nametitle label内容/label textarea namecontent/textarea label作者/label input typetext nameauthor button typesubmit发布/button /form表单里每个输入控件的name是 Servlet 端getParameter(title)的依据。新手常见的翻车是name写错比如写成titles、conentServlet 拿到的就是 null插入数据库时直接报 SQL 错误。页面和 Java 的参数名保持一致是这个项目里最基础也对齐成本最低的约定。4.2 PhotoUploadmultipart 表单与文件保存图片上传在新闻系统里属于「看着简单、做起来坑多」的模块。前提是表单必须声明enctypemultipart/form-data否则文件内容只会以普通文本传上来。Tomcat 8 及以上可以用Part接口处理不需要额外引入第三方库但必须加MultipartConfig注解否则运行时会报getParts()不支持。form action${pageContext.request.contextPath}/photoUpload methodpost enctypemultipart/form-data input typefile namephoto acceptimage/* button typesubmit上传封面/button /form对应的 Servlet 侧大致这样WebServlet(/photoUpload) MultipartConfig public class PhotoUpload extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Part part req.getPart(photo); String fileName Paths.get(part.getSubmittedFileName()).getFileName().toString(); String savePath /upload/ System.currentTimeMillis() _ fileName; part.write(getServletContext().getRealPath(savePath)); req.setAttribute(imagePath, savePath); req.getRequestDispatcher(/upload_success.jsp).forward(req, resp); } }这里有几个细节值得讲。part.getSubmittedFileName()拿到的可能带着客户端路径用Paths.get(...).getFileName()再取一次能去掉浏览器传来的目录前缀文件名前面拼时间戳是为了避免两个用户传了同名文件互相覆盖。getServletContext().getRealPath(savePath)返回的是部署目录下的真实磁盘路径part.write会把文件写到那儿。但这条路有一个坑写进部署目录的文件在 IDE 里重启 Tomcat 时可能被清理掉因为部署目录每次可能重新生成。我一般会改成把图片存到服务器固定目录比如/data/news_images再在 Tomcat 里配一层虚拟映射让/upload/**指到那个磁盘目录。课设阶段用getRealPath省事但要知道这个限制在哪。4.3 JSP 渲染列表页、详情页与模板标签JSP 页面里尽量用 EL 表达式和 JSTL 标签少直接写 Java 脚本。列表页的核心是拿到 Servlet 放进去的集合循环输出% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:forEach items${newsList} varnews div classnews-item h3a href${pageContext.request.contextPath}/news?id${news.id} ${news.title}/a/h3 p${news.author} / ${news.createdAt}/p /div /c:forEachitems${newsList}对应 Servlet 里req.setAttribute(newsList, list)的键名varnews是循环变量的名字循环体内${news.title}就是在调 News 对象的 getter。这里能跑通的前提是 News 类提供了标准的 getter/setterJSP 的 EL 表达式本质上是反射调方法。如果页面报 500优先看两点是否在页面顶部引入了 JSTL 的 taglib缺少会直接编译失败newsList是否真的被放进 request 了最常用的排查办法是临时在页面输出${newsList}如果显示空白多半是 Servlet 转发路径或 setAttribute 键名写错了。把列表、详情、发布三个页面走通这个系统的核心展示流程就闭环了。4.4 编辑回显把旧数据带回表单编辑和发新的差别在于进编辑页时要先把旧新闻查出来再把值填进表单。Servlet 侧收到 edit 请求时查出 News 对象setAttribute 到 request再 forward 到 edit.jsp。JSP 里把 value 写成${news.title}注意不要直接写死成空字符串。input typetext nametitle value${news.title} textarea namecontent${news.content}/textarea input typehidden nameaction valueupdate input typehidden nameid value${news.id}这里的隐藏域是编辑功能能不能正确落库的关键action告诉 OperatorServlet 走 update 分支id告诉 DAO 改哪一行。有人图省事把action写在 URL 参数里形如/operator?actionupdate也不是不行但隐藏域和表单字段放在一起语义上更清楚也更容易维护。实现完记得测一遍「查询 → 编辑 → 回显 → 保存」的完整闭环回显数据没出来多半是news这个 attribute 的键名在 Servlet 和 JSP 两边不一致。5. 部署、运行与调试一份避坑清单这一段是拿这套系统最容易踩坑的地方。以下问题我都在实训项目里见过至少十次逐条对应现象、原因和解决办法。5.1 项目能启动访问首页却 404现象Tomcat 正常启动数据库也有数据浏览器输入http://localhost:8080/news/却提示 404。原因分几类项目没被正确发布成 web 应用或者默认首页配置指向了不存在的文件。Tomcat 找欢迎页有固定顺序它会在web.xml里找welcome-file-list找不到再去根目录找 index.jsp/index.html。另外WEB-INF 目录下的页面不能通过浏览器直接访问这是容器层面的安全限制页面要么放在 webapp 根目录下要么由 Servlet forward 过去。解决在web.xml中显式声明欢迎页。welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list同时确认项目部署路径。如果是 IDEA 里配置的 Artifacts看一下输出目录下 WEB-INF/classes 里有没有编译好的 class部署目录缺文件也会造成 404。5.2 ClassNotFoundException: com.mysql.jdbc.Driver现象程序一运行就报ClassNotFoundException找到的类名就是数据库驱动类。原因MySQL 驱动 jar 没有放进WEB-INF/lib或者放进了但 IDE 没有同步到部署目录。另一个常见原因是驱动类名弄混MySQL 5.x 用com.mysql.jdbc.DriverMySQL 8.x 改成com.mysql.cj.jdbc.Driver。解决下载对应版本的mysql-connector-jjar 放到WEB-INF/lib如果项目用 Maven把依赖加进pom.xml后刷新。判断 jar 是否真的被部署到 Tomcat直接打开webapps/项目名/WEB-INF/lib看一眼这个方法最快。注意 jar 包不要同时放多个版本驱动类名和 jar 版本不一致时也会报错。5.3 中文乱码请求编码和响应编码要分开设现象新闻标题在数据库里是正常中文页面显示乱码或者 JSP 里输入中文保存后数据库里变成问号。原因Tomcat 对不同传输阶段的默认编码不一致。请求参数在 Servlet 里按 ISO-8859-1 解码JSP 输出又依赖pageEncoding和响应头层层不一致就会乱码。解决分三段处理。第一所有 JSP 页面第一行写% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %第二Servlet 读取任何参数前先执行req.setCharacterEncoding(UTF-8)第三数据库连接串加characterEncodingutf8表结构用 utf8mb4。还有一处是 Tomcat 的server.xml里配置 URIEncoding但那只对 GET 请求的查询串生效POST 不受它控制所以在应用层统一设编码才是正解。5.4 图片上传后访问不到重启 Tomcat 图片消失现象上传成功后页面显示裂图刷新也不好使或者当时能看重启后图片全没。原因part.write(getServletContext().getRealPath(savePath))把文件写进了当前部署目录而 IDE 每次重新部署时可能清空并重建该目录另外getRealPath在某些容器配置下返回 null直接导致保存失败。解决不要在项目部署目录里存业务文件。做法是准备一个独立的上传目录例如/opt/news_upload然后配置 Tomcat 虚拟映射把/upload/*指到磁盘目录Context docBase/opt/news_upload path/upload reloadabletrue/数据库的 image_path 字段存成/upload/xxx.jpg页面用${pageContext.request.contextPath}${news.imagePath}拼出完整地址。这样重启、重新部署都不影响图片。5.5 访问人数一多就卡死数据库连接耗尽现象项目在本地没问题多开几个页面或连续刷新几次后请求开始长时间转圈控制台出现Connection refused或连接超时。原因每个请求都新建物理连接用完没关连接数很快被占满如果代码里只关了 ResultSet 忘了关 Connection会导致连接泄漏。这种问题在并发低的时候完全看不出来一压测就现原形。解决分两层。第一层是代码层面统一用 try-with-resources 或 finally 块关闭连接保证任何异常路径下 Connection、Statement、ResultSet 都被关闭。第二层是引入连接池常见的是 HikariCP 或 DBCP连接池固定上限比如 20 个连接用完归还而不是重建能大幅降低开销。对课设项目第一层是底线第二层可以作为加分项写进报告。6. 跑起来之后的三个习惯验证链路、加分页、留日志6.1 用 curl 验证整条链路的响应项目部署完成后我不急着点页面先用 curl 打一遍关键 URL确认不是浏览器缓存造成的假象。例如验证首页和详情curl -s http://localhost:8080/news/index.jsp | head -20 curl -s http://localhost:8080/news/news?id1 | grep 新闻标题第一条看 JSP 是否渲染出内容第二条直接确认详情 Servlet 转发正常。如果服务器返回 500Tomcat 的logs/catalina.out里一定有完整堆栈顺着异常类名的包路径能定位到是 Servlet 的问题还是 DAO 的问题。6.2 顺手给列表加分页新闻多了之后首页一次性把所有新闻都查出来不合理。分页的本质就是两条 SQL一条 count 总数算页数一条带 limit 查当前页。DAO 里加两个参数即可。public ListNews findNewsPage(int pageNo, int pageSize) { String sql SELECT id, title, content, author, image_path, created_at FROM news ORDER BY created_at DESC LIMIT ?, ?; try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (pageNo - 1) * pageSize); ps.setInt(2, pageSize); // 执行查询并组装列表 } catch (SQLException e) { throw new RuntimeException(分页查询失败, e); } }页面上的上一页、下一页按钮本质上就是改 pageNo 的链接可以在 index.jsp 里用${pageNo 1}直接拼出下一页地址。6.3 留一行日志别让系统当黑匣子任何发布、删除操作都尽量留一条日志记录操作人、操作类型、目标 id。没有日志的系统排错像蒙着眼睛改代码。从那以后我每次部署完都会先跑一遍「清浏览器缓存、重启 Tomcat、盯住 catalina.out」的流程确认无异常再交给别人用。这套项目对老手来说不算复杂但能按这篇的顺序把架构理清、把代码跑通对第一次接触 ServletJSPMySQL 的读者就是实打实的进展。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网