资讯动态

原生Servlet+Vue+MySQL留言板:前后端分离最小闭环实战

发布时间:2026/10/6 8:19:27 来源:尧图企业网站定制
简介这是一套面向Java Web初学者与课程设计、毕业设计需求者的留言板系统源码采用原生Servlet搭配Vue与MySQL实现适合想脱离框架、理解请求响应底层流程的开发者练手。压缩包共122个文件约13.49MB其中jar包与class文件构成运行依赖java源码承载Servlet核心逻辑vue与js负责前端交互xml、json、jsp等完成配置与页面渲染另有md文档辅助说明。目前已有96人学习下载。项目经本地编译可直接运行按文档配置环境即可启动难度适中且经助教审定涵盖登录、留言列表、回复等典型模块能帮助读者掌握Servlet生命周期、前后端数据交互与数据库操作也可作为课程设计或毕业设计的参考方案遇到问题还可私信作者获取解答。1. 原生 Servlet Vue MySQL 留言板一套能跑通的前后端分离最小闭环很多人第一次接触 Java Web都是从留言板开始的。但真正动手时才发现卡住自己的不是业务逻辑而是环境Tomcat 版本对不上、Servlet 映射 404、Vue 打包后跨域、MySQL 8 的驱动类名换了。这套「原生 Servlet Vue MySQL」的组合恰好绕开了 Spring 全家桶的封装把 HTTP 请求、JSON 序列化、JDBC 连接这些底层环节全部暴露出来适合想搞懂「一个请求到底怎么从浏览器走到数据库再回来」的人。它解决的核心问题是在不引入任何框架的前提下跑通一套前后端分离的增删改查。前端 Vue 负责渲染和交互后端 Servlet 负责接收请求、操作 MySQL、返回 JSON两边通过 HTTP 接口通信。适合刚学完 Java 语法、想找一个完整项目练手的学生也适合长期写框架、想回头补一补原生基础的开发者。下面按「环境怎么搭 → 接口怎么写 → 前端怎么接 → 坑在哪」的顺序拆开讲。2. 环境搭建JDK、Tomcat、MySQL 与 Vue 脚手架的版本对齐2.1 四个组件的版本选择与安装顺序这套方案涉及四个独立组件版本不匹配是最常见的翻车点。我一般按 JDK → Tomcat → MySQL → Node/Vue 的顺序装因为前面的组件会影响后面的配置。组件推荐版本关键说明JDK8 或 11Servlet 3.1 需要 JDK 7 以上JDK 8 兼容性最好Tomcat9.xTomcat 10 把javax.servlet改成了jakarta.servlet老代码直接报错MySQL5.7 或 8.08.0 驱动类名是com.mysql.cj.jdbc.Driver5.7 是com.mysql.jdbc.DriverNode.js16 或 18Vue 3 的 Vite 需要 Node 14.18Vue CLI 需要 Node 8Tomcat 10 的包名迁移是血泪经验。如果你从网上抄的代码里写的是import javax.servlet.http.HttpServlet在 Tomcat 10 上部署会直接抛ClassNotFoundException。要么换 Tomcat 9要么把所有javax.servlet改成jakarta.servlet。新手建议直接用 Tomcat 9省掉这层麻烦。MySQL 8.0 安装时有个细节认证插件默认是caching_sha2_password老版本的 JDBC 驱动连不上。要么升级驱动到 8.0.x要么在 MySQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;改回旧插件。2.2 后端项目结构与依赖配置用 Maven 建一个 webapp 项目pom.xml里只需要三个依赖Servlet API、MySQL 驱动、JSON 处理库。dependencies !-- Servlet APIscope 必须是 provided因为 Tomcat 自带 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- MySQL 8.0 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 手动拼 JSON 太痛苦用 Gson 省事 -- dependency groupIdcom.google.code.gson/groupId artifactIdgson/artifactId version2.10.1/version /dependency /dependenciesscope设为provided是关键。Servlet API 由 Tomcat 容器提供如果打成 war 包时带进去会和容器自带的版本冲突启动时报NoSuchMethodError。MySQL 驱动和 Gson 则必须打包进去因为容器不管这两个。数据库建一张表就够了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;utf8mb4而不是utf8因为 MySQL 的utf8只支持 3 字节存 emoji 会报错。create_time用DEFAULT CURRENT_TIMESTAMP让数据库自动填时间省掉 Java 端格式化日期的代码。2.3 Vue 前端初始化与目录约定前端用 Vue CLI 或 Vite 都行我倾向 Vite启动快。初始化命令npm create vitelatest message-board-frontend -- --template vue cd message-board-frontend npm install npm install axios npm run devaxios用来发 HTTP 请求。Vue 3 的script setup语法比 Options API 简洁下面统一用它。目录上把接口请求单独抽到src/api/message.js组件里只调函数不直接写 URL方便后期改地址。提示Vue 开发服务器默认跑在 5173 端口Tomcat 跑在 8080两个端口不同源浏览器会拦跨域请求。解决办法在下一章讲。3. 后端接口Servlet 接收请求、操作 MySQL、返回 JSON3.1 一个 Servlet 处理增删改查的映射方式原生 Servlet 没有路由框架每个 URL 对应一个 Servlet 类或者用一个 Servlet 根据参数分发。留言板接口少我一般用两个 ServletMessageListServlet处理查询和新增MessageDeleteServlet处理删除。用WebServlet注解配置路径不用改web.xml。WebServlet(/api/message) public class MessageServlet extends HttpServlet { private final Gson gson new Gson(); // GET 请求查询所有留言 Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(application/json;charsetUTF-8); ListMessage list new ArrayList(); String sql SELECT id, username, content, create_time FROM message ORDER BY id 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; } resp.getWriter().write(gson.toJson(list)); } }doGet里用 try-with-resources 自动关闭Connection、PreparedStatement、ResultSet避免连接泄漏。ORDER BY id DESC让最新的留言排在最前面。返回时用 Gson 把 List 直接序列化成 JSON 数组前端拿到就能渲染。doPost处理新增从请求体读 JSONOverride protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(application/json;charsetUTF-8); // 读取请求体 StringBuilder sb new StringBuilder(); try (BufferedReader reader req.getReader()) { String line; while ((line reader.readLine()) ! null) { sb.append(line); } } Message msg gson.fromJson(sb.toString(), Message.class); // 参数校验空值直接拒绝 if (msg.getUsername() null || msg.getUsername().trim().isEmpty() || msg.getContent() null || msg.getContent().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, msg.getUsername().trim()); ps.setString(2, msg.getContent().trim()); ps.executeUpdate(); } catch (SQLException e) { resp.setStatus(500); resp.getWriter().write({\error\:\ e.getMessage() \}); return; } resp.getWriter().write({\code\:0,\msg\:\success\}); }req.setCharacterEncoding(UTF-8)必须在读取请求体之前调用否则中文会乱码。参数校验放在 SQL 执行前空字符串直接返回 400不要等到数据库报错。PreparedStatement的?占位符既防 SQL 注入又省去手动转义引号的麻烦。3.2 数据库连接工具类与连接池的取舍上面代码里的DBUtil.getConnection()需要自己写。最简版本用DriverManager每次新建连接public 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 你的密码; 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); } }URL 里三个参数必须加useSSLfalse关掉 SSL 警告serverTimezoneAsia/Shanghai解决时区差 8 小时的问题characterEncodingutf8保证中文正常。Class.forName在 JDBC 4.0 之后其实可以省略但显式加载能更早暴露驱动缺失的问题。每次请求新建连接在留言板这种低并发场景够用但连接创建开销大。如果要做压力测试换成 Druid 或 HikariCP 连接池把getConnection改成从池里借。留言板项目我一般先用DriverManager跑通确认逻辑没问题再换池避免一开始就引入额外配置。3.3 跨域处理一个 Filter 解决 OPTIONS 预检前端跑在 5173后端跑在 8080浏览器同源策略会拦截。最省事的办法是写一个 CORS FilterWebFilter(/api/*) public class CorsFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletResponse response (HttpServletResponse) resp; response.setHeader(Access-Control-Allow-Origin, http://localhost:5173); response.setHeader(Access-Control-Allow-Methods, GET, POST, DELETE, OPTIONS); response.setHeader(Access-Control-Allow-Headers, Content-Type); response.setHeader(Access-Control-Max-Age, 3600); HttpServletRequest request (HttpServletRequest) req; // OPTIONS 预检请求直接返回 200不往下走 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); return; } chain.doFilter(req, resp); } }Access-Control-Allow-Origin写具体的前端地址不要写*因为带 Cookie 的请求不允许通配符。Access-Control-Max-Age设为 3600 秒让浏览器缓存预检结果减少 OPTIONS 请求次数。OPTIONS 请求直接返回不要传给 Servlet否则 Servlet 没有doOptions方法会返回 405。注意生产环境前端打包后和 Tomcat 部署在同一域名下就不需要这个 Filter 了。跨域只是开发阶段的临时方案。4. 前端对接Vue 组件发请求、渲染列表、提交表单4.1 axios 封装与接口调用在src/api/message.js里统一管理接口import axios from axios // 开发环境指向 Tomcat生产环境改成同域路径 const baseURL http://localhost:8080/api const request axios.create({ baseURL, timeout: 5000 }) // 响应拦截器统一处理错误 request.interceptors.response.use( res res.data, err { console.error(请求失败:, err.message) return Promise.reject(err) } ) export const getMessages () request.get(/message) export const addMessage (data) request.post(/message, data) export const deleteMessage (id) request.delete(/message?id${id})axios.create把baseURL和超时时间抽出来后面改地址只改一处。响应拦截器直接返回res.data组件里就不用每次写.data。timeout设 5000 毫秒超过就报错避免请求卡死时界面一直转圈。4.2 留言列表组件与表单提交主组件MessageBoard.vuetemplate div classboard form submit.preventhandleSubmit input v-modelform.username placeholder昵称 maxlength50 / textarea v-modelform.content placeholder说点什么... maxlength500/textarea button typesubmit :disabledloading发布/button /form ul classlist li v-foritem in messages :keyitem.id strong{{ item.username }}/strong span{{ item.createTime }}/span p{{ item.content }}/p button clickhandleDelete(item.id)删除/button /li /ul /div /template script setup import { ref, onMounted } from vue import { getMessages, addMessage, deleteMessage } from ../api/message const messages ref([]) const loading ref(false) const form ref({ username: , content: }) const loadMessages async () { messages.value await getMessages() } const handleSubmit async () { if (!form.value.username.trim() || !form.value.content.trim()) return loading.value true try { await addMessage(form.value) form.value { username: , content: } await loadMessages() } finally { loading.value false } } const handleDelete async (id) { await deleteMessage(id) await loadMessages() } onMounted(loadMessages) /scriptsubmit.prevent阻止表单默认提交行为否则页面会刷新。v-model双向绑定输入框maxlength在前端限制长度和后端数据库字段长度对齐。提交成功后清空表单并重新拉取列表保证显示的是数据库最新状态。loading状态防止用户连点按钮重复提交。4.3 时间格式与列表渲染的细节后端返回的createTime是 MySQL 的DATETIME格式形如2024-01-15 10:30:00前端直接显示就行。如果想让显示更友好可以在 Vue 里加一个格式化函数const formatTime (str) { if (!str) return // 把 2024-01-15 10:30:00 转成 01-15 10:30 return str.slice(5, 16) }列表渲染用:keyitem.id不要用 index。用 index 做 key 时删除中间某条会导致后续元素复用错误的 DOM出现内容错位。item.id是数据库主键唯一且稳定。提示如果列表数据量大v-for渲染几百条会卡。留言板一般不会但如果要优化用虚拟滚动或分页。分页在 Servlet 里加LIMIT和OFFSET即可。5. 避坑与排查从 404 到乱码的 5 个真实问题5.1 访问接口返回 404Tomcat 日志无报错现象前端请求/api/message返回 404Tomcat 的catalina.out里没有任何异常。原因WebServlet注解没生效或者web.xml里metadata-completetrue导致容器不扫描注解。解决检查web.xml的web-app标签metadata-complete必须是false或干脆不写这个属性。另外确认pom.xml里 Servlet API 的scope是provided如果打成compile会冲突。最后看 Tomcat 的conf/server.xml里appBase指向的目录war 包是否真的解压了。5.2 中文乱码请求体和响应各有一套解法现象提交的留言在数据库里显示为???或者前端收到的 JSON 里中文变成乱码。原因请求体没设 UTF-8响应头没设charset。解决doPost第一行写req.setCharacterEncoding(UTF-8)doGet和doPost都要写resp.setContentType(application/json;charsetUTF-8)。数据库连接 URL 加characterEncodingutf8建表用utf8mb4。四个地方缺一个都会乱码按顺序排查。5.3 MySQL 8 连接报Public Key Retrieval is not allowed现象JDBC 连接 MySQL 8.0 时抛java.sql.SQLException: Public Key Retrieval is not allowed。原因MySQL 8.0 默认用caching_sha2_password认证JDBC 驱动需要额外参数允许公钥检索。解决在连接 URL 后面加allowPublicKeyRetrievaltrue。完整 URL 变成jdbc:mysql://localhost:3306/message_board?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue。或者改用户认证插件为mysql_native_password。5.4 前端打包后刷新页面 404现象npm run build生成的dist放到 Tomcat 里首页能打开但刷新/list这类路由就 404。原因Vue Router 的 history 模式需要服务端把所有路径都转发到index.htmlTomcat 默认按文件路径找找不到就 404。解决要么把 Vue Router 改成 hash 模式URL 带#要么在 Tomcat 里配一个web.xml的 error-page 把 404 转发到index.html。留言板页面少直接用 hash 模式最省事。5.5 删除接口用 GET 请求被浏览器预取导致误删现象用户没点删除数据却少了。原因删除接口写成了 GET浏览器或爬虫会预取链接触发删除。解决删除必须用 DELETE 或 POST不要用 GET。前端axios.delete发的是 DELETE 请求后端 Servlet 要重写doDelete方法。如果 Tomcat 版本低不支持 DELETE用 POST 加一个_methoddelete参数模拟。6. 进阶技巧把接口响应时间从 200ms 压到 20ms 的验证方法留言板跑通之后下一步是验证它到底扛不扛得住。我一般用两步先加连接池再用ab或wrk压测对比。连接池换 HikariCPDBUtil改成public class DBUtil { private static final HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/message_board?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue); config.setUsername(root); config.setPassword(你的密码); config.setMaximumPoolSize(10); // 最大连接数 config.setMinimumIdle(2); // 最小空闲连接 config.setConnectionTimeout(3000); // 获取连接超时 3 秒 config.setIdleTimeout(60000); // 空闲连接 60 秒回收 dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }maximumPoolSize设 10 是因为留言板并发低设太大反而浪费数据库连接。connectionTimeout设 3000 毫秒拿不到连接就快速失败不要让请求线程一直等。压测命令# 安装 abApache Bench # 100 个并发总共 1000 个请求 ab -n 1000 -c 100 http://localhost:8080/api/message重点看Requests per second和Time per request两个指标。用DriverManager时100 并发下每秒大概 200-300 请求平均响应 200ms 左右换 HikariCP 后每秒能到 2000 以上平均响应降到 20ms 以内。差距主要来自连接复用省掉了 TCP 握手和 MySQL 认证的开销。验证时还要看Failed requests是否为 0。如果有失败检查连接池是否耗尽把maximumPoolSize调大或看慢 SQL。留言板的查询语句简单一般不会慢但如果留言量到几十万条ORDER BY id DESC需要索引主键自带索引所以没问题。我自己的习惯是任何接口上线前都跑一遍ab哪怕只是留言板。因为压测能暴露连接泄漏——如果Failed requests随时间递增多半是某个分支没关连接。原生 Servlet 没有框架帮你管连接try-with-resources必须写全漏一个ResultSet关闭跑几百次请求后连接池就满了。最后一个小技巧把create_time的格式化放到前端做后端直接返回时间戳或原始字符串减少 Java 端的SimpleDateFormat开销。SimpleDateFormat不是线程安全的每次 new 一个也浪费不如交给前端toLocaleString。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑