资讯动态

Java+Servlet+JSP+MySQL新闻发布系统实战:从零搭建到避坑指南

发布时间:2026/9/28 7:13:12 来源:尧图企业网站定制
简介这是一套面向Java Web初学者与课程设计开发者的完整新闻发布系统源码基于Servlet、JSP、MySQL与JDBC技术栈构建采用MVC架构实现新闻的发布、编辑、浏览与后台管理并涵盖用户登录认证、角色权限控制、分页展示及SQL注入防护等常见业务场景适合用作毕业设计、实训项目或Java Web综合练习的参考方案。资源包共269个文件包含56个java源文件、50个jsp页面、112个class编译文件以及10个jar依赖、9个xml配置、6个css与14个png等静态资源另附1个sql建库脚本压缩包约6.13MB目录结构清晰便于按模块查阅与二次开发。目前已有3432人学习下载。通过研读源码读者可掌握Servlet请求处理、JSP视图渲染、JDBC数据库交互与session状态保持等核心技能并借鉴其分层设计与安全优化思路快速搭建属于自己的新闻管理系统。1. 从零搭一套 JavaServletJSPMySQL 新闻发布系统这套老技术栈为什么还值得动手如果你正在搜「JavaServletJspMysql实现Web新闻发布系统」大概率不是想听人劝你上 Spring Boot而是手头就有一个课程设计、实训作业或者毕设选题要求用最原始的三件套把一套能跑起来的新闻发布系统做出来。这套组合看着老但它把 HTTP 请求、会话、数据库连接、页面渲染这几件事全摊在你面前没有框架帮你兜底反而最能看清 Web 应用到底怎么运转。新闻发布系统这个场景又刚好覆盖了增删改查、分页、登录鉴权、富文本录入这几类高频需求做完一遍Servlet 生命周期、JSP 九大内置对象、MySQL 连接池这些概念就不再是面试八股文里的名词而是你亲手调过的东西。这篇笔记按「环境搭起来 → 数据库建好 → 后端接口写通 → 前端页面接上 → 排错收尾」的顺序走新手能照着复现熟手能对照检查自己的参数和边界。2. 环境与工程骨架IDEA 新建 JSP 项目到 Tomcat 跑通第一个页面2.1 为什么这套栈选 Tomcat 而不是别的容器Servlet 和 JSP 本质是 Java EE现在叫 Jakarta EEWeb 规范的一部分它们需要一个实现了 Servlet 规范的容器才能跑。Tomcat 是最轻、资料最多、和 IDEA 集成最顺的选择。有人会问 nginx 支持 jsp 吗——不支持nginx 是静态资源服务器和反向代理它处理不了 JSP 的编译和执行JSP 必须交给 Tomcat 这类 Servlet 容器。所以架构上常见做法是 nginx 放前面处理静态资源和负载Tomcat 在后面跑 JSP 和 Servlet但本地开发阶段直接用 Tomcat 就够了不用引入 nginx 增加复杂度。版本搭配上我一般会选 JDK 8 或 JDK 11 Tomcat 8.5/9 MySQL 8.0。JDK 8 兼容性最好网上大部分 JSP 教程和头歌实训答案都是基于它写的MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver和 5.x 的com.mysql.jdbc.Driver不一样这点后面配连接池时会踩到。2.2 IDEA 新建项目的具体步骤在 IDEA 里新建项目选 Java Enterprise老版本叫 Java EE勾选 Web Application同时勾上 Servlet 支持。如果你用的是社区版没有这个选项就新建普通 Java 项目然后手动在pom.xml或项目结构里加 Web 模块。用 Maven 管理依赖的话pom.xml里至少要有这几个dependencies !-- Servlet APIscope 用 provided因为 Tomcat 自带 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同样 provided -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL页面循环和判断要用scope 是 compile -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 8 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependenciesServlet 和 JSP 的 API 包一定要用provided因为 Tomcat 运行时自带这些类如果你打成compile一起塞进WEB-INF/lib启动时会出现类冲突报ClassCastException或者NoSuchMethodError这是新手最容易翻车的地方之一。2.3 目录结构与 web.xml 版本标准 Maven Web 项目结构是这样的src └─ main ├─ java Servlet、DAO、实体类 ├─ resources db.properties 等配置 └─ webapp ├─ WEB-INF │ ├─ web.xml │ └─ lib ├─ css ├─ js └─ *.jspweb.xml的版本声明决定了你能不能用注解方式配 Servlet。Servlet 3.0 以后支持WebServlet注解就不用每个 Servlet 都在 web.xml 里写一遍映射了。声明写成 4.0 版本web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 /web-app配好 Tomcat 运行配置Deployment 里加上 artifact启动后访问http://localhost:8080/项目名/能看到默认 index.jsp 就说明骨架通了。这一步别急着写业务先把「能跑」这件事确认掉后面出问题才好定位是环境还是代码。3. 数据库设计新闻发布系统的表结构与 MySQL 8 连接配置3.1 三张核心表怎么设计新闻发布系统最小可用模型需要三张表用户表、新闻分类表、新闻表。用户表管登录和权限分类表管栏目新闻表存正文。设计时把外键关系理清楚后面写联表查询才不别扭。CREATE DATABASE news_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE news_db; -- 用户表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 或 SHA-256不存明文 real_name VARCHAR(50), role TINYINT DEFAULT 0, -- 0 普通用户1 管理员 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 分类表 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort INT DEFAULT 0 ); -- 新闻表 CREATE TABLE t_news ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, content TEXT, category_id INT, author_id INT, view_count INT DEFAULT 0, -- 默认值设 0避免 null 参与计算 status TINYINT DEFAULT 1, -- 1 已发布0 草稿 publish_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES t_category(id), FOREIGN KEY (author_id) REFERENCES t_user(id) );字符集一定用utf8mb4不要用utf8。MySQL 里的utf8其实是阉割版存不了 emoji 和部分生僻字新闻正文里出现这些字符会直接报错或者变问号。view_count和status这类字段设默认值能省掉插入时漏传字段导致的 null 判断。3.2 JDBC 连接与连接池配置直接DriverManager.getConnection每次请求都建连接并发一上来数据库就扛不住。常见做法是用 Druid 或 HikariCP 连接池。这里用 Druid 举例加依赖dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependencyresources/db.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/news_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue usernameroot password你的密码 initialSize5 maxActive20 maxWait3000几个参数必须说清楚serverTimezone不写会报时区错误MySQL 8 默认时区是 UTC和东八区差 8 小时新闻发布时间会莫名其妙少 8 小时useSSLfalse是本地开发关掉 SSL否则会弹 SSL 连接警告allowPublicKeyRetrievaltrue是 MySQL 8 用 caching_sha2_password 认证插件时需要的不加会报Public Key Retrieval is not allowed。maxActive20是最大连接数课程设计级别够用生产环境要按 QPS 调。连接池初始化工具类public class DBUtil { private static DataSource dataSource; static { try { Properties props new Properties(); // 从 classpath 读配置 props.load(DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据源初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 按 rs - stmt - conn 顺序关闭每个都判空 try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }静态代码块保证数据源只初始化一次getConnection从池里借连接用完close是归还而不是真关闭。这里有个血泪经验Druid 的close方法归还连接如果你手动conn.close()之后又拿这个 conn 去操作会报连接已关闭所以 DAO 里一定要用 try-with-resources 或者确保关闭顺序。4. 后端实现Servlet 生命周期、新闻 CRUD 与分页查询4.1 Servlet 生命周期决定了你该在哪写初始化逻辑Servlet 生命周期分四个阶段加载实例化、初始化init()、服务service()内部根据请求方法分发到doGet/doPost、销毁destroy()。init()只调用一次适合放数据库连接池初始化、加载配置这类重操作service()每次请求都调业务逻辑写这里destroy()在 Tomcat 关闭时调用来释放资源。理解这个顺序很关键。比如你把连接池初始化写在doGet里每个请求都建一次池内存直接爆。正确做法是放在init()或者用上面的静态代码块。另外 Servlet 默认是单实例多线程成员变量会被多个请求共享所以不要在 Servlet 里定义可变的成员变量存请求数据要用局部变量或者ThreadLocal。4.2 新闻列表 Servlet 与分页分页是新闻系统绕不开的。核心是LIMIT offset, sizeoffset 是跳过的行数size 是每页条数。第 page 页的 offset 是(page-1)*size。WebServlet(/news/list) public class NewsListServlet extends HttpServlet { private NewsDao newsDao new NewsDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 页码参数默认第 1 页 int page 1; String pageStr req.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { try { page Integer.parseInt(pageStr); if (page 1) page 1; } catch (NumberFormatException e) { page 1; // 参数非法时兜底别让页面 500 } } int size 10; // 查总数和当前页数据 int total newsDao.countAll(); int totalPages (total size - 1) / size; // 向上取整 if (page totalPages totalPages 0) page totalPages; ListNews newsList newsDao.findByPage((page - 1) * size, size); req.setAttribute(newsList, newsList); req.setAttribute(currentPage, page); req.setAttribute(totalPages, totalPages); req.getRequestDispatcher(/news_list.jsp).forward(req, resp); } }参数说明page从请求参数取做了非法值兜底防止用户手动改 URL 传pageabc导致 500totalPages用(total size - 1) / size向上取整比Math.ceil更直观forward是服务器内部跳转URL 不变适合查询后渲染页面和sendRedirect的区别要分清——后者是让浏览器重新发请求URL 会变且 request 域数据丢失。对应的 DAO 方法public ListNews findByPage(int offset, int size) { ListNews list new ArrayList(); String sql SELECT n.id, n.title, n.view_count, n.publish_time, c.name AS categoryName, u.real_name AS authorName FROM t_news n LEFT JOIN t_category c ON n.category_id c.id LEFT JOIN t_user u ON n.author_id u.id WHERE n.status 1 ORDER BY n.publish_time DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, size); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { News n new News(); n.setId(rs.getInt(id)); n.setTitle(rs.getString(title)); n.setViewCount(rs.getInt(view_count)); n.setPublishTime(rs.getTimestamp(publish_time)); n.setCategoryName(rs.getString(categoryName)); n.setAuthorName(rs.getString(authorName)); list.add(n); } } } catch (SQLException e) { e.printStackTrace(); } return list; }用PreparedStatement而不是Statement拼接字符串一是防 SQL 注入二是预编译性能更好。LEFT JOIN保证即使分类或作者被删了新闻还能查出来只是对应字段为 null。ORDER BY publish_time DESC让最新新闻排前面这是新闻系统的默认预期。4.3 发布新闻与富文本处理发布新闻的 Servlet 接收表单 POST 数据插入数据库。如果用了富文本编辑器比如 wangEditor、UEditor正文是 HTML 字符串直接存 TEXT 字段即可但要注意 XSS 过滤——用户可能提交script标签展示时如果直接输出就会执行。简单做法是用 JSTL 的c:out标签输出它会自动转义 HTML 特殊字符。WebServlet(/news/add) public class NewsAddServlet extends HttpServlet { private NewsDao newsDao new NewsDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 必须在取参数前设置 String title req.getParameter(title); String content req.getParameter(content); String categoryIdStr req.getParameter(categoryId); // 服务端校验别只靠前端 if (title null || title.trim().isEmpty()) { req.setAttribute(msg, 标题不能为空); req.getRequestDispatcher(/news_add.jsp).forward(req, resp); return; } User user (User) req.getSession().getAttribute(loginUser); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } News news new News(); news.setTitle(title.trim()); news.setContent(content); news.setCategoryId(Integer.parseInt(categoryIdStr)); news.setAuthorId(user.getId()); boolean ok newsDao.insert(news); if (ok) { resp.sendRedirect(req.getContextPath() /news/list); } else { req.setAttribute(msg, 发布失败请重试); req.getRequestDispatcher(/news_add.jsp).forward(req, resp); } } }req.setCharacterEncoding(UTF-8)必须在第一次getParameter之前调用否则中文会乱码。这是 JSP/Servlet 项目里最经典的坑很多人调半天发现是顺序问题。服务端校验不能省前端 JS 校验能被绕过标题为空、分类 ID 非法这些都要在服务端拦一道。发布成功后用sendRedirect跳列表页避免用户刷新页面重复提交表单。5. 前端页面JSP 渲染新闻列表与个人信息展示页5.1 JSTL 循环渲染列表JSP 里不要写 Java 脚本片段% %用 JSTL 和 EL 表达式更干净。列表页核心是c:forEach循环% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html headtitle新闻列表/title/head body table trth标题/thth分类/thth作者/thth发布时间/thth浏览量/th/tr c:forEach items${newsList} varnews tr tda href${pageContext.request.contextPath}/news/detail?id${news.id} c:out value${news.title}//a/td tdc:out value${news.categoryName}//td tdc:out value${news.authorName}//td tdfmt:formatDate value${news.publishTime} patternyyyy-MM-dd HH:mm//td td${news.viewCount}/td /tr /c:forEach /table !-- 分页 -- div classpagination c:if test${currentPage 1} a href?page${currentPage - 1}上一页/a /c:if span第 ${currentPage} / ${totalPages} 页/span c:if test${currentPage totalPages} a href?page${currentPage 1}下一页/a /c:if /div /body /htmlc:out会自动转义 HTML防止新闻标题里带标签导致页面结构错乱或 XSS。fmt:formatDate格式化时间比在 Java 里格式化再传过来更灵活。分页用currentPage和totalPages判断边界首页不显示上一页末页不显示下一页。5.2 个人信息展示页与 Session 鉴权个人信息展示页jsp 个人信息展示页面是高频搜索词通常从 Session 取当前登录用户展示用户名、角色、注册时间等。关键是登录拦截——没登录不能访问。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:if test${empty sessionScope.loginUser} c:redirect url/login.jsp/ /c:if html body h2个人信息/h2 p用户名c:out value${sessionScope.loginUser.username}//p p姓名c:out value${sessionScope.loginUser.realName}//p p角色${sessionScope.loginUser.role 1 ? 管理员 : 普通用户}/p /body /html更规范的做法是用 Filter 统一拦截而不是每个 JSP 页面写一遍判断。Filter 配置WebFilter(urlPatterns {/news/add, /news/edit, /user/*}) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); // 放行 } }urlPatterns只拦截需要登录的路径列表页和详情页可以匿名访问。Filter 里chain.doFilter是放行到下一个过滤器或目标资源不调用就直接返回了。注意 Filter 和 Servlet 一样是单实例多线程别在里面存请求相关状态。6. 避坑与排查这套技术栈最容易翻车的五个地方6.1 中文乱码POST 和 GET 处理方式不一样现象表单提交的中文在数据库里变成???或者页面显示乱码。原因POST 请求默认用 ISO-8859-1 解码GET 请求在 Tomcat 8 以后默认 URIEncoding 是 UTF-8 但老版本不是。解决POST 在取参数前调req.setCharacterEncoding(UTF-8)GET 要么改 Tomcat 的server.xml里 Connector 加URIEncodingUTF-8要么手动new String(str.getBytes(ISO-8859-1), UTF-8)转码。数据库连接 URL 也要带characterEncodingutf8mb4JSP 页面pageEncoding和contentType都写 UTF-8三处对齐才不乱。6.2 MySQL 8 驱动报错时区和公钥两个坑现象启动报The server time zone value ?D1ú±ê×?ê±?? is unrecognized或者Public Key Retrieval is not allowed。原因MySQL 8 默认时区是 UTC 且用 caching_sha2_password 认证。解决连接 URL 加serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse。如果还报错检查驱动版本是不是 8.x5.x 驱动连 MySQL 8 会有兼容问题。6.3 JSP 页面 404路径和编译问题现象访问 JSP 报 404。原因通常有三种JSP 放在WEB-INF下直接访问WEB-INF 下的资源不能直接 URL 访问必须 forwardweb.xml里 welcome-file 没配或者配错JSP 编译失败但错误被吞了。解决JSP 放 webapp 根目录或子目录不放 WEB-INF检查web.xml的welcome-file-list看 Tomcat 的logs/localhost.log有没有 JSP 编译异常JSP 里的 Java 语法错误会导致编译失败但页面只显示 500。6.4 连接池耗尽连接没归还现象跑一会儿报wait millis 3000, active 20, maxActive 20。原因DAO 里拿了连接没关或者异常路径下没走到 close。解决用 try-with-resources 自动关闭或者 finally 块里确保 close。Druid 的maxWait是借连接超时时间报这个错说明池里连接全被占着没还。排查时可以在 Druid 监控页面看活跃连接数或者临时把maxActive调大确认是不是泄漏。6.5 打包 war 部署到 Tomcat 后 404现象IDEA 里跑得好好的打成 war 丢到 Tomcat webapps 下访问 404。原因war 包名和访问路径不一致或者contextPath没对上。解决war 包名就是访问路径比如news.war访问http://localhost:8080/news/。另外检查 war 里WEB-INF/lib有没有把 servlet-api 打进去打进去会和 Tomcat 自带的冲突。Maven 打包用mvn clean package确认pom.xml里 servlet-api 的 scope 是 provided。7. 进阶技巧用 Filter 统一字符编码和登录校验少写重复代码到这一步系统基本能跑了但你会发现每个 Servlet 都在写setCharacterEncoding每个需要登录的页面都在判断 Session重复代码多。我一般会抽两个 Filter 出来一个管编码一个管鉴权配一次全局生效。编码 Filter 这么写WebFilter(/*) // 拦截所有请求 public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }urlPatterns用/*拦截全部这样所有请求进来先统一编码Servlet 里就不用再写setCharacterEncoding了。注意 Filter 的执行顺序web.xml 里按filter-mapping声明顺序注解方式顺序不确定所以如果编码和鉴权有依赖关系建议用 web.xml 配而不是注解。鉴权 Filter 可以做得更细比如区分管理员和普通用户WebFilter(urlPatterns {/admin/*}) public class AdminFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; User user (User) request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } if (user.getRole() ! 1) { response.sendError(403, 无权限访问); return; } chain.doFilter(req, resp); } }/admin/*下的所有路径都要求管理员角色普通用户访问返回 403。这样权限控制集中在一处新增管理页面不用改代码只要放在/admin/目录下就自动受保护。验证 Filter 有没有生效最简单的办法是在doFilter里打一行日志看请求进来时有没有输出。如果没输出检查WebFilter的urlPatterns有没有写对以及web.xml的metadata-complete是不是设成了true——设成 true 会忽略所有注解Filter 和 Servlet 的注解都不生效这个坑很隐蔽我调过一次才发现。最后说个习惯这套技术栈虽然老但每次做完我都会把web.xml、连接池配置、Filter 顺序这三样单独过一遍因为它们出问题往往不是报错而是「行为不符合预期」比如编码 Filter 没生效导致偶发乱码比直接报错更难查。把这三处固定成模板下次做类似项目直接复用能省掉大量重复调试。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑