简介面向Java Web初学者与课程设计、毕业设计场景的留言板项目采用原生Servlet、Vue与MySQL实现覆盖前端交互、后端逻辑和数据库读写难度适中源码已在本地编译通过按文档配置环境即可运行。压缩包共122个文件大小约13.49MB包括Java源文件、Vue组件、class编译文件、jar依赖库、XML配置及少量JSP页面其中class文件适合对照理解服务端逻辑jar包便于快速还原运行环境XML配置则帮助梳理请求映射与数据源设置。项目经助教老师审定功能模块划分清晰前端页面与后端接口对应明确便于逐步调试与扩展遇到问题也可直接私信博主获取解答学习过程较有保障。已有95人学习浏览案例贴近多数初学者的实战需求下载后既能获得一套完整可运行的留言板系统也可基于现有Servlet处理流程与Vue组件继续扩展功能对课程设计或毕业设计有直接参考价值。1. ServletVueMySQL留言板这组合在今天还有没有动手价值ServletVueMySQL留言板系统这个名字看起来像十几年前的课程设计但它恰恰是“从浏览器到数据库”这条链路最直观的一种示范Vue页面通过axios发起请求原生Servlet接收并处理再用JDBC读写MySQL最后把JSON交回前端渲染。没有Spring Boot的自动装配没有前端CLI的工程化包装每一个环节都暴露在你能看到、能改到的地方。对还没系统性做过全栈的初学者来说它能把Servlet生命周期、HTTP方法、JDBC连接、Vue实例的mounted钩子这些“书上的概念”一次性打成实操对需要快速搭一个内部留言反馈小页面的开发老手这套组合也够短平快。本文就用一个可复现的留言板把这条路完整走一遍顺带记下那些容易让人翻车的细节。2. 先跑起来从建库到第一个Servlet的完整链路2.1 版本选型Tomcat 9还是Tomcat 10决定了javax还是jakarta拿到“Servlet”这个关键词第一步不是写代码而是先把Servlet版本和容器版本对齐。原生Servlet项目最常见的翻车点之一就是在新装的Tomcat 10里跑老代码结果直接报ClassNotFoundException。原因是Tomcat从10.0开始把Servlet API的命名空间从javax.servlet改成了jakarta.servlet。也就是说Tomcat 9用的是Servlet 4.0规范包名是javax.servlet.*Tomcat 10对应Servlet 5.0规范包名是jakarta.servlet.*。如果你手里的代码写的是javax.servlet.http.HttpServlet又用了Tomcat 10Tomcat会找不到对应的类反过来说写了jakarta.servlet的代码丢进Tomcat 9也一样挂。容器版本Servlet API版本包名JDK要求Tomcat 8.5.x3.1javax.servletJava 7Tomcat 9.0.x4.0javax.servletJava 8Tomcat 10.x5.0jakarta.servletJava 8Tomcat 11.x6.0jakarta.servletJava 17写原生Servlet项目的常规做法是选Tomcat 9.0.x原因有两个一是绝大多数教材、代码片段、公司老项目都跑在javax上复制粘贴过来的代码不用改名二是Tomcat 9对JDK 8的兼容性好哪怕你机器上只装了JDK 8也能直接跑。除非你有明确的理由用Tomcat 10否则不要为了“追新”而把名字空间切换这一步坑留给自己。2.2 建库建表留言业务的核心表与字段设计留言板看起来简单但一张表很难支撑“谁留的言、说了什么、是否可见、留了多久”。我一般会建两张表user存用户message存留言。如果后面要扩展楼中楼回复再增加一张带message_id的回复表就行。CREATE DATABASE IF NOT EXISTS guestbook DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE guestbook; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(64) NOT NULL COMMENT SHA-256(salt password)后的64位十六进制哈希, salt CHAR(8) NOT NULL COMMENT 8位随机盐, nickname VARCHAR(50) DEFAULT , role TINYINT NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE message ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, content TEXT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常显示 1隐藏, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT留言表; -- 开发期管理员种子账号admin / 123456 INSERT INTO user (username, password, salt, nickname, role) SELECT admin, SHA2(CONCAT(s8k2x9f, 123456), 256), s8k2x9f, 管理员, 1;这段SQL里有两个细节值得注意。第一个是字符集数据库、表、连接串三处都要用utf8mb4否则用户输入一个emoji直接变问号这是留言板最常见的“乱码事故”。第二个是status字段的默认值设为0表示留言默认正常展示管理员把某条留言置为1就能隐藏这套设计在内容审核场景下非常实用也符合“mysql设置默认值为0”的业务习惯。user表里salt字段一定要单独存不要用固定盐。固定盐的问题在于如果两张表的用户密码相同哈希结果就相同攻击者拿到库后一眼就能看出哪些账号用了同一密码。随机盐能让每个用户的哈希结果都不一样。password字段定长CHAR(64)正好对应SHA-256输出64位十六进制字符串长度写死比VARCHAR(255)更规矩——它不可能更长。2.3 IDEA里跑通第一个Servlet项目结构、Artifact与运行验证建完表接下来搭项目。在IntelliJ IDEA里常规做法是新建一个普通的Java Web项目选Java Enterprise中的Web Application模板然后把Tomcat 9配置进Run/Debug Configurations。这个步骤有个关键点Deployment选项卡里要把当前项目war exploded包路径配到Tomcat的/根路径否则访问的时候URL要带一长串项目名前端接口也容易配错。这个Web项目的目录结构很固定别乱叠目录/文件作用src/main/java存放Servlet类、DAO类、工具类src/main/resources放JDBC配置等资源文件src/main/webapp存放前端页面与静态资源src/main/webapp/WEB-INF/web.xmlWeb部署描述配置session超时等src/main/webapp/WEB-INF/lib放JDBC驱动jar包然后写第一个Servlet来验证整条链路通不通。注意不要返回“Hello World”纯文本直接返回JSON因为后面前端接数据的格式就是JSON。package com.guestbook.web; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; WebServlet(/api/health) public class HealthServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\status\:\ok\,\code\:200}); } }这段代码做了三件事用WebServlet注解注册路径/api/health省去在web.xml里写Servlet映射的冗余配置设置响应类型为application/json;charsetUTF-8告诉浏览器按JSON解析同时处理中文编码写入一行JSON。启动Tomcat后浏览器访问http://localhost:8080/api/health能看到JSON就说明容器、项目、Servlet三层都通了。这一步是后续所有功能的“地基”Servlet能响应请求Tomcat没有报ClassNotFoundExceptionweb.xml没有语法错误那就可以放心往里面填业务代码了。很多人的习惯是直接开始写大量页面和DAO结果问题一堆根本分不清是Tomcat问题还是代码问题。先跑通一个最小抓手后面排查才不乱。3. 把留言板拆成三层从Vue页面到Servlet的数据链路3.1 Vue不装脚手架也能用静态引入与最小页面骨架很多初学者看到“Vue”就以为必须npm install vue、跑vue create、上一整套工程化。在一个原生Servlet项目里完全没必要。最常见的做法是直接在webapp/js目录下放vue.min.js和axios.min.js两个文件HTML里用script标签引入一样能用Vue的data绑定、methods方法、mounted生命周期钩子。少了webpack的必要性部署时也不用担心dist目录打进war包后接口路径对不上。下面这个index.html是留言板页面的最小骨架输入昵称和内容提交后调addMessage页面加载完成后调loadMessages。!DOCTYPE html html langzh head meta charsetUTF-8 title留言板/title style body { font-family: Microsoft YaHei, sans-serif; max-width: 640px; margin: 2rem auto; } .message-item { border-bottom: 1px solid #eee; padding: 0.8rem 0; } .meta { color: #999; font-size: 0.85rem; } /style /head body div idapp h2留言板/h2 div input v-modelform.username placeholder昵称 / input v-modelform.content placeholder说点什么 / button clickaddMessage提交/button /div div v-foritem in messages :keyitem.id classmessage-item div{{ item.nickname }} span classmeta{{ item.created_at }}/span/div div{{ item.content }}/div /div /div script srcjs/vue.min.js/script script srcjs/axios.min.js/script script new Vue({ el: #app, data() { return { form: { username: , content: }, messages: [] }; }, mounted() { this.loadMessages(); }, methods: { loadMessages() { axios.get(/api/message) .then(res { this.messages res.data.data; }) .catch(err { console.error(加载失败, err); }); }, addMessage() { axios.post(/api/message, this.form) .then(() { this.form.content ; this.loadMessages(); }) .catch(err { console.error(提交失败, err); }); } } }); /script /body /html这里有一个新手最容易踩的坑mounted()钩子写成mounted: function() {}没问题但要是漏了mounted整个调用页面永远是空的而且控制台不一定报错。el: #app指向的DOM元素必须存在于页面中如果div idapp写成了div idappp实例照样创建但页面一点反应都没有。接口路径用的是相对路径/api/message前提是前端页面和Servlet部署在同一个Tomcat根路径下。如果分开部署比如Vue跑在8080端口、Servlet跑在8081端口就会出现跨域问题那就需要给Servlet加跨域响应头或者用Nginx做一次转发。刚动手做这个系统的阶段强烈建议放在同一个Tomcat下先把主链路跑通再考虑前后端分离部署。3.2 一个Servlet收两类请求用doGet出列表、doPost存留言Vue端只用/api/message这一个路径后台就可以用一个MessageServlet同时接GET和POST两类请求。GET用于查询留言列表POST用于提交新留言这样接口路径简洁Servlet层的职责也清晰。package com.guestbook.web; import com.guestbook.dao.MessageDao; import com.guestbook.entity.Message; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedReader; import java.io.IOException; import java.util.List; WebServlet(/api/message) public class MessageServlet extends HttpServlet { private MessageDao messageDao new MessageDao(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType(application/json;charsetUTF-8); ListMessage messages messageDao.listVisible(); StringBuilder json new StringBuilder({\data\:[); for (int i 0; i messages.size(); i) { Message m messages.get(i); if (i 0) json.append(,); json.append({) .append(\id\:).append(m.getId()).append(,) .append(\nickname\:\).append(m.getNickname()).append(\,) .append(\content\:\).append(m.getContent()).append(\,) .append(\created_at\:\).append(m.getCreatedAt()).append(\) .append(}); } json.append(]}); response.getWriter().write(json.toString()); } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String content request.getParameter(content); // 开发期未做登录时指定 user_id 为 1后续接入登录态后从 Session 中取 boolean ok messageDao.insert(username, content, 1); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\success\: ok }); } }这段代码体现了Servlet处理请求最原始的样子request.getParameter拿参数response.getWriter().write写结果没有任何框架包装。注意doPost里必须调用request.setCharacterEncoding(UTF-8)并且要放在第一次读取参数之前否则中文会乱码。很多老教程只写在doGet里这是不完整的。上面直接拼JSON的方式是为了让代码足够直白实际项目我更推荐用Jackson或Gson库来序列化对象避免手工转义导致XSS边界不清晰。手拼JSON最大的问题是content里出现双引号或者换行字符串就断了。3.3 分层要克制DAO与工具类的边界怎么切Servlet拿着参数之后不直接写JDBC代码而是交给DAO层。这个分层不是摆架子而是让MessageServlet中只出现HTTP处理逻辑数据库操作全部收口在MessageDao里。这样哪天把MySQL换成了别的库或者要把DAO从JDBC换成MyBatis只需要替换一个类。package com.guestbook.dao; import com.guestbook.entity.Message; import com.guestbook.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; public class MessageDao { public ListMessage listVisible() { String sql SELECT m.id, u.nickname, m.content, m.created_at FROM message m JOIN user u ON m.user_id u.id WHERE m.status 0 ORDER BY m.created_at DESC; ListMessage list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Message m new Message(); m.setId(rs.getInt(id)); m.setNickname(rs.getString(nickname)); m.setContent(rs.getString(content)); m.setCreatedAt(rs.getString(created_at)); list.add(m); } } catch (Exception e) { e.printStackTrace(); } return list; } public boolean insert(String username, String content, int userId) { String sql INSERT INTO message (user_id, content) VALUES (?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setString(2, content); return ps.executeUpdate() 1; } catch (Exception e) { e.printStackTrace(); return false; } } }这里最值得说明的就是PreparedStatement的占位符。直接用字符串拼SQL的写法在留言板这种公开可提交的页面上是致命的比如输入); DELETE FROM message; --拼接出来的SQL就会把整张表清空。预编译把参数和SQL语句分离不管用户输入里有什么都只会被当作数据处理这正是servlet项目里对抗SQL注入最重要的一道闸门。另外注意try-with-resources写法连接、Statement、ResultSet都放在try括号里方法结束自动关闭。JDBC连接不释放的问题在留言板里特别常见Tomcat跑一阵就无响应日志里全是“Connection is not available, request timed out”这就是连接泄漏。这个层次的划分不必过重。很多人习惯加一层Service但在“查列表、插入一条留言”这种简单增删查里没有复杂事务和业务规则时加Service就是过度设计。给留言表加一个com.guestbook.entity.Message实体类让DAO返回对象而不是散装字段这个程度刚刚好。4. 登录态与安全三件事Session、加盐密码和参数校验4.1 会话控制用HttpSession维护“谁在留言”超时在哪配前面MessageServlet里写死user_id 1是开发期的临时玩法真实系统不能这样别人只要伪造参数就能替任何人留言。正确做法是登录成功后把用户信息放进HttpSession写留言时从Session里取用户ID而不是从请求参数里取。登录Servlet的常规写法如下package com.guestbook.web; import com.guestbook.dao.UserDao; import com.guestbook.entity.User; import com.guestbook.util.PasswordUtil; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebServlet(/api/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); User user userDao.findByUsername(username); if (user ! null PasswordUtil.verify(password, user.getSalt(), user.getPassword())) { HttpSession session request.getSession(); session.setAttribute(userId, user.getId()); session.setAttribute(nickname, user.getNickname()); response.getWriter().write({\success\:true}); } else { response.getWriter().write({\success\:false,\msg\:\账号或密码错误\}); } } }登录成功后执行session.setAttribute后续的Servlet只要request.getSession().getAttribute(userId)就能知道登录者是谁。Session超时时间在web.xml里配默认是Tomcat的30分钟session-config session-timeout30/session-timeout /session-config退出登录不必写DELETE接口Servlet里一句话就够了HttpSession session request.getSession(); session.invalidate();Session这套机制理解起来很直接服务端把“当前请求属于哪个用户”记在一个内存表里通过Cookie里那个JSESSIONID找到对应记录。它不像JWT那样需要前端存token、传header对“原生Servlet多页面”的留言板来说是最省事的登录态方案。不需要为了赶时髦上JWT这里是给内部系统用的工具减少维护成本才是第一位的。4.2 密码别存明文用JDK自带的SHA-256做加盐哈希用户名密码这个场景最不能做的一件事就是把明文密码直接往user表里塞。数据库一旦被拖库所有账号就裸奔了。这一步不需要引入额外依赖JDK自带的MessageDigest就能做SHA-256哈希再加上随机盐安全性足够应付留言板这种系统。package com.guestbook.util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; public class PasswordUtil { private static final char[] HEX 0123456789abcdef.toCharArray(); public static String generateSalt() { SecureRandom random new SecureRandom(); byte[] bytes new byte[4]; random.nextBytes(bytes); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(HEX[(b 4) 0xf]).append(HEX[b 0xf]); } return sb.toString(); } public static String hash(String salt, String password) { try { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] digest md.digest((salt password).getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(HEX[(b 4) 0xf]).append(HEX[b 0xf]); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(密码哈希失败, e); } } public static boolean verify(String inputPassword, String salt, String storedHash) { String inputHash hash(salt, inputPassword); return inputHash.equals(storedHash); } }这个工具类的输出格式必须和2.2节建表时的SQL保持一致。建表SQL里用的是MySQL的SHA2(CONCAT(s8k2x9f, 123456), 256)这里Java代码就是SHA-256算法对盐 密码的字符串做哈希然后把字节转成64位小写十六进制。两边算法一致登录校验才能通过。SecureRandom生成盐比Random更可靠Random基于时间种子可预测在一定条件下能被猜出来。8位盐算是底线生成时注意去掉大小写之外的符号避免盐里出现特殊字符影响字符串拼接时的歧义。别用MD5它的碰撞攻击成本已经很低再加盐也弥补不了算法本身的先天不足。4.3 面对注入和XSSServlet层的最后一道闸门SQL注入交给PreparedStatement数据库层面基本无虞但XSS这一关要在Servlet的响应侧堵。留言内容是用户可控的如果原样输出到页面别人可以在留言里塞一段script每个访问页面的人都会中招。XSS的防御核心是“输入按数据存储输出按文本转义”。public static String htmlEncode(String input) { if (input null) return ; return input .replace(, amp;) .replace(, lt;) .replace(, gt;) .replace(\, quot;) .replace(, #39;); }在Servlet拼JSON输出时对content字段先做一次htmlEncode再写入响应。注意顺序一定要先转义再转义其他特殊字符否则会把lt;里的再次转成amp;lt;页面显示就坏了。这个顺序问题是XSS工具类里最常见的隐藏坑。至于“用户ID不能从前端传”这件事前面已经提到过。写留言时从Session拿userId从请求参数里只拿content这是留言板安全设计里最便宜也最有效的一条规则。很多翻车事故不是因为密码被破解而是因为后端无条件信任前端传参。5. 避坑指南原生ServletVue项目常见的5个翻车点5.1 乱码成片POST进来中文全变问号现象前端提交“你好”Servlet收到的却是???或者写入MySQL后变成ä½ å¥½这样的乱码串。 原因request.setCharacterEncoding(UTF-8)没在读取参数之前调用或者MySQL连接串没指定字符集。Tomcat 8之后GET请求的URI编码默认是UTF-8但POST请求体的解码用的是平台默认字符集。 解决在Servlet入口统一做两件事第一所有doPost第一行写request.setCharacterEncoding(UTF-8)第二JDBC连接串上加characterEncodingutf8。更省事的办法是写一个CharacterEncodingFilter把这两行逻辑放进去WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); } }5.2 Tomcat 10下ClassNotFoundjavax与jakarta的命名空间切换现象代码在别人电脑上跑得好好的自己下载了最新版Tomcat一启动控制台报ClassNotFoundException: javax.servlet.http.HttpServlet。 原因Tomcat 10把Servlet API从javax.servlet迁移到了jakarta.servlet旧代码引用的包名在新容器里不存在。 解决如果代码全是javax.*开头直接换用Tomcat 9.0.x如果已经用了Tomcat 10就把所有javax.servlet替换为jakarta.servlet一个包名对应一个单词的事。别把旧war包直接丢进新容器那是纯拼运气今天运气好启动成功了明天遇到特定类还是会炸。5.3 axios发的是JSONServlet却读不到参数现象前端用axios.post(/api/message, this.form)提交request.getParameter(content)返回null。 原因axios默认以Content-Type: application/json发送请求体请求体是JSON字符串而getParameter只能解析表单编码类型application/x-www-form-urlencoded的请求体。两边介质不对当然读不到。 解决二选一。要么前端改成axios.post(/api/message, new URLSearchParams(this.form))让请求体变成表单格式要么Servlet里不调getParameter改读request.getReader()然后手动解析JSON字符串。我一般推荐后者因为前端传JSON格式更统一后端用Gson一行就能解析成对象BufferedReader reader request.getReader(); MessageForm form new Gson().fromJson(reader, MessageForm.class);5.4 MySQL连不上error 2002与useSSL、时区报错的组合拳现象启动Tomcat访问接口控制台报ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock或者报The server time zone value ... is unrecognized。 原因2002说明客户端根本没连上MySQL服务要么MySQL没启动要么连接串里指向的socket路径不对时区报错是MySQL 8.0版本对JDBC连接时区的校验更严格连接串没指定serverTimezone就报错。 解决先把MySQL服务启动确认一遍再用一个靠谱的连接串模板jdbc:mysql://localhost:3306/guestbook?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8其中useSSLfalse是本地开发时的常用配置避免MySQL 8.0默认开SSL握手导致本地连接变慢serverTimezoneAsia/Shanghai把服务器时区固定到东八区不写的话数据库真有可能会按UTC存时间查询结果差8小时让人一头雾水。5.5 Vue数据死活不渲染控制台还没报错现象axios.get返回了正常的JSON数据this.messages也被赋值了但页面上v-for区域一片空白。 原因最常见是mounted()没被调用Vue实例没有在DOM加载完成后去拉数据其次是数据返回的字段名和模板不一致比如接口返回nickname但模板写了user_name还有一种是div idapp放在了script加载之后Vue实例挂载时找不到容器。 解决按这个顺序排查——先在mounted里加一行console.log(mounted called)确认有没有执行再在axios的.then里打印res.data看JSON结构最后打开浏览器DevTools的Vue面板直接看this.messages的实时值。这套排查动作做完九成问题都水落石出。6. 把它焊死给我一条总不翻车的最小验收路径这套东西跑通之后我养成了一个固定习惯每次改动代码不走完整UI流程而是先用一条命令验证后端接口再打开页面看渲染。这条路径我已经用了好几年基本成了肌肉记忆。验证后端接口用curl就够了不用每次都开浏览器。在项目根路径启动Tomcat的前提下Linux和macOS直接执行curl -G http://localhost:8080/api/messageWindows的PowerShell用户用这个等价命令Invoke-RestMethod -Uri http://localhost:8080/api/message能看到JSON数组说明Servlet、DAO、JDBC、MySQL四层全通。此时再打开页面如果列表空白问题一定出在Vue层而不是后端。这套顺序能帮你立刻把问题范围缩到一半。我的完整自测顺序是先执行mysql -u root -p init.sql建库建表再改一遍DBUtil里的连接串账号密码然后启动Tomcat先访问/api/health确认容器正常再用curl打一次/api/message确认后端通最后打开页面登录、发留言、看列表、退出登录把主链路的每一步都踩一遍。改动任何一处后再回归也是这个顺序。当年我第一次做这类项目时图省事直接开页面点结果前后端代码都有问题绕了好几个小时才定位到是Servlet路径写错——一个小错却因为没按层级验收而付出了一下午的代价。如果你要把这个项目交给别人或者交作业还有一个小技巧给DBUtil的连接参数留一个集中配置的入口比如一个db.properties文件交出去之前写一句注释“只需改这里”。这个动作省下的沟通成本远比你想象中的大。这套ServletVueMySQL的组合不是性能最强、不是代码最优雅但它是一台“透明机器”每个请求从浏览器出发经过Servlet、DAO、JDBC最终落到MySQL再带着数据原路返回。把它完整跑通一次你对Web后端的理解会踏实很多。希望这篇笔记对你有所帮助。本文还有配套的精品资源点击获取