
简介基于JSP与MySQL开发的火车查询系统是一套面向高校Java Web课程设计及毕业设计的完整项目源码。系统采用浏览器/服务器模式实现了车次信息、站台管理、用户注册与公告发布等核心功能业务逻辑与数据库访问通过Java类分层完成有助于初学者理解前后台交互和持久化设计思路。压缩包内共有一百四十一个文件其中包含四十余个JSP动态页面、十个Java业务类及对应编译后的class文件配套SQL数据库脚本和若干配置文件另有大量GIF图片用于页面展示整包大小仅七百零七KB结构清晰可直接导入开发工具运行。默认管理员账号为admin密码为admin数据库配置集中在DBO.java文件中方便快速搭建调试环境。目前已有三百二十二人学习参考适合需要对照完整代码完成课程设计、复习Java Web开发细节或扩展火车票务管理功能的人群。1. 为什么“JSP火车查询系统”至今还是 JavaWeb 课程设计里最稳的练手项目每年到课程设计季“基于 JSP 的毕设选题”“javaweb 头歌实训答案”这类检索词就会被刷一轮。火车查询系统恰好是 JSP Servlet MySQL 三类技术最典型的交汇点它有明确的实体关系、有带参查询、有表单提交后的页面跳转又没有电商项目那些订单状态机、支付回调之类的复杂事务。对刚走完 Java 基础、想搞清楚 JSP 到底怎么和数据库联动的人来说这个选题能把“请求—处理—响应”整条链路完整跑通中间还能顺带掌握 JDBC 封装、连接池配置、war 包部署这些工作里真正会用到的东西。但网上能找到的同类源码质量参差不齐很多版本还是十几年前的教学写法Servlet 里堆满 System.out.printlnJSP 页面里直接写 JDBC 连接。这类代码在 IDEA 里新建 JSP 项目后跑通很容易一旦面对 Tomcat 版本差异、MySQL 8 的驱动加载方式变化翻车概率直线上升。这篇笔记就从零开始把“JSP火车查询系统”拆成可复现的六步先立住技术选型再给数据库和页面的完整实现然后是 Servlet 查询链路的代码与参数说明最后集中排查五个高频踩坑点并在结尾把部署和验证的进阶做法一次讲透。2. 技术选型先立住JSP Servlet JDBC 为什么不选框架2.1 这个选题的场景边界查询为主、写入简单正好卡在框架的“不需要区”火车查询系统的核心操作是余票查询、车次信息展示、票价查询后台顶多加一个车次维护。这类需求天然是“读多写少”的形态最有价值的部分在于查询条件和结果集的映射关系。用 Spring Boot MyBatis 当然能做但对准备课程设计答辩或者刚学完 JavaWeb 的人来说框架会掩盖掉一层关键认知HTTP 请求怎样被容器接收、参数怎样从 request 里取出、连接怎样从连接池借出又归还。把这层链路看清楚以后去理解过滤器、拦截器、AOP 才有着力点。如果你要把这个系统作为毕设选题还得考虑另一个现实因素答辩时老师几乎一定会问“你的项目用了哪些技术为什么这么选”。用 Servlet JSP JDBC 的答案是“为了更清晰地展示请求处理流程和 SQL 执行逻辑”这个理由站得住如果直接用框架被追问“Spring 的 IoC 在这里解决了什么问题”反而容易答不上来。不是说框架不好而是这个题目选轻量方案更诚实、更容易展示自己真正动手的部分。提示JSP 本质上是 Servlet 的模板化封装容器会把 .jsp 文件编译成 Servlet 类再执行。你写的 JSP 页面最终都会变成 Java 类所以调试时看到 Tomcat 的 work 目录里出现“org/apache/jsp/xxx_jsp.java”不是异常是正常编译产物。2.2 运行环境与版本选型JDK、Tomcat、MySQL 的匹配关系版本匹配是新手最先踩的坑。这个项目我一般用 JDK 8 Tomcat 9 MySQL 5.7 或 MySQL 8.0。JDK 8 至今仍是很多教材和课程设计的基础版本Tomcat 9 对应 Servlet 4.0 规范支持 JSP 2.3足够用且对老代码兼容好MySQL 用 5.7 最省心如果机器上已经装了 MySQL 8记得驱动要换成com.mysql.cj.jdbc.Driver而且连接 URL 里必须带serverTimezoneAsia/Shanghai否则会报时区错误。IDEA 里新建 JSP 项目时不要选 Maven 骨架里的 webapp 模板再手动补依赖直接新建普通 Java 项目右键添加 Web 支持更干净。项目结构保持最朴素的布局src 下放 Java 源码web 目录下放 JSP 页面WEB-INF/lib 放驱动 jar 包。Maven 可以引入但对这个体量的项目手动把mysql-connector-java的 jar 包丢进 lib 目录反而更直观——你能清楚地看见项目依赖了什么而不是在 pom.xml 里猜坐标。2.3 数据访问层的取舍JDBC 直连还是配一个简单的工具类常见做法是写一个 BaseDao 或 DBUtil 类把连接获取和关闭收敛到一个方法里。这样业务代码里不出现DriverManager.getConnection的重复调用后续换连接池时只改一处。网上很多源码直接在 JSP 里写Class.forName(com.mysql.jdbc.Driver)这种写法在“把 JDBC 写进 JSP”的演示里能跑但真实项目不这么做——JSP 的职责是展示数据库访问应该隔离出去。数据层我会拆成两层一个DBUtil负责拿连接一个TrainDao负责拼 SQL 和执行查询。Servlet 只调用 DAO 的方法拿到ListTrain后转发给 JSP。这样每个类的职责只有一条答辩时讲代码结构也从容。别为了省事把 DAO 写成静态方法满天飞静态方法意味着所有线程共享同一个 DAO 实例的状态查询类项目问题不大但会养坏习惯。3. 数据库设计与 JSP 页面先有表结构再有查询页3.1 火车查询系统的核心表列车、停靠站、余票三张表的设计思路火车查询系统的数据模型比学生管理系统多一层“站”的维度。一张车次表只有车次号、始发站、终点站、发车时间、到达时间无法表达“途经站”所以需要独立的停靠站表。余票也不是一个死字段不同区间余票不同用票额表更合理。下面是完整建表语句CREATE DATABASE train_db DEFAULT CHARACTER SET utf8mb4; USE train_db; CREATE TABLE train ( train_no VARCHAR(10) PRIMARY KEY COMMENT 车次号如 G102, start_station VARCHAR(50) NOT NULL COMMENT 始发站, end_station VARCHAR(50) NOT NULL COMMENT 终点站, start_time TIME NOT NULL COMMENT 发车时间, end_time TIME NOT NULL COMMENT 到达时间, seat_price DECIMAL(8,2) NOT NULL COMMENT 二等座票价, sleeper_price DECIMAL(8,2) NOT NULL COMMENT 卧铺票价 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE station ( id INT PRIMARY KEY AUTO_INCREMENT, train_no VARCHAR(10) NOT NULL, station_name VARCHAR(50) NOT NULL COMMENT 停靠站名称, arrive_time TIME NULL COMMENT 到达本站时间, leave_time TIME NULL COMMENT 离开本站时间, stop_order INT NOT NULL COMMENT 停靠顺序从1开始, FOREIGN KEY (train_no) REFERENCES train(train_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE ticket ( id INT PRIMARY KEY AUTO_INCREMENT, train_no VARCHAR(10) NOT NULL, start_station VARCHAR(50) NOT NULL COMMENT 上车区间, end_station VARCHAR(50) NOT NULL COMMENT 下车区间, remain_count INT NOT NULL DEFAULT 0 COMMENT 余票数量, FOREIGN KEY (train_no) REFERENCES train(train_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字符集用 utf8mb4 而不是 utf8是因为 MySQL 的 utf8 实际只支持部分 Unicode 字符遇到生僻站名或少见全角符号会报“Incorrect string value”错误。把start_stime、end_time设成 TIME 类型而不是 VARCHAR是让数据库帮你保证数据格式合法性也方便后续做“发车时间晚于 X 点”这类范围筛选。站表用stop_order而不是靠arrive_time排序因为同一站可能有多趟车时间上不一定严格递增手工维护一个顺序字段更可控。提示外键在这个项目里建议保留。虽然很多互联网业务为了分库分表会去掉外键但你做的是课设级别的系统保留外键能在插入数据时自动校验车次号是否存在避免出现“站点指向了一个不存在的车次”这种脏数据。3.2 查询页面的 JSP 写法表单、下拉框与结果展示的分离查询页我分成两个 JSPquery.jsp是查询表单result.jsp是结果列表。表单页只负责收集条件不做任何业务判断结果页只负责展示 Servlet 传过来的 List。这样职责清晰也方便后续在 result.jsp 上加分页而不动表单。下面是一个带始发站、终点站、出发日期三个条件的表单页% page contentTypetext/html;charsetUTF-8 languagejava % html head title火车余票查询/title /head body h2火车余票查询/h2 form action${pageContext.request.contextPath}/searchTrain methodget label出发站/label input typetext namestartStation placeholder请输入出发站 required / label到达站/label input typetext nameendStation placeholder请输入到达站 required / label出发日期/label input typedate nametravelDate / button typesubmit查询/button /form /body /htmlform 的 action 指向 Servlet 的 URL 映射而不是 JSP 文件本身这是关键设计页面不直接访问数据库而是交给 Servlet 处理后再转发回来。${pageContext.request.contextPath}是 EL 表达式取应用上下文路径也就是部署后在 Tomcat 里的访问前缀。比如你这个应用名叫train_web那么这台 Tomcat 上所有页面的链接前都要加/train_web硬编码会让项目在换部署名时全部失效用 EL 表达式可以规避。travelDate用typedate而不是文本框是为了让浏览器弹出原生日期选择器Servlet 端收到的是yyyy-MM-dd格式字符串正好可以直接传给 JDBC 的setString。注意这里的日期只是演示查询条件如果后面要做真实的票价浮动或节假日余票预测日期就要落到单独的表里。3.3 JSP 的页面指令与编码一行乱码问题的根源JSP 文件第一行% page contentTypetext/html;charsetUTF-8 languagejava %不是摆设。contentType控制响应给浏览器的内容类型和编码pageEncoding如果没写就默认取 charset控制 JSP 文件本身在编译期的读取编码。这两者不一致就会出现“页面显示正常但表单提交后乱码”或者反过来。IDEA 新建 JSP 默认可能是 ISO-8859-1改 UTF-8 的常见做法是 Editor 里的 File Encodings 全部设为 UTF-8同时在 JSP 头显式声明。这里特别提醒一点% page ... %指令必须写在 JSP 文件最顶部前面不能有任何空行、空格或 BOM 头。Tomcat 编译 JSP 时对 BOM 的兼容性不稳定如果文件被编辑器加了 UTF-8 BOM运行起来可能报“org.apache.jasper.JasperException: PWC6033”这类难排查的错误。用 IDEA 的“Remove BOM”功能或者在保存时选 UTF-8 without BOM可以规避。4. Servlet JDBC 查询链路从表单参数到 List 的完整走通4.1 DBUtil 与 TrainDao连接管理、重复代码收敛、SQL 参数化先写 DBUtil。这里采用 Properties 文件配置连接参数而不是在 Java 代码里写死连接串这样以后从底层 MySQL 的测试环境切换到生产库不需要重新编译 Java 类。连接参数放在src/db.properties里package com.train.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream is DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(is); Class.forName(props.getProperty(driver)); url props.getProperty(url); username props.getProperty(username); password props.getProperty(password); } catch (Exception e) { throw new ExceptionInInitializerError(数据库配置加载失败); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }静态代码块在类第一次被加载时执行作用是完成驱动注册和配置读取。getResourceAsStream从 classpath 读取 properties 文件所以db.properties必须放在 src 源码目录下编译后才会进入 classes。Class.forName在 JDBC 4.0 以后其实可以省略——新版驱动 jar 包里有META-INF/services/java.sql.Driver文件DriverManager 会自动加载。但保留这句兼容性更好尤其在 MySQL 8 驱动上显式加载能更快暴露驱动缺失问题而不是等连接超时。TrainDao 里的查询必须用PreparedStatement而不是Statement核心原因是防 SQL 注入。用户输入的车站名直接拼进 SQL 字符串比如输入 or 11查询条件就变成恒真会把整张表拉回来。PreparedStatement 用setString绑定参数JDBC 驱动的 escape 机制会处理单引号从根上堵住注入。这是 Django、ORM 之外的 Java 原生开发里少见的“安全是语法层面的”时刻答辩时讲这个点很加分。代码示意如下public ListTrain searchByStation(String start, String end) { String sql SELECT * FROM train WHERE start_station ? AND end_station ?; ListTrain result new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, start); ps.setString(2, end); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Train t new Train(); t.setTrainNo(rs.getString(train_no)); t.setStartStation(rs.getString(start_station)); t.setEndStation(rs.getString(end_station)); t.setStartTime(rs.getTime(start_time)); t.setEndTime(rs.getTime(end_time)); t.setSeatPrice(rs.getBigDecimal(seat_price)); t.setSleeperPrice(rs.getBigDecimal(sleeper_price)); result.add(t); } } } catch (SQLException e) { e.printStackTrace(); return new ArrayList(); } return result; }Java 7 引入的 try-with-resources 会自动关闭Connection、PreparedStatement、ResultSet三个资源不用再写 finally 里层层判空。关闭顺序是逆序的先关 ResultSet再关 Statement最后关 Connection。如果你手动管理写错顺序会导致“连接已归还但结果集还在被引用”的问题。这里e.printStackTrace()是学习阶段的正常做法但生产上应该记录到日志文件避免错误信息淹没在 Tomcat 控制台。4.2 Servlet 的 doGet 写法request 参数处理、转发与重定向的边界Servlet 接收表单 GET 请求的 doGet 方法里参数处理有三个维度取值、去空白、默认值。上面表单里两个输入框都标记了required但浏览器端的约束可以被绕过所以 Servlet 端仍要做判空。从 request 拿到的值如果为 null参数不存在或空字符串表单提交了空内容都要统一当成“条件不完整”处理返回错误提示而不是去数据库执行一次注定查不到结果的查询WebServlet(/searchTrain) public class SearchTrainServlet extends HttpServlet { private TrainDao trainDao new TrainDao(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String start request.getParameter(startStation); String end request.getParameter(endStation); if (start null || end null || start.trim().isEmpty() || end.trim().isEmpty()) { request.setAttribute(errorMsg, 出发站和到达站不能为空); request.getRequestDispatcher(/query.jsp).forward(request, response); return; } start start.trim(); end end.trim(); ListTrain list trainDao.searchByStation(start, end); request.setAttribute(trainList, list); request.getRequestDispatcher(/result.jsp).forward(request, response); } }WebServlet(/searchTrain)是 Servlet 3.0 起的注解式映射省去了 web.xml 里繁琐的 servlet-mapping 配置。逻辑上这里做的是“转发”而非常见的sendRedirect——forward由服务器内部完成请求传递浏览器地址栏不会变request 里的属性可以继续用redirect会让浏览器发起第二次请求request 对象会重建。查询参数在两次请求间需要保留时得手动拼 URL非常容易漏。所以查询类结果返回一律用 forward。request.setCharacterEncoding(UTF-8)放在最前面是针对 POST 请求的正文编码GET 请求的参数编码由 Tomcat 的URIEncoding决定默认是 UTF-8所以这里写这行对 GET 请求本身不直接生效。很多教材让所有人把这一行写在 doGet 开头严格说是种防御式写法保留它无妨但要知道它的真实作用域。4.3 result.jsp 的展示逻辑JSTL 还是纯 Scriptlet边界在哪结果页展示 List最佳实践是用 JSTL 的c:forEach遍历而不是在 JSP 里写% for (...)Scriptlet。JSTL 让页面更像模板而且 EL 表达式会自动处理空集合避免list.get(i)越界异常。下面是一个完整的 result.jsp 片段% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body h2查询结果/h2 c:if test${not empty trainList} table border1 cellspacing0 cellpadding6 tr th车次/thth始发站/thth终点站/thth发车时间/thth到达时间/th th二等座/thth卧铺/th /tr c:forEach items${trainList} vart tr td${t.trainNo}/td td${t.startStation}/td td${t.endStation}/td td${t.startTime}/td td${t.endTime}/td td${t.seatPrice}/td td${t.sleeperPrice}/td /tr /c:forEach /table /c:if c:if test${empty trainList} p未查询到符合条件的车次请更换条件后再试。/p /c:if /body /html使用 JSTL 需要两个 jar 包jstl.jar和standard.jar或者用 Tomcat 10 时代的新坐标jakarta.servlet.jsp.jstl。如果页面报“The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved”99% 是 lib 目录缺第二个 jar 包。另一种选择是把查询结果在 Servlet 端转成 JSON 输出再配合前端渲染但那个路线就直接走向前后端分离了对课设来说多引入 ajax 和 JSON 解析库反而增加了不稳定性。result.jsp 里Time类型的字段 EL 直接输出是HH:mm:ss比如 08:30:00。如果想显示成 08:30简单做法是在 Train 实体里加一个startTimeStr在 DAO 的getTime后格式化。常见格式化写法是new SimpleDateFormat(HH:mm).format(rs.getTime(start_time))不要写在 JSP 页面里实体类的方法更适合。5. 避坑手册JSP 火车查询系统最常见的五个故障及排查路径5.1 现象运行 Tomcat 后访问页面报 404但项目明明部署了原因往上查通常是两个一是应用部署名和访问 URL 不匹配IDEA 里 Artifact 的部署名是train_web浏览器却访问了http://localhost:8080/query.jsp而实际路径是http://localhost:8080/train_web/query.jsp。二是 Servlet 的WebServlet路径写成了/searchTrain/带了尾部斜杠Tomcat 匹配不到。我处理这类问题先把 IDEA 的 Deployment 页截图核对 Application context如果显示/train_web访问前缀就敲http://localhost:8080/train_web/query.jsp然后打开 Tomcat 的web.xml找不到映射时再检查注解是否写全。解决统一 application context 与请求路径前缀。确认 IDEA 中 Artifact 的 “Deployment” 选项卡里 Application context 的值以及浏览器 URL 是否完整带上项目名去掉WebServlet路径末尾的斜杠点击 Tomcat 控制台的“Redeploy”而非“Restart”有时 Tomcat 缓存了旧的 classRedeploy 才重新编译。5.2 现象MySQL 8 下启动项目报ClassNotFoundException: com.mysql.jdbc.Driver原因MySQL 8 的驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver而且连接 URL 必须加时区参数。老的 5.x 驱动包如果继续使用即使类名写对也可能因为协议版本不兼容报Public Key Retrieval is not allowed。解决驱动包换成mysql-connector-java8.x版本号自己标注入座db.properties 里 driver 写成com.mysql.cj.jdbc.Driverurl 末尾追加?serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue。useSSLfalse只是因为本地开发没有证书连远程库时按需开。这个组合我在 Windows 安装 MySQL 8 后没有再遇到连接层面的怪问题。5.3 现象表单提交中文参数到 Servlet 变成乱码原因要分请求方向。浏览器到 Tomcat 这一段GET 请求的编码由 Tomcat 的URIEncoding控制POST 由request.setCharacterEncoding控制。Tomcat 8 之后 URIEncoding 默认是 UTF-8所以 GET 乱码少见POST 乱码则多是因为 JSP 页面本身的 pageEncoding 是 ISO-8859-1提交出去的字节就是错误的。这类问题有个经典玄学本地 IDEA 跑不乱码打成 war 包部署到 Linux 上的 Tomcat 就乱因为 Linux 系统默认 locale 是 POSIXTomcat 读 URI 的默认编码行为不同。解决JSP 头统一pageEncodingUTF-8Servlet 里 doPost 第一行写request.setCharacterEncoding(UTF-8)Tomcat 的server.xml里Connector标签加URIEncodingUTF-8。三层全部统一后乱码概率趋近于零。还有一个血泪经验IDEA 里 Database 工具窗口连接的库字符集和项目连的库字符集不一致可能导致你看到的数据是好的但程序读出来乱码——用命令行客户端查一次最可靠。5.4 现象Tomcat 启动时报Address already in use: JVM_Bind原因启动过第二个 Tomcat 实例或者 IDEA 之前异常退出8080 端口还被僵尸进程占用。MySQL 的error 2002 (HY000): cant connect to local mysql server through socket /tmp/mysql.sock是另一个类似问题——客户端找 socket 文件失败服务端没起来或者路径不对。两个问题的排查思路其实一样先看端口/套接字有没有进程在监听。解决Windows 下执行netstat -ano | findstr 8080拿到占用 PID到任务管理器结束进程或者改 Tomcat 的 port 为 8081。MySQL 的 socket 问题则检查mysqld进程是否存在以及my.cnf里socket路径和客户端配置是否一致。注意 Linux 上如果你用 rpm 方式安装的 MySQLsocket 默认在/var/lib/mysql/mysql.sock客户端默认找/tmp/mysql.sock不一致就会遇到上面那条错误业内通常用建软链来解决。5.5 现象查询结果页显示“未查询到车次”但数据库里明明有数据原因有两个层次。第一次遇到时先别怀疑 SQL——先用 Navicat 或命令行执行同样的查询语句确认条件能查到。查不到再怀疑代码层最常见的其实是用户输入了“上海”而表里存的是“上海虹桥”模糊匹配没做。另一层原因是request.getParameter取到的值带不可见空白trim()方法在 Servlet 里已经做了但如果参数从 URL 直接拼接过来浏览器地址栏里的%20也可能没有被解码。解决查询条件改成 LIKE 模糊匹配而不是等值匹配SQL 写成WHERE start_station LIKE ?后参数设置为% start %。代价是索引失效风险但数据量小影响可忽略。更稳妥的体验方式是页面表单里用datalist列出所有车站用户必须从备选列表里选既减少输入不一致又降低查不到的概率。另外可以在 Servlet 里打日志输出最终执行的 SQL 参数确认 Service 层拿到的值到底是什么这条排查手段能解决一半的“数据存在但查不到”。6. 从课设到可演示作品war 包部署、连接池替换与答辩加分改造项目在 IDEA 里跑通不算完关键一步是打成 war 包丢到独立 Tomcat 下验证。IDEA 菜单 Build Build Artifacts 选择xxx:war exploded之外的那个xxx:war点击 Build产物在out/artifacts/下。把 war 包复制到 Tomcat 的 webapps 目录启动后自动解压访问http://localhost:8080/项目名/query.jsp。这一步的目的是暴露 IDEA 内嵌 Tomcat 隐藏的问题——最常见的坑是db.properties没有进 war 包如果出现ExceptionInInitializerError或找不到配置文件的报错检查 Artifact 的 Output Layout 里有没有把 resources 目录打进去。再进一步把 DBUtil 的直连换成连接池。这里推荐 DBCP 或 HikariCP注意 HikariCP 需要slf4j-api别漏依赖。改造后的 DBUtil 核心是HikariDataSource初始化一次后反复调用getConnection这比 DriverManager 每次新建物理连接要快一个数量级也是 MySQL 连接被耗尽问题的根治方案。连接池的核心参数就三个maximumPoolSize默认 10minimumIdle默认与 maximum 相等connectionTimeout默认 30 秒。课设系统并发低maximumPoolSize 设 5 就够设太大会浪费数据库连接资源。如果还有余力做答辩加分我建议加一个“按发车时间排序”的 SQL 排序条件或者在 DAO 里写一个searchByStationAndDate的重载——把 travelDate 参数加进查询条件哪怕只是演示“当日有票”的判断逻辑。对 JSP Java 方向想深入的同学这套东西能不能扩展成 Spring Boot 版不是重点重点是能说清楚“JSP 页面为什么在 Spring Boot 里变成了模板引擎的天下”。面试被问 Java 八股文里的数据一致性、连接池、SQL 注入时用这个项目的代码举例比背概念生动得多。我把这个项目做了三遍以后才意识到火车查询系统真正的价值不在“查询”本身而在让你把 Servlet 生命周期、JDBC 资源管理、Tomcat 部署机制这三块黑匣子逐个打开过一次。以后不管用什么框架遇到 404、乱码、连接超时第一反应都是沿着这条链路去定位而不是换个工具再试一次。希望帮到你。提示打成 war 包部署后每次改 Java 代码都要重新 Build Artifact改 JSP 则不需要重启 Tomcat——JSP 文件会被容器检测到修改并自动重新编译。这个特性调试页面样式时能省大量时间。本文还有配套的精品资源点击获取