无框架也能写好 MVC:一个酒店管理系统的分层架构实践

10 0
谦豫渐有 发表于 2026-8-12 18:23:53 | 查看全部 阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
本文基于开源项目 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 源文件
54 个
Servlet
18 个(全部 @WebServlet 注解注册)
数据库表
9 张
前端框架
layui + Win10-UI 响应式框架
模块数
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- K
Web 容器
Servlet 3.1
@WebServlet 全注解路由,web.xml 仅 10 行
数据库
MySQL 5.7
9 张表,UTF-8 编码
连接池
C3P0 0.9.5.2
c3p0-config.xml 配置,自动回收空闲连接
JSON
Gson 2.2.4
Java 对象序列化,AJAX 接口数据交换
Excel
Apache POI 3.17
反射 + 泛型实现通用导出,无需为每模块单独编写
前端 UI
layui 2.x
table 模块的表格渲染 + form 模块的表单验证
前端主题
Win10-UI
模仿 Windows 10 桌面风格,提升视觉效果
通信
jQuery AJAX
前后端 JSON 交互,统一回调处理
* 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
  1. com.inks.hb/
    , C5 ~# M7 j4 Y0 w+ L" ]
  2. ├── login/              ← 登录模块- `- Y% u2 e2 p
  3. │   ├── pojo/Login.java  ]3 K4 L& Q% Y% `: T# H7 k
  4. │   ├── dao/LoginDao.java
    . K: F$ t  P( X) o' a/ ?5 A8 [
  5. │   ├── service/LoginService.java(接口)
    / F5 [8 R; d$ W; b- s
  6. │   ├── service/LoginServiceImpl.java(实现)' S% Q- N2 [) E
  7. │   └── controller/QueryLoginNameServlet.java
    $ S4 i/ f0 q4 O9 `; ~
  8. ├── orderinfo/          ← 预订单模块. k* A& a  K2 }" [$ H' |
  9. │   ├── pojo/OrderInfo.java + OrderToTable.java(DTO)- ~9 k! G& D6 y1 ^3 ?5 N+ z
  10. │   ├── dao/OrderInfoDao.java, h9 i# x  U  P
  11. │   ├── service/OrderInfoService.java + OrderInfoServiceImpl.java" ^6 k' n/ E# F9 M. @5 G
  12. │   └── controller/OrderInfoInsertServlet.java + ... (5 个 Servlet)
    " B0 w3 m+ f$ D! |+ \# k
  13. ├── roomtype/           ← 房间类型模块) f4 D) k# g, E: o  s
  14. ├── floorinfo/          ← 楼层模块
    8 R, Q7 O" ^! T6 `% T+ Q
  15. ├── authinfo/           ← 权限模块
    ' F' g/ ~5 K! E4 p9 R/ P; q
  16. ├── logInfo/            ← 操作日志模块, g5 `8 ^9 m8 W7 c
  17. ├── checkin/            ← 入住登记模块(已建表但未实现业务). o) o' I5 K, ~# J9 v
  18. ├── orderitem/          ← 订单项模块+ b! y9 @7 e1 w# t2 Y1 W1 V9 n
  19. └── common/             ← 公共基础设施
    8 f/ L9 `1 u- p; Q4 l* g, Y
  20.     ├── DBUtil.java            ← ThreadLocal 连接管理. u3 A& g: l. Q2 g: ?% P# d
  21.     ├── CommonDao.java         ← DAO 接口统一契约
    9 e$ {  D$ y" ~8 X/ t( V/ P1 h
  22.     ├── CommonFilter.java      ← 全局登录拦截器& c# G- b$ ?: ?5 i; T( j
  23.     ├── ExportExcel.java       ← 泛型反射导出器" L1 x- x- h/ R* l" ~% e
  24.     ├── PojotoGson.java        ← JSON 统一响应包装2 I/ d. ^* n8 |% J) d
  25.     └── DateJsonSerializer.java ← 日期序列化
复制代码
[td]
概念
hotelbook 手动实现
Spring Boot 自动实现
; w; q: S# S" l% l2 b4 n5 s3 ^
依赖注入
ServiceImpl 中手动 new DaoImpl()
@Autowired 自动注入
路由注册
每个 Servlet 加 @WebServlet
@RequestMapping 注解
JSON 序列化
PojotoGson 手动调用 Gson
@RestController 自动 Jackson
异常处理
每个 Servlet 内 try-catch
@ControllerAdvice 全局拦截
连接管理
DBUtil + ThreadLocal
HikariCP + @Transactional 自动管理
参数绑定
request.getParameter()
@RequestParam 自动类型转换
分页
手动 LIMIT offset, size
PageHelper 自动拦截 SQL
( 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
  1. public class DBUtil {, A) c3 `/ u' m
  2.     // ThreadLocal 确保每个线程拥有独立的连接8 h# i; ~3 n' i7 `
  3.     private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
    5 e( J9 Y; ]" ?8 i
  4.     private static ComboPooledDataSource dataSource;# m* X; a5 M/ q8 z& @' |* J
  5.     ; k( {6 l  A2 i
  6.     static {
    , ]  V4 {5 B5 M) A# ~* V
  7.         dataSource = new ComboPooledDataSource();  // 读取 c3p0-config.xml
    + T5 r) Q# e6 e' K; l
  8.     }
    7 j/ \, p% U5 K8 d5 j, B' ]0 Y
  9.     * i/ {* T( n5 r" {; ?1 P
  10.     public static Connection getConnection() throws SQLException {
    : [) U' ?0 e9 H4 q8 c1 j8 T, R
  11.         Connection conn = threadLocal.get();, ?" t+ c% _) g
  12.         if (conn == null || conn.isClosed()) {
    + R8 r+ j6 F1 h4 u2 J
  13.             conn = dataSource.getConnection();
    ! t% M0 T( K* y; C8 _/ }
  14.             threadLocal.set(conn);
    , X% X" }( Z, ]# r' S0 t& B7 j
  15.         }
    6 [+ [: c3 l% c7 o8 s
  16.         return conn;. e% d$ U3 R9 G* W4 z
  17.     }& X$ @$ k9 ~, B9 s
  18.    
    3 J( J+ w( l; y9 n$ e; Z+ `
  19.     // 在 Filter 结束时调用,释放连接
    ( V/ g# W. ^( m9 Z( @
  20.     public static void closeConnection() throws SQLException {" f9 c4 Z" t$ N& g
  21.         Connection conn = threadLocal.get();& U& Q8 O3 Z" X" u! j& |0 T
  22.         if (conn != null && !conn.isClosed()) {
    ) H$ D! G, `4 c) d0 Q# e( J- K
  23.             conn.close();* n0 Z  \0 I7 }$ W
  24.             threadLocal.remove();  // 防止内存泄漏
    ) o! y: b3 r0 ^; z5 ]: `1 p( ~, ]
  25.         }5 f7 h" y6 t0 h; \' ~- c
  26.     }
    $ ?9 I0 u$ B  Y: {. @
  27.     / J; i! t9 t" f: a; w
  28.     public static void beginTransaction() throws SQLException {1 D, u! p# N$ n% e
  29.         getConnection().setAutoCommit(false);2 c# d/ m6 p$ P) i+ W! B5 o0 p
  30.     }
    3 P4 }: B5 L/ [% o* }
  31.     ' M& l' ?$ E# L' O8 b5 S
  32.     public static void commitTransaction() throws SQLException {
    - w3 Z) v/ q5 Q" Y! q% V* o& m
  33.         Connection conn = threadLocal.get();, I; l1 I1 b: n# `, G
  34.         if (conn != null) conn.commit();
    , d/ R4 @5 E$ R/ U1 w. ?
  35.     }7 C' L& u$ {( D- F) L
  36.    
    / f; [1 ^* w  v2 @  J2 `9 K: U
  37.     public static void rollbackTransaction() throws SQLException {+ I! c+ T5 j( M) T9 ]
  38.         Connection conn = threadLocal.get();
    : r; p, B. X6 F+ I, ?1 ]: q* U
  39.         if (conn != null) conn.rollback();
    1 Y* R! P1 ^) }) `# l
  40.     }5 u0 c7 \! ^6 j# _
  41. }  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
  1. @WebFilter("/*")
    ) F3 Z1 n: V" c' y1 S
  2. public class CommonFilter implements Filter {
    0 V. F" z8 j# a
  3.     @Override
    8 E, B9 ^/ L5 `- }6 n2 Z! l
  4.     public void doFilter(ServletRequest req, ServletResponse resp, 1 ~/ g/ S8 M# O0 u0 x# u7 A
  5.                          FilterChain chain) {
    % M2 J# T2 ~- a3 s7 w4 b
  6.         HttpServletRequest request = (HttpServletRequest) req;# Y7 V( P4 K, `7 P  {  I
  7.         HttpServletResponse response = (HttpServletResponse) resp;
    4 L: d: h* t: d1 d5 o! d
  8.         
    . A8 L& J. g" p5 k5 G
  9.         // 1. 设置编码
    - ], |  a# h  O( h$ @
  10.         request.setCharacterEncoding("UTF-8");3 y* `+ @2 n8 {- b/ e! f2 \
  11.         response.setContentType("application/json;charset=UTF-8");
    ' ~8 _6 e. ]; j0 _8 e
  12.         6 b# c% e# p9 Z+ X  A! {
  13.         // 2. 登录拦截:放行登录/注册请求和静态资源/ x. z0 {( O4 O% N
  14.         String uri = request.getRequestURI();& e) h; V* q3 V
  15.         if (uri.contains("QueryLoginName") || uri.contains("login.html")
    0 U2 M$ T; O5 V1 P  j
  16.             || uri.endsWith(".css") || uri.endsWith(".js")) {& {2 p# r- \* k/ W& N! [) l
  17.             chain.doFilter(req, resp);
    , x8 T$ g6 z3 U( |+ s
  18.             return;$ P' r% U8 J. M) T; \
  19.         }8 i5 O, c6 I3 j  f
  20.         
    ( C* d5 B- l5 `' ^# c5 G7 O4 u
  21.         // 3. 检查 session6 R7 B0 N1 c1 `+ F" \$ @8 y* \
  22.         Object loginName = request.getSession().getAttribute("loginName");
    " ^: q; X3 p: T- R, L8 s
  23.         if (loginName == null) {/ r7 ]& `' |; g. U/ q% }
  24.             response.getWriter().write("{"code":401,"msg":"未登录"}");
    & F- _0 L! m5 _2 J, E. Z5 W* @
  25.             return;8 Q7 ]  e7 A) P; B/ ]  D& g; l
  26.         }
    5 t4 l+ H7 \% Y$ e$ B% I
  27.         % \  I9 j+ _  I& J  l+ D
  28.         // 4. 放行并确保连接释放
    8 a# [* i5 T9 S; K( y8 E
  29.         try {
    8 m1 n. S( l( T4 a; e
  30.             chain.doFilter(req, resp);
    & c+ z* W1 V5 S4 q
  31.         } finally {% G$ q5 w6 X: |
  32.             DBUtil.closeConnection();  // 关键:确保连接一定被释放) M' k/ X8 o% o! @1 _
  33.         }
    $ c& N* b4 G- w, f9 o! y
  34.     }7 b% v: |' T& O/ C& N$ t
  35. }
复制代码
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
  1. public class PojotoGson {
    % v. J& b8 W+ |0 u/ U) c+ f
  2.     private static Gson gson = new GsonBuilder()  D( [) t% b% c) x% b3 ]
  3.         .setDateFormat("yyyy-MM-dd HH:mm:ss")
    % l6 `( {  i' w' W
  4.         .create();0 @% l7 d6 F1 g7 l. d$ B
  5.    
    - R7 z7 s0 j: [* j8 W. F0 o
  6.     // 成功响应) f; I" Z, [* R# g
  7.     public static void success(HttpServletResponse resp, Object data) {
    " ^# K2 p) G, I9 X8 V2 v. {
  8.         Map<String, Object> result = new HashMap<>();
    & q! r. z2 E( _* ~2 C, h* R( Y
  9.         result.put("code", 0);     // layui 表格要求 code=0 为成功4 x5 d# v: w0 y) a, ?" b: U5 R8 S
  10.         result.put("msg", "");3 F& _& N" W' c# W9 b& z/ L; K
  11.         result.put("count", getCount(data));7 T1 _2 [- G$ e& l+ ?; @9 r
  12.         result.put("data", data);
    4 s1 n, R3 D" q3 U' [9 b, u& Q
  13.         write(resp, result);
    ( r) F1 r/ E/ b6 |( ]% U
  14.     }* _2 |2 z  |. ]* Z' U4 C
  15.    
    + v" K; N: m: ^; I9 d
  16.     // 失败响应
    4 [# g6 O8 N2 L: m5 C
  17.     public static void fail(HttpServletResponse resp, String msg) {
    7 r% K5 t8 u7 o: z9 i& X
  18.         Map<String, Object> result = new HashMap<>();: U# u! N/ Q$ j. d! U
  19.         result.put("code", 1);0 `' u8 e" S, b: o1 Z# ^  K
  20.         result.put("msg", msg);. \$ c0 h7 L+ O# _) a
  21.         write(resp, result);, z: h# w9 E/ n- ], i
  22.     }
    5 Z$ b+ r4 c& c
  23.    
    9 o2 x' N  T# e/ |4 w: w
  24.     private static void write(HttpServletResponse resp, Object data) {) x/ K* u* P2 w; h/ P# u
  25.         resp.getWriter().write(gson.toJson(data));
    4 `2 }' J$ k8 e2 C5 u
  26.     }; y$ S. E- h# [; H+ K, S% I
  27. }
复制代码
这里有一个值得注意的细节: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
  1. public class ExportExcel<T> {
    2 p* @9 |" S% D$ V% K8 f1 F
  2.    
    / _% R2 e0 I' G- x
  3.     public void export(String title, String[] headers, ' N5 q( I* @# H3 V) U
  4.                        List<T> dataList, HttpServletResponse response) {% c8 r( E* i9 \, C
  5.         // 1. 创建 XSSFWorkbook(.xlsx 格式). K( y; O% m& B# M% ]* z
  6.         XSSFWorkbook workbook = new XSSFWorkbook();& `+ ~8 o' P) _% r
  7.         XSSFSheet sheet = workbook.createSheet(title);% ^  r7 p% V, |8 P# ^) f
  8.         
    / w2 ~' ~/ r1 }9 L" l' L+ s" K$ q/ `
  9.         // 2. 写表头( _% V- Y, K! m4 W
  10.         XSSFRow headerRow = sheet.createRow(0);
    - p( h" w3 L" r5 |; K! F
  11.         for (int i = 0; i < headers.length; i++) {0 l2 W$ k& _8 V9 x/ ^$ f5 J
  12.             headerRow.createCell(i).setCellValue(headers[i]);* }$ R' }: @$ ^
  13.         }
    ' ~7 D4 F8 g" f: g# O: N
  14.         
    8 b$ _7 \7 w+ J
  15.         // 3. 通过反射获取 POJO 的所有字段* N# A4 e  Z% h! U, h
  16.         Field[] fields = dataList.get(0).getClass().getDeclaredFields();6 Z9 b8 j3 v7 M9 k- |
  17.         
    3 L+ B0 d4 M/ E4 t! y: M1 b& V6 O
  18.         // 4. 逐行写入数据
    5 ]* Y( G" \  E
  19.         for (int i = 0; i < dataList.size(); i++) {# Y+ @' H5 e7 m7 O3 m/ y' D. ~
  20.             XSSFRow row = sheet.createRow(i + 1);- I; j# h3 j, w& z8 S* O* a8 y
  21.             T item = dataList.get(i);
    5 v3 `; ^" }% R6 p
  22.             % ?1 U/ r2 u0 q$ v
  23.             for (int j = 0; j < fields.length; j++) {( q: L; V9 N$ W. D' y" z
  24.                 fields[j].setAccessible(true);  // 突破 private 限制
    5 f1 ]) }2 k9 }
  25.                 Object value = fields[j].get(item);$ e9 s  M# E: y) }; [
  26.                 row.createCell(j).setCellValue(
    9 o* O: i! E7 x) ]
  27.                     value == null ? "" : value.toString());
    + Q5 Z& T8 d+ i, S& Z" d6 Q
  28.             }
    # m& \: h: j& `7 o5 ]. X
  29.         }
    6 A! v2 c0 p9 q" O6 A2 W
  30.         0 b& h# K, ~$ N8 X; M$ A
  31.         // 5. 输出到 response
    ' X$ z, ?; w$ U* W  m
  32.         response.setContentType("application/vnd.openxmlformats-"! g0 m# j& N" g
  33.             + "officedocument.spreadsheetml.sheet");" J; D" ]- D( W3 q7 y: u
  34.         response.setHeader("Content-Disposition",
    , j" }3 s/ |& v. n$ V
  35.             "attachment;filename=" + URLEncoder.encode(title + ".xlsx", "UTF-8"));
    + N! }$ z. O) v3 b1 x
  36.         workbook.write(response.getOutputStream());8 w; w2 ~. O# q) K- n  ^
  37.     }* K" [( ~- [4 H# W" D# w. H" V" d
  38. }; 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
  1. // OrderInfo 中持有完整对象引用,而非外键字符串
    5 ^. T/ }, ~' |2 f, g
  2. public class OrderInfo {
    7 W. W+ o& t4 E* ]
  3.     private String orderId;
    ' F! Y+ \7 W7 ], h- L& k2 {' z! ]
  4.     private String orderName;
    ) K/ Y( [- E. I$ K2 n! b2 v
  5.     private String orderPhone;
    0 Z6 L* R$ z& g5 o7 c8 I
  6.     private RoomType typeId;      // ← 持有 RoomType 对象
    + x0 z9 H5 o' t2 e9 X# }2 z/ a' p
  7.     private Login operatorId;     // ← 持有 Login 对象# G; R! z! A2 U2 q! M
  8.     private Date createDate;( l9 n, q7 U; v( G8 }- N- _2 J
  9.     private int orderState;) Z8 A, ?0 F; i
  10.     private String checkinName;8 T6 S6 B; p; I& l" ]- ~6 a3 \2 B4 Z
  11. }- ~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
  1. // OrderInfoDao.getOrderInfo() 伪代码
    + }6 i1 `4 T' l- V% F$ d( D
  2. OrderInfo order = new OrderInfo();
    5 }; T0 X8 D; h  J9 x2 ~
  3. order.setOrderId(rs.getString("orderId"));# u6 j' Y. v3 ?6 K2 {
  4. // 构造关联对象
    5 {& }. k1 P+ E0 V  Z* T
  5. RoomType type = new RoomType();$ `8 h' ], B: H. n% W# M) y0 J
  6. type.setTypeId(rs.getString("typeId"));
    . U3 }+ `! c; h0 Z) @# }) O
  7. type.setTypeName(rs.getString("typeName"));5 |. n' d+ X* X$ J. k" g7 {. f
  8. 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, `
  1. // DTO:将嵌套对象展平为纯字符串
    : P: ~9 E; z) B8 z/ u+ g$ v  ?) _
  2. public class OrderToTable {
    * b8 X+ K% D' |7 k6 U
  3.     private String orderId;
    # C8 f( W) c8 [% M8 {& m- w
  4.     private String orderName;9 Q: m6 i" Q! b& o# ^1 P
  5.     private String typeId;       // 从 RoomType 对象提取 typeId 字符串
    & g& L3 p6 q: p7 h1 i! g" S& c
  6.     private String typeName;     // 从 RoomType 对象提取 typeName 字符串
    ! v% V: k5 K& ]$ L: J1 N% N2 ?2 M
  7.     private String operatorName; // 从 Login 对象提取 loginName 字符串9 u" P) K& G+ B
  8.     // ...& |, b" R- B) i) W! v8 f* t  e
  9. }" h: }% v+ y- r4 G5 @
复制代码
在 Service 层做转换:6 Y. k4 P! J6 J; X7 g0 O1 ^) A
java
1 s/ ~9 C: [! p3 f" E2 ]
  1. OrderToTable dto = new OrderToTable();: z, u( d: u* P
  2. dto.setOrderId(order.getOrderId());
    $ p- b7 Q# k8 p- [
  3. dto.setTypeName(order.getTypeId().getTypeName());        // ← 展平嵌套对象3 p" q7 M) E! ^! n. O( K7 E' _( O
  4. 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 加盐而非哈希
改用 BCrypt,Base64 可逆解码不安全
盐值硬编码在 login.jsp 中
提取到配置文件或环境变量
无全局事务管理
DBUtil 已提供 begin/commit/rollback,需要 Service 层包装调用
全量加载再内存搜索
房间类型查询改为 SQL 分页;未来可引入 Elasticsearch
checkInInfo / billInfo 仅建表未编码
补充入住登记、消费记录、结账核心流程
异常处理粗放
部分 catch 块仅为 e.printStackTrace(),应引入统一异常处理
无 API 文档
Swagger 注解生成接口文档
$ [- 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 `& ]& \

hotelbook-JavaWeb-master.zip

24.03 MB

回复 转播

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

学研领航向全体高校师生打造的一站式综合交流与资源服务平台,集知识学习、经验分享、资源下载、互动问答、职场成长、兼职实践于一体,覆盖校园生活、专业学习、求职就业、兴趣发展等全场景需求。

快捷导航

小黑屋
Copyright © 2026 学研领航 版权所有 陕ICP备2025077879号-1
关灯 在本版发帖 返回顶部
快速回复 返回顶部 返回列表