马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
本文基于开源项目 hotelbook-JavaWeb,深入解析在不使用 Spring 框架的情况下,如何用纯 Servlet + JDBC + C3P0 构建一个分层严格、模块清晰的酒店预订管理系统。从 ThreadLocal 连接管理到泛型反射 Excel 导出器,从 POJO 关联建模到 DTO 转换模式,完整展示"框架帮你省掉了什么"。附完整源码包。3 S2 ~( W8 f# Z/ t/ J
一、前言5 m. i2 p- ]5 ~0 B/ @" r9 O
. Z6 c$ k w: V& T8 M0 ?* b
很多 Java 开发者从入门第一天就被 Spring 包围——Spring MVC 做路由、Spring DI 做注入、Spring TX 做事务、Spring Data 做持久化。以至于有人会产生一种错觉:没有 Spring 就写不好 Java Web。
+ _1 _- Z8 p0 G6 u4 }hotelbook-JavaWeb 恰恰是一个"无 Spring 也能写出好代码"的证明。作者是 2017 年还在学校的学生,在没有使用任何 DI 框架的情况下,用纯 Servlet 3.1 + JDBC + C3P0 连接池构建了一个分层严格、模块清晰的酒店管理系统。项目包含 54 个 Java 源文件、18 个 Servlet、9 张数据库表,实现了登录认证、预订单管理、房间类型管理、权限控制、操作日志、通用 Excel 导出等完整功能。
3 I* `+ M& E- i本文将从分层架构、基础设施、数据建模、工具类设计四个维度,带你深度剖析这个项目的设计思想和工程实践。' ?2 F0 m2 e, b# k" Q
二、项目概览与技术栈
9 b5 K5 m% s7 ]5 P7 M6 H
' l$ r% s8 A" l" Y U" k: n2.1 项目规模
/ j5 R2 r. V: H0 M$ o- I
) e2 x6 x" @$ @$ \ K3 E[td]5 f4 G. p$ b a3 K
Java 源文件 | | Servlet | 18 个(全部 @WebServlet 注解注册) | 数据库表 | | 前端框架 | | 模块数 | 8 个业务模块(login / orderinfo / roomtype / floorinfo / authinfo / logInfo / checkin / orderitem) | 公共组件 | common 包含 DBUtil / CommonDao / CommonFilter / ExportExcel / PojotoGson | ; q( o. F9 Z$ b7 G4 q
2.2 技术栈
0 k% x3 V4 c$ i7 _9 V& M; B P/ @+ R& q2 L: X# ]( N
[td]
. ~7 C3 |+ ]6 V a- KWeb 容器 | Servlet 3.1 | @WebServlet 全注解路由,web.xml 仅 10 行 | 数据库 | MySQL 5.7 | | 连接池 | C3P0 0.9.5.2 | c3p0-config.xml 配置,自动回收空闲连接 | JSON | Gson 2.2.4 | | Excel | Apache POI 3.17 | | 前端 UI | layui 2.x | table 模块的表格渲染 + form 模块的表单验证 | 前端主题 | Win10-UI | 模仿 Windows 10 桌面风格,提升视觉效果 | 通信 | jQuery AJAX | | * h' P9 E* |. l4 l
三、无 Spring 的分层架构
0 Y- ~! m1 m% O5 c' @ G* S& B
* T$ u2 W4 A. a6 D- g& }, P3.1 模块化设计2 ]" m# C3 D4 v9 c: j0 r
) w! K: ~4 J; b m7 y
- com.inks.hb/
, C5 ~# M7 j4 Y0 w+ L" ] - ├── login/ ← 登录模块- `- Y% u2 e2 p
- │ ├── pojo/Login.java ]3 K4 L& Q% Y% `: T# H7 k
- │ ├── dao/LoginDao.java
. K: F$ t P( X) o' a/ ?5 A8 [ - │ ├── service/LoginService.java(接口)
/ F5 [8 R; d$ W; b- s - │ ├── service/LoginServiceImpl.java(实现)' S% Q- N2 [) E
- │ └── controller/QueryLoginNameServlet.java
$ S4 i/ f0 q4 O9 `; ~ - ├── orderinfo/ ← 预订单模块. k* A& a K2 }" [$ H' |
- │ ├── pojo/OrderInfo.java + OrderToTable.java(DTO)- ~9 k! G& D6 y1 ^3 ?5 N+ z
- │ ├── dao/OrderInfoDao.java, h9 i# x U P
- │ ├── service/OrderInfoService.java + OrderInfoServiceImpl.java" ^6 k' n/ E# F9 M. @5 G
- │ └── controller/OrderInfoInsertServlet.java + ... (5 个 Servlet)
" B0 w3 m+ f$ D! |+ \# k - ├── roomtype/ ← 房间类型模块) f4 D) k# g, E: o s
- ├── floorinfo/ ← 楼层模块
8 R, Q7 O" ^! T6 `% T+ Q - ├── authinfo/ ← 权限模块
' F' g/ ~5 K! E4 p9 R/ P; q - ├── logInfo/ ← 操作日志模块, g5 `8 ^9 m8 W7 c
- ├── checkin/ ← 入住登记模块(已建表但未实现业务). o) o' I5 K, ~# J9 v
- ├── orderitem/ ← 订单项模块+ b! y9 @7 e1 w# t2 Y1 W1 V9 n
- └── common/ ← 公共基础设施
8 f/ L9 `1 u- p; Q4 l* g, Y - ├── DBUtil.java ← ThreadLocal 连接管理. u3 A& g: l. Q2 g: ?% P# d
- ├── CommonDao.java ← DAO 接口统一契约
9 e$ { D$ y" ~8 X/ t( V/ P1 h - ├── CommonFilter.java ← 全局登录拦截器& c# G- b$ ?: ?5 i; T( j
- ├── ExportExcel.java ← 泛型反射导出器" L1 x- x- h/ R* l" ~% e
- ├── PojotoGson.java ← JSON 统一响应包装2 I/ d. ^* n8 |% J) d
- └── DateJsonSerializer.java ← 日期序列化
复制代码 [td]; w; q: S# S" l% l2 b4 n5 s3 ^
依赖注入 | ServiceImpl 中手动 new DaoImpl() | | 路由注册 | 每个 Servlet 加 @WebServlet | | JSON 序列化 | PojotoGson 手动调用 Gson | @RestController 自动 Jackson | 异常处理 | 每个 Servlet 内 try-catch | | 连接管理 | DBUtil + ThreadLocal | HikariCP + @Transactional 自动管理 | 参数绑定 | request.getParameter() | | 分页 | 手动 LIMIT offset, size | | ( L6 n7 m% ?+ h) L5 M1 h; b3 m
理解了 hotelbook 的每一行手工代码,再看 Spring Boot 的注解,你就会真正明白"框架帮你省掉了什么"。$ _- v) z$ Q% v8 {" M3 l
四、基础设施详解3 j2 T. \# S" h, t) I
5 _8 m& k U% X8 p4.1 ThreadLocal 连接管理
& Z: A7 v+ f, [( [+ y! P
0 X" E% v$ [$ [; I% r" QDBUtil 是整个项目基础设施的基石:
' o$ h: M0 j# i- y9 v) q# rjava1 s6 ]8 U1 k( X+ l; u; a
- public class DBUtil {, A) c3 `/ u' m
- // ThreadLocal 确保每个线程拥有独立的连接8 h# i; ~3 n' i7 `
- private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
5 e( J9 Y; ]" ?8 i - private static ComboPooledDataSource dataSource;# m* X; a5 M/ q8 z& @' |* J
- ; k( {6 l A2 i
- static {
, ] V4 {5 B5 M) A# ~* V - dataSource = new ComboPooledDataSource(); // 读取 c3p0-config.xml
+ T5 r) Q# e6 e' K; l - }
7 j/ \, p% U5 K8 d5 j, B' ]0 Y - * i/ {* T( n5 r" {; ?1 P
- public static Connection getConnection() throws SQLException {
: [) U' ?0 e9 H4 q8 c1 j8 T, R - Connection conn = threadLocal.get();, ?" t+ c% _) g
- if (conn == null || conn.isClosed()) {
+ R8 r+ j6 F1 h4 u2 J - conn = dataSource.getConnection();
! t% M0 T( K* y; C8 _/ } - threadLocal.set(conn);
, X% X" }( Z, ]# r' S0 t& B7 j - }
6 [+ [: c3 l% c7 o8 s - return conn;. e% d$ U3 R9 G* W4 z
- }& X$ @$ k9 ~, B9 s
-
3 J( J+ w( l; y9 n$ e; Z+ ` - // 在 Filter 结束时调用,释放连接
( V/ g# W. ^( m9 Z( @ - public static void closeConnection() throws SQLException {" f9 c4 Z" t$ N& g
- Connection conn = threadLocal.get();& U& Q8 O3 Z" X" u! j& |0 T
- if (conn != null && !conn.isClosed()) {
) H$ D! G, `4 c) d0 Q# e( J- K - conn.close();* n0 Z \0 I7 }$ W
- threadLocal.remove(); // 防止内存泄漏
) o! y: b3 r0 ^; z5 ]: `1 p( ~, ] - }5 f7 h" y6 t0 h; \' ~- c
- }
$ ?9 I0 u$ B Y: {. @ - / J; i! t9 t" f: a; w
- public static void beginTransaction() throws SQLException {1 D, u! p# N$ n% e
- getConnection().setAutoCommit(false);2 c# d/ m6 p$ P) i+ W! B5 o0 p
- }
3 P4 }: B5 L/ [% o* } - ' M& l' ?$ E# L' O8 b5 S
- public static void commitTransaction() throws SQLException {
- w3 Z) v/ q5 Q" Y! q% V* o& m - Connection conn = threadLocal.get();, I; l1 I1 b: n# `, G
- if (conn != null) conn.commit();
, d/ R4 @5 E$ R/ U1 w. ? - }7 C' L& u$ {( D- F) L
-
/ f; [1 ^* w v2 @ J2 `9 K: U - public static void rollbackTransaction() throws SQLException {+ I! c+ T5 j( M) T9 ]
- Connection conn = threadLocal.get();
: r; p, B. X6 F+ I, ?1 ]: q* U - if (conn != null) conn.rollback();
1 Y* R! P1 ^) }) `# l - }5 u0 c7 \! ^6 j# _
- } o$ x/ v2 F" K0 R8 x
复制代码
, u: _* n8 t# ]/ p$ V1 [% }' e$ O为什么用 ThreadLocal?
. n! n3 T: c# W; j+ i: `" l, ]/ `每个 HTTP 请求由 Tomcat 线程池中的一个线程处理。如果不使用 ThreadLocal,多个线程共享同一个 Connection,会出现线程 A 提交了线程 B 的事务这种灾难性 bug。ThreadLocal 确保每个线程(即每个请求)拥有独立的 Connection 实例,天然线程安全。0 o' w& Z0 o9 |1 v% }* h7 Z$ Y. C
与 Spring @Transactional 的对比:) Q3 H+ P$ ^7 `8 z0 \& N9 j
Spring 的 @Transactional 底层也是通过 ThreadLocal<Connection> 绑定当前事务的连接。当业务方法被 @Transactional 标注时,Spring 的 TransactionInterceptor 会:8 ]5 M1 t' P- z3 E/ @
- 从 ThreadLocal 获取当前事务的 Connection
- 将 autoCommit 设为 false
- 执行业务方法
- 成功则 commit,异常则 rollback
- ThreadLocal.remove() 释放连接
% n/ @9 k/ ^: F* v* |* K hotelbook 的 DBUtil 已经实现了这个流程的前三步(get / begin / commit/rollback / close),只需要在 Service 层手动调用即可。理解了这个,@Transactional 就不再是魔法。, `9 t; P2 a- J/ n$ M
4.2 全局 Filter 拦截
' u* e: C$ _1 D7 x R: e9 K6 t Z: ]) r; I$ ?# t1 g
CommonFilter 实现了 Servlet 规范中标准的 Filter 接口,统一处理编码和登录拦截:2 M. g* \& ?% ]2 F9 M" _
java
) n+ @4 O6 M; Z% f& f! E- @WebFilter("/*")
) F3 Z1 n: V" c' y1 S - public class CommonFilter implements Filter {
0 V. F" z8 j# a - @Override
8 E, B9 ^/ L5 `- }6 n2 Z! l - public void doFilter(ServletRequest req, ServletResponse resp, 1 ~/ g/ S8 M# O0 u0 x# u7 A
- FilterChain chain) {
% M2 J# T2 ~- a3 s7 w4 b - HttpServletRequest request = (HttpServletRequest) req;# Y7 V( P4 K, `7 P { I
- HttpServletResponse response = (HttpServletResponse) resp;
4 L: d: h* t: d1 d5 o! d -
. A8 L& J. g" p5 k5 G - // 1. 设置编码
- ], | a# h O( h$ @ - request.setCharacterEncoding("UTF-8");3 y* `+ @2 n8 {- b/ e! f2 \
- response.setContentType("application/json;charset=UTF-8");
' ~8 _6 e. ]; j0 _8 e - 6 b# c% e# p9 Z+ X A! {
- // 2. 登录拦截:放行登录/注册请求和静态资源/ x. z0 {( O4 O% N
- String uri = request.getRequestURI();& e) h; V* q3 V
- if (uri.contains("QueryLoginName") || uri.contains("login.html")
0 U2 M$ T; O5 V1 P j - || uri.endsWith(".css") || uri.endsWith(".js")) {& {2 p# r- \* k/ W& N! [) l
- chain.doFilter(req, resp);
, x8 T$ g6 z3 U( |+ s - return;$ P' r% U8 J. M) T; \
- }8 i5 O, c6 I3 j f
-
( C* d5 B- l5 `' ^# c5 G7 O4 u - // 3. 检查 session6 R7 B0 N1 c1 `+ F" \$ @8 y* \
- Object loginName = request.getSession().getAttribute("loginName");
" ^: q; X3 p: T- R, L8 s - if (loginName == null) {/ r7 ]& `' |; g. U/ q% }
- response.getWriter().write("{"code":401,"msg":"未登录"}");
& F- _0 L! m5 _2 J, E. Z5 W* @ - return;8 Q7 ] e7 A) P; B/ ] D& g; l
- }
5 t4 l+ H7 \% Y$ e$ B% I - % \ I9 j+ _ I& J l+ D
- // 4. 放行并确保连接释放
8 a# [* i5 T9 S; K( y8 E - try {
8 m1 n. S( l( T4 a; e - chain.doFilter(req, resp);
& c+ z* W1 V5 S4 q - } finally {% G$ q5 w6 X: |
- DBUtil.closeConnection(); // 关键:确保连接一定被释放) M' k/ X8 o% o! @1 _
- }
$ c& N* b4 G- w, f9 o! y - }7 b% v: |' T& O/ C& N$ t
- }
复制代码 finally 块中的 DBUtil.closeConnection() 是点睛之笔——无论业务代码是否抛出异常,数据库连接都会被正确释放回连接池,避免连接泄漏。
% R! p# f) b3 r( [0 t4.3 统一响应格式
( n" C$ B" w3 D$ x# }4 j; q) T* D& j: s8 ^3 R* }
PojotoGson 封装了 Gson,提供统一的 JSON 响应构建:! y z1 M* {1 I- Q/ S' W- O6 {
java
6 V0 X1 F1 L. L: o4 N3 n8 G- public class PojotoGson {
% v. J& b8 W+ |0 u/ U) c+ f - private static Gson gson = new GsonBuilder() D( [) t% b% c) x% b3 ]
- .setDateFormat("yyyy-MM-dd HH:mm:ss")
% l6 `( { i' w' W - .create();0 @% l7 d6 F1 g7 l. d$ B
-
- R7 z7 s0 j: [* j8 W. F0 o - // 成功响应) f; I" Z, [* R# g
- public static void success(HttpServletResponse resp, Object data) {
" ^# K2 p) G, I9 X8 V2 v. { - Map<String, Object> result = new HashMap<>();
& q! r. z2 E( _* ~2 C, h* R( Y - result.put("code", 0); // layui 表格要求 code=0 为成功4 x5 d# v: w0 y) a, ?" b: U5 R8 S
- result.put("msg", "");3 F& _& N" W' c# W9 b& z/ L; K
- result.put("count", getCount(data));7 T1 _2 [- G$ e& l+ ?; @9 r
- result.put("data", data);
4 s1 n, R3 D" q3 U' [9 b, u& Q - write(resp, result);
( r) F1 r/ E/ b6 |( ]% U - }* _2 |2 z |. ]* Z' U4 C
-
+ v" K; N: m: ^; I9 d - // 失败响应
4 [# g6 O8 N2 L: m5 C - public static void fail(HttpServletResponse resp, String msg) {
7 r% K5 t8 u7 o: z9 i& X - Map<String, Object> result = new HashMap<>();: U# u! N/ Q$ j. d! U
- result.put("code", 1);0 `' u8 e" S, b: o1 Z# ^ K
- result.put("msg", msg);. \$ c0 h7 L+ O# _) a
- write(resp, result);, z: h# w9 E/ n- ], i
- }
5 Z$ b+ r4 c& c -
9 o2 x' N T# e/ |4 w: w - private static void write(HttpServletResponse resp, Object data) {) x/ K* u* P2 w; h/ P# u
- resp.getWriter().write(gson.toJson(data));
4 `2 }' J$ k8 e2 C5 u - }; y$ S. E- h# [; H+ K, S% I
- }
复制代码 这里有一个值得注意的细节:code=0 而非 200。这是因为前端 layui 的 table 模块固定检查 code 是否为 0 来判断请求成功。这个"框架约束驱动接口设计"的案例,在实际项目中非常常见。" w+ v+ Z& h. J9 T9 r* X- g
五、泛型反射 Excel 导出器
8 l5 A8 x! X. G* O" ^; R4 n3 [* N3 [5 z
ExportExcel<T> 是整个项目中最能体现 Java 反射机制实用价值的工具类:+ H0 A5 x, y- p
java, X) [. r( C# W3 H
- public class ExportExcel<T> {
2 p* @9 |" S% D$ V% K8 f1 F -
/ _% R2 e0 I' G- x - public void export(String title, String[] headers, ' N5 q( I* @# H3 V) U
- List<T> dataList, HttpServletResponse response) {% c8 r( E* i9 \, C
- // 1. 创建 XSSFWorkbook(.xlsx 格式). K( y; O% m& B# M% ]* z
- XSSFWorkbook workbook = new XSSFWorkbook();& `+ ~8 o' P) _% r
- XSSFSheet sheet = workbook.createSheet(title);% ^ r7 p% V, |8 P# ^) f
-
/ w2 ~' ~/ r1 }9 L" l' L+ s" K$ q/ ` - // 2. 写表头( _% V- Y, K! m4 W
- XSSFRow headerRow = sheet.createRow(0);
- p( h" w3 L" r5 |; K! F - for (int i = 0; i < headers.length; i++) {0 l2 W$ k& _8 V9 x/ ^$ f5 J
- headerRow.createCell(i).setCellValue(headers[i]);* }$ R' }: @$ ^
- }
' ~7 D4 F8 g" f: g# O: N -
8 b$ _7 \7 w+ J - // 3. 通过反射获取 POJO 的所有字段* N# A4 e Z% h! U, h
- Field[] fields = dataList.get(0).getClass().getDeclaredFields();6 Z9 b8 j3 v7 M9 k- |
-
3 L+ B0 d4 M/ E4 t! y: M1 b& V6 O - // 4. 逐行写入数据
5 ]* Y( G" \ E - for (int i = 0; i < dataList.size(); i++) {# Y+ @' H5 e7 m7 O3 m/ y' D. ~
- XSSFRow row = sheet.createRow(i + 1);- I; j# h3 j, w& z8 S* O* a8 y
- T item = dataList.get(i);
5 v3 `; ^" }% R6 p - % ?1 U/ r2 u0 q$ v
- for (int j = 0; j < fields.length; j++) {( q: L; V9 N$ W. D' y" z
- fields[j].setAccessible(true); // 突破 private 限制
5 f1 ]) }2 k9 } - Object value = fields[j].get(item);$ e9 s M# E: y) }; [
- row.createCell(j).setCellValue(
9 o* O: i! E7 x) ] - value == null ? "" : value.toString());
+ Q5 Z& T8 d+ i, S& Z" d6 Q - }
# m& \: h: j& `7 o5 ]. X - }
6 A! v2 c0 p9 q" O6 A2 W - 0 b& h# K, ~$ N8 X; M$ A
- // 5. 输出到 response
' X$ z, ?; w$ U* W m - response.setContentType("application/vnd.openxmlformats-"! g0 m# j& N" g
- + "officedocument.spreadsheetml.sheet");" J; D" ]- D( W3 q7 y: u
- response.setHeader("Content-Disposition",
, j" }3 s/ |& v. n$ V - "attachment;filename=" + URLEncoder.encode(title + ".xlsx", "UTF-8"));
+ N! }$ z. O) v3 b1 x - workbook.write(response.getOutputStream());8 w; w2 ~. O# q) K- n ^
- }* K" [( ~- [4 H# W" D# w. H" V" d
- }; n0 Q: k1 t7 E
复制代码 设计亮点:
5 _* q1 r% o2 y- s9 B- D- 泛型 <T>:接受任意 POJO 类型,六个业务模块共用一个导出器,无需为每个模块单独编写导出代码
- getDeclaredFields() + setAccessible(true):突破 private 封装,直接读取字段值——虽然破坏了封装性,但在工具类场景中是合理且高效的权衡
- 流式输出到 Response:不写临时文件,直接写入 response.getOutputStream(),用户体验更好
u$ ~( U- y# \- S) C 这个工具类本质上实现了 Spring 中 AbstractXlsxView 的核心功能——反射读取对象属性、格式化为 Excel 单元格、流式输出。+ x9 e! w( B& F7 X& q) N" J
六、POJO 关联建模与 DTO 转换; f% b6 Q u; E: i
' P [+ X% T, @) N0 u: ]/ p
预订单模块中有一个值得关注的 OOP 设计选择:
$ h9 L, Q! Y# e1 n/ c6 Zjava% T! k( x! |5 C+ M( @& |# T$ Z
- // OrderInfo 中持有完整对象引用,而非外键字符串
5 ^. T/ }, ~' |2 f, g - public class OrderInfo {
7 W. W+ o& t4 E* ] - private String orderId;
' F! Y+ \7 W7 ], h- L& k2 {' z! ] - private String orderName;
) K/ Y( [- E. I$ K2 n! b2 v - private String orderPhone;
0 Z6 L* R$ z& g5 o7 c8 I - private RoomType typeId; // ← 持有 RoomType 对象
+ x0 z9 H5 o' t2 e9 X# }2 z/ a' p - private Login operatorId; // ← 持有 Login 对象# G; R! z! A2 U2 q! M
- private Date createDate;( l9 n, q7 U; v( G8 }- N- _2 J
- private int orderState;) Z8 A, ?0 F; i
- private String checkinName;8 T6 S6 B; p; I& l" ]- ~6 a3 \2 B4 Z
- }- ~7 d4 N7 N7 y3 ^) I, M
复制代码 ; c* Q7 k, a9 n. P8 ]" [
DAO 在读取 ResultSet 时构造关联对象:
* u- u! V* K: K3 _/ `java" Z' i$ P5 _7 a |1 ~& u
- // OrderInfoDao.getOrderInfo() 伪代码
+ }6 i1 `4 T' l- V% F$ d( D - OrderInfo order = new OrderInfo();
5 }; T0 X8 D; h J9 x2 ~ - order.setOrderId(rs.getString("orderId"));# u6 j' Y. v3 ?6 K2 {
- // 构造关联对象
5 {& }. k1 P+ E0 V Z* T - RoomType type = new RoomType();$ `8 h' ], B: H. n% W# M) y0 J
- type.setTypeId(rs.getString("typeId"));
. U3 }+ `! c; h0 Z) @# }) O - type.setTypeName(rs.getString("typeName"));5 |. n' d+ X* X$ J. k" g7 {. f
- order.setTypeId(type);$ }5 ^- s [1 X" d
复制代码 这样设计的好处是 Service 层可以直接通过 order.getTypeId().getTypeName() 获取房型名称,无需再做一次额外的数据库查询。
5 f- S, D+ ~8 ]4 L" I- E. d3 l但问题来了:前端 layui 的 table 组件需要扁平数据——它不认识嵌套的 Java 对象。于是项目引入了 OrderToTable 作为 DTO(Data Transfer Object):
0 n6 t/ Z1 v; P* u ajava7 f' g. b7 A, `
- // DTO:将嵌套对象展平为纯字符串
: P: ~9 E; z) B8 z/ u+ g$ v ?) _ - public class OrderToTable {
* b8 X+ K% D' |7 k6 U - private String orderId;
# C8 f( W) c8 [% M8 {& m- w - private String orderName;9 Q: m6 i" Q! b& o# ^1 P
- private String typeId; // 从 RoomType 对象提取 typeId 字符串
& g& L3 p6 q: p7 h1 i! g" S& c - private String typeName; // 从 RoomType 对象提取 typeName 字符串
! v% V: k5 K& ]$ L: J1 N% N2 ?2 M - private String operatorName; // 从 Login 对象提取 loginName 字符串9 u" P) K& G+ B
- // ...& |, b" R- B) i) W! v8 f* t e
- }" h: }% v+ y- r4 G5 @
复制代码 在 Service 层做转换:6 Y. k4 P! J6 J; X7 g0 O1 ^) A
java
1 s/ ~9 C: [! p3 f" E2 ]- OrderToTable dto = new OrderToTable();: z, u( d: u* P
- dto.setOrderId(order.getOrderId());
$ p- b7 Q# k8 p- [ - dto.setTypeName(order.getTypeId().getTypeName()); // ← 展平嵌套对象3 p" q7 M) E! ^! n. O( K7 E' _( O
- dto.setOperatorName(order.getOperatorId().getLoginName()); // ← 展平嵌套对象1 C5 |0 D6 P* f
复制代码 . h1 Q4 f7 P- T3 S) l7 }
这就是手动实现的 DTO 模式,与今天 Spring 项目中的 Converter / MapStruct / ModelMapper 功能完全一致——只是 hotelbook 用了最朴素的方式手动完成。
7 b' z5 Y! R* Y2 K$ f6 d, h 七、不足与改进方向
9 I( J$ a& Q- A# W, x) i" o b/ j; q& U8 `( H& R- q
[td]1 |7 v2 k6 ~ X4 |- [. F; o6 B3 ~) M
日期/金额用 VARCHAR 存储 | 改用 DATETIME / DECIMAL(m,2) 类型,支持日期函数和金额运算 | 密码为 Base64 加盐而非哈希 | | 盐值硬编码在 login.jsp 中 | | 无全局事务管理 | DBUtil 已提供 begin/commit/rollback,需要 Service 层包装调用 | 全量加载再内存搜索 | 房间类型查询改为 SQL 分页;未来可引入 Elasticsearch | checkInInfo / billInfo 仅建表未编码 | | 异常处理粗放 | 部分 catch 块仅为 e.printStackTrace(),应引入统一异常处理 | 无 API 文档 | | $ [- D `0 H# t9 C4 S
八、总结
0 z z/ K9 a3 f) @
; y8 T u E8 j5 z0 _hotelbook-JavaWeb 的价值不在于它的业务完成度(核心入住流程缺失),而在于它在一个没有任何框架辅助的环境下,展示了分层架构应该怎么写、基础设施应该怎么搭、设计模式应该怎么用。2 g# V+ c/ K1 v3 `& ~" c0 `$ A
如果你正在学习 Spring Boot,建议你把 hotelbook 的以下部分与 Spring 框架做对照阅读:) K* }- T& h- W
- DBUtil ThreadLocal → @Transactional:理解事务连接的绑定和释放机制
- PojotoGson → @RestController + Jackson:理解 JSON 序列化的统一封装
- ExportExcel → AbstractXlsxView:理解反射 + 泛型在导出场景中的应用
- OrderInfo POJO + OrderToTable DTO → MapStruct Converter:理解数据模型的分层转换
- CommonFilter → HandlerInterceptor + OncePerRequestFilter:理解拦截器的生命周期0 ?* `. h* W% @3 Q6 Q' ^+ }
搞懂了这些手工实现,再看 Spring 的注解,你就不再是"会用",而是"理解为什么会这样设计"。6 `$ ^7 }* b9 u
$ ]; a. Q3 C2 `& ]& \
|