ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

原生Servlet+Vue+MySQL留言板实战:从环境搭建到部署排错

原生Servlet+Vue+MySQL留言板实战:从环境搭建到部署排错 简介这是一套面向Java Web初学者与课程设计、毕业设计需求者的留言板系统源码采用原生Servlet搭配Vue与MySQL实现适合想理解前后端分离基础流程、巩固JSP与Servlet知识的学生参考。压缩包共122个文件约13.49MB其中jar包与class文件构成运行依赖与编译产物java源码与jsp页面体现后端逻辑vue、js、json与css、html负责前端交互与界面xml、md等用于配置和说明整体结构完整、层次清晰。目前已有96人学习下载。源码均经本地编译验证按文档配置环境即可运行难度适中内容经助教老师审定可满足学习与使用需求。读者可借此掌握登录、留言列表、回复等核心模块的实现思路理解Servlet请求处理与数据库交互方式并对照前端组件完成调试与二次开发遇到问题也可私信作者获取解答。1. 原生 Servlet Vue MySQL 留言板为什么这套“老架构”反而最适合练手很多人一听到“原生 Servlet”就觉得过时直接上 Spring Boot。但如果你真想搞懂一个 Web 请求从浏览器发出到数据库落盘再回到页面的完整链路原生 Servlet Vue MySQL 这套组合反而是最透明的。没有自动配置帮你藏黑匣子没有 Starter 帮你封装连接池每一个HttpServlet的doGet/doPost、每一条 JDBC 连接、每一次前后端 JSON 交互你都得自己写清楚。留言板系统恰好覆盖了增删改查、分页、时间格式化、跨域这几个最核心的场景代码量不大但该踩的坑一个不少。这篇文章面向的是想从“只会调框架”进阶到“知道框架底下干了什么”的开发者也适合刚学完 Servlet 和 Vue 基础、需要一个完整项目把知识点串起来的人。下面从环境搭建、后端接口、前端联调到部署排错一步步把可复现的路径写清楚。2. 环境准备JDK、Tomcat、MySQL 与 Vue 脚手架的版本对齐2.1 后端环境JDK 与 Tomcat 的匹配关系原生 Servlet 项目最省事的运行方式是打成 WAR 包丢进 Tomcat。JDK 版本和 Tomcat 版本必须对齐否则启动时直接报UnsupportedClassVersionError。常见组合是 JDK 8 Tomcat 8.5/9.0或者 JDK 11 Tomcat 9.0。如果你用的是 JDK 17Tomcat 必须上到 10.0 以上但 Tomcat 10 把javax.servlet包名改成了jakarta.servlet老代码直接编译不过。我一般建议新手锁死 JDK 8 Tomcat 9.0网上资料最多遇到问题好搜。安装完 JDK 后配置JAVA_HOMETomcat 解压后配置CATALINA_HOME。验证命令java -version # 输出应为 1.8.0_xxx 或 11.0.x echo %JAVA_HOME% # Windows 下应输出 JDK 安装路径 %CATALINA_HOME%\bin\startup.bat # 启动后浏览器访问 http://localhost:8080 看到 Tomcat 欢迎页即成功参数说明JAVA_HOME指向 JDK 根目录而不是bin目录这是新手最常犯的错。CATALINA_HOME指向 Tomcat 解压目录。如果 8080 端口被占用改conf/server.xml里的Connector port8080为其他端口。2.2 MySQL 安装与建库建表MySQL 用 5.7 或 8.0 都行但 8.0 的 JDBC 驱动类名和连接 URL 参数有变化。安装时记住 root 密码字符集选utf8mb4。建库语句CREATE DATABASE message_board DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE message_board; CREATE TABLE message ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, content TEXT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明id自增主键create_time用CURRENT_TIMESTAMP让数据库自动填时间省去 Java 层格式化。字符集必须用utf8mb4否则用户输入 emoji 会报Incorrect string value。参数上VARCHAR(50)对用户名够用TEXT对留言内容够用如果要做富文本再换LONGTEXT。2.3 Vue 环境Node 版本与脚手架初始化Vue 用 Vue CLI 或 Vite 都行。Vue CLI 对 Node 版本要求宽松Vite 要求 Node 14.18 或 16。我一般用 Vite启动快。初始化命令npm create vitelatest message-board-frontend -- --template vue cd message-board-frontend npm install npm install axios npm run dev逻辑说明--template vue指定用 Vue 3 模板axios用来发 HTTP 请求。启动后默认跑在 5173 端口。注意 Vite 默认只监听 localhost如果后端和前端不在同一台机器需要在vite.config.js里加server.host: 0.0.0.0。3. 后端接口用原生 Servlet 写留言的增删查与 JSON 响应3.1 Servlet 类结构与 web.xml 还是注解原生 Servlet 有两种注册方式web.xml配置和WebServlet注解。现在一般用注解少写一堆 XML。一个留言查询的 Servlet 示例WebServlet(/api/messages) public class MessageListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(application/json;charsetUTF-8); resp.setHeader(Access-Control-Allow-Origin, *); ListMessage list new ArrayList(); String sql SELECT id, username, content, create_time FROM message ORDER BY create_time DESC; 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.setUsername(rs.getString(username)); m.setContent(rs.getString(content)); m.setCreateTime(rs.getString(create_time)); list.add(m); } } catch (SQLException e) { resp.setStatus(500); resp.getWriter().write({\error\:\ e.getMessage() \}); return; } StringBuilder json new StringBuilder([); for (int i 0; i list.size(); i) { Message m list.get(i); json.append(String.format( {\id\:%d,\username\:\%s\,\content\:\%s\,\createTime\:\%s\}, m.getId(), m.getUsername(), m.getContent(), m.getCreateTime())); if (i list.size() - 1) json.append(,); } json.append(]); resp.getWriter().write(json.toString()); } }逻辑说明doGet处理查询请求设置响应类型为 JSON 并允许跨域。用PreparedStatement防 SQL 注入try-with-resources自动关连接。手动拼 JSON 是为了不引入 Gson/Jackson 依赖但要注意转义问题——如果留言内容里有双引号拼出来的 JSON 会坏掉。生产环境建议用 Gson。参数说明Access-Control-Allow-Origin: *允许任何域名跨域开发阶段方便上线要改成具体域名。ORDER BY create_time DESC让最新留言排前面。3.2 数据库连接工具类与连接池选择原生 JDBC 每次请求都DriverManager.getConnection开销大但留言板并发低直接用也行。封装一个DBUtilpublic class DBUtil { private static final String URL jdbc:mysql://localhost:3306/message_board?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD your_password; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动未找到, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }逻辑说明静态块加载驱动MySQL 8.0 用com.mysql.cj.jdbc.Driver5.7 用com.mysql.jdbc.Driver。URL 里useSSLfalse关掉 SSL 警告serverTimezoneAsia/Shanghai解决时区差 8 小时的问题characterEncodingutf8保证中文不乱码。参数说明如果报Unknown system variable query_cache_size说明驱动版本和 MySQL 版本不匹配换对应版本的mysql-connector-java。连接池可以用 Druid 或 HikariCP但原生项目里手动管理连接更能看清 JDBC 流程。3.3 新增留言的 POST 处理与请求体解析新增留言用doPost从请求体读 JSON。原生 Servlet 没有内置 JSON 解析得手动读BufferedReaderWebServlet(/api/message/add) public class MessageAddServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(application/json;charsetUTF-8); resp.setHeader(Access-Control-Allow-Origin, *); StringBuilder sb new StringBuilder(); String line; try (BufferedReader reader req.getReader()) { while ((line reader.readLine()) ! null) { sb.append(line); } } // 简单解析生产环境用 Gson String body sb.toString(); String username extractValue(body, username); String content extractValue(body, content); if (username null || username.trim().isEmpty() || content null || content.trim().isEmpty()) { resp.setStatus(400); resp.getWriter().write({\error\:\用户名和内容不能为空\}); return; } String sql INSERT INTO message (username, content) VALUES (?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, content); ps.executeUpdate(); resp.getWriter().write({\success\:true}); } catch (SQLException e) { resp.setStatus(500); resp.getWriter().write({\error\:\ e.getMessage() \}); } } private String extractValue(String json, String key) { String pattern \ key \\\s*:\\s*\([^\]*)\; java.util.regex.Matcher m java.util.regex.Pattern.compile(pattern).matcher(json); return m.find() ? m.group(1) : null; } }逻辑说明req.setCharacterEncoding(UTF-8)必须在读请求体之前调用否则中文乱码。extractValue用正则从 JSON 里取值简单但脆弱内容里有转义字符会出错。参数上ps.setString自动处理特殊字符比拼 SQL 安全。4. 前端联调Vue 组件发请求、渲染列表与跨域处理4.1 axios 封装与请求拦截在 Vue 项目里建src/api/message.jsimport axios from axios const request axios.create({ baseURL: http://localhost:8080, timeout: 5000 }) request.interceptors.response.use( response response.data, error { console.error(请求失败, error) return Promise.reject(error) } ) export const getMessages () request.get(/api/messages) export const addMessage (data) request.post(/api/message/add, data)逻辑说明baseURL指向 Tomcat 地址timeout防止请求挂死。响应拦截器直接返回response.data组件里不用再.data。参数上如果后端和前端同域部署baseURL可以留空用相对路径。4.2 留言列表组件与表单提交MessageBoard.vue核心逻辑template div classboard form submit.preventhandleSubmit input v-modelform.username placeholder昵称 / textarea v-modelform.content placeholder留言内容/textarea button typesubmit提交/button /form ul li v-formsg in messages :keymsg.id strong{{ msg.username }}/strong span{{ msg.createTime }}/span p{{ msg.content }}/p /li /ul /div /template script setup import { ref, onMounted } from vue import { getMessages, addMessage } from ../api/message const messages ref([]) const form ref({ username: , content: }) const loadMessages async () { messages.value await getMessages() } const handleSubmit async () { if (!form.value.username || !form.value.content) return await addMessage(form.value) form.value { username: , content: } await loadMessages() } onMounted(loadMessages) /script逻辑说明v-model双向绑定表单submit.prevent阻止默认提交。提交成功后清空表单并重新拉列表。参数上ref定义响应式数据onMounted在组件挂载时加载列表。4.3 跨域问题的三种解法与选择开发阶段前端 5173 端口、后端 8080 端口浏览器直接拦跨域。三种解法方案做法适用场景后端加响应头Access-Control-Allow-Origin: *开发快速验证Vite 代理vite.config.js配server.proxy开发阶段推荐Nginx 反代生产环境统一域名上线部署Vite 代理配置// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })逻辑说明代理把/api开头的请求转发到 8080浏览器以为同域不触发跨域。参数上changeOrigin: true修改请求头里的 Host某些后端会校验。5. 避坑排查中文乱码、时区差 8 小时、JSON 拼串翻车5.1 中文乱码从请求到数据库全链路排查现象前端输入中文数据库里显示???或者乱码。原因可能在三处请求体编码、数据库连接编码、表字符集。解决Servlet 里req.setCharacterEncoding(UTF-8)必须在getReader()之前JDBC URL 加characterEncodingutf8建表用utf8mb4。三处缺一不可血泪经验是只改一处没用。5.2 时间显示差 8 小时现象数据库存的是2025-01-01 10:00:00前端显示2025-01-01 02:00:00。原因是 JDBC 驱动默认用 UTC 时区解析DATETIME。解决URL 加serverTimezoneAsia/Shanghai或者把 MySQL 全局时区设为08:00。注意serverTimezone参数在 MySQL 8.0 驱动里才生效5.7 驱动用useTimezonetrueserverTimezoneGMT%2B8。5.3 手动拼 JSON 遇到特殊字符就坏现象留言内容里带双引号或换行前端JSON.parse报错。原因是String.format拼 JSON 没转义。解决引入 Gsonnew Gson().toJson(list)一行搞定。如果坚持手拼至少把替换成\\n替换成\\n。这个坑在测试时不容易发现上线后用户一输入特殊字符就翻车。5.4 Tomcat 启动报 404 但控制台没报错现象访问/api/messages返回 404Tomcat 日志无异常。原因通常是WebServlet注解没生效检查web.xml的metadata-complete是否为true如果是则注解被忽略。解决删掉metadata-completetrue或者改用web.xml显式配置。另一个原因是 WAR 包没部署到webapps目录或者上下文路径不对。5.5 Vue 打包后刷新页面 404现象npm run build后丢进 Tomcat首页能开刷新子路由就 404。原因是前端路由是 history 模式服务器找不到对应路径。解决改用 hash 模式或者在 Tomcat 配web.xml的error-page重定向到index.html。更省事的是用 Nginx 配try_files $uri $uri/ /index.html。6. 进阶技巧把留言板改成前后端分离部署的完整验证清单项目跑通之后别急着丢一边。我一般会做三件事来验证自己真的掌握了这套架构。第一把后端打成 WAR 丢进 Tomcat 的webapps前端npm run build生成dist用 Nginx 同时托管静态文件和反代/api模拟真实上线环境。Nginx 配置片段server { listen 80; root /var/www/message-board/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } }逻辑说明try_files解决前端路由刷新 404proxy_pass把 API 请求转给 Tomcat。参数上proxy_set_header Host $host保证后端拿到的 Host 是真实域名。第二用 Postman 或 curl 直接压后端接口确认没有前端参与时接口依然正常。命令curl -X POST http://localhost:8080/api/message/add \ -H Content-Type: application/json \ -d {username:test,content:hello}如果返回{success:true}说明后端独立可用。第三把数据库连接信息抽到db.properties文件用Properties类加载避免密码硬编码在 Java 文件里。这个习惯在后续任何项目里都用得上。最后说一个我自己的教训早期写原生 Servlet 时我总想把所有逻辑塞进一个 Servlet 里结果doGet里又是查询又是删除改一个功能牵动全身。后来强制自己按资源拆分——MessageListServlet只管列表MessageAddServlet只管新增MessageDeleteServlet只管删除——代码立刻清爽了。原生 Servlet 没有框架的路由约束反而更需要自己守住单一职责。希望帮到你。本文还有配套的精品资源点击获取
返回列表