
简介这是一份基于JSPServletMyBatis的客户管理系统Java Web项目源码包面向需要完成课程设计、毕业设计或快速上手传统Java Web开发的读者。项目采用B/S架构前端由Layui、jQuery、CSS等技术构成后端使用JSPServlet处理请求配合MyBatis操作MySQL数据库覆盖登录、客户管理、系统管理、市场管理、数据统计、线索管理、交易管理、联系人等典型功能模块能够体现客户从线索到成交的完整管理流程。资源共697个文件压缩包约23.06MB除Java源文件和编译后的class外还包含48个JSP页面、98个JS脚本、40个CSS样式、52个XML配置、数据库SQL以及依赖jar包并含gif与mp4媒体文件便于预览界面和操作效果。已有169人学习下载项目导入Eclipse/IDEA配置JDK1.8、MySQL5.7和Tomcat8即可运行。代码中包含客户、线索、交易、联系人等业务的Controller控制类层级划分直观便于学习ServletMyBatis整合方式也适合在此基础上按业务需求扩展模块。1. 从毕业设计到生产环境JSPServletMySQL客户管理系统到底还能不能打如果你接手过任何一个“历史悠久”的客户管理系统大概率见过这种组合Tomcat 里跑着 JSPServlet 做跳转MySQL 存着几十万条客户数据。很多人一听 JSP 就皱眉觉得是上世纪的玩意可真到了中小型企业内部工具、实训项目、或者要一周内交付一个能用的 CRM 时这套组合反而比微服务那套轻得多。本文要讲的就是基于 JSPServletMySQL 的客户管理系统从选型、建表、写登录和增删改查到上线前排查的全过程。适合三类人正在做 Java Web 课程设计的学生、需要快速交付客户管理模块的团队以及要接手老旧 JSP 项目却不知从哪下手的维护者。你会看到它的上限也会看到它最真实的坑。2. JSPServletMySQL组合的选型逻辑与请求生命周期先弄清楚黑匣子再动手2.1 为什么还在用JSPServletMySQL一套能审计的简单架构客户管理系统的核心就是“人”和“关系”客户是谁、谁在跟进、最近聊了什么、下次什么时候联系。这类系统的特征非常明确——数据量不大但字段杂、权限简单但要有、业务逻辑以增删改查为主偶尔来个统计报表。面对这种特征JSPServletMySQL 的三层结构恰好比 Spring Boot 全家桶更容易讲清楚。常见分层是JSP 只负责显示Servlet 只接收请求并调度一个 CustomerDao 类独占所有 SQLMySQL 负责存储。这里有一个反直觉的结论正因为 JSP 被无数人诟病所以用 JSP 写的项目反而能逼着你把 MVC 边界划清楚。因为 JSP 里一旦写了大段 Java 代码页面会立刻变得不可维护你被迫把逻辑往 Servlet 和 DAO 里搬。相比之下前后端分离项目里如果有人把业务写在 Vue 的 mounted 里代码评审时不仔细根本发现不了。选型前要诚实评估三条边界。第一并发量如果只是几十个内部用户、每天几千次操作Tomcat 单节点加 MySQL 完全扛得住没必要上缓存和消息队列。第二团队能力团队成员如果熟悉原生 Servlet 和 SQL交付速度会远快于边学 Spring 边写。第三维护周期这类系统通常活三到五年原生的请求-响应链路没有框架黑匣子任何一个新人都能沿着 web.xml 或注解把流程追完。MySQL 在这个组合里承担的是“数据真相”的角色。不要试图在 JSP 里缓存客户列表一切以数据库为准。安装 MySQL 时我一般直接选 8.0 或 5.7.44 这类稳定版本字符集指定 utf8mb4排序规则用 utf8mb4_general_ci。开发机用 Docker 安装 MySQL 镜像可以减少很多系统服务层面的问题但生产环境我仍建议用系统原生安装便于用 mysqldump 做备份和恢复。2.2 Servlet与JSP的分工一次客户查询请求的完整生命周期很多新手搞不清明明 JSP 也能写 Java为什么还要 Servlet 转发一下答案是职责。Servlet 适合做“接收参数、调用业务、决定下一步”的控制器JSP 适合做“拿数据、拼 HTML”的视图。如果让 JSP 直接查数据库意味着页面加载慢、错误处理分散、HTML 和 Java 代码混在一个文件里。所以正确姿势是浏览器请求 - Tomcat 按映射找到 Servlet - Servlet 调用 CustomerDao - DAO 执行 MySQL 查询返回结果 - Servlet 把 List 放进 request - 请求转发到 customer_list.jsp - JSP 用 EL 表达式渲染列表。Servlet 映射有两种写法注解和 web.xml。我习惯在项目里用 WebServlet 注解简单直观但如果你用的是老项目或 web.xml 里的 Filter 较多保持一致用 web.xml 反而少踩坑。一个最小示例WebServlet(urlPatterns /customer/list, loadOnStartup 1) public class CustomerListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); String keyword req.getParameter(keyword); if (keyword null) { keyword ; } ListCustomer list new CustomerDao().findByKeyword(keyword.trim()); req.setAttribute(customerList, list); req.getRequestDispatcher(/WEB-INF/jsp/customer_list.jsp).forward(req, resp); } }这段代码的参数说明有三处要留意。urlPatterns 里的地址是浏览器访问的路径别带 .jsp 后缀否则用户能看到页面直连绕过 Servlet 的权限控制。loadOnStartup 1 表示 Tomcat 启动时就实例化这个 Servlet适合加载成本高的控制器对于普通列表页可以不加。forward 是请求转发浏览器地址栏不变request 里的属性在 JSP 里用 ${customerList} 直接取如果你用了 sendRedirectrequest 属性全部丢失必须改从 session 或重新查询。请求生命周期里最容易翻车的是编码。Tomcat 8 之后 GET 请求的 URI 编码默认是 UTF-8但 POST 请求的 body 编码默认是 ISO-8859-1所以 doPost 里必须手动 setCharacterEncoding。这也是很多客户管理系统中文乱码的第一源头。后面第 5 章我会把乱码、SSL 连接错误这类问题集中拆开讲。3. MySQL侧先行客户管理系统的表结构、连接池与事务/锁设计3.1 客户表、用户表与跟进记录表字段级别怎么设计才不用返工客户管理系统最忌讳“一张表存所有客户、一个字段存备注、联系方式全拼在一个单元格里”。我做过一个真实项目第一版把客户的公司名、联系人、电话、微信、地址全塞进一张表结果上线三个月后要加“客户来源渠道”时发现改表结构要动十几个 SQL。所以建表前先画业务边界用户表存登录账号客户表存客户基本属性跟进记录表存每次沟通内容。下面是一个经过三次迭代后比较稳的建表脚本用 utf8mb4 字符集引擎选 InnoDBCREATE DATABASE IF NOT EXISTS crm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE crm; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE customer ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, contact_person VARCHAR(50), phone VARCHAR(20), email VARCHAR(100), level TINYINT DEFAULT 3 COMMENT 1高 2中 3低, source VARCHAR(50), address VARCHAR(255), remark TEXT, owner_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_owner_id (owner_id), INDEX idx_name (name) ) ENGINEInnoDB; CREATE TABLE follow_record ( id INT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL, content TEXT, next_follow_time DATE, create_by INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_follow_customer FOREIGN KEY (customer_id) REFERENCES customer(id) ) ENGINEInnoDB;字段设计上最关键的几个决定第一sys_user 的 password 存的是加密后的密文而不是明文哪怕是课程设计也不要图省事用 MD5 或 BCrypt 都行。第二level 用 TINYINT 存数字而不是字符串“高/中/低”排序、统计时不折腾。第三customer.update_time 用 ON UPDATE CURRENT_TIMESTAMP 自动维护不用在每条 SQL 里手动写这是避免“数据改了但看不出什么时候改的”最省事的办法。索引不要盲目乱加。这个场景下通常在 owner_id客户归属人和 name模糊搜索前缀上建索引就够。注意 LIKE %keyword% 这种写法用不上常规 B-Tree 索引如果客户量过了百万且搜索是核心功能再考虑前缀索引。MySQL 创建索引的语法是 CREATE INDEX idx_customer_name ON customer(name(20))但对这个量级的系统保持简单更重要。另外备注类字段用 TEXT 而不是 VARCHARVARCHAR 在 MySQL 里最长是 65535 字节TEXT 可以更大且不影响主表行存储的常见性能问题。3.2 连接配置与事务边界MySQL 8.0/5.7下的JDBC连接串、连接池与锁数据库连不上是 JSP 项目最常见的黑匣子。我见过太多人把 JDBC 连接写在 JSP 里每次页面刷新新建一个 Connection页面稍微多点 MySQL 直接报 Too many connections。正确做法是把连接信息放 properties 文件由连接池统一管理。先看一份经过验证的配置jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/crm?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernamecrm_user jdbc.passwordcrm_pass jdbc.pool.maxActive20 jdbc.pool.initialSize3这份配置里的参数每一个都有明确用途。driver 类名在 MySQL 8.x 驱动下是 com.mysql.cj.jdbc.Driver如果你引的是 5.x 老驱动还写 com.mysql.jdbc.DriverClassNotFound 是迟早的事。useSSLfalse 解决的是本地无证书时的 SSL 握手报错这就是热搜里 mysql ssl 连接错误的典型来源。serverTimezoneAsia/Shanghai 解决日期时间相差 8 小时的问题。allowPublicKeyRetrievaltrue 是 MySQL 8.0 默认 caching_sha2_password 插件下非 SSL 连接首次连库时的必要参数。事务是客户管理系统的道德底线删除客户时发现删了客户主记录但漏删跟进记录或者转账类操作只执行了一半这些都是事务没用好。MySQL 的 InnoDB 支持事务但前提是你要在代码里明确边界。常见做法是在 DAO 层手动控制Connection conn null; try { conn DataSourceUtil.getConnection(); conn.setAutoCommit(false); CustomerDao.delete(conn, customerId); FollowRecordDao.deleteByCustomerId(conn, customerId); conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } }关键点是 setAutoCommit(false) 必须在所有 SQL 执行之前commit 必须放在所有操作都成功后而 finally 里 close 时把 autoCommit 还原成 true避免连接池复用连接时事务状态污染下一个请求。MySQL 锁的分类在这个场景里只需要掌握两类行锁InnoDB 默认 UPDATE/DELETE 锁行和表锁MyISAM 全局锁。客户管理系统并发量不高但如果你在事务里先 SELECT 再 UPDATE 客户且担心两个用户同时改同一客户导致互相覆盖可以给 UPDATE 语句加 WHERE version ?这是乐观锁的思路比 SELECT FOR UPDATE 更容易理解且不持有锁。4. 用ServletJSP把客户增删改查跑通登录、分页、模糊搜索的完整实现4.1 登录拦截与BaseServlet先搭好控制器的骨架客户管理系统的所有页面都不该裸奔。如果用户没登录就能直接访问 /customer/list那这套系统就没有存在意义。我一般先写一个 LoginServlet再写一个拦截用的 Filter而不是在每个 Servlet 里重复判断 session。登录的核心逻辑很朴素接收用户名密码 - 查库比对 - 写入 session - 跳转首页。这里要注意比对密码时不要直接拼 SQL要配合后面第 6 章的预处理语句否则单引号就能把整张表拖走。WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); User user new UserDao().findByUsernameAndPassword(username, password); if (user ! null) { req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /customer/list); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }这段代码里 resp.sendRedirect 和 forward 的差异值得说透。登录成功后必须用重定向否则用户按 F5 刷新时表单会被重复提交等于执行两次登录。登录失败用 forward这样可以带着 errorMsg 回到登录页并保留用户输入的用户名。req.getContextPath() 是为了部署到非根路径时重定向地址不写死这是很多新手习惯写死 /login 导致 404 的根源。拦截器 Filter 只需要拦“需要登录才能看的路径”。常见的坑是把 login.jsp、登录 Servlet、css/js 也拦了导致登录按钮都出不来。放行规则用排除法WebFilter(urlPatterns /*) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); if (uri.contains(/login) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png)) { chain.doFilter(request, response); return; } if (req.getSession().getAttribute(loginUser) ! null) { chain.doFilter(request, response); } else { resp.sendRedirect(req.getContextPath() /login.jsp); } } }urlPatterns 用 /* 表示拦截所有请求但你必须在 Filter 内部做白名单判断。这一版我把静态资源用后缀放行实际项目中更推荐把静态资源统一放 /static 目录这样放行条件只需要 uri.startsWith(/static/)更干净。4.2 客户列表的分页与模糊搜索核心SQL与JSP渲染客户列表页是整个系统里最容易写崩的部分。一崩通常是三件事分页算不准、搜索条件丢、JSP 里写 Java 代码。先设计一个 PageBean 来装分页数据再用 DAO 做条件查询。DAO 里的分页 SQL 用 LIMIT 实现这是 MySQL 特有的语法。注意 LIMIT 的两个参数第一个是偏移量第二个是每页条数。偏移量 (currentPage - 1) * pageSize。第二个参数 pageSize 建议固定为 10 或 20把它做成下拉选择反而容易让用户困惑。同时查总数用 COUNT(*) 而不是 SELECT *性能差距明显public ListCustomer findByPage(String keyword, int currentPage, int pageSize) { String sql SELECT * FROM customer WHERE name LIKE ? OR contact_person LIKE ? ORDER BY update_time DESC LIMIT ?, ?; // 使用 PreparedStatement 防止 SQL 注入 }看到 LIKE 时要格外留意这里的占位符是 ?, ? 两个参数但 LIKE 的内容是 %keyword%所以传参时要手动拼param % keyword %。这年头写原生 JDBC 的人少但我强烈建议你把所有 SQL 都用 PreparedStatement而不是 Statement。除了防注入PreparedStatement 还能让 MySQL 走预编译缓存同一个 SQL 模板执行多次时代价小很多。Servlet 组装分页参数时最该做的一步是参数校验。currentPage 如果传了字符串 abcInteger.parseInt 会直接抛 NumberFormatException。我一般写一个 parseIntSafe 方法解析失败返回 1int currentPage parseIntSafe(req.getParameter(page), 1); int pageSize 10; int totalCount customerDao.countByKeyword(keyword); int totalPages (totalCount pageSize - 1) / pageSize; currentPage Math.max(1, Math.min(currentPage, totalPages)); ListCustomer list customerDao.findByPage(keyword, currentPage, pageSize); req.setAttribute(list, list); req.setAttribute(currentPage, currentPage); req.setAttribute(totalPages, totalPages); req.setAttribute(keyword, keyword); req.getRequestDispatcher(/customer_list.jsp).forward(req, resp);这里 totalPages 的计算用 (totalCount pageSize - 1) / pageSize是向上取整的经典写法。如果 totalCount 是 0totalPages 也是 0Math.min 会把 currentPage 钳到 0所以最好再 max 到 1。这些边界条件才是分页不出 bug 的关键。JSP 渲染我建议只用 EL 和 JSTL不要在 %% 里写 Java 循环。一个合格的客户列表页面应该是纯 HTML 加 ${}比如用 c:forEach items${list} varc 输出每一行。这样做还有个额外好处页面改完刷新更快不会因为 JSP 里嵌了复杂 Java 代码导致 Tomcat 编译报错。这是你从新手的 JSP 页面进化到能维护的关键一步也是热搜里 jsp 页面让加载完后刷新一次 的需求来源——如果你用 JSP 标准标签写页面重编译通常在首次访问时完成改完代码清一下浏览器缓存即可。模糊搜索还有两个细节。第一搜索框里输入的空格要 trim 掉否则搜“ 张三 ”时 SQL 里 LIKE % 张三 % 匹配不到。第二排序字段不要接受用户输入页面传过来的 sortname;DROP TABLE customer 这种直接拼接进 ORDER BY 会出大事。我的做法是白名单映射前端传 sortname后端映射成 ORDER BY name其他值一律按 update_time 处理。MySQL 排序本身不难ORDER BY update_time DESC 就能让最近更新的客户排前面但“字段名来自用户输入”这个隐患必须拦住。5. 常见问题与避坑排查从中文乱码到MySQL服务无法启动的六条血泪经验5.1 现象到解决六条高频踩坑记录第一条中文乱码这是 JSPServletMySQL 项目里出现频率最高的玄学问题。现象是页面显示“客户名???”或存进数据库变成乱码。原因通常是三处编码不一致JSP 页面本身的 pageEncoding 不是 UTF-8、Servlet 或 Filter 没设置 setCharacterEncoding(UTF-8)、MySQL 表或连接串没用 utf8mb4。解决方法是三层都对齐在最前面加一个 EncodingFilter 统一设置 req 和 resp 的编码连接串里带 characterEncodingutf8建表时指定 CHARACTER SET utf8mb4。只要这三处一致乱码基本绝迹。注意 tomcat 的 server.xml 里如果加了 Connector URIEncoding 配置一定要确认它也是 UTF-8有的老项目是 GBK改起来牵一发动全身。第二条mysql ssl 连接错误。现象是首次启动项目时报 SSL 握手失败或 Public Key Retrieval is not allowed。原因是从 MySQL 8.0 开始默认启用 caching_sha2_password 认证非 SSL 连接首次需要获取公钥而连接串里没给权限。解决方式是在 jdbc.url 里加上 useSSLfalse 和 allowPublicKeyRetrievaltrue。如果公司安全策略强制 SSL那就反过来配置 SSL 证书而不是关闭但大多数内部系统直接关掉 SSL 就够了因为数据链路是内网。第三条net start mysql 服务无法启动。现象是在 Windows 开发机上执行 net start mysql 提示服务无法启动错误日志里写着各种路径或权限问题。原因是 my.ini 里的 datadir 路径不存在、data 目录权限不足或者 3306 端口被占用。解决方法是先打开 mysql 的 error log 看真实报错多数情况是 datadir 路径不一致用 mysqld --initialize-insecure 重新初始化 data 目录注意不要用 --initialize 生成随机密码却又不知道密码存在哪。端占用用 netstat -ano | findstr 3306 查找到 PID 后顺手清理掉占用进程。第四条JDBC 驱动 ClassNotFoundException。现象是启动 Tomcat 后访问页面报 ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因是驱动 jar 没有放到 WEB-INF/lib 目录或者放到了 Tomcat 的 lib 目录但版本不对。解决方法是把 mysql-connector-j 的 jar 放进项目的 WEB-INF/lib 下同时确认驱动类名和 jar 版本匹配8.x 驱动用 com.mysql.cj.jdbc.Driver5.x 驱动用 com.mysql.jdbc.Driver。我曾经因为本地 lib 和 Tomcat lib 各放了一份 5.7 和 8.0 的驱动导致运行时类加载顺序异常折腾了半下午后来统一成一份就清净了。第五条Too many connections 连接池耗尽。现象是系统用着用着突然所有页面都报无法获取连接MySQL 错误日志里有 Too many connections。原因多数不是连接池配置太小而是代码里没 close。原生 JDBC 的 Connection、Statement、ResultSet 都是要手动关闭的资源很多人在 DAO 里查完就扔连接永远不还给连接池。解决方法是所有连接操作都用 try-with-resources或者至少把关闭动作放 finally同时把连接池的 maxActive 调到一个合理值比如 20配合 waitTimeout 让空闲连接在 60 秒内回收。第六条JSP 改完不生效、页面加载完后还自动刷新一次。现象是改了 JSP 的 HTML 或样式浏览器里刷新还是旧页面。原因有两层一是浏览器缓存了静态资源二是 Tomcat 对 JSP 的编译依赖文件时间戳如果你用 IDE 热部署不彻底会编辑到旧的 work 目录。解决方法是先用 CtrlF5 强制刷新排除浏览器缓存再确认 Tomcat 的 server.xml 里 docBase 指向的是你正在改的目录。至于“加载完后刷新一次”这个需求常见做法是在 上挂 window.onload 定时器但真正治本的是改完代码后清掉 Tomcat work/Catalina 下的编译缓存让 JSP 强制重编译。5.2 排查方法论不看表面报错先看日志与异常栈面对 500 页面时新手会盯着浏览器那行英文看半天老手第一件事是去 Tomcat 的 logs 目录打开 localhost.yyyy-MM-dd.log。JSP 项目的报错链路通常是“页面红了 - 控制台没打印 - localhost.log 里有完整异常栈”。所以排查第一步永远是找日志。如果日志没开 SQL 打印就在 DAO 的 executeQuery 前后打印 SQL 和参数我见过大量问题都是参数传错了而不是 SQL 写错了。第二步是缩小验证范围。用 curl 或 Postman 直接打 Servlet 地址避开浏览器缓存和 JSP 渲染干扰。比如请求 /customer/list?page2 返回的是 JSON 或完整 HTML 片段先确认 Servlet 返回了什么再去看 JSP 渲染。这样可以快速定位问题在控制器还是视图层。第三步是关注连接资源。客户管理系统常见的“偶尔 500、刷新又好了”基本都和连接池泄漏或连接超时有关。在 MySQL 侧执行 SHOW PROCESSLIST 能看到大量 Sleep 状态的连接那就说明代码里连接没还回去如果看到大量 Timeout 状态的连接检查 wait_timeout 和连接池的 testConnectionOnBorrow 是否开启。6. 上线前的最后一公里Druid连接池、SQL防注入与性能验证技巧6.1 用Druid和PreparedStatement把系统做成“能上线”的样子如果你不想在毕业答辩或项目验收时被问“你这个系统防得住 SQL 注入吗”那就要把两点做扎实连接池换成 Druid所有 SQL 走 PreparedStatement。Druid 的配置比前面自己写的连接池多一个监控面板生产上还能看慢 SQLDruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://localhost:3306/crm?useSSLfalseserverTimezoneAsia/Shanghai); ds.setUsername(crm_user); ds.setPassword(crm_pass); ds.setInitialSize(3); ds.setMaxActive(20); ds.setMinIdle(3); ds.setValidationQuery(SELECT 1); ds.setTestWhileIdle(true); ds.setTimeBetweenEvictionRunsMillis(60000);这里 setValidationQuery(SELECT 1) 是借出连接前的轻量探活防止 MySQL 重启后连接池还握着死连接。setTestWhileIdle 搭配空闲驱逐线程每 60 秒检查一次确保空闲连接不会因为 wait_timeout 被 MySQL 服务端断开。监控页通过 Druid 自带的 /druid/index.html 访问第一眼先看慢 SQL 列表把所有执行超过 200ms 的 SQL 拉出来加索引。6.2 上线前验证清单与我的习惯我的验证清单按顺序做第一用不同浏览器跑通“登录 - 新增客户 - 修改客户 - 删除客户 - 退出”全流程第二开两个浏览器分别登录两个账号同时修改同一个客户确认乐观锁或最后更新时间能反映出来第三停掉 MySQL 再启动确认连接池能自动恢复而不是报死连接第四mysqldump 做一次全量备份然后故意执行一个错误的 UPDATE测试能不能用备份还原。mysql update 还原这件事我吃过一次亏生产库做批量更新时忘了加 WHERE两万条客户的公司名全部被改错幸好有前一天晚上的备份花了二十分钟恢复。从那以后凡是 UPDATE 和 DELETE我先 SELECT 一遍确认影响行数再执行执行完立刻看 rowCount。还有一点个人习惯想分享给你给关键表加索引时不要靠猜先 EXPLAIN SELECT * FROM customer WHERE owner_id ?看 type 列是 ALL 全表扫描还是 ref 索引查找。这一步能帮你避免“建了一堆索引但 SQL 根本没用上”的尴尬。对于客户管理系统这种数据量把 owner_id、name 前缀的索引建好就足够再多就是负担。最后送你一句我的真实教训JSPServletMySQL 这套组合的价值不在技术新而在链路短。任何一台装了 JDK 和 Tomcat 的机器都能把它跑起来任何后来接手的人都能通过日志和代码顺序看懂业务。把表结构设计好、连接池配好、防注入做到位它就不仅仅是能跑而是能长期稳定地跑。希望帮到你。本文还有配套的精品资源点击获取