原生Servlet留言板:Java Web底层原理实战指南 简介这是一套基于原生ServletVueMySQL实现的轻量级留言板系统面向Java Web初学者、课程设计与毕业设计学生帮助掌握前后端分离开发基础流程与传统Java Web技术栈整合实践。资源包共122个文件含43个运行依赖jar包、28个编译后class字节码、13个核心Java后端逻辑源码如LoginServlet、MessageListServlet、ReplyServlet等、9个Vue前端组件及交互脚本、8个XML配置文件含web.xml与MyBatis映射以及JS、JSON、JSP、HTML等配套文件整体13.49MB结构清晰模块职责分明。已有96人学习下载适合快速上手部署与二次开发。下载即用所有代码均经本地编译验证可运行附详细环境配置说明内容由助教审定覆盖用户登录、留言发布、分页展示、回复管理等完整业务闭环是理解Servlet生命周期、Vue数据绑定与MySQL增删改查协同实践的典型教学案例。1. 为什么还在用原生 Servlet 写留言板——一个被低估的“后端裸写”训练场你可能刚学完 Spring Boot随手建个RestController就能返回 JSON也可能正被微服务、网关、鉴权链路绕得头晕。但当你打开这个名为使用原生ServletVueMySql实现的留言板系统.zip的压缩包看到web.xml、HelloServlet.java、手写的 JDBC 工具类、甚至index.jsp里混着% %表达式时第一反应或许是“这玩意儿过时了吧”恰恰相反——它是最接近 Web 底层真相的“可执行教科书”。这不是教学 Demo 的摆设工程而是一个完整闭环用户在 Vue 前端发 POST 请求 → Tomcat 容器调用MessageServlet.doPost()→ Servlet 手动解析request.getParameter(content)→ 用Connection.prepareStatement()拼 SQL 插入 MySQL → 再查出最新 20 条数据封装成 JSON → Vue 用axios.get(/list)拿到并渲染。全程无框架自动装配、无 ORM 隐式事务、无跨域中间件——所有 HTTP 生命周期、线程安全边界、SQL 注入风险点都赤裸裸地摊在你眼皮底下。适合三类人刚学完 Java Web 基础想验证知识闭环的新手Spring 开发者想回溯“容器到底替我干了什么”的进阶者以及需要快速交付轻量级内部工具、拒绝引入复杂依赖的运维/测试同学。它不追求高并发但每行代码都经得起debug单步追踪。2. 从零解压到本地运行Tomcat Vue Dev Server 双端联调最小路径这个.zip包不是单体项目而是明确分层的三件套backend/Servlet 工程、frontend/Vue CLI 项目、db/SQL 初始化脚本。别急着mvn clean install——原生 Servlet 项目压根没用 Maven 管理依赖它靠的是最原始的WEB-INF/lib/目录塞 JAR。我们走一条零配置污染、纯手动验证的路径确保你能看清每个环节的输入输出。2.1 启动后端用最简 Tomcat 9 运行 Servlet 工程先确认你本地有 Tomcat 9必须是 9.x因web.xml中version4.0与 Tomcat 10 的 Jakarta EE 命名空间不兼容。解压backend/到任意目录结构应为backend/ ├── WEB-INF/ │ ├── web.xml ← 核心配置servlet-mapping 和 welcome-file │ └── lib/ │ ├── mysql-connector-java-8.0.28.jar ← 必须放这里 │ └── gson-2.8.9.jar ← 用于 JSON 序列化 ├── index.jsp └── MessageServlet.java ← 实际业务逻辑入口提示mysql-connector-java-8.0.28.jar是关键。若用 8.0.33MySQL 8.0 默认开启caching_sha2_password认证而老版驱动不支持会报Unknown initial character set index 255。血泪经验宁可用 8.0.28别贪新。将整个backend/文件夹复制到 Tomcat 的webapps/目录下重命名为messageboard启动 Tomcat# Linux/macOS $TOMCAT_HOME/bin/startup.sh # Windows %TOMCAT_HOME%\bin\startup.bat访问http://localhost:8080/messageboard/若看到index.jsp渲染的静态页面含表单和“加载中”提示说明 Servlet 容器已接管请求。此时打开浏览器开发者工具 Network 面板点击“提交留言”按钮——你会看到POST /messageboard/add返回 404别慌这是预期行为前端还没连上我们先验证后端 API 是否就绪。2.2 手动触发后端 API用 curl 绕过前端直测 Servletweb.xml中定义了两个关键映射servlet servlet-nameMessageServlet/servlet-name servlet-classcom.example.MessageServlet/servlet-class /servlet servlet-mapping servlet-nameMessageServlet/servlet-name url-pattern/add/url-pattern !-- 接收 POST -- url-pattern/list/url-pattern !-- 接收 GET -- /servlet-mapping这意味着/add和/list是相对于上下文路径messageboard的。用 curl 测试# 发送一条测试留言注意Content-Type 必须是 application/x-www-form-urlencoded curl -X POST http://localhost:8080/messageboard/add \ -H Content-Type: application/x-www-form-urlencoded \ -d contentHello%20from%20curl \ -d authortester # 预期响应{success:true,msg:留言成功} # 获取留言列表 curl http://localhost:8080/messageboard/list # 预期响应[{id:1,content:Hello from curl,author:tester,createTime:2024-06-15 10:20:30}]如果返回 JSON 且数据库有记录说明 Servlet 层完全跑通。关键点在于MessageServlet.java中doPost()方法手动调用request.getParameter()解析参数而非 Spring 的RequestParam自动绑定——这就是“原生”的代价与掌控感。2.3 启动前端Vue CLI 代理解决跨域不改后端一行代码进入frontend/目录确认有package.json和vue.config.js执行npm install npm run serve默认启动在http://localhost:8080—— 等等这和 Tomcat 端口冲突了Vue CLI 的开发服务器默认端口是 8080必须改。打开package.json修改scriptsscripts: { serve: vue-cli-service serve --port 8081, // 改为 8081 build: vue-cli-service build }再运行npm run serve前端将在http://localhost:8081启动。此时访问该地址页面空白因为 Vue 的axios请求仍指向http://localhost:8080/messageboard/list而 Tomcat 在 8080前端在 8081跨域拦截。解决方案利用 Vue CLI 的devServer.proxy在vue.config.js中添加module.exports { devServer: { port: 8081, proxy: { /messageboard: { // 匹配所有以 /messageboard 开头的请求 target: http://localhost:8080, // 转发到 Tomcat changeOrigin: true, // 修改 Origin 头避免 403 pathRewrite: { // 把 /messageboard 去掉再发给后端 ^/messageboard: } } } } }现在 Vue 中的请求可以简化为// frontend/src/api/message.js export function fetchMessages() { return axios.get(/messageboard/list) // 开发时自动代理到 http://localhost:8080/list } export function postMessage(data) { return axios.post(/messageboard/add, data) // 自动代理到 http://localhost:8080/add }保存后刷新http://localhost:8081留言框出现提交即生效。整个过程未修改后端任何代码也未在 Tomcat 中配置 CORS Filter——代理是前端开发阶段的“后悔药”上线时才需后端真正处理跨域。3. 数据库初始化与 JDBC 连接池手写工具类比 HikariCP 更暴露问题db/目录下只有一个init.sql文件内容极简CREATE DATABASE IF NOT EXISTS messageboard DEFAULT CHARACTER SET utf8mb4; USE messageboard; CREATE TABLE messages ( id INT PRIMARY KEY AUTO_INCREMENT, content TEXT NOT NULL, author VARCHAR(50) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;执行它只需两步登录 MySQLmysql -u root -p执行source /path/to/init.sql但真正的坑在MessageServlet.java的数据库连接部分。它没有用任何连接池每次请求都新建连接// MessageServlet.java 片段 private Connection getConnection() throws SQLException { String url jdbc:mysql://localhost:3306/messageboard?useSSLfalseserverTimezoneAsia/Shanghai; return DriverManager.getConnection(url, root, password); }这在高并发下必然翻车。我们来把它升级为轻量级连接池不引入 HikariCP太重而用commons-dbcp2Apache 经典方案3.1 添加 DBCP2 依赖并初始化全局连接池下载commons-dbcp2-2.9.0.jar和commons-pool2-2.11.1.jar放入backend/WEB-INF/lib/。在MessageServlet类顶部添加静态连接池import org.apache.commons.dbcp2.BasicDataSource; public class MessageServlet extends HttpServlet { private static final BasicDataSource dataSource new BasicDataSource(); static { dataSource.setUrl(jdbc:mysql://localhost:3306/messageboard?useSSLfalseserverTimezoneAsia/Shanghai); dataSource.setUsername(root); dataSource.setPassword(password); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxTotal(20); // 最大连接数 dataSource.setMaxIdle(10); // 最大空闲连接 dataSource.setMinIdle(5); // 最小空闲连接 dataSource.setTestOnBorrow(true); // 借出前检测连接有效性 dataSource.setValidationQuery(SELECT 1); // 检测 SQL } private Connection getConnection() throws SQLException { return dataSource.getConnection(); // 从此处获取连接 } }参数说明initialSize5避免冷启动时首次请求慢maxTotal20是经验值按留言板场景QPS10足够testOnBorrowtrue加validationQuerySELECT 1是防 MySQL 连接超时断开wait_timeout默认 8 小时但连接池可能更久否则会出现Communications link failure。3.2 修复 SQL 注入从Statement升级到PreparedStatement原代码中插入留言的写法可能是// ❌ 危险字符串拼接 SQL String sql INSERT INTO messages(content, author) VALUES( content , author ); statement.executeUpdate(sql);必须改为预编译// ✅ 安全参数化查询 String sql INSERT INTO messages(content, author) VALUES(?, ?); try (PreparedStatement ps connection.prepareStatement(sql)) { ps.setString(1, content); ps.setString(2, author); ps.executeUpdate(); }为什么必须改因为content可能是); DROP TABLE messages; --拼接后 SQL 变成INSERT INTO messages(content, author) VALUES(); DROP TABLE messages; --, hacker)直接执行就是删库。PreparedStatement会把?当作占位符由驱动转义特殊字符彻底杜绝此风险。这是原生 Servlet 开发者必须亲手刻进肌肉记忆的安全底线。4. 避坑指南5 个让新手卡住 2 小时以上的典型问题原生 Servlet 项目没有框架兜底错误信息往往晦涩。以下是我在某高校实训课带学生时高频出现的 5 个问题按“现象→原因→解决”给出可立即操作的方案4.1 现象访问http://localhost:8080/messageboard/显示 404但 Tomcat 日志无报错原因backend/文件夹未正确放入webapps/或文件夹名与web.xml中display-name不一致某些旧版 Tomcat 会校验。更常见的是index.jsp文件编码为 GBK而 Tomcat 默认用 UTF-8 读取导致 JSP 编译失败但错误被静默吞掉。解决检查webapps/messageboard/下是否存在index.jsp非index.html用 VS Code 或 Notepad 打开index.jsp确认右下角编码显示为UTF-8无 BOM若是 GBK另存为 UTF-8重启 Tomcat。4.2 现象curl -X POST ... /add返回HTTP 405 Method Not Allowed原因MessageServlet.java中只实现了doGet()未重写doPost()方法或web.xml中url-pattern写成了/add/多了一个斜杠导致匹配失败。解决检查MessageServlet类中是否有protected void doPost(HttpServletRequest req, HttpServletResponse resp)方法对照web.xml中url-pattern/add/url-pattern确认 URL 路径与代码中WebServlet(/add)如果用了注解或 XML 配置完全一致在doPost()开头加System.out.println(POST received!);重启 Tomcat 后看控制台是否打印——这是最直接的验证。4.3 现象Vue 提交留言后后端返回{success:false,msg:数据库操作失败}但 Tomcat 日志只显示java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver原因MySQL 8.0 驱动类名从com.mysql.jdbc.Driver变更为com.mysql.cj.jdbc.Driver而MessageServlet.java中Class.forName(com.mysql.jdbc.Driver)未更新。解决打开MessageServlet.java找到Class.forName(...)行改为Class.forName(com.mysql.cj.jdbc.Driver)确保mysql-connector-java-8.0.28.jar在WEB-INF/lib/中不是lib/或项目根目录。4.4 现象留言中文显示为???数据库字段存的是乱码原因三个环节任一缺失 UTF-8① MySQL 服务端字符集character_set_serverutf8mb4② 数据库/表字符集CREATE DATABASE ... DEFAULT CHARSET utf8mb4③ JDBC URL 未指定characterEncodingutf8。解决登录 MySQL执行SHOW VARIABLES LIKE character_set_%;确认character_set_server为utf8mb4执行ALTER DATABASE messageboard CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;修改 JDBC URLjdbc:mysql://localhost:3306/messageboard?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8。4.5 现象Vue 页面空白浏览器 Console 报Error: Network ErrorNetwork 面板显示Failed to load resource: net::ERR_CONNECTION_REFUSED原因Vue 的axios请求地址写死了http://localhost:8080/messageboard/list但开发时前端在 8081 端口且未配置devServer.proxy导致请求发向了不存在的 8080 前端服务而非 Tomcat。解决检查vue.config.js中devServer.proxy配置是否生效重启npm run serve在 Vue 组件中console.log(process.env.NODE_ENV)确认是development模式在src/api/message.js中临时将 URL 改为绝对路径http://localhost:8080/messageboard/list仅用于测试——若此时能通证明是代理配置问题。5. 前后端分离的临界点用 Fetch API 替代 Axios理解原生请求生命周期当 Vue 项目稳定运行后不妨做一次“降级实验”删除axios改用浏览器原生fetchAPI。这不是为了炫技而是为了看清框架封装了什么。在frontend/src/api/message.js中将// 原 axios 版本 import axios from axios export function fetchMessages() { return axios.get(/messageboard/list) }替换为// fetch 版本 export function fetchMessages() { return fetch(/messageboard/list) .then(response { if (!response.ok) { throw new Error(HTTP error! status: ${response.status}) } return response.json() }) } export function postMessage(data) { return fetch(/messageboard/add, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded, }, body: new URLSearchParams(data) // 将 {content:xxx,author:yyy} 转为 contentxxxauthoryyy }).then(response response.json()) }关键差异说明axios自动将data对象序列化为x-www-form-urlencoded而fetch需手动用URLSearchParams转换axios默认处理 JSON 解析response.datafetch需显式调用.json()fetch不会因 HTTP 状态码非 2xx 抛错如 404、500必须手动if (!response.ok)判断axios的拦截器如统一加 token在fetch中需封装成函数或用AbortController控制请求取消。这个改动让前端代码行数增加但你会立刻意识到Vue 生态中所谓“简单”本质是框架用约定掩盖了 HTTP 的复杂性。当某天你需要对接一个要求自定义Authorization头、且返回非标准 JSON 的遗留系统时fetch的透明性反而成为优势。更进一步打开浏览器 Network 面板对比axios和fetch发出的请求你会发现fetch的Request Headers中多了一行sec-fetch-mode: cors而axios没有——这是因为axios内部用XMLHttpRequestfetch是新标准它们触发 CORS 预检的条件不同。这种细节在 Spring Boot 项目里永远看不到因为CrossOrigin注解一键屏蔽了全部。最后我养成了一个习惯每当接手一个新项目先关掉所有框架文档用curl和fetch手写一遍核心接口调用。不是为了不用框架而是为了在框架失灵时还有能力打开黑匣子用最原始的工具定位问题。这个留言板系统就是你的第一个黑匣子训练场。希望帮到你。本文还有配套的精品资源点击获取