ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

JSP+Servlet+MySQL客户管理系统实战:架构、表设计与部署

2026/10/1 4:11:26 拓冰建站 浏览量
JSP+Servlet+MySQL客户管理系统实战:架构、表设计与部署 简介这是一份基于JSP、Servlet与MySQL的客户管理系统完整源码适合初学Java Web的开发者也可作为课程设计或毕业设计的参考项目。系统采用主流浏览器服务器架构后端使用JSP、Servlet和MyBatis处理业务逻辑与数据持久化前端整合Layui、JQuery等组件实现了登录认证、客户信息维护、市场活动管理、数据统计、线索跟踪、交易管理、联系人管理和系统权限管理等模块基本还原了企业客户管理流程。压缩包内共六百九十七个文件压缩后大小约二十三兆其中包含Java源文件、Jsp页面、Class字节码、Js脚本、Css样式、Xml配置文件以及数据库脚本和一百五十张操作演示截图便于按图索骥搭建环境。截至目前已有169人学习下载源码目录结构清晰注释较完整可在Eclipse、MyEclipse或IDEA中配合JDK1.8、Tomcat8及以上版本快速部署。整个项目从用户登录、线索转换到交易统计形成完整闭环适合二次开发继续扩展。1. 基于JSPServletMySQL的客户管理系统这套老技术栈在2025年依然是毕业设计和中小企业内部工具的首选很多人在选型时一看JSP就直接摇头觉得这是“过气技术”。但客户管理系统这一类典型CRUD业务恰恰是JSPServletMySQL最能发挥优势的场景业务模型清晰、运行环境轻量、没有复杂的框架依赖连学生用一台2G内存的老电脑都能跑起来。这套技术栈解决的核心问题是把客户资料、跟进记录、员工权限这些数据用一个结构明了的Web应用管理起来而不是靠Excel传来传去。JSP负责用户界面Servlet负责请求控制MySQL负责数据落地三者各司其职任何一个部分出了问题都容易定位。尤其适合两类读者一类是需要快速完成JavaWeb课程设计或毕业设计的在校生另一类是团队里需要维护老项目的在职工程师——传统JSP项目打包war并部署在Tomcat上的运维方式至今还在大量企业内网里运行着。下文会把这套系统的架构建法、表结构设计、核心模块实现和踩坑经验从头到尾讲一遍。2. 架构选型与数据建模为什么客户管理系统还在用JSPServlet这套组合2.1 三个不可替代的落地场景教学、老项目维护、低并发内部系统JSPServlet这套组合能活到今天靠的并不是技术上的领先而是落地成本极低。Servlet生命周期由容器管理从init到service再到destroy都有明确规范一个请求进来后如何被doGet和doPost分发代码里看一眼就明白这对初学者理解HTTP协议和Web容器协作机制非常有帮助。很多JavaEE课程和头歌实训平台至今仍用这套模型做实训题目原因就是它没有框架魔法出问题了能顺着调用链一层层找。第二个使用场景是老项目维护。很多中小型企业的内部系统是2015年前后开发的用的就是JSPServletMySQL。这类系统通常运行在内网并发量可能只有几十人业务逻辑又和页面渲染耦合得很深贸然重写成SpringBoot会带来很大的回归风险。与其推翻重来不如掌握这套技术栈的细节在原有结构上做增量开发。第三个场景是低并发的内部工具比如管理几百个客户资料的销售团队工具用JSPServletMySQL部署在Windows服务器上半小时就能交付一个可用版本后续维护成本也低。2.2 表结构设计一张能跑完整套业务的MySQL模型客户管理系统的核心表一般就四张用户表、客户表、跟进记录表、数据字典表。设计时不需要过度范式化但要注意MySQL的常见习惯——表名和字段名用下划线分隔字符集一律用utf8mb4排序规则选utf8mb4_general_ci就够用了不需要去碰那些复杂的排序规则否则在联表查询时会出现字符集不匹配的报错。-- 用户表 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码建议MD5或BCrypt加密, real_name VARCHAR(50) DEFAULT COMMENT 真实姓名, role TINYINT DEFAULT 0 COMMENT 0-销售 1-管理员, status TINYINT DEFAULT 1 COMMENT 1-启用 0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, INDEX idx_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; -- 客户表 CREATE TABLE customer ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 客户ID, customer_name VARCHAR(100) NOT NULL COMMENT 客户名称或姓名, phone VARCHAR(20) DEFAULT COMMENT 联系电话, email VARCHAR(100) DEFAULT COMMENT 邮箱, company VARCHAR(200) DEFAULT COMMENT 公司名称, level TINYINT DEFAULT 0 COMMENT 客户等级 0-普通 1-重点 2-VIP, source VARCHAR(50) DEFAULT COMMENT 客户来源, owner_id INT DEFAULT 0 COMMENT 归属销售ID关联sys_user.id, status TINYINT DEFAULT 0 COMMENT 客户状态 0-潜在 1-成交 2-流失, remark VARCHAR(500) DEFAULT COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_owner (owner_id), INDEX idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户表; -- 跟进记录表 CREATE TABLE customer_follow ( id INT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL COMMENT 客户ID, user_id INT NOT NULL COMMENT 跟进人ID, content TEXT COMMENT 跟进内容, follow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 跟进时间, next_time DATETIME DEFAULT NULL COMMENT 下次跟进时间, INDEX idx_customer (customer_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户跟进记录表;这段建表SQL里有个容易被忽略的细节MySQL设置默认值为0的字段用了TINYINT DEFAULT 0而不是直接省略默认值这样在插入数据时少写一个字段也不会受到严格模式影响。owner_id字段建立索引是很有必要的因为客户列表页最常用的筛选条件就是“归属销售”如果没有索引数据量超过一万条后分页查询就会明显变慢。给status也建索引是因为统计潜在客户和流失客户时会按这个字段做条件过滤。2.3 连接池选型与JDBC参数为什么推荐Druid而不是自己写连接管理JSPServlet项目最常见的数据库连接方式是直接在DAO里用DriverManager.getConnection开发时方便但上线后并发稍微一高就会出现数据库连接耗尽的现象。正确做法是使用数据库连接池。常见选择有DBCP、C3P0和Druid。Druid在国内用得最多不仅是因为性能更重要的是它自带监控页面SQL执行次数、慢查询、活跃连接数都能在页面上直接看到排错效率高出一大截。用Druid时会在项目的src/main/resources里放一个druid.properties配置文件然后在Application启动阶段用代码加载它。这里要特别提醒连接池参数不是抄一份就能用initialSize、minIdle、maxActive这几个值的设置要和部署机器的实际内存与数据库最大连接数匹配。很多Tomcat部署的JavaWeb项目启动很慢排查到最后发现是initialSize设成了50而本地机器只有4G内存Druid初始化时一次性建50个物理连接导致启动卡顿。# druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/customer_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue usernameroot passwordyour_password initialSize5 minIdle5 maxActive20 maxWait30000 validationQuerySELECT 1 testWhileIdletrue testOnBorrowfalseurl里必须带上useSSLfalse和serverTimezoneAsia/Shanghai否则连MySQL 8会抛SSL连接错误或时区异常。allowPublicKeyRetrievaltrue解决了MySQL 8密码插件在非SSL连接时出现的公钥检索问题。连接池初始化5个连接、最大20个对一个小型客户管理系统完全够用切记不要把maxActive调得过大否则数据库端max_connections不够时会报“Too many connections”而不是项目启动失败。想现场确认连接数的情况可以执行SHOW PROCESSLIST如果看到大量来自应用服务器IP的Sleep连接优先怀疑是连接没有归还或池参数过大。3. 搭建项目骨架与开发环境IDEA创建传统Servlet项目并打包war部署3.1 IDEA新建JSP项目Maven的war工程还是纯手工Web目录目前在IDEA里新建JSP项目业内主流的做法是直接用Maven Archetype创建一个war项目而不是在IDEA里手工勾选Web Application。原因很简单用Maven构建能统一依赖版本打包war只需要一个命令团队协作时也不会出现谁的classpath里有、谁没有的尴尬。特别是用VSCode编写Servlet代码的开发者更需要借助Maven来管理依赖纯手工配置Tomcat的classpath很费时还容易漏jar包。创建步骤是先打开IDEA选择New Project类型选MavenArchetype选择org.apache.maven.archetypes:maven-archetype-webapp。创建完成后补上java目录结构在pom.xml里加入Servlet API、JSP API、JSTL和Druid依赖。需要注意Servlet API的scope必须是provided因为Tomcat自带了Servlet容器实现打成war的时候如果把这个依赖也打进去会和Tomcat容器里的类冲突启动时可能出现ClassCastException。dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies3.2 web.xml与Servlet映射注解能用但别忽略顺序问题Servlet 3.0之后可以使用WebServlet注解代替web.xml配置新项目里我一般也用注解。但要注意一个顺序问题如果在web.xml里配置了Filter拦截了/*而Servlet又用注解注册那么Filter的执行顺序取决于web.xml里的声明顺序和注解的排序规则。项目里出现403或者404后很多人的第一反应是路径不对其实往往是Filter把请求拦截掉了。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list session-config session-timeout30/session-timeout /session-config /web-appweb.xml里设置了欢迎页为login.jsp那么访问项目根路径时Tomcat会先找到这个JSP文件。session超时设成30分钟客户管理系统这种偏后台的工具建议短一些15到30分钟为宜太长了存在安全隐患。一个常见做法是登录成功后把用户对象放进session然后通过Filter检查session是否存在来实现权限控制这个在后文实现模块里会给出完整代码。3.3 部署到Tomcat传统JSP项目打包war的两种姿势传统JSP项目的部署方式和SpringBoot内嵌Tomcat完全不同。SpringBoot打出来的是jar直接java -jar启动而JSP项目必须打包成war文件然后放到Tomcat的webapps目录下启动Tomcat时它会自动解压部署。这个过程新手翻车最多的地方是用IDEA直接Run开发时这样没问题但交付给别人的时候必须提供war包。打包war在Maven项目里只需要执行mvn clean package然后到target目录下找到war文件。Tomcat解压后的目录名就是context path比如customer_crm.war会映射到http://localhost:8080/customer_crm/。这里有同学喜欢把war改名为ROOT.war再部署让项目直接跑在根路径下省去访问路径里带项目名生产环境也是推荐这样做但要注意原ROOT目录要先删掉否则Tomcat会解压合并导致页面资源和class文件新旧混杂出现一些诡异的问题。部署完成后建议看Tomcat的catalina.out日志出现Deployment of web application archive .war has finished才是真的成功。3.4 VSCode配置Servlet环境可选但费手现在有些同学习惯在VSCode里编写Servlet代码环境配置确实比IDEA繁琐一些。核心步骤是在VSCode里安装Java Extension Pack然后配置Tomcat插件再把Tomcat路径指到本地解压目录。这里容易卡住的是编译阶段VSCode默认的Java项目不会自动把src目录下的class文件输出到WEB-INF/classes需要手动在settings.json里配置java.project.outputPath或者在项目里加一个.classpath文件。如果只是临时改页面样式和少量Java代码VSCode能用做完整项目还是建议IDEA省下的时间足够做完整个业务模块了。4. 核心业务模块实现登录过滤、客户列表分页和新增编辑合一页面4.1 用户登录与Session控制Filter拦截未登录请求的标准写法登录功能是客户管理系统的第一个拦路虎很多人写完登录页面发现直接访问其他JSP文件也能跳过去这就是没有做权限校验。Servlet标准做法是登录成功后将用户对象set到session里然后写一个LoginFilter实现javax.servlet.Filter在doFilter方法里取出session用户如果为空就重定向回login.jsp。关键点是排除登录接口本身、JSP页面的静态资源路径否则会出现css和js全被拦掉的尴尬。WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录页面、登录提交接口和静态资源 if (uri.endsWith(login.jsp) || uri.endsWith(/login) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } Object user request.getSession().getAttribute(loginUser); if (user null) { // 未登录的请求发给重定向不是转发 response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这段Filter的坑在于重定向要加request.getContextPath()否则项目部署在非根路径时重定向后的地址会缺少项目名导致404。放行规则里uri.endsWith(login.jsp)只能精确匹配这一种页面所以在login.jsp里引用静态资源时的路径前缀一定要统一用${pageContext.request.contextPath}这样才不会出现页面拿到手了但样式全无的情况。Session的创建时机也要注意默认情况下JSP页面会隐式创建一个sessionLoginFilter里获取session不需要担心它为空但是取值之前必须手动检查否则第一次未登录请求拿到的就是一个新session里面自然没有loginUser。4.2 客户列表与分页SQLlimit参数和模糊查询条件怎么拼客户列表页是CRUD的核心。这里的查询条件一般包括客户名称模糊匹配、客户等级、状态、归属销售还要支持按创建时间排序。最稳妥的方式是用PreparedStatement拼接SQL不推荐字符串直接拼接否则用户输入了单引号或者%符号就会导致SQL语法错误甚至注入风险。MySQL的排序功能在客户列表里主要体现在ORDER BY create_time DESC和ORDER BY update_time DESC两个字段上很多人只按create_time排序修改过客户资料后记录不会提前到列表上方产品经理多半会提优化需求所以表设计时加上update_time字段并自动更新是很有必要的。public ListCustomer queryPage(String customerName, Integer level, Integer ownerId, int pageNum, int pageSize) { StringBuilder sql new StringBuilder(SELECT * FROM customer WHERE 11 ); ListObject params new ArrayList(); if (StringUtils.hasText(customerName)) { sql.append(AND customer_name LIKE ? ); params.add(% customerName %); } if (level ! null) { sql.append(AND level ? ); params.add(level); } if (ownerId ! null) { sql.append(AND owner_id ? ); params.add(ownerId); } // 排序放到分页之前且排序字段不要接受外部直接传入的列名 sql.append(ORDER BY update_time DESC, create_time DESC ); sql.append(LIMIT ?, ?); params.add((pageNum - 1) * pageSize); params.add(pageSize); // 用PreparedStatement填充上面的参数后执行查询 }这段逻辑里有一个非常关键的细节LIMIT的偏移量计算是(pageNum - 1) * pageSize很多新手直接传pageNum进去导致第二页开始永远查不到数据。另外一个安全细节是ORDER BY后面的字段名不要用用户传来的参数直接拼接否则存在注入点排序字段应该在后端用白名单映射比如前端传sortFieldtime后端映射成update_time。分页查询的总记录数要用SELECT COUNT(*)单独查一次把这个数字放在request作用域里传给JSP用来渲染分页条的总页数。4.3 新增与编辑合一一张JSP页面处理两条业务分支新增客户和编辑客户在页面上通常共用一张customer_edit.jsp通过URL后面的id参数区分是新增还是编辑。没有id就是新增有id就是编辑先把数据库里的记录查出来回显到表单里。这样做能让页面代码更统一避免两套表单长期维护产生不一致。实现时用一个CustomerServlet统一处理doGet和doPostdoGet负责打开编辑页doPost负责保存数据。WebServlet(/customer/edit) public class CustomerEditServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String id request.getParameter(id); if (StringUtils.hasText(id)) { CustomerDao dao new CustomerDao(); Customer customer dao.findById(Integer.parseInt(id)); request.setAttribute(customer, customer); } request.getRequestDispatcher(/customer_edit.jsp).forward(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); Customer customer new Customer(); String id request.getParameter(id); customer.setCustomerName(request.getParameter(customerName)); customer.setPhone(request.getParameter(phone)); customer.setLevel(Integer.parseInt(request.getParameter(level))); customer.setOwnerId(Integer.parseInt(request.getParameter(ownerId))); CustomerDao dao new CustomerDao(); if (StringUtils.hasText(id)) { customer.setId(Integer.parseInt(id)); dao.update(customer); } else { dao.insert(customer); } response.sendRedirect(request.getContextPath() /customer/list); } }JSP页面里要处理的是表单回显时的条件判断用JSTL的c:if判断customer是否为空为空就显示新增标题和空表单不为空就把customer的属性值填到input的value里。这里有个JSPServletMysql项目很常见的坑EL表达式里$ {customer.customerName}取不到值第一反应应该是检查Customer这个类的getter方法是不是写成了getcustomerName而不是getCustomerName或者属性的可见性是不是private。另外一个不起眼但很要命的问题是在doPost里没有调用request.setCharacterEncoding(UTF-8)就在读参数导致中文名字变成乱码这个设置必须在读取任何参数之前执行放在最前面一行。5. 避坑与排查JSPServletMySQL项目跑不起来的常见故障清单5.1 MySQL 8驱动与SSL连接报错驱动类名不要照抄老教程现象启动Tomcat后DAO层一执行查询就抛出javax.net.ssl.SSLHandshakeException或者提示Public Key Retrieval is not allowed。 原因连接MySQL 8时驱动加载了错误的SSL配置老教程里写的com.mysql.jdbc.Driver在MySQL 8中已经被标记为过时正确驱动类是com.mysql.cj.jdbc.Driver。同时MySQL 8默认启用SSL和新的caching_sha2_password认证插件如果JDBC连接串里没指定useSSLfalse和allowPublicKeyRetrievaltrue就会在握手阶段直接报错。 解决在druid.properties和所有JDBC连接串里把driverClassName改成com.mysql.cj.jdbc.Driver并在url末尾加上useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。这个配置用一套就能通用不需要为每个环境单独调。5.2 mysql e0434352服务起不来Windows安装残留与日志排查现象Windows上安装MySQL后用net start mysql启动服务时提示服务无法启动服务名称可能显示为MySQL事件查看器里看到错误编号e0434352。 原因这个错误在MySQL里通常是旧版本服务残留、数据目录权限不对、或者my.ini里的basedir路径失效。最常见的是之前装过一个MySQL实例卸载时没有删除服务项新安装包又把服务注册到同一个名称上导致启动时加载的还是旧配置。 解决先用管理员身份运行命令提示符执行sc query type service | findstr mysql确认服务列表里的MySQL服务名再执行sc delete mysql删除残留服务。如果删不掉打开注册表编辑器在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services下找到对应服务项手动删除或者换一个服务名安装新版本。安装新版MySQL时data目录建议清空并让初始化程序重新生成不要沿用旧的data文件这是头疼的根源。5.3 error 2002 (HY000)Linux下连不上本地MySQL的socket问题现象在Linux服务器上通过mysql命令连接本地数据库报ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock (2)。 原因MySQL客户端默认走socket文件连接本地实例但服务端没有生成这个文件或者socket文件路径不是/tmp/mysql.sock。在Linux安装MySQL的场景里经常遇到两种深层原因一种是MySQL服务根本没起来另一种是socket路径被改到了/var/run/mysqld/目录下。 解决首先用systemctl status mysqld确认服务状态再执行mysql -h 127.0.0.1 -P 3306 -u root -p走TCP协议连接绕过socket文件。如果TCP能连上而socket连不上说明client和server端的socket路径不统一检查/etc/my.cnf里的socket配置客户端和服务端共同指向同一个路径即可。这个问题对JSP项目的影响在于如果web.xml或连接池配置里用了jdbc:mysql://localhost:3306/JDBC驱动默认会先用socket连接改成127.0.0.1强制走TCP通常就能绕过socket文件的限制。5.4 页面404与静态资源被拦Filter放行规则里的门道现象登录后跳转到客户列表页页面框架和样式全部丢失控制台里一堆.css和.js请求返回404。 原因如果项目使用了Filter拦截所有请求并且放行规则里只写了login.jsp那么静态资源路径也会被当作需要登录的页面拦截Tomcat找不到这些文件自然返回404。还有一个常见原因是JSP页面里引用静态资源时用了绝对路径/static/css/common.css但项目部署后带上了context path绝对路径没有拼接项目名导致404。 解决Filter放行规则需要把/static/、/css/、/js/、/images/等前缀加进去JSP页面的引用路径一律用${pageContext.request.contextPath}拼接。另一个容易漏掉的点是web.xml里配置了welcome-file为login.jsp但Filter对根路径/也执行拦截并且没放行未登录状态下访问项目根路径时会先被Filter干掉而不是走到欢迎页。建议在Filter里对请求URI等于项目根路径/的场景单独放行让它能正常进入欢迎页登录。5.5 中文乱码和EL表达式读不到值编码顺序与getter规范现象客户名称在JSP页面上显示为????或者在编辑回显时表单input是空的但数据库里明明有值。 原因中文乱码一般发生在三层——请求参数读取时没有指定UTF-8、数据库连接串没有指定characterEncoding、JSP页面本身没设置pageEncoding和contentType。EL表达式读不到值则集中在JavaBean问题、Bean属性命名不规范、或者attribute存的key和JSP里取的key不一致这三个地方。 解决网站上的乱码修复顺序是这样的——先在web.xml里配置CharacterEncodingFilter强制request和response都走UTF-8然后在druid.properties的url里带上characterEncodingutf8最后在JSP页面最顶部写上% page contentTypetext/html;charsetUTF-8 languagejava %。EL表达式这个问题打开Customer类检查每个私有字段是否都有public的getter注意JavaBean规范是字段名customerName对应getCustomerName写成getCustomername会直接取不到。排查时把request.getAttribute(customer)在控制台打印出来确认对象是否存在是最有效的手段之一。5.6 连接池用着用着就Too many connections泄漏排查清单现象系统运行一两天后应用开始抛Too many connections重启Tomcat就好了但过一段时间又复发。 原因这种故障里连接泄漏占了绝大多数。典型的泄漏场景是DAO层代码里执行完查询后没有关闭PreparedStatement或ResultSet数据库连接被connection包装类引用住连接池无法回收。Druid虽然配置了testWhileIdle和归还检测但如果代码里直接把Connection字段存在静态变量或者请求线程里连接根本不会被归还到池里。 解决先开启Druid监控页查看ActiveCount和PoolingCount两个指标如果ActiveCount接近maxActive而PoolingCount一直很低说明连接被占用没归还。在Java代码层面养成在finally块里关闭ResultSet、PreparedStatement、Connection的习惯推荐直接用try-with-resources语法简化代码。特别注意那些在Filter或基类里获取了Connection、却在子类里把Connection传给别的方法使用的场景这种跨方法传递很容易忘记关闭。如果消息提醒出现该问题最先动的不是调大maxActive而是清理泄漏点如果连接池本来就够用还需要调大那是在掩盖问题根源。6. 进阶增强与验证技巧监控慢SQL、导出CSV和NginxTomcat分离部署6.1 加一个Druid监控页十分钟看清慢SQL给项目接入Druid之后在web.xml里配置一个StatViewServlet就能看到一个访问地址为/druid/index.html的监控页。数据源、慢查询、活跃连接数都在这个页面里。配置时要注意访问密码不要裸奔否则内网的数据也会被别人看到项目答辩时访问这个页面也是加分项。servlet servlet-nameDruidStatView/servlet-name servlet-classcom.alibaba.druid.support.http.StatViewServlet/servlet-class init-param param-nameloginUsername/param-name param-valueadmin/param-value /init-param init-param param-nameloginPassword/param-name param-valueadmin/param-value /init-param /servlet servlet-mapping servlet-nameDruidStatView/servlet-name url-pattern/druid/*/url-pattern /servlet-mapping很多查询性能问题在数据量小的时候完全察觉不到数据量上来之后才爆发。客户列表的查询数据量过万后如果发现列表页打开要两三秒去Druid页面看慢SQL列表定位到具体SQL后先加EXPLAIN看索引。这个动作在完工后的验证阶段至少做一遍能提前发现MySQL创建索引是从哪个字段开始的隐患。6.2 客户列表导出CSV流式写出与Excel打开乱码的解法客户管理系统里导出功能很有实用价值最常见的做法是直接汇总到JSP页面里输出表格文本遇到数据量大的时候页面代码很难维护。做导出建议直接用Servlet写流将查询结果输出为CSV文件用户可以在Excel里打开。这里要特别注意编码问题导出的内容里面包含中文时response指定UTF-8会有可能在Excel里打开乱码因为Excel默认按ANSIGBK解析CSV导出时可以在Response编码上设置成GBK或者给CSV首行加BOM。WebServlet(/customer/export) public class CustomerExportServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType(text/csv;charsetGBK); response.setHeader(Content-Disposition, attachment;filenamecustomers.csv); Writer out response.getWriter(); out.write(\uFEFF); // 加入UTF-8 BOM避免Excel打开中文乱码 out.write(客户名称,电话,公司,等级,状态\n); // 查询list后逐行写 for (Customer c : list) { out.write(c.getCustomerName() , c.getPhone() , c.getCompany() \n); } out.flush(); } }如果在Windows下用Excel打开还是乱码检查内容里是否混入了英文逗号并作了转义客户名称字段里的内容本身如果含有换行符整行结构都会错乱写入前要对这类特殊字符做替换。在Linux环境部署时导出的文件用notepad打开看到的是乱码但Excel打开正常这个现象大概率是文件编码是GBK而查看工具默认用了UTF-8解析这时不要盲目改代码先确认业务使用场景在哪端。6.3 交付前的验证习惯NginxTomcat分离与传统JSP部署的一个提醒客户管理系统开发完上线前至少要做三轮验证。第一轮是数据验证新建一个客户再编辑一次数据库里的update_time有没有自动变化如果没有变化去检查建表语句的ON UPDATE CURRENT_TIMESTAMP。第二轮是权限验证退出登录后直接访问/customer/list和/customer/edit?xx这些地址会被Filter拦截不能直接放行。第三轮就是部署验证把war包丢到Tomcat的webapps下从catalina.out确认部署完成然后连续点击列表页和导出功能用jstack看有没有死锁或线程阻塞。有些团队喜欢在Tomcat前面再挂Nginx做反向代理基于JSP项目这一点很感慨Nginx本身不支持JSP渲染必须把动态请求转发给Tomcat静态资源才由Nginx处理这部分配置挺常见我在配置文件里写过几次一般把location / 的proxy_pass指向Tomcat端口就可以提交后的JSP页面还是由Tomcat渲染。项目放在子路径时还要注意Nginx的location匹配规则和Tomcat的context path联动我曾经因为proxy_pass路径后没有加“/”导致所有静态资源全部返回到根路径上。后面养成的习惯是交付前最后一次导出CSV清空一次日志表确认没有SQL报错后再关机。这个习惯帮我避免了多次现场调试的尴尬希望也能帮到你。本文还有配套的精品资源点击获取