
简介一套基于Java技术栈的Web新闻发布系统完整项目面向Java Web初学者及有课程设计、毕业设计需求的学生采用Servlet、JSP、MySQL与JDBC构建涵盖新闻发布、编辑删除、用户登录与权限管理等核心业务具备清晰的MVC分层思路。资源包共269个文件压缩后约6.13MB包含56个Java源码、50个JSP页面、112个编译后class文件、数据库建表SQL脚本及少量CSS/图片资源可对照源码、页面与数据表快速理解前后端数据流向。目前已有3433人学习下载被较多开发者作为Servlet/JSP项目参考。通过研读可以掌握Servlet请求分发、JSP页面渲染、JDBC预编译SQL防注入、文件上传处理等实用技能适合在此基础上扩展新闻分类、评论或后台统计等功能。1. 用 JavaServletJspMysql 写新闻发布系统为什么这套老技术栈还是绕不开在 SpringBoot 一行注解搞定 Web 服务的年代还有人用 JavaServletJspMysql 手搓新闻发布系统看着像考古。但真去翻招聘网站和面试题就会发现Servlet 的生命周期、JSP 的请求流转、Mysql 的连表查询依然是 Java 工程师面试绕不过去的底层层。新闻发布系统恰好覆盖了这些点列表分页、后台增删改查、登录 Session、文件上传、富文本过滤。这套技术栈能让你看见一个 Web 请求从浏览器到数据库再返回的完整路径没有框架替你藏起任何一个环节。本文从建库开始把项目拆成三层架构、六个 Servlet、五个必踩的坑最终落到能本地跑通的最小命令集。2. 先定数据模型再写代码新闻系统的建表 SQL 与三层架构设计2.1 三张表撑起一个新闻站category、news、user 的字段设计新闻发布系统的核心不是页面是数据模型。我见过不少新手上来就写CREATE TABLE news把所有字段塞进一张表结果做到栏目筛选和后台权限时被迫回头重构。常见的做法是拆三张表栏目表、新闻表、用户表。栏目和新闻是一对多用户和新闻也是一对多这两层关联关系足够覆盖大部分面试和课设要求。CREATE DATABASE news_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE news_db; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE COMMENT 栏目名, sort_order INT DEFAULT 0 COMMENT 排序权重越小越靠前 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 存加密后的值绝不存明文, role TINYINT NOT NULL DEFAULT 1 COMMENT 1编辑 2管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE news ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL COMMENT 所属栏目, title VARCHAR(200) NOT NULL, summary VARCHAR(500) COMMENT 列表页摘要, content MEDIUMTEXT NOT NULL COMMENT 正文Mysql 5.7 用 MEDIUMTEXT 够用, cover_image VARCHAR(255) COMMENT 封面图相对路径, author_id INT COMMENT 发布人, hits INT DEFAULT 0 COMMENT 点击量等于做了一次 count 缓存, status TINYINT DEFAULT 1 COMMENT 1草稿 2已发布, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_news_category FOREIGN KEY (category_id) REFERENCES category(id), CONSTRAINT fk_news_author FOREIGN KEY (author_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明news表里的category_id和author_id故意保留为普通索引字段通过外键约束保证数据完整性。hits字段是典型的空间换时间做法——每次访问详情页UPDATE news SET hitshits1比COUNT(*)之类的实时统计快一个量级。status字段方便做草稿箱不给用户直接删新闻的权限避免误操作后找不回数据。参数说明字符集统一用utf8mb4不是utf8。utf8在 Mysql 里最多存 3 字节遇到 Emoji 或者生僻字直接报错utf8mb4才是完整的 UTF-8。排序规则utf8mb4_unicode_ci在中文场景下按拼音排序适合栏目表的ORDER BY sort_order。Mysql 5.7 默认存储引擎是 InnoDB建表时不写ENGINEInnoDB也能创建但显式声明更保险。2.2 三层架构DAO 层、Service 层、Servlet 的职责边界教材里常把项目分成 DAO、Service、Servlet 三层但很多初学者写出来的代码这三层其实是同一段逻辑换了个类名。我的划分标准是DAO 只允许出现 SQL 和 JDBC 操作Service 只处理业务规则和事务边界Servlet 只做参数接收、对象装配和页面跳转。举一个反例——把SELECT * FROM news写在 Servlet 里看起来省事但等到要做分页条件拼接、多表查询时Servlet 里全是数据库细节前端改个展示字段都能把 SQL 弄坏。// 一段典型的 DAO 层代码只负责按条件查新闻列表 public class NewsDao { private JdbcTemplate jdbc new JdbcTemplate(DBUtil.getDataSource()); public ListNews queryList(Integer categoryId, int offset, int limit) { StringBuilder sql new StringBuilder( SELECT id, title, summary, hits, create_time, status FROM news WHERE 11); ListObject args new ArrayList(); if (categoryId ! null) { sql.append( AND category_id ?); args.add(categoryId); } sql.append( ORDER BY create_time DESC LIMIT ? OFFSET ?); args.add(limit); args.add(offset); return jdbc.query(sql.toString(), args.toArray(), (rs, rowNum) - { News n new News(); n.setId(rs.getInt(id)); n.setTitle(rs.getString(title)); n.setSummary(rs.getString(summary)); n.setHits(rs.getInt(hits)); n.setCreateTime(rs.getTimestamp(create_time)); n.setStatus(rs.getInt(status)); return n; }); } }逻辑说明这里的WHERE 11是故意的不是偷懒。动态拼接AND category_id ?时不需要单独判断首条条件要不要加AND。LIMIT ? OFFSET ?是分页的标准写法offset从 0 开始计算前端传pageNo时要在 Service 层转换成(pageNo-1) * pageSize。不要在 DAO 层做这个转换否则每换一套页面逻辑就要改一次 SQL。查询结果映射到News对象时只选列表页需要的字段content这种大字段放到详情页再查。参数说明LIMIT ? OFFSET ?在 Mysql 5.7 里支持预编译传参但必须确认使用的 JDBC 驱动版本在 5.1.8 以上旧驱动在LIMIT子句传参偶尔会报Parameter index out of range这不是代码问题是驱动 bug。create_time映射成java.sql.Timestamp实体类里用java.util.Date接收JDBC 会自动转换省掉手工解析字符串的麻烦。2.3 数据库连接池配置Druid 的取舍与最小配置不配连接池的项目只能跑教学演示但凡有 50 个人同时访问DriverManager.getConnection()的每次请求建连开销就能让 Tomcat 直接卡死。我一般用 Druid 而不是 C3P0 或 DBCP因为 Druid 自带监控页面排查慢 SQL 时能直接看到是哪条语句拖垮了首页。最小配置只需要一份druid.properties文件加一个工具类。// Druid 连接池的最小启动配置 public class DBUtil { private static DruidDataSource dataSource; static { try { Properties props new Properties(); // 从 classpath 根目录加载配置文件 props.load(DBUtil.class.getClassLoader() .getResourceAsStream(druid.properties)); dataSource (DruidDataSource) DruidDataSourceFactory .createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(连接池初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static DataSource getDataSource() { return dataSource; } }配置文件的三个关键参数一定要理解而不是照抄driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/news_db?characterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot password你的数据库密码 initialSize5 maxActive20 maxWait10000逻辑说明initialSize5表示应用启动时预创建 5 个连接避免第一个访问者慢吞吞地等待建连。maxActive20是线上跑到 20 个并发连接时的上限超过该数值的请求进入maxWait10000的等待队列10 秒拿不到连接就抛异常。对于课设和中小型新闻站20 个连接足够支撑 200 并发左右因为新闻系统大多数请求是查询事务很短连接复用率很高。3. 用 Servlet 写完新闻的增删改查前端页面与后台接口的完整链路3.1 新闻列表页EL JSTL 渲染Servlet 只塞数据JSP 页面里最容易犯的错是写 Java 脚本片段%开头写一大坨 for 循环页面看着像意大利面。我从第一版开始就强制自己只用 EL 表达式和 JSTL 标签。Servlet 的职责是查数据、放request、转发JSP 里没有任何 Java 代码这样前端改样式不会碰坏后端逻辑后端加字段不会影响页面结构。WebServlet(/news/list) public class NewsListServlet extends HttpServlet { private NewsService newsService new NewsService(); private CategoryService categoryService new CategoryService(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 统一处理请求编码POST 和 GET 都要防乱码 request.setCharacterEncoding(UTF-8); String cidParam request.getParameter(categoryId); Integer categoryId null; if (cidParam ! null !cidParam.isEmpty()) { categoryId Integer.parseInt(cidParam); // 不建议直接 parse真实项目要捕获异常 } int pageNo parsePageNo(request.getParameter(pageNo)); // pageNo 默认 1pageSize 固定 10课程设计没必要让前端自定义 PageNews page newsService.queryPublishedNews(categoryId, pageNo, 10); ListCategory categories categoryService.queryAll(); // 数据放进 request 域forward 到 JSP 渲染 request.setAttribute(page, page); request.setAttribute(categories, categories); request.getRequestDispatcher(/WEB-INF/jsp/news/list.jsp) .forward(request, response); } private int parsePageNo(String param) { if (param null || param.trim().isEmpty()) return 1; try { int value Integer.parseInt(param); return value 1 ? 1 : value; } catch (NumberFormatException e) { return 1; // 非法参数回落默认值不抛 500 } } }列表页 JSP 使用 JSTL 渲染这是新闻系统前台页面的核心模板% 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 body div classcategory-nav a href${pageContext.request.contextPath}/news/list全部/a c:forEach items${categories} varcat a href${pageContext.request.contextPath}/news/list?categoryId${cat.id} ${cat.name} /a /c:forEach /div c:forEach items${page.list} varnews div classnews-item h3 a href${pageContext.request.contextPath}/news/detail?id${news.id} ${news.title} /a /h3 p${news.summary}/p span classmeta fmt:formatDate value${news.createTime} patternyyyy-MM-dd HH:mm/ | 点击 ${news.hits} /span /div /c:forEach c:if test${empty page.list} p classempty-tip当前栏目暂无新闻/p /c:if /body /html逻辑说明${pageContext.request.contextPath}是获取项目上下文路径的标准写法不要写死成/news-web否则改部署名就炸。fmt:formatDate把Date对象格式化成可读字符串避免在 JSP 里调用toString()。c:if test${empty page.list}处理空列表这是很多新手漏掉的分支数据库里没数据时页面渲染一片空白用户以为系统坏了。参数说明Servlet 里的parsePageNo做了防护——前端传pageNoabc或者负数时直接回落 1。这一步是为自己省心真实项目里这种参数会被扫描器刷不校验的话日志里一天能堆几千条NumberFormatException。转发到/WEB-INF/jsp/news/list.jsp而不是/jsp/list.jspWEB-INF 目录下的 JSP 不能通过 URL 直接访问这是第一道安全防线。3.2 发布与编辑接口处理富文本、封面图片和 XSS 的三个细节新闻发布是后台系统的核心接口牵涉表单提交、文件上传、富文本内容过滤三个环节。富文本编辑器我用的是轻量级的 wangEditor它提交的 HTML 片段经过 Servlet 时必须做一次标签白名单过滤。不能放任用户提交任意script标签否则一篇文章就能让整个新闻站沦为别人执行脚本的跳板。WebServlet(/admin/news/create) public class NewsPublishServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 登录校验从 Session 里取用户取不到直接跳登录页 HttpSession session request.getSession(false); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login); return; } request.setCharacterEncoding(UTF-8); String categoryId request.getParameter(categoryId); String title request.getParameter(title); String summary request.getParameter(summary); // 富文本内容先清洗再入库这是不能省的一道工序 String content HtmlSanitizer.clean(request.getParameter(content)); // 处理封面图上传getPart 是 Servlet 3.0 标准方法不用引入 commons-fileupload Part coverPart request.getPart(coverImage); String coverPath FileUploadUtil.save(coverPart, /uploads/covers); News news new News(); news.setCategoryId(Integer.parseInt(categoryId)); news.setTitle(title); news.setSummary(summary); news.setContent(content); news.setCoverImage(coverPath); news.setAuthorId(loginUser.getId()); boolean created newsService.createNews(news); if (created) { response.sendRedirect(request.getContextPath() /admin/news/list?msgsaved); } else { request.setAttribute(error, 保存失败); request.getRequestDispatcher(/admin/news/edit.jsp).forward(request, response); } } }逻辑说明request.getPart是 Servlet 3.0 提供的标准上传接口WebServlet注解配上即可不需要再引入commons-fileupload依赖这是很多老教程的过时写法。HtmlSanitizer.clean是自定义工具类核心逻辑是正则白名单允许p、h1~h6、img、a、b、i其他标签全部剥离。// 富文本标签白名单清洗的核心方法 public class HtmlSanitizer { private static final SetString ALLOWED_TAGS new HashSet(Arrays.asList( p, br, h1, h2, h3, img, a, b, i, strong, blockquote )); public static String clean(String html) { if (html null || html.isEmpty()) return ; // 先去除 script、iframe、object 等危险标签及其内容 String cleaned html.replaceAll((?is)script[^]*.*?/script, ) .replaceAll((?is)iframe[^]*.*?/iframe, ) .replaceAll((?is)object[^]*.*?/object, ); // 再按白名单过滤剩下标签 cleaned cleaned.replaceAll((?!/?(?: String.join(|, ALLOWED_TAGS) )\\b[^]*), lt;); return cleaned; } }参数说明白名单正则里的(?is)表示忽略大小写且点号匹配换行这是处理富文本 HTML 时必须开启的标志否则Script大小写变体就能绕过过滤。img标签一定要保留src属性但禁掉onerror事件属性具体做法是再跑一遍replaceAll((?i)\\son[a-z]\\s*, )把onload、onerror这类事件属性全部移除。这层清洗不是后端唯一的防御数据库层面还要配合PreparedStatement做参数化查询双保险才能过安全审计。3.3 登录与 Session 控制后台管理页面的权限过滤后台发布、编辑、删除接口不能每个 Servlet 里都写一遍登录校验那样代码冗余且容易漏。我使用 Filter 做统一拦截/admin/*路径下的所有请求先进过滤器Session 里没有loginUser就重定向到登录页这样即使以后新增后台功能也不会忘掉权限控制。WebFilter(urlPatterns {/admin/*}) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); // 已登录用户直接放行 if (session ! null session.getAttribute(loginUser) ! null) { chain.doFilter(request, response); return; } // 未登录给三个选项AJAX 请求返回 401普通请求跳登录页 String requestedWith req.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestedWith)) { resp.setStatus(401); // 前端收到 401 后统一弹登录框 } else { resp.sendRedirect(req.getContextPath() /login); } } }逻辑说明getSession(false)参数很关键。false表示如果当前请求没有 Session 就返回null而默认的getSession()会强制创建一个新 Session导致未登录用户每访问一次后台页面就生成一个无用 Session内存泄漏的隐患就从这里开始。AJAX 请求不能像普通页面那样整体重定向否则前端拿到的是登录页的 HTML 片段所以通过X-Requested-With请求头区分处理方式。参数说明登录接口本身要在LoginServlet的doPost里先查数据库用户表再用session.setAttribute(loginUser, user)写入登录态。Session 默认超时时间是 30 分钟可以在web.xml里通过session-configsession-timeout60/session-timeout/session-config调整。新闻后台是低频操作60 分钟比较合适太短会被编辑人员吐槽太长又容易被他人利用登录态。4. 常见问题排查Mysql 时区、中文乱码、连接池泄漏、路径 404 的五个坑4.1 中文乱码页面、请求、数据库三层都要统一 UTF-8现象新闻标题在浏览器显示成æ–°é—»数据库里存的也是乱码后台表单提交的中文到 Servlet 里打印出来是???。原因三层字符集不统一。JSP 页面声明了UTF-8但 Servlet 接收请求时没有调用setCharacterEncodingTomcat 默认用ISO-8859-1解码中文全部变乱码。解决三处必须一致。JSP 顶部写pageEncodingUTF-8Servlet 的doGet和doPost开头都调request.setCharacterEncoding(UTF-8)Mysql 连接 URL 带上characterEncodingutf8。还要检查 Mysql 表字段SHOW CREATE TABLE news看到charsetgbk就要用ALTER TABLE news CONVERT TO CHARACTER SET utf8mb4转换。这三层漏掉任何一个排查起来都是玄学而且表现还不一样——有时只有输入乱有时只有输出乱。4.2 serverTimezone 不是玄学时区报错的本质是驱动和数据库版本不匹配现象项目一启动查询新闻列表抛异常The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因Mysql 驱动 5.1.33 之后强制检查服务器时区而 Windows 上安装的 Mysql 默认时区是CST这种本地化描述驱动解析不了。解决连接 URL 里显式指定时区。开发环境用serverTimezoneAsia/Shanghai不要用GMT8因为 JDK 8 的时区解析对GMT8偶尔会报Unknown time-zone ID。这个报错不是代码问题是 Mysql 配置规范问题重装驱动没用改 URL 参数立刻好。4.3 连接池泄漏Connection 没关两个月后数据库连接耗尽现象系统跑了一个多月某天突然全部接口报Connection is not available, request timed out after 10000ms重启 Tomcat 后恢复但过几天又复发。原因代码里调用getConnection()后在异常分支忘记调conn.close()连接池里的连接被耗尽。这是最典型的 Java Web 翻车点——正常路径finally里关了连接但catch分支里随手return了连接就漏了。解决用 try-with-resources 语法Connection实现了AutoCloseable能保证任何路径都自动释放。另外建议打开 Druid 监控页/druid/weburi.json能看到哪些 URI 的活跃连接数持续不降快速定位泄漏源头。血泪经验告诉我连接泄漏不会立刻爆它像温水煮青蛙等日志里出现报错已经是下午高峰时段了。// 用 try-with-resources 彻底解决连接泄漏 public ListCategory queryAll() { String sql SELECT id, name, sort_order FROM category ORDER BY sort_order; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { ListCategory list new ArrayList(); while (rs.next()) { Category c new Category(); c.setId(rs.getInt(id)); c.setName(rs.getString(name)); list.add(c); } return list; } catch (SQLException e) { // 这里不要 swallow 异常打日志或抛业务异常 throw new RuntimeException(查询栏目失败, e); } }逻辑说明try-with-resources 的括号里声明三个资源执行完毕后按逆序自动关闭先关ResultSet再关PreparedStatement最后关Connection恰好是 JDBC 要求的关闭顺序。这段代码里没有一行手动close()但任何异常路径包括RuntimeException都能保证连接归还连接池。注意捕获异常后不能只e.printStackTrace()然后返回空列表——新闻栏目查不出来还静默返回null前端页面会直接 NPE这种错误比抛异常更隐蔽。4.4 路径 404/css/style.css在页面里加载不出来现象浏览器访问http://localhost:8080/news-web/admin/news/list页面样式全部丢失控制台报GET http://localhost:8080/css/style.css 404。原因JSP 里写了link href/css/style.css绝对路径以项目根开头但/指向的是webapps根目录而不是news-web这个项目的根。解决一律使用${pageContext.request.contextPath}拼接静态资源路径即link href${pageContext.request.contextPath}/css/style.css。这个坑在新手期几乎人人必踩因为它不影响接口数据返回只影响页面样式有人甚至裸奔无样式页面交差了直到验收时被老师指出。4.5 Session 失效登录后台写文章写到一半被踢出现象管理员在后台编辑长文页面停留十几分钟后点击发布跳回登录页写的内容全部丢失。原因Session 默认超时是 30 分钟这个时长按「最后一次请求时间」算不是按登录时间算所以写文章过程中没有请求就会超时。解决分两层处理。第一层把session-config的超时时间调到 90 分钟第二层在编辑页面加草稿自动保存的 AJAX每 5 分钟POST一次当前内容到/admin/news/autosave。自动保存请求会刷新 Session 活动时间管理者长期停留也不会被踢而且草稿数据还在即使真超时了重登回来草稿箱里还在。5. 本地跑通的最小命令集Mysql 建库、Tomcat 部署与浏览器验证5.1 版本组合选择JDK 8 Tomcat 8.5 MySQL 5.7 为什么最省事新闻发布系统这个体量版本选择首要原则是「找到最多人踩过坑的稳定组合」。JDK 8 至今仍是大多数 Java 工程师的生产环境主力javac不搞花活兼容性最好。Tomcat 8.5 恰好对应 Servlet 3.1 规范支持WebServlet注解和getPart文件上传不用额外依赖。MySQL 5.7 在 Windows 10 和 Linux 上都有成熟的安装教程utf8mb4支持完善性能对新闻站这种查询密集型应用绰绰有余。这套组合不是性能最优而是试错成本最低搜任何报错信息都能找到前人的解决方案。组件推荐版本关键兼容性说明JDK88u202最后一个免费商用版本Linux 和 Windows 安装包均易获取Tomcat8.5.x支持 Servlet 3.1 与 JSP 2.3无需额外配置即可用注解MySQL5.7.44直到 2027 年仍是主流线上版本Oracle 提供长期支持Mysql JDBC 驱动5.1.49 或 8.0.x用 8.0.x 时 URL 要加useSSLfalseallowPublicKeyRetrievaltrueIDEEclipse EE 或 IntelliJ IDEAIDEA 社区版即可无需旗舰版参数说明MySQL 5.7.44 的安装包在 Windows 上选mysql-installer-community版本最省事一路默认配置到root密码设置记住选UTF-8字符集选项。Linux 上建议用apt或编译安装重点是初始化后执行mysql_secure_installation关掉匿名用户。JDBC 驱动如果选 8.0.33连接 URL 里必须加allowPublicKeyRetrievaltrue否则连本机 Mysql 会报Public Key Retrieval is not allowed这是新驱动和旧版认证方式的兼容性问题不是密码错误。5.2 初始化数据库建库、建表、插入测试数据的命令数据库初始化不能靠手工点 Navicat 一条条执行要写成 SQL 脚本在命令行一步跑完。这样别人接手项目时只需执行一条命令不用问你「数据库密码是多少、建了哪些表」。# 在项目根目录准备 init.sql 文件内容就是第二章的建表语句 # 然后执行下面这条命令完成初始化 mysql -u root -p init.sql # 验证数据库是否创建成功 mysql -u root -p -e USE news_db; SHOW TABLES; # 插入两条测试栏目方便登录后立刻发布新闻看到效果 mysql -u root -p -e USE news_db; INSERT INTO category(name, sort_order) VALUES(公司动态, 1),(行业资讯, 2); INSERT INTO user(username, password) VALUES(admin, e10adc3949ba59abbe56e057f20f883e);逻辑说明INSERT语句里的用户密码我存的是 MD5 加密值e10adc3949ba59abbe56e057f20f883e是123456的 MD5这是为了让新人能按步骤跑通。实际生产环境务必换成BCrypt加盐加密Mysql 用户表里的password字段长度设为VARCHAR(64)就是为了容纳 MD5 或 BCrypt 输出。测试栏目数据插两条就够插入太多反而影响后续分页验证。5.3 部署到 Tomcatwar 包位置、启动日志与浏览器验证项目开发完成后打包部署有两种常见做法。IDE 里右键Export WAR是最直接的缺点是每次改代码都要手动点一次。我在本地练习时更常用第二种把项目直接放到 Tomcat 的webapps目录下Tomcat 启动时自动编译部署。下面展示纯命令行部署流程不依赖 IDE。# 1. 把项目打包成 war在项目根目录执行pom.xml 或 build.gradle 已配置 mvn clean package -DskipTests # 2. 将打包好的 war 复制到 Tomcat 的 webapps 目录 cp target/news-web.war $CATALINA_HOME/webapps/ # 3. 启动 TomcatWindows 用 startup.batLinux/macOS 用 startup.sh cd $CATALINA_HOME/bin ./startup.sh # 4. 实时查看启动日志确认没有任何异常堆栈 tail -f $CATALINA_HOME/logs/catalina.out # 5. 确认端口监听和首页可访问 curl -s -o /dev/null -w HTTP状态码: %{http_code}\n http://localhost:8080/news-web/逻辑说明war 包放进webapps目录后Tomcat 解压并生成同名目录news-web这就是项目的部署根路径。curl -w参数输出 HTTP 状态码200 表示首页部署成功。查看日志时重点看有没有Deployment of web application archive [news-web.war] has finished这行出现它才代表部署完整结束。Tomcat 默认端口 8080如果被占用可以在conf/server.xml里改Connector port8080。5.4 用浏览器和命令行验证核心接口状态码与响应耗时系统部署完成不等于能交差要按接口清单验证一遍。新闻站的核心接口就五个游客访问列表页、游客点击详情页、管理员登录、管理员发布新闻、管理员退出登录。我用一个循环命令批量检查状态码能在五分钟内发现路径配置错误或 Session 拦截器误伤。# 验证公开接口是否全部 200注意返回 302/401 需要单独排查 for path in /news-web/ /news-web/news/list /news-web/login; do code$(curl -s -o /dev/null -w %{http_code} http://localhost:8080$path) echo $path - $code done # 验证管理员未登录状态下访问后台是否被拦截期望 302 跳登录 curl -s -o /dev/null -w 未登录访问后台: %{http_code} Redirect: %{redirect_url}\n \ http://localhost:8080/news-web/admin/news/list # 模拟登录并测试详情页接口 curl -s -c cookies.txt -d usernameadminpassword123456 \ http://localhost:8080/news-web/login curl -s -b cookies.txt -o /dev/null -w 登录后访问后台: %{http_code}\n \ http://localhost:8080/news-web/admin/news/list参数说明-c cookies.txt将登录后的 Session Cookie 写入文件-b cookies.txt在后续请求中携带它这是命令行模拟浏览器会话的标准方式。登录成功的标志是第一个请求返回302然后跟随跳转第二个请求带Cookie后返回200。如果登录后依然302说明LoginFilter里的session.getAttribute和LoginServlet里setAttribute的 key 不一致这是我的过滤器最常见的配置失误检查两个类的字符串是否完全相等。验证响应耗时用-w %{time_total}新闻列表页 Java 直出的正常耗时在 50 到 200 毫秒之间超过 500 毫秒的要回头看是不是 SQL 没有索引或循环调用数据库。6. 把系统做成能演示的作品功能清单验收、性能体检与两个升级思路交项目前先按这个清单过一遍比翻代码找 bug 高效得多。第一项未登录直接访问/admin/*全部跳登录页第二项登录后能发布新闻、上传封面图、编辑草稿、下线文章第三项游客打开首页栏目筛选和分页都正常点击详情hits加 1第四项Mysql 命令行执行SHOW PROCESSLIST确认没有残留的Sleep连接堆积第五项用 Postman 做一次POST /admin/news/create拿一份包含script标签的富文本内容保存后到详情页检查script被剥离。性能体检不需要上压测工具一个技巧就够了给每条 SQL 前面加EXPLAIN重点看rows字段。新闻表超过一万条数据时按category_id筛选必须走索引possible_keys里出现fk_news_category才算健康。两个升级思路按投入产出比排序。第一个是给详情页加 Redis 缓存对 JavaServlet 技术栈来说入门成本低且效果立竿见影。把新闻详情JSON序列化后存 Rediskey news:detail:{id}过期时间 10 分钟。热点新闻的数据库压力立刻消除MySql 的连接数陡降。第二个升级是引入全文检索新闻站的数据量到了一定规模WHERE title LIKE %关键%会全表扫描用 Solr 或 Elasticsearch 建立标题和正文索引查询速度从几百毫秒降到十几毫秒。我自己做完这套系统后最深的一条教训是不要急着写代码先把建表 SQL 和三层的类结构画在纸上。这个项目我是边写边加字段最后news表被ALTER TABLE了七次每次改动都要同步改实体类、DAO 的ResultSet映射、JSP 展示字段改到第三次时就发现时间全耗在同步维护上了。如果你照着本文从建表开始一次把字段设计清楚写代码的过程会快得多。这套技术栈虽然老但它是理解框架的钥匙SpringBoot 只是把这个新闻系统的配置自动化了底层还是 Servlet 那套请求处理模型在跑。把这里的每个坑都踩过一遍再回去看框架源码会顺畅很多。希望帮到你。本文还有配套的精品资源点击获取