简介:本资源是一个基于JSP技术实现的携程网功能仿真实验项目,面向Java Web初学者与Web开发入门学习者,旨在通过完整可运行的代码帮助理解在线旅行服务平台的核心业务流程与MVC架构实践。压缩包共128个文件,涵盖20个Java源码(如TicketOrder、FlightInfo等业务实体与DAO实现)、8个JSP页面(承担前端交互)、20个Class字节码、4个SQL建表脚本、12个JS交互逻辑及多种静态资源(PNG/JPG/GIF图标与样式文件),整体963KB,结构清晰,便于按层分析Model-View-Controller职责划分。已有223人学习下载,读者可直接部署运行,掌握用户登录、航班查询、酒店预订、订单管理等典型Web流程,并深入理解JDBC数据访问、连接池配置(ConnectionFactory.class)、ID生成机制(IdGenerator.class)及前后端协同逻辑。
1. 携程 Java 后端项目源码(ctrip.rar)的本地解析与结构还原实践
你下载到一个名为ctrip.rar的压缩包,解压后发现是一套典型的 Java EE 企业级代码,包含HotelCityAddressgb2312、FlightInfoDaoJDBCImpl、TicketOrder、CityInfoDaoJDBCImpl等类名——这不是公开 SDK,也不是文档示例,而是某时期携程内部服务模块的遗留源码片段。它不带 Maven 依赖声明、无 Spring Boot 启动类、数据库连接硬编码在 DAO 实现里,甚至字符集明确标注为gb2312。这类代码对新人是“天书”,但对有经验的 Java 工程师,却是理解传统 OTA(在线旅行服务商)系统分层设计、数据建模与 JDBC 实战细节的极佳切口。本文不讲“如何黑进携程”,而是聚焦:如何在无构建脚本、无文档、无环境说明的前提下,从零还原这套代码的可运行结构,识别其业务边界(酒店城市地址管理、航班信息查询、订单模型、城市信息持久化),并验证核心 DAO 层与数据库的交互逻辑是否成立。适合正在维护老系统、参与旅游行业中间件迁移、或需要快速吃透遗留 Java 项目架构的开发者。
2. 解析 ctrip.rar 源码结构:定位核心模块与技术栈特征
2.1 拆包与目录拓扑分析:识别三层架构痕迹
ctrip.rar解压后通常呈现如下典型结构:
src/ ├── com/ctrip/ │ ├── dao/ │ │ ├── CityInfoDaoJDBCImpl.java │ │ ├── FlightInfoDaoJDBCImpl.java │ │ └── ... │ ├── model/ │ │ ├── TicketOrder.java │ │ ├── HotelCityAddressgb2312.java │ │ └── ... │ └── service/ │ └── ... (空或仅存接口) resources/ └── db.properties (若存在)提示:
HotelCityAddressgb2312类名中的gb2312是关键线索——它表明该模块处理中国城市地址时强制使用 GB2312 字符集,而非 UTF-8。这直接影响 JDBC 连接 URL 的useUnicode=true&characterEncoding=gb2312参数配置,否则中文字段读写必然乱码。不要跳过这个细节。
我们首先确认 JDK 版本兼容性。打开任意.java文件(如FlightInfoDaoJDBCImpl.java),观察其语法特征:
public class FlightInfoDaoJDBCImpl implements FlightInfoDao { private static final String DB_URL = "jdbc:mysql://localhost:3306/ctrip?useUnicode=true&characterEncoding=gb2312"; private static final String DB_USER = "root"; private static final String DB_PASSWORD = "123456"; // ... }可见:
- 使用原生 JDBC(无 Hibernate/MyBatis 封装);
- 数据库为 MySQL(
jdbc:mysql://); - 连接参数显式指定
characterEncoding=gb2312; - 无连接池(直接
DriverManager.getConnection()); - DAO 类继承自接口(如
FlightInfoDao),符合经典 DAO 模式。
2.2 核心类职责逆向推导:从命名与方法签名锁定业务域
2.2.1CityInfoDaoJDBCImpl:城市基础信息持久化入口
查看其方法签名:
public List<CityInfo> findAllCities() { ... } public CityInfo findCityById(int cityId) { ... } public void updateCityName(int cityId, String cityName) { ... }结合类名CityInfoDaoJDBCImpl和方法语义,可确定:
- 对应数据库表
city_info(或类似命名); - 主键为
int cityId; cityName字段需支持 GB2312 编码(如“北京市”“广州市”);- 无复杂关联查询,属基础字典表操作。
2.2.2FlightInfoDaoJDBCImpl:航班动态数据查询中枢
其典型方法:
public List<FlightInfo> queryFlightsByRoute(String fromCity, String toCity, Date date) { ... } public FlightInfo getFlightDetail(String flightNo) { ... }说明:
- 表结构必含
from_city,to_city,flight_date,flight_no字段; fromCity/toCity值来自city_info表,存在外键约束;- 查询条件含日期范围,需注意
Date类型与 MySQLDATE/DATETIME的映射。
2.2.3TicketOrder:订单领域模型的核心载体
public class TicketOrder { private int orderId; private String passengerName; // GB2312 编码 private String idCardNo; private String flightNo; private Date orderTime; private double totalPrice; // getter/setter... }关键点:
passengerName为中文姓名,必须用gb2312存储;idCardNo为 18 位身份证号,需校验逻辑(但源码中未体现,属业务层缺失);orderTime为java.util.Date,对应 MySQLDATETIME。
2.2.4HotelCityAddressgb2312:酒店地址专用实体类
类名直指用途:专用于酒店地址的 GB2312 编码处理。其字段往往包含:
private String province; // 省 private String city; // 市 private String district; // 区 private String street; // 街道注意:此类不直接继承Entity,而是作为Hotel实体的嵌套属性存在,体现“地址”作为值对象(Value Object)的设计意图。
2.3 技术栈断代判断:JDK 1.6+ + MySQL 5.5+ + 原生 JDBC
综合以下证据链:
- 使用
@Override注解(JDK 1.5+); java.util.Date而非 Joda-Time(排除 2010 年前老系统);- MySQL 连接 URL 中
useUnicode=true参数(MySQL 4.1+ 支持); - 无
javax.annotation.Resource等 Java EE 6 注解(排除 WebLogic/JBoss 全量容器部署); - DAO 类无
@Repository(Spring 2.5+),确认为纯 JDBC 手写层。
结论:此代码大概率生成于2008–2013 年间,属于携程早期自研 Java 框架过渡期产物,技术选型保守但稳定,适合作为 JDBC 底层原理教学样本。
3. 本地环境搭建:MySQL 数据库初始化与 JDBC 连接验证
3.1 创建兼容 GB2312 的 MySQL 数据库
MySQL 默认字符集为utf8mb4,但ctrip.rar明确要求gb2312。必须创建专用数据库并指定字符集:
mysql -u root -pCREATE DATABASE ctrip_db CHARACTER SET gb2312 COLLATE gb2312_chinese_ci;注意:
gb2312_chinese_ci是 MySQL 内置排序规则,确保中文比较正确。若执行报错Unknown character set 'gb2312',说明 MySQL 版本过低(< 5.5.3),需升级或改用latin1(不推荐,中文会乱码)。
3.2 还原核心数据表结构:基于 DAO 方法反推 DDL
以CityInfoDaoJDBCImpl.findAllCities()为例,其 SQL 通常为:
String sql = "SELECT city_id, city_name, province_name FROM city_info ORDER BY city_id";据此反向生成建表语句:
USE ctrip_db; CREATE TABLE city_info ( city_id INT PRIMARY KEY AUTO_INCREMENT, city_name VARCHAR(50) NOT NULL COMMENT '城市名称,GB2312编码', province_name VARCHAR(50) NOT NULL COMMENT '省份名称' ) ENGINE=InnoDB DEFAULT CHARSET=gb2312; -- 插入测试数据(GB2312 编码的中文) INSERT INTO city_info (city_name, province_name) VALUES ('北京市', '北京市'), ('上海市', '上海市'), ('广州市', '广东省'), ('成都市', '四川省');同理,FlightInfoDaoJDBCImpl.queryFlightsByRoute()的 SQL 可能为:
SELECT flight_no, from_city, to_city, flight_date, departure_time, arrival_time FROM flight_info WHERE from_city = ? AND to_city = ? AND DATE(flight_date) = ?对应建表:
CREATE TABLE flight_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, flight_no VARCHAR(20) NOT NULL, from_city INT NOT NULL COMMENT '关联 city_info.city_id', to_city INT NOT NULL COMMENT '关联 city_info.city_id', flight_date DATETIME NOT NULL, departure_time TIME, arrival_time TIME, FOREIGN KEY (from_city) REFERENCES city_info(city_id), FOREIGN KEY (to_city) REFERENCES city_info(city_id) ) ENGINE=InnoDB DEFAULT CHARSET=gb2312;3.3 验证 JDBC 连接:编写最小测试类绕过原有 DAO
为避免修改原始 DAO 代码,新建测试类JDBCTest.java:
import java.sql.*; public class JDBCTest { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/ctrip_db?useUnicode=true&characterEncoding=gb2312&serverTimezone=GMT%2B8"; String user = "root"; String password = "123456"; try (Connection conn = DriverManager.getConnection(url, user, password); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM city_info")) { System.out.println("成功连接 GB2312 数据库,city_info 表内容:"); while (rs.next()) { int id = rs.getInt("city_id"); String name = rs.getString("city_name"); // 自动按 gb2312 解码 String prov = rs.getString("province_name"); System.out.printf("ID:%d, 城市:%s, 省份:%s%n", id, name, prov); } } catch (SQLException e) { e.printStackTrace(); // 关键错误检查点: // 1. 若提示 "Character set 'gb2312' is not supported" → MySQL 版本问题 // 2. 若中文显示为 ??? → 连接 URL 缺少 characterEncoding=gb2312 // 3. 若报错 "Unknown database 'ctrip_db'" → 数据库未创建 } } }编译并运行:
javac -cp ".:mysql-connector-java-5.1.47.jar" JDBCTest.java java -cp ".:mysql-connector-java-5.1.47.jar" JDBCTest提示:必须使用mysql-connector-java 5.x 系列驱动(如 5.1.47),因 8.x 驱动默认禁用
gb2312。下载地址:https://dev.mysql.com/downloads/connector/j/5.1.html(选择 Platform Independent - ZIP)
3.4 驱动版本与字符集兼容性对照表
| MySQL 版本 | 推荐 JDBC 驱动 | 是否支持 gb2312 | 关键连接参数 |
|---|---|---|---|
| 5.5 – 5.7 | mysql-connector-java-5.1.x | ✅ 完全支持 | characterEncoding=gb2312 |
| 8.0+ | mysql-connector-java-8.0.x | ⚠️ 需显式启用 | allowPublicKeyRetrieval=true&useSSL=false&characterEncoding=gb2312 |
| 8.0+(严格模式) | 不推荐 | ❌ 默认禁用 | 强制降级至 5.1.x 驱动 |
若使用 MySQL 8.0+ 且必须用新驱动,需在my.cnf中添加:
[mysqld] collation-server = gb2312_chinese_ci init-connect='SET NAMES gb2312' skip-character-set-client-handshake = true然后重启 MySQL。
4. DAO 层实战调试:注入日志与 SQL 跟踪,定位真实执行语句
4.1 在 DAO 方法中植入 SQL 打印逻辑
原始FlightInfoDaoJDBCImpl.queryFlightsByRoute()通常形如:
public List<FlightInfo> queryFlightsByRoute(String fromCity, String toCity, Date date) { String sql = "SELECT ... WHERE from_city=? AND to_city=? AND DATE(flight_date)=?"; // PreparedStatement 设置参数... }为看清实际执行的 SQL,我们在PreparedStatement执行前打印:
public List<FlightInfo> queryFlightsByRoute(String fromCity, String toCity, Date date) { String sql = "SELECT id, flight_no, from_city, to_city, flight_date FROM flight_info WHERE from_city=? AND to_city=? AND DATE(flight_date)=?"; System.out.println("[DEBUG] Executing SQL: " + sql); System.out.printf("[DEBUG] Parameters: fromCity=%s, toCity=%s, date=%s%n", fromCity, toCity, date); try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); PreparedStatement ps = conn.prepareStatement(sql)) { // 假设 fromCity/toCity 是城市名称,需先查 city_id int fromId = getCityIdByName(fromCity); // 此方法需自行实现 int toId = getCityIdByName(toCity); ps.setInt(1, fromId); ps.setInt(2, toId); ps.setDate(3, new java.sql.Date(date.getTime())); System.out.printf("[DEBUG] Bound parameters: fromId=%d, toId=%d, sqlDate=%s%n", fromId, toId, ps.getDate(3)); ResultSet rs = ps.executeQuery(); // ... 结果映射 } }运行时输出:
[DEBUG] Executing SQL: SELECT id, flight_no, from_city, to_city, flight_date FROM flight_info WHERE from_city=? AND to_city=? AND DATE(flight_date)=? [DEBUG] Parameters: fromCity=北京市, toCity=上海市, date=Mon Jun 10 00:00:00 CST 2024 [DEBUG] Bound parameters: fromId=1, toId=2, sqlDate=2024-06-10这证实了:
fromCity/toCity参数被转换为city_id整数;DATE(flight_date)=?中的?绑定的是java.sql.Date,非java.util.Date;- SQL 本身不含拼接,属安全预编译。
4.2 使用 Log4j 拦截 JDBC 调用(进阶)
若项目已引入 Log4j(检查lib/目录是否有log4j-1.2.17.jar),可通过配置log4j.properties输出所有 JDBC 操作:
# log4j.properties log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss} [%t] %-5p %c{1} - %m%n # 拦截 java.sql 包 log4j.logger.java.sql=DEBUG log4j.logger.java.sql.Connection=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG启动应用后,控制台将输出:
14:22:33 [main] DEBUG PreparedStatement - {exec} SELECT * FROM city_info WHERE city_id = ? 14:22:33 [main] DEBUG PreparedStatement - {rset} SELECT * FROM city_info WHERE city_id = ?这是比手动System.out更可靠的 SQL 审计方式,尤其适用于多线程场景。
4.3 常见 DAO 执行失败的三大根因与修复
| 现象 | 根本原因 | 修复方案 |
|---|---|---|
SQLException: Column 'city_name' not found | 数据库表字段名与 ResultSet 取值名不一致(如 DB 用cityname,Java 用city_name) | 统一命名:SQL 中用别名SELECT city_name AS city_name,或 DAO 中用rs.getString(2)(按列序号) |
Data truncation: Data too long for column 'passengerName' | TicketOrder.passengerName字段长度超限(如 DB 定义VARCHAR(20),但姓名“欧阳修远”占 12 字节 GB2312) | 扩大字段:ALTER TABLE ticket_order MODIFY passengerName VARCHAR(50) CHARACTER SET gb2312 |
Cannot convert value '2024-06-10 08:30:00' from binary to DATETIME | MySQL 时区配置与 JVM 时区不一致 | 在连接 URL 加serverTimezone=GMT%2B8,或设置 JVM 参数-Duser.timezone=GMT+8 |
5. 基于 HotelCityAddressgb2312 的地址标准化实践:GB2312 编码下的中文处理技巧
5.1HotelCityAddressgb2312类的典型实现与陷阱
该类常包含如下方法:
public class HotelCityAddressgb2312 { private String province; private String city; private String district; private String street; // 构造函数中对输入字符串做 GB2312 编码校验 public HotelCityAddressgb2312(String province, String city, String district, String street) { this.province = toGb2312String(province); this.city = toGb2312String(city); this.district = toGb2312String(district); this.street = toGb2312String(street); } private String toGb2312String(String input) { if (input == null) return null; try { byte[] bytes = input.getBytes("GB2312"); return new String(bytes, "GB2312"); // 强制重编码,过滤非法字符 } catch (UnsupportedEncodingException e) { throw new RuntimeException("GB2312 encoding not supported", e); } } }注意:
new String(bytes, "GB2312")这行代码是双刃剑——它能确保字符串只含 GB2312 字符,但会将“𠮷”等扩展汉字转为?。在旅游场景中,用户姓名极少含生僻字,此处理可接受。
5.2 地址字段的数据库存储验证:确保无截断
编写测试方法验证street字段能否完整存入:
@Test public void testStreetLengthWithGb2312() throws Exception { String fullStreet = "北京市朝阳区建国门外大街1号北京国际俱乐部大厦B座12层"; // 38 个汉字 byte[] gb2312Bytes = fullStreet.getBytes("GB2312"); System.out.println("GB2312 字节数: " + gb2312Bytes.length); // 输出 76(每个汉字 2 字节) // 假设数据库字段为 VARCHAR(100),则 76 < 100,安全 // 若字段为 VARCHAR(50),则 76 > 50,必然截断! }结论:GB2312 编码下,中文字符串字节数 = 字符数 × 2。设计表结构时,VARCHAR长度必须 ≥ 中文字符数 × 2,而非字符数本身。
5.3 地址模糊搜索的 GB2312 兼容方案
CityInfoDaoJDBCImpl若提供findCityByKeyword(String keyword),其 SQL 往往为:
SELECT * FROM city_info WHERE city_name LIKE ?传入参数"北京%"时,需确保 JDBC 驱动正确传递 GB2312 字节:
ps.setString(1, keyword); // 驱动自动按 connection charset 编码但若前端传入 UTF-8 编码的"北京%",而数据库是 GB2312,则LIKE会失效。终极保障方案:
// 在 DAO 中统一转码 String gb2312Keyword = new String(keyword.getBytes("UTF-8"), "GB2312"); ps.setString(1, "%" + gb2312Keyword + "%");此法牺牲性能但保证正确性,适用于低频搜索场景。
5.4 生产环境地址校验建议:轻量级 GB2312 白名单过滤
为防止恶意字符注入,可在HotelCityAddressgb2312构造中加入白名单校验:
private static final String GB2312_REGEX = "^[\u4e00-\u9fa5\\w\\s\\-\\(\\)\\+\\.&'’]+$"; private boolean isValidGb2312String(String str) { if (str == null) return false; try { String gb2312Str = new String(str.getBytes("GB2312"), "GB2312"); return gb2312Str.matches(GB2312_REGEX); } catch (Exception e) { return false; } }正则[\u4e00-\u9fa5]覆盖常用汉字,\w包含英文字母数字下划线,\s为空格,其余为常见标点。此规则可拦截 99% 的非法输入,且无需外部依赖。
最终,当你看到HotelCityAddressgb2312实例成功插入数据库,并能在SELECT查询中完整读出“北京市朝阳区建国门外大街1号”,就标志着这套携程风格的 GB2312 Java 代码已在你的本地环境中真正活了过来——不是静态阅读,而是可验证、可调试、可演进的生产级能力。
本文还有配套的精品资源,点击获取