基于JSP+Servlet+MySQL的火车票查询系统开发实战与原理剖析 简介在Java Web开发领域Servlet和JSP构成了经典MVC架构的基础模型。Servlet作为服务器端程序负责处理HTTP请求和业务逻辑而JSP则用于动态生成HTML页面实现数据展示。这种技术组合的价值在于清晰地分离了控制逻辑与视图呈现为理解Web应用的数据流转提供了直观范例。在实际工程实践中通过JDBC操作MySQL数据库完成数据持久化是核心环节其中使用PreparedStatement能有效防范SQL注入保障系统安全。连接池技术如HikariCP的引入则解决了数据库连接频繁创建销毁的性能瓶颈是提升应用吞吐量的关键优化。本文以火车票查询这一典型应用场景为切入点详细阐述了如何从环境搭建、数据库设计到功能编码完整实现一个基于JSP与Servlet的Web系统帮助开发者深入理解Java Web经典开发模式的核心原理与实践要点。1. 项目缘起与核心价值为什么现在还需要一个JSP火车查询系统最近在整理硬盘里的老项目翻到了一个十多年前用JSPJavaMySQL做的火车查询系统。说实话第一反应是“这玩意儿现在还有人看吗”。毕竟现在主流都是Spring Boot Vue/React前后端分离JSP听起来像是上个时代的产物。但仔细想想这个“古董”项目恰恰是理解Java Web开发从经典模式到现代架构演进的最佳标本。对于很多在校学生、刚入行的Java开发者或者想从零开始理解一个完整Web应用数据流转的人来说它比一个用满了最新框架但封装过度的“黑盒”项目更有教学和参考价值。这个系统的核心很简单用户通过浏览器访问JSP页面输入出发地、目的地、日期点击查询后台Java代码Servlet连接MySQL数据库执行SQL查询将结果集封装成Java对象再传递回JSP页面渲染成HTML表格展示出来。整个过程涉及了Java Web开发最经典的MVC雏形虽然可能不那么标准、JDBC数据库操作、JSP的脚本和标签使用以及最基本的前后端交互。理解了这些你再去看MyBatis、Spring MVC甚至任何Web框架都会有一种“哦原来它是在解决这个痛点”的豁然开朗感。所以这篇内容不是教你做一个能上线商用的系统而是带你亲手搭建、并彻底吃透一个经典的JSP项目。我们会从环境搭建、数据库设计、核心功能编码一路讲到那些老鸟才知道的调试技巧和性能坑点。你会发现很多所谓的“Java八股文”面试题比如Servlet生命周期、JDBC连接池、SQL注入防范在这个项目里都是活生生的实践。2. 环境准备与项目骨架搭建告别“配置地狱”很多人学Java Web倒在了第一步环境配置。各种版本不兼容、jar包冲突、Tomcat启动报404足以劝退新手。我们这里用最主流、最稳定的组合确保你能一次跑通。2.1 开发环境选型与安装Java环境选择JDK 8或JDK 11LTS长期支持版。不建议用最新的JDK 17因为一些老旧的库或教程可能兼容性有问题。安装后务必配置JAVA_HOME环境变量。检查方法是在命令行输入java -version和javac -version都能正确显示版本信息即可。集成开发环境IDE强烈推荐使用IntelliJ IDEA Ultimate版社区版对JSP和Java EE支持不完整。很多人用Eclipse但IDEA在智能提示、代码导航和项目管理上更胜一筹。特别是处理JSP页面时IDEA的语法高亮和错误提示能省很多事。Web服务器使用Apache Tomcat 9。这是最广泛使用的Servlet/JSP容器成熟稳定。去官网下载zip包解压到一个没有中文和空格的路径下比如D:\dev\tomcat9。数据库使用MySQL 8.0。同样从官网下载安装包安装过程中记住你设置的root密码。同时建议安装MySQL Workbench这是一个图形化的数据库管理工具比命令行更直观方便我们建表、导入数据和调试SQL。2.2 在IDEA中创建并配置Web项目打开IDEA选择“New Project”。左侧选择“Java Enterprise”。在“Project SDK”处选择你安装的JDK。在“Application Server”这里点击“New...” - “Tomcat Server”然后定位到你解压的Tomcat目录。“Additional Libraries and Frameworks”这里只勾选“Web Application”并且确保勾选了“Create web.xml”。我们不用任何框架就从最原始的Servlet和JSP开始。给项目起个名字比如TrainQuerySystem选择项目位置点击Finish。项目创建好后你会看到一个标准的目录结构TrainQuerySystem ├── src │ └── (这里放Java源代码主要是Servlet) ├── web │ ├── WEB-INF │ │ ├── web.xml (部署描述符核心配置文件) │ │ └── lib (存放项目依赖的jar包如MySQL驱动) │ └── index.jsp (默认首页) └── 其他配置文件...第一个关键配置添加MySQL驱动。去Maven仓库或MySQL官网下载mysql-connector-java的jar包版本建议8.0.x。然后将这个jar包复制到web/WEB-INF/lib/目录下。IDEA会自动将这个目录下的jar包添加到项目的构建路径中。这是很多新手会忽略的一步导致后面代码里Class.forName(com.mysql.cj.jdbc.Driver)报错。第二个关键配置配置Tomcat运行。点击IDEA右上角的“Add Configuration...”点击“”号选择“Tomcat Server” - “Local”。在“Server”标签页确认Tomcat Home路径正确。在“Deployment”标签页点击“” - “Artifact”选择你项目生成的war exploded包。注意下方“Application context”可以设置为/这样访问地址就是http://localhost:8080/。 配置完成后点击运行按钮如果能在浏览器看到Tomcat的默认页面或你的index.jsp内容说明环境搭建成功。注意如果你遇到“404 - 未找到”错误大概率是“Application context”没配置对或者index.jsp没放在web目录根下而是放到了WEB-INF里面WEB-INF下的内容客户端无法直接访问。3. 数据库设计与数据建模如何设计一张“聪明”的车次表系统核心是数据。火车车次信息该怎么存这直接决定了查询的效率和功能的可扩展性。一个糟糕的表设计会让后续编码举步维艰。3.1 核心表结构设计我们至少需要两张表train车次表和station车站表。这里采用一种经典且实用的设计在train表中存储车次的完整运行线路信息。station表车站基础信息CREATE TABLE station ( id int(11) NOT NULL AUTO_INCREMENT COMMENT 车站ID主键, station_name varchar(50) NOT NULL COMMENT 车站名, city varchar(50) DEFAULT NULL COMMENT 所属城市, pinyin_code varchar(10) DEFAULT NULL COMMENT 拼音简码用于快速搜索, PRIMARY KEY (id), UNIQUE KEY uniq_station_name (station_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车站信息表;设计理由单独建车站表是为了数据一致性。避免在车次表中重复存储“北京南”、“北京南站”这类不统一的数据。pinyin_code字段如BJN代表北京南是为后续做车站名拼音搜索预留的这是一个提升用户体验的小技巧。train表车次信息表这是核心设计上需要一点技巧。我们不仅存储车次号还存储它经过的所有车站及时间。CREATE TABLE train ( id int(11) NOT NULL AUTO_INCREMENT, train_no varchar(20) NOT NULL COMMENT 车次号如G101, start_station_id int(11) NOT NULL COMMENT 始发站ID, end_station_id int(11) NOT NULL COMMENT 终点站ID, departure_time time NOT NULL COMMENT 始发站发车时间, arrival_time time NOT NULL COMMENT 终点站到达时间, duration time GENERATED ALWAYS AS (TIMEDIFF(arrival_time, departure_time)) STORED COMMENT 运行时长计算列, seat_info json DEFAULT NULL COMMENT 座位信息JSON格式如{business: 20, first: 100, second: 500}, PRIMARY KEY (id), KEY idx_train_no (train_no), KEY idx_stations (start_station_id,end_station_id), CONSTRAINT fk_train_start FOREIGN KEY (start_station_id) REFERENCES station (id), CONSTRAINT fk_train_end FOREIGN KEY (end_station_id) REFERENCES station (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车次基础信息表;设计理由start_station_id和end_station_id通过外键关联station表确保车站存在。duration使用了MySQL 5.7的生成列GENERATED COLUMN自动计算运行时间避免应用层重复计算。seat_info使用JSON类型存储动态的座位类型和数量比设计多个固定的seat_business、seat_first字段更灵活方便后续增加新的座位类型。查询时可以使用JSON_EXTRACT(seat_info, $.business)来获取具体数量。但是这只能解决“从A到B”的直达查询。用户更常见的需求是“从中间的C站到D站”。这就需要另一张表来记录车次经过每个车站的详情。train_schedule表车次时刻表CREATE TABLE train_schedule ( id int(11) NOT NULL AUTO_INCREMENT, train_id int(11) NOT NULL COMMENT 关联train.id, station_id int(11) NOT NULL COMMENT 关联station.id, station_sequence int(11) NOT NULL COMMENT 站序从1开始, arrival_time time DEFAULT NULL COMMENT 到达时间始发站此项为NULL, departure_time time DEFAULT NULL COMMENT 出发时间终点站此项为NULL, stay_duration time GENERATED ALWAYS AS (TIMEDIFF(departure_time, arrival_time)) STORED COMMENT 停靠时长, PRIMARY KEY (id), UNIQUE KEY uniq_train_station (train_id,station_id), KEY idx_train_seq (train_id,station_sequence), CONSTRAINT fk_schedule_train FOREIGN KEY (train_id) REFERENCES train (id) ON DELETE CASCADE, CONSTRAINT fk_schedule_station FOREIGN KEY (station_id) REFERENCES station (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车次时刻详情表;这才是查询功能的灵魂。它记录了车次train_id在station_sequence第几站停靠station_id以及具体的到达和出发时间。通过这张表我们可以轻松查出任意车次经过任意两个车站的信息。3.2 初始化测试数据设计好表之后需要插入一些真实的测试数据。这里以京沪高铁G1次为例先在station表插入北京南、济南西、南京南、上海虹桥。在train表插入G1始发站北京南(ID:1)终点站上海虹桥(ID:4)发车时间09:00到达时间13:30。在train_schedule表插入(train_id: G1的ID, station_id:1, sequence:1, departure_time:09:00)(train_id: G1的ID, station_id:2, sequence:2, arrival_time:10:30, departure_time:10:33)(train_id: G1的ID, station_id:3, sequence:3, arrival_time:12:00, departure_time:12:03)(train_id: G1的ID, station_id:4, sequence:4, arrival_time:13:30)有了这些数据我们就能回答“从济南西到南京南有G1次吗几点发车几点到” 这样的问题了。查询SQL的核心思路是从train_schedule表中找到同一train_id下station_sequence在起始站和到达站之间的所有记录。4. 后端核心Servlet与JDBC的经典搭配前端JSP负责展示后端的业务逻辑和数据库交互则由Servlet来完成。这是Java Web最经典的模式。4.1 创建数据库连接工具类直接在每一个Servlet里写连接代码是灾难性的会导致资源泄露和难以维护。我们必须创建一个工具类来管理数据库连接。// DBUtil.java package com.train.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/train_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD your_password; // 务必改成你自己的密码 static { try { // 加载驱动MySQL 8.0 驱动类名是这个 Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); throw new RuntimeException(找不到MySQL数据库驱动); } } // 获取数据库连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 关闭连接、Statement、ResultSet等资源 public static void close(AutoCloseable... closables) { for (AutoCloseable closable : closables) { if (closable ! null) { try { closable.close(); } catch (Exception e) { e.printStackTrace(); // 生产环境应使用日志记录而非打印 } } } } }关键点解析static静态代码块在类加载时执行一次用于注册驱动。这是老式写法JDBC 4.0之后可以省略但显式声明更清晰。连接参数useUnicodetruecharacterEncodingUTF-8确保正确处理中文。serverTimezoneAsia/Shanghai解决MySQL 8.0时区问题否则可能插入或查询时间出错。这是非常容易踩的坑。close方法使用可变参数和AutoCloseable接口可以优雅地关闭任何实现了该接口的资源Connection, Statement, ResultSet避免在finally块里写冗长的多层try-catch。4.2 实现查询功能的Servlet我们创建一个QueryServlet它负责接收前端传来的出发地、目的地查询数据库并返回结果。// QueryServlet.java package com.train.servlet; import com.train.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.*; import java.util.ArrayList; import java.util.List; WebServlet(/query) // 通过注解配置Servlet映射路径替代web.xml配置 public class QueryServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); // 通常查询操作也允许GET这里简单处理统一由doPost处理 } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 获取请求参数来自JSP表单 String fromStationName req.getParameter(fromStation); String toStationName req.getParameter(toStation); String date req.getParameter(date); // 暂时先留着后续可以用于筛选 // 2. 参数校验防止SQL注入的第一道防线 if (fromStationName null || fromStationName.trim().isEmpty() || toStationName null || toStationName.trim().isEmpty()) { req.setAttribute(errorMsg, 出发地和目的地不能为空); req.getRequestDispatcher(/index.jsp).forward(req, resp); return; } Connection conn null; PreparedStatement pstmt null; ResultSet rs null; ListTrainInfo trainList new ArrayList(); try { // 3. 获取数据库连接 conn DBUtil.getConnection(); // 4. 编写SQL查询语句核心逻辑 // 查询逻辑找到同时包含出发站和到达站且出发站顺序在到达站之前的车次 String sql SELECT t.train_no, t.departure_time as start_time, t.arrival_time as end_time, ts1.departure_time as actual_departure, ts2.arrival_time as actual_arrival, TIMEDIFF(ts2.arrival_time, ts1.departure_time) as travel_duration FROM train t JOIN train_schedule ts1 ON t.id ts1.train_id JOIN station s1 ON ts1.station_id s1.id AND s1.station_name ? JOIN train_schedule ts2 ON t.id ts2.train_id JOIN station s2 ON ts2.station_id s2.id AND s2.station_name ? WHERE ts1.station_sequence ts2.station_sequence ORDER BY ts1.departure_time; // 5. 使用PreparedStatement防止SQL注入 pstmt conn.prepareStatement(sql); pstmt.setString(1, fromStationName.trim()); pstmt.setString(2, toStationName.trim()); // 6. 执行查询 rs pstmt.executeQuery(); // 7. 遍历结果集封装成对象 while (rs.next()) { TrainInfo info new TrainInfo(); info.setTrainNo(rs.getString(train_no)); info.setStartTime(rs.getTime(start_time)); info.setEndTime(rs.getTime(end_time)); info.setActualDeparture(rs.getTime(actual_departure)); info.setActualArrival(rs.getTime(actual_arrival)); info.setTravelDuration(rs.getTime(travel_duration)); trainList.add(info); } // 8. 将结果列表存入请求属性传递给JSP req.setAttribute(trainList, trainList); req.setAttribute(fromStation, fromStationName); req.setAttribute(toStation, toStationName); } catch (SQLException e) { e.printStackTrace(); req.setAttribute(errorMsg, 数据库查询出错 e.getMessage()); } finally { // 9. 务必关闭资源防止内存泄漏 DBUtil.close(rs, pstmt, conn); } // 10. 转发到结果展示页面 req.getRequestDispatcher(/result.jsp).forward(req, resp); } } // 一个简单的JavaBean用于封装车次信息 class TrainInfo { private String trainNo; private Time startTime; private Time endTime; private Time actualDeparture; private Time actualArrival; private Time travelDuration; // 省略getter和setter... }代码深度解析与避坑指南WebServlet注解这是Servlet 3.0之后的方式比在web.xml里配置servlet和servlet-mapping更简洁。但要注意有些老旧的教程或项目可能还在用XML配置两者等价。参数校验在接触数据库之前进行校验是必须的。这里只是简单的非空检查实际还应包括长度、字符合法性等。SQL注入防御绝对不要使用字符串拼接来构造SQL这是安全红线。我们使用了PreparedStatement并用?作为占位符。pstmt.setString(1, ...)方法会将参数进行转义处理从根本上杜绝SQL注入。这是本项目最重要的安全实践。SQL查询逻辑这是核心。我们通过两次JOIN将train_schedule和station表关联起来分别定位出发站s1和到达站s2。条件ts1.station_sequence ts2.station_sequence确保了出发站在到达站之前。这个查询能高效地找出所有符合条件的车次。资源关闭在finally块中关闭ResultSet、Statement和Connection是铁律。即使发生异常也要保证连接被释放回连接池我们目前是直接关闭后续优化为连接池。我们的DBUtil.close()方法让这个操作变得简洁。请求转发使用req.getRequestDispatcher(/result.jsp).forward(req, resp);将处理结果trainList和请求对象一起“转发”给result.jsp页面去渲染。这是服务器端跳转地址栏不会变。5. 前端交互JSP页面的动态渲染与表单处理JSP页面负责两件事一是提供查询表单二是展示查询结果。我们将创建两个JSP文件index.jsp首页/查询页和result.jsp结果页。5.1 首页查询表单 (index.jsp)% page contentTypetext/html;charsetUTF-8 languagejava % html head title火车票查询系统/title style /* 简单的内联样式让页面看起来不那么丑 */ body { font-family: Arial, sans-serif; margin: 40px; } .container { width: 500px; margin: 0 auto; padding: 20px; border: 1px solid #ccc; border-radius: 5px; } .form-group { margin-bottom: 15px; } label { display: inline-block; width: 80px; } input[typetext], input[typedate] { padding: 8px; width: 200px; } button { padding: 10px 20px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; } .error { color: red; margin-top: 10px; } /style /head body div classcontainer h2火车车次查询/h2 form actionquery methodpost %-- 表单提交到QueryServlet --% div classform-group label forfromStation出发地/label input typetext idfromStation namefromStation placeholder例如北京南 required /div div classform-group label fortoStation目的地/label input typetext idtoStation nametoStation placeholder例如上海虹桥 required /div div classform-group label fordate日期/label input typedate iddate namedate /div button typesubmit查询/button /form %-- 显示错误信息 --% c:if test${not empty errorMsg} div classerror strong错误/strong ${errorMsg} /div /c:if %-- 一个小提示告诉用户可以怎么查 --% div stylemargin-top: 20px; font-size: 0.9em; color: #666; p提示目前数据库中有 G1北京南-济南西-南京南-上海虹桥、G2上海虹桥-...等测试车次数据。/p /div /div %-- 引入JSTL核心标签库用于条件判断、循环等 --% % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % /body /html关键点解析% page ... %这是JSP指令设置页面属性contentType和charset必须正确设置以支持中文。form actionquery methodpost表单的action属性值query对应我们Servlet中配置的WebServlet(/query)。methodpost对应Servlet的doPost方法。name属性namefromStation这个值就是Servlet中通过req.getParameter(fromStation)获取的键。JSTL标签我们使用了c:if标签来动态显示错误信息。${errorMsg}是EL表达式用于获取Servlet通过req.setAttribute(errorMsg, ...)设置的值。要使用JSTL需要将jstl.jar和standard.jar两个包放到WEB-INF/lib下。这是JSP开发中比直接写% ... %脚本更优雅、更安全的方式。内联CSS为了简化样式直接写在style里。实际项目建议使用外部CSS文件。5.2 结果展示页面 (result.jsp)% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title查询结果/title style table { border-collapse: collapse; width: 100%; margin-top: 20px; } th, td { border: 1px solid #ddd; padding: 12px; text-align: center; } th { background-color: #f2f2f2; } tr:hover { background-color: #f5f5f5; } .no-data { text-align: center; color: #999; padding: 40px; } .back-link { display: block; margin-top: 20px; text-align: center; } /style /head body div classcontainer h2从 span stylecolor: #4CAF50;${fromStation}/span 到 span stylecolor: #4CAF50;${toStation}/span 的车次查询结果/h2 c:choose %-- 判断是否有查询结果 --% c:when test${not empty trainList} table thead tr th车次/th th出发站发车时间/th th到达站到达时间/th th区间发车时间/th th区间到达时间/th th区间运行时间/th th操作/th /tr /thead tbody %-- 使用JSTL的forEach循环遍历List --% c:forEach vartrain items${trainList} tr tdstrong${train.trainNo}/strong/td td${train.startTime}/td td${train.endTime}/td td${train.actualDeparture}/td td${train.actualArrival}/td td${train.travelDuration}/td td %-- 一个模拟的“预订”按钮点击后跳转到详情页暂未实现 --% a hrefdetail?trainNo${train.trainNo}from${fromStation}to${toStation} stylepadding: 5px 10px; background: #008CBA; color: white; text-decoration: none; border-radius: 3px; 查看详情 /a /td /tr /c:forEach /tbody /table /c:when c:otherwise %-- 没有查询到结果时的显示 --% div classno-data p很抱歉未找到从 strong${fromStation}/strong 到 strong${toStation}/strong 的直达车次。/p p您可以尝试查询中转方案或检查车站名称是否正确。/p /div /c:otherwise /c:choose a hrefindex.jsp classback-link返回重新查询/a /div /body /html关键点解析JSTL与EL表达式这是JSP的精髓。c:choose,c:when,c:otherwise用于条件判断替代了复杂的% if ... %脚本。c:forEach用于循环遍历Servlet传过来的trainList。${train.trainNo}这种EL表达式直接调用JavaBean的getTrainNo()方法非常简洁。数据展示我们将查询结果以表格形式清晰展示。注意时间类型的显示直接从java.sql.Time对象输出格式可能不是最友好的实际项目中可以用JSTL的格式化标签fmt:formatDate进行美化。用户体验通过c:otherwise处理了无结果的情况并给出了友好提示。这是专业应用必备的细节。动态链接detail?trainNo${train.trainNo}from${fromStation}to${toStation}构造了一个带参数的URL为后续实现车次详情页留下了扩展接口。6. 性能优化与实战踩坑指南一个能跑通的Demo和一个健壮的系统之间隔着无数个坑。下面分享几个在这个项目中必然会遇到且必须解决的典型问题。6.1 数据库连接池为什么不用DriverManager我们之前的DBUtil每次请求都创建新连接用完后关闭。这在低并发下没问题但一旦访问量上来频繁创建和销毁TCP连接数据库连接底层是TCP将带来巨大的性能开销。连接池是解决这个问题的标准方案。我们可以使用像HikariCP或Druid这样的高性能连接池。以HikariCP为例首先在WEB-INF/lib下添加它的jar包然后修改DBUtil// DBUtil.java (使用HikariCP版本) package com.train.util; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static final HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/train_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(10); // 连接池最大连接数 config.setMinimumIdle(5); // 最小空闲连接数 config.setConnectionTimeout(30000); // 获取连接超时时间毫秒 config.setIdleTimeout(600000); // 连接空闲超时时间 config.setMaxLifetime(1800000); // 连接最大生命周期 config.addDataSourceProperty(cachePrepStmts, true); config.addDataSourceProperty(prepStmtCacheSize, 250); config.addDataSourceProperty(prepStmtCacheSqlLimit, 2048); // 这些是MySQL性能优化参数 dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); // 从池中获取连接 } public static void close(AutoCloseable... closables) { // 关闭方法不变但注意Connection.close()在这里的含义是“将连接归还给连接池”而非真正关闭。 // ... 同上 ... } // 在应用关闭时如ServletContextListener中需要关闭数据源 public static void shutdownDataSource() { if (dataSource ! null !dataSource.isClosed()) { dataSource.close(); } } }改动要点我们从直接使用DriverManager改为从HikariDataSource这个连接池获取连接。连接池会预先建立好一些连接并维护起来当应用需要时快速分配用完后回收避免了重复创建的开销。getConnection()和close()的调用代码完全不用改这是连接池的透明性带来的好处。6.2 中文乱码问题从数据库到浏览器的完整链条中文乱码是Java Web开发的老大难问题必须确保整个数据流编码一致。数据库层面建表时使用utf8mb4字符集支持所有Unicode字符包括emoji。连接URL中指定characterEncodingUTF-8。Servlet层面在doPost方法最开头设置请求和响应的编码。req.setCharacterEncoding(UTF-8); // 解决POST请求参数乱码 resp.setContentType(text/html;charsetUTF-8); // 设置响应编码对于GET请求参数在URL中Tomcat 8.5默认是UTF-8但旧版本需要在server.xml的Connector标签中增加URIEncodingUTF-8属性。JSP页面层面页面指令% page pageEncodingUTF-8 contentTypetext/html;charsetUTF-8 %必须设置。6.3 JSP开发中的常见“坑”JSP file [/xxx.jsp] not found错误这通常是因为文件路径不对或者文件没有放在web目录或它的子目录下。WEB-INF目录下的文件不能被客户端直接访问只能通过请求转发forward或包含include来访问。脚本片段与表达式尽量避免使用% ... %写大量Java代码称为Scriptlet这会使页面难以维护。优先使用JSTL和EL表达式。如果必须用确保代码简洁。页面缓存在开发阶段浏览器可能会缓存旧的JSP页面。如果你修改了JSP但刷新浏览器没变化可以尝试强制刷新CtrlF5或者在IDEA中配置Tomcat的“On frame deactivation”为“Update classes and resources”。IDEA中JSP语法高亮或跳转失效检查File - Project Structure - Facets确保Web资源目录被正确识别。有时需要手动将web目录标记为“Web Resource Directory”。6.4 SQL查询性能与索引优化我们的核心查询SQL涉及多表JOIN和条件过滤如果数据量很大比如几十万条车次记录性能可能成为瓶颈。为关联字段和查询条件字段添加索引我们已经为train_schedule表的(train_id, station_sequence)和station表的station_name创建了索引。这是最重要的优化手段。避免SELECT *在SQL中明确指定需要的字段减少网络传输和数据解析开销。分析执行计划在MySQL Workbench中对复杂SQL使用EXPLAIN命令查看是否用到了索引是否有全表扫描。根据结果调整索引或SQL写法。7. 项目扩展与进阶思考完成基础查询后这个系统还有很多可以深化和扩展的地方这也是面试中常被问到的“你的项目有什么亮点”。7.1 实现车次详情与座位查询在result.jsp中我们预留了“查看详情”的链接。可以创建一个DetailServlet接收trainNo、from、to参数查询更详细的信息比如车次全程时刻表所有经停站。基于seat_infoJSON字段动态展示不同席别的余票和价格这里需要引入一个“余票”的概念涉及到更复杂的库存扣减逻辑是另一个大课题。车次类型高铁G、动车D、直达Z等、车型、是否提供Wi-Fi等附加信息。这需要你设计更多的表和更复杂的业务逻辑是练习数据库关联查询和业务封装的好机会。7.2 引入MVC框架思想我们这个项目虽然Servlet处理逻辑、JSP负责展示有点MVC的样子但还不够清晰。尤其是当业务复杂后Servlet会变得非常臃肿被称为MVC Model 1的缺点。进阶方向可以尝试引入真正的MVC框架如Spring MVC。将QueryServlet中的业务逻辑抽离到Service层数据库操作放到Dao层Servlet或Spring的Controller只负责参数接收和视图跳转。JSP则彻底变为纯视图。带来的好处代码结构清晰易于维护和测试各层职责分离。7.3 前后端分离的演进JSP技术最大的问题是前后端耦合太紧前端工程师需要懂JSP语法后端改动也容易影响页面。现代开发早已转向前后端分离。技术选型后端提供RESTful API可以用Spring Boot快速搭建返回JSON数据。前端使用Vue.js、React等框架通过Ajax调用API获取数据并渲染页面。对于本项目的意义你可以保留现有的Servlet作为API接口修改doPost方法将trainList转换成JSON字符串通过resp.getWriter().write(jsonString)输出然后单独做一个HTMLJavaScript的前端项目来调用它。这样就能直观地体会到前后端分离的优劣势。7.4 应对“Java面试八股文”这个项目几乎涵盖了Java Web基础面试题的所有实操场景Servlet生命周期init(),service(),doGet()/doPost(),destroy()。你的QueryServlet就是一个活例子。GET vs POST我们的表单用了POST但Servlet里也处理了GET。可以思考什么情况下用GET幂等、安全什么情况下用POST修改数据、有副作用。JDBC流程加载驱动、获取连接、创建Statement、执行SQL、处理ResultSet、关闭资源。我们的代码完整走了一遍。PreparedStatement vs Statement我们用了PreparedStatement防止SQL注入并且预编译SQL能提升性能。MVC模式虽然简单但你能说出哪部分是ModelTrainInfo JavaBean数据库哪部分是ControllerServlet哪部分是ViewJSP吗连接池原理我们提到了HikariCP。面试官可能会问为什么用连接池它的工作原理是什么。中文乱码解决方案我们给出了从浏览器到数据库的完整链条。这是一个经典的综合性问题。通过这个项目你不仅仅是“写了一个查询系统”而是亲手搭建并理解了Java Web应用从数据存储、业务处理到界面展示的完整闭环。下次面试被问到“请谈谈你对Servlet和JSP的理解”或者“你如何设计一个查询功能”时你完全可以把这个项目作为案例娓娓道来这比干巴巴地背概念要生动和深刻得多。本文还有配套的精品资源点击获取