ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaWeb酒店管理系统实战:Servlet+JSP+MySQL全流程解析

JavaWeb酒店管理系统实战:Servlet+JSP+MySQL全流程解析 做了小半年的JavaWeb项目终于把一个完整的酒店管理系统从前到后捋了一遍。这套东西用的是javajspservletbootstrapjquerymysql的组合典型到不能再典型的JavaWeb教学案例同时也是很多学校毕业设计里反复出现的题目。你说它老也好说它基础也罢但真正把这个栈从头到尾跑通把业务逻辑和技术细节都理清楚你对JavaWeb的理解会扎实非常多。这篇文章我不打算像写说明书一样逐行贴代码而是把一个完整的酒店管理系统在设计和实现过程中最关键的决策、最容易被忽略的坑、以及怎么一步步把功能落地全部展开讲清楚。不管是正在做课设的大学生还是想系统补JavaWeb基础的自学者这篇文章都能给你一套可以直接参考的完整思路。1. 项目整体设计与技术选型思路1.1 为什么会选jspservlet这套老组合现在很多人一上来就是Spring Boot MyBatis Plus Vue确实方便但你让他解释一个HTTP请求从浏览器出发到数据库再返回浏览器的完整过程他可能说不清楚。jspservlet这套组合最大的价值不是先进而是透明。Servlet负责接收请求、调用业务逻辑、控制页面跳转JSP负责渲染动态页面MySQL负责数据持久化整个请求链路每一步都是显式的没有任何框架替你包办。做酒店管理系统这种业务复杂度适中的项目用这套组合反而能把每个环节都看得明明白白。再说毕业设计和课程设计的场景。不少学校对项目的要求是能跑起来、有完整业务逻辑、有数据库设计文档这套技术栈完全够用。而且从答辩角度讲你能够把Servlet生命周期、请求转发与重定向的区别、JDBC连接数据库的步骤这些基础概念讲清楚反而比背一堆Spring注解更能让老师认可。这一点实际到了找工作面试阶段也有用很多面试官就喜欢从底层问起你做过原生Servlet项目回答这类问题时明显有底气。1.2 为什么用BootstrapjQuery来做前端BootstrapjQuery这套前端方案看起来也不新但放在这个项目里非常合适。先说Bootstrap它提供了一套完整的CSS组件库栅格系统、导航栏、表格、表单、模态框、标签页这些后台管理系统最常用的UI元素全都封装好了。做酒店管理系统的后台界面无非就是左侧菜单栏、右侧内容区、顶部导航栏这种经典布局Bootstrap的栅格系统可以快速搞定响应式布局不需要你自己写太多CSS。再说jQuery它的选择器、事件绑定、AJAX方法都极其简洁尤其ajax那一块$.ajax、$.post、$.get三个方法能覆盖几乎所有需求。酒店管理系统里最常见的异步交互是选择房间类型后异步刷新可用房间列表和提交表单前做前端校验jQuery处理这类场景非常顺手。更重要的是这套前端方案不需要Node.js环境不需要webpack构建直接把CSS和JS文件丢到webapp目录下引入就行和JSP天然融合部署到Tomcat就不会有跨域和打包的问题。1.3 哪些功能模块是必须规划的酒店管理系统的核心业务场景是围绕房间和客人展开的。我从需求角度梳理了一遍最终确定的功能模块是这几块用户登录模块、房间类型管理、房间信息管理、客户入住登记、退房结算。如果你还有余力可以再加上预订管理和营业额统计。登录模块负责控制访问权限防止未登录用户直接访问后台房间类型管理用来维护大床房、标准间、豪华套房等分类房间信息管理则是具体到每个房间号的维护包括房间号、所属类型、价格、当前状态客户入住登记是最核心的业务需要选择空闲房间、登记客人信息、生成入住记录退房结算则根据入住时长和房价计算金额。这五个模块已经构成了一个完整的业务闭环。从技术角度讲它们正好覆盖了增删改查的全部操作并且包含一个非常关键的场景——入住登记时需要把房间状态从空闲改为已入住、同时生成一条入住记录这两个操作必须放在同一个事务里否则就会出现房间被重复分配的严重Bug。这是整个项目里最有技术含量的部分后面我会详细展开。2. 数据库设计酒店管理系统的地基2.1 核心表结构设计与字段说明数据库设计是这类管理系统最重要的一步表设计得不好后面写代码全是泪。我最终定下来五张表用户表t_user、房间类型表t_room_type、房间表t_room、入住信息表t_checkin、客户表t_customer。用户表比较简单就存管理员账号密码和姓名字段包括id、username、password、real_name、create_time。这里有一个容易忽略的点密码在数据库里绝对不能存明文至少要做一个MD5加密虽然这个项目的安全级别不用太高但这是一个必须养成的习惯。房间类型表t_room_type用来存类型名称和基础价格字段是id、type_name、base_price。把房间类型单独拆一张表而不是直接在房间表里写字符串是为了避免数据冗余。试想一下如果你在房间表里直接存大床房三个字哪天想统一改成豪华大床房你得更新好几条记录而且手一抖就可能漏掉一条。拆成类型表之后房间表只要存一个type_id就行了。房间表t_room是本项目的关键表字段包括id、room_no、type_id、floor、status。room_no是房间号比如101、202必须加唯一索引这是防止同一栋楼出现两个相同房间号的最底层保障。status字段用TINYINT类型存1代表空闲2代表已入住3代表维修中。为什么不用字符串因为状态值是固定枚举用数字占用空间小查询速度快程序里再做一个状态映射就可以显示成中文了。入住信息表t_checkin是业务流程里的核心表记录每次入住和退房的操作字段包括id、customer_id、room_id、check_in_time、check_out_time、total_amount、status。customer_id和room_id都是外键分别指向客户表和房间表。2.2 表关系与外键设计思路这几张表之间的关系很清晰房间类型表和房间表是一对多关系一个类型下面有多个房间客户表和入住信息表是一对多关系一个客户可以多次入住房间表和入住信息表也是一对多关系一个房间在不同时间会对应多条入住记录。客户表单独拆出来是为了一个场景——同一个客户可能多次来住如果每次入住都把客人姓名、身份证号、手机号重复录入一次数据冗余不说想统计哪些是回头客都不知道怎么查。单独的客户表可以让你通过身份证号来判断老客户入住时只要根据身份证号查询如果存在就直接关联已有客户ID。有一个细节值得展开说入住信息和房间的关系。从业务上看房间在某个时间点只有一个入住状态但入住记录表里会有多条同房间的历史记录。所以查询当前谁住在205房间不能简单地查t_checkin表而要加上status条件查status为在住的记录同时还要和t_room表的状态配合。这个逻辑如果没想清楚后面做退房的时候很容易出现房间已经退了但状态没更新的Bug。我实际测试的时候还发现一个很有意思的问题如果退房的时候不校验当前房间确实被这个客户住着直接更新状态就会出现把空闲房间退房的离谱操作所以退房必须带room_id和入住记录id两个条件去更新这点在写SQL时一定要仔细。2.3 建表SQL中容易被忽略的细节字段类型的选型里藏了很多经验。价格字段一定要用DECIMAL(10,2)绝对不能使用FLOAT或DOUBLE。浮点数在计算金钱时有精度丢失的问题比如0.1加0.2的结果并不是0.3这在退房结算的时候是致命的。DECIMAL是定点数按十进制存储做价格计算是安全的。另一个细节是时间字段入住时间一般用DATETIME类型如果需要自动记录当前时间可以直接在字段定义里加上DEFAULT CURRENT_TIMESTAMP插入数据的时候就不用再手动填当前时间了。外键约束这个问题我要特别提醒一下初学者为了展示我懂外键喜欢给所有关联字段都加FOREIGN KEY约束。实际做项目的时候我更倾向于不加数据库层面的物理外键而是通过程序逻辑维护关联关系。原因是物理外键会在插入、更新、删除时做额外的完整性检查性能有损耗而且后期做数据迁移或者批量导入数据时经常因为外键约束报错非常麻烦。程序里通过事务和业务校验来保证数据一致性比物理外键更灵活。当然索引是必须要加的外键关联字段、身份证号、房间号这些查询频繁的字段都要建索引。3. 后端实现ServletJDBC实战解析3.1 JDBC连接管理的演进从DriverManager到连接池很多初学JDBC的人写代码是这样的每次查询都Class.forName注册驱动然后DriverManager.getConnection获取连接用完再close。这套流程单看没问题但放在真实的Web项目里就是灾难。Tomcat是并发处理请求的每个用户点一个按钮后台就可能同时有好几个请求要查数据库。如果每个请求都重新建立物理连接数据库的连接数瞬间就会被耗尽而且建立连接本身是开销很大的操作TCP握手加认证来回要好几次性能会肉眼可见地变慢。我在这个项目里用的是Druid连接池阿里巴巴开源的功能强大而且中文文档全。连接池的思路说白了就是提前建好一批连接放着谁用谁取用完还回去。初始化的时候创建比如10个连接业务代码里通过dataSource.getConnection()获取用完调用close()实际上不是真的关闭物理连接而是把连接还回池子里复用。这样连接池里的连接始终是热的性能提升非常明显。配置Druid的核心代码很简单一个配置文件加几行Java代码就能用起来。// druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/hotel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot password123456 initialSize10 maxActive50 maxWait3000public class DBUtil { private static DataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader().getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败); } } public static Connection getConnection() { try { return dataSource.getConnection(); } catch (SQLException e) { throw new RuntimeException(获取数据库连接异常, e); } } }这里有个连接字符串的细节一定要说。url里的useUnicodetruecharacterEncodingutf8是保证中文能正确写入数据库的关键timezone参数是MySQL 8.x版本必需的因为从MySQL 8.0开始默认时区是UTC如果不指定serverTimezone你存进去的时间会和你本地时间相差8个小时排查起来极其崩溃。我第一次踩这个坑的时候明明数据库里显示的时间和实际时间差了8小时查了半天才发现是时区问题。3.2 Servlet的职责划分与路由设计整个系统的Servlet划分我是按业务模块来分的一个模块一个Servlet每个Servlet里根据action参数区分具体操作。这样做的好处是类数量不会太多同时每个类的职责又足够清晰。用户相关的就一个LoginServlet负责登录和退出房间类型相关的RoomTypeServlet负责类型列表的展示和增删改房间管理用RoomServlet负责房间的列表、新增、修改、删除和状态查询入住和退房放在CheckinServlet里这两个操作虽然业务上是一进一出但都属于房间与客户关联的模块放一起维护起来顺一些。有个更简洁的写法是只写一个统一的ControllerServlet用action参数分发到不同的方法所有HTTP请求都进同一个入口。这种写法最大的好处是web.xml或注解配置特别少但缺点是类会越来越庞大一个Servlet几百行甚至上千行维护起来很痛苦。我的建议是项目规模小就按我前面说的一个模块一个Servlet来既能保证每个类的行数在可控范围内代码结构又清晰答辩或者代码审查的时候讲起来也好讲。WebServlet(/room) public class RoomServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (list.equals(action)) { listRooms(request, response); } else if (toEdit.equals(action)) { toEditPage(request, response); } else if (delete.equals(action)) { deleteRoom(request, response); } } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (save.equals(action)) { saveRoom(request, response); } } }这里还有一个小知识点doGet和doPost为什么要分开处理或者都转到一个方法里。项目里我倾向于让doGet处理查询请求doPost处理新增修改请求这是符合HTTP语义的。但实际开发中很多前端表单提交用的是POST浏览器地址栏直接访问是GET所以有时候会出现页面能打开但提交按钮点了没反应的情况排查下来发现是Servlet里只重写了doGetdoPost没处理。最稳妥的做法是让其中一个方法调用另一个或者统一转发到一个execute方法里。3.3 DAO模式与三层架构的必要性因为用ServletJSP这套技术栈很多人写完Servlet直接把JDBC代码塞在Servlet里存取数据全在一个类里完成。小Demo确实可以这么做但放到酒店管理系统这种有多张表、多个业务模块的项目里这种写法过不了一周你就会后悔。我使用的是经典的三层架构Servlet层只负责接收请求参数、调用Service层、控制页面跳转Service层处理业务逻辑比如入住登记时校验房间是否空闲、计算退房金额DAO层封装对数据库的增删改查操作。这样做最大的好处是职责边界清楚出了问题第一个就知道去哪个类里查。DAO层还有一个细节值得说一下。我习惯把一个模块的查询方法先定义成接口然后写实现类。比如RoomDao是一个接口RoomDaoImpl是实现类。为什么多此一举因为面试的时候这可以说清楚面向接口编程的思想项目里也方便以后替换实现比如从JDBC换成MyBatis或者写一个Mock实现做单元测试。当然对于课程设计这个规模的项目你也可以不定义接口直接写实现类代码量会少一些但养成接口习惯对你以后写Spring项目有很大帮助Spring最核心的依赖注入就是建立在接口之上的。public class CheckinDaoImpl implements CheckinDao { Override public Checkin findByRoomAndStatus(Integer roomId, Integer status) { String sql select * from t_checkin where room_id ? and status ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, roomId); ps.setInt(2, status); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return mapToCheckin(rs); } } } catch (SQLException e) { throw new RuntimeException(查询入住记录失败, e); } return null; } }3.4 入住登记必须使用事务的经典场景这个项目里最值得演示事务处理的场景就是入住登记。整个入住操作包含三步根据客户信息获取或创建客户往t_checkin表插入一条入住记录把t_room表中对应房间的状态从空闲改成已入住。如果这三步中间任何一步出了问题而其他步骤已经执行成功就会出现严重的数据不一致。比如房间状态已经改成已入住但入住记录没插进去那这个房间就永远被神秘占用了前台也没法查出是谁住的。事务解决的思路是把这三步操作放进同一个数据库连接里先设置setAutoCommit(false)关闭自动提交三步全部执行成功后调用commit()提交任何一步抛异常就在catch里调用rollback()回滚。这样要么三步全部成功要么全部失败不可能出现中间状态。public void checkin(Checkin checkin, Integer roomId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 保存或获取客户 Customer customer customerDao.findOrCreateByPhone(conn, checkin.getCustomer()); // 2. 插入入住记录 checkin.setCustomerId(customer.getId()); checkinDao.insert(conn, checkin); // 3. 更新房间状态为已入住 roomDao.updateStatus(conn, roomId, 2); conn.commit(); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException(入住登记失败, e); } finally { if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }事务处理是区分会写增删改查和真正懂业务数据一致性的分水岭。面试时候如果问你多个表同时更新怎么保证一致性你能马上说出事务机制和回滚逻辑这绝对是加分项。做这个项目的时候我强烈建议把这段事务代码亲手敲一遍理解连接、提交、回滚三者之间的关系后面学Spring的Transactional注解就轻松多了。4. 前端实现JSP页面与BootstrapjQuery的配合4.1 JSP中的Java代码应该怎么写才不那么乱JSP的定位是视图层它的职责是展示数据而不是处理业务逻辑。很多老项目里JSP的页面头部全是% %循环加ifelse整个页面又臭又长维护起来特别遭罪。我在这个项目里做一个约定JSP页面里不写Java业务代码所有需要展示的数据都在Servlet里放进request域或者session域JSP里只用EL表达式${}和JSTL标签来渲染。比如房间列表页Servlet里查询完数据后这样设置request.setAttribute(roomList, roomList); request.getRequestDispatcher(/admin/room_list.jsp).forward(request, response);JSP页面里这样取数据table classtable table-bordered table-hover thead tr th房间号/th th类型/th th楼层/th th状态/th th价格/th th操作/th /tr /thead tbody c:forEach items${roomList} varroom tr td${room.roomNo}/td td${room.typeName}/td td${room.floor}/td td c:choose c:when test${room.status 1}span classlabel label-success空闲/span/c:when c:when test${room.status 2}span classlabel label-danger已入住/span/c:when c:otherwisespan classlabel label-warning维修中/span/c:otherwise /c:choose /td td${room.price}/td td c:if test${room.status 1} button classbtn btn-primary btn-sm onclickopenCheckinModal(${room.id})入住/button /c:if /td /tr /c:forEach /tbody /table要是没用JSTL这个表格你得这样写% for(...) { %...% } %不仅难看还容易在HTML标签里写错Java语法导致整个页面编译报错。用JSTL之后代码清爽非常多。这里提醒一下JSTL需要往WEB-INF/lib目录里放jstl.jar和standard.jar或者用Maven的话引入依赖很多同学忘了这一步导致页面报Can not find the tag library descriptor的错误看到这条报错要先想到JSTL包没引入。4.2 Bootstrap搭建后台框架的常用套路Bootstrap做后台管理界面的套路非常固定就是把页面分成三块顶部导航栏、左侧菜单栏、右侧内容区。顶部导航栏一般放系统标题、当前登录用户、退出登录按钮左侧菜单栏用Bootstrap的列表组或者折叠面板组件做导航右侧内容区用栅格系统放数据表格或者表单。这套布局在网上有大量模板可以直接参考不需要自己从头写CSS。我用的Bootstrap版本是3.x虽然Bootstrap 4和5有更新的特性但3.x的资料最多、兼容性最好在老的教程和教学文档里出现频率最高。这里有个实际经验BootCDN有时候加载慢万一你上课或者答辩的时候网络不好整个页面就会变成没有样式的纯HTML排版非常尴尬。最稳妥的做法是把Bootstrap的css和js文件下载到本地放进项目的webapp/css和webapp/js目录里通过相对路径引用。这样项目在没有任何外网的情况下也能正常显示而且部署到Tomcat后加载速度也更快。还有一个路径问题是新手很容易犯的JSP页面的静态资源引用路径要以项目的ContextPath开头不能写死成/bootstrap/css/bootstrap.min.css。比如项目部署名是hotel写死的路径在本地可能是对的但换一个部署名路径就全部404了。正确写法是在页面顶部先获取一下项目路径% String path request.getContextPath(); % link relstylesheet href%path%/css/bootstrap.min.css或者用EL表达式和JSTL配合的写法link href${pageContext.request.contextPath}/css/bootstrap.min.css relstylesheet4.3 jQuery实现异步交互的几个场景酒店管理系统里用jQuery做异步交互主要有三个场景。第一个是登录表单的提交校验点击登录按钮后先用jQuery校验用户名密码不为空再通过ajax提交到后台这样用户不用等整个页面刷新就知道结果。第二个是入住登记页面里的选择房间类型后异步刷新房间列表客人选了标准间页面立刻通过ajax查询当前空闲的标准间房间号展示在下拉框里这个交互体验比同步刷新整个页面好很多。第三个是退房的时候点击退房按钮弹一个确认框确认后ajax请求后台完成退房操作页面局部刷新房间列表就行了。function loadAvailableRooms(typeId) { $.ajax({ url: contextPath /room, type: GET, data: {action: available, typeId: typeId}, dataType: json, success: function (data) { var select $(#roomSelect); select.empty(); $.each(data, function (i, room) { select.append($(option).val(room.id).text(room.roomNo)); }); }, error: function () { alert(加载可用房间失败); } }); }ajax的dataType这里我直接定义成json这要求后台Servlet里返回的数据必须是JSON格式的字符串。Servlet里可以用手拼字符串的方式返回JSON比如{id:1,roomNo:101}但字段一多就特别容易出错。我推荐使用阿里巴巴的fastjson或者Gson这个工具库把Java对象直接序列化成JSON字符串代码简单还不会出错。Gson用法就是一行new Gson().toJson(roomList)返回的字符串前端直接解析。前后端交互时有一个很常见的问题Servlet返回的JSON里如果有中文前端有可能出现乱码。解决方法是在Servlet里设置响应编码response.setContentType(application/json;charsetUTF-8);这行代码必须每次返回JSON前都设置漏掉就会导致浏览器解析时使用了错误的编码。5. 完整实操从零搭建一个可运行的酒店管理系统5.1 开发环境准备与项目结构动手前先列一下我使用的环境版本避免版本不对导致各种奇怪的报错。JDK用的1.8版本不是因为它新而是因为市面上绝大多数教材和文档都是围绕JDK 8写的遇到问题搜解决方案的时候最方便。IDE我用的是Eclipse虽然IDEA更现代但Eclipse对JavaWeb项目的原生支持其实很省心新建Dynamic Web Project直接就是标准的Web目录结构。Tomcat用了8.5或者9.0版本都可以都支持Servlet 3.1规范用WebServlet注解不需要在web.xml里注册。MySQL用的8.0主要是因为新版还在更新安装包和文档都好找要注意8.0的JDBC驱动类名是com.mysql.cj.jdbc.Driver老版本是com.mysql.jdbc.Driver类名写错就报ClassNotFoundException。项目的目录结构遵循标准的Maven Web项目约定或者Eclipse Dynamic Web Project结构源代码和配置文件的划分要清晰。src/main/java放Java源码按包名分层管理我当时分包是com.hotel.servlet、com.hotel.service、com.hotel.dao、com.hotel.entity、com.hotel.util这几个。webapp目录下放JSP页面、css、js、images等静态资源。JSP页面建议再按功能细分目录比如admin目录放后台管理页面common目录放登录页和公共组件。这样分好之后文件多了也不会乱。整个项目运行的基本链路是这样的浏览器发出一个请求比如GET /hotel/room?actionlistTomcat根据WebServlet注解匹配到RoomServletServlet调用Service层方法Service调用DAO层方法DAO通过连接池获取连接执行SQL把结果封装成Java对象返回Servlet再把List设置到request属性里转发到room_list.jspJSP用JSTL循环输出整个表格。把这套链路默写出来你对JavaWeb项目就有一个完整的心智模型了。5.2 登录模块的完整实现链路登录模块虽然简单但它包含了JavaWeb最常见的知识点非常适合作为第一个动手功能。前端login.jsp页面里放一个表单表单里有username和password两个输入框以及一个提交按钮。按钮点击后用jQuery做非空校验校验通过后表单正常提交到LoginServlet的doPost方法。也可以用ajax提交返回结果体验更好我两版都写过建议至少先写同步提交版本理解了整个请求-响应过程之后再加ajax版本。LoginServlet里第一件事是设置请求编码否则从表单传过来的中文会乱码。然后通过request.getParameter获取用户名和密码调用UserService的login方法内部查询数据库把用户输入的密码做MD5加密后和数据库里存的密码比对。如果正确把用户对象存进session里response.sendRedirect重定向到主页同时设置session超时时间。如果失败通过request.setAttribute传一个错误提示转发回login.jsp页面显示。登录成功之后在主页上显示当前登录用户xxx这个从session里取值就行。有个细节很容易出错登录成功用sendRedirect重定向登录失败用forward转发。两者有本质区别重定向是浏览器重新发起一个新的请求地址栏URL会变适合登录成功后跳到一个新页面的语义同时可以避免刷新页面时表单重复提交。如果登录成功用forward页面上按F5刷新就会重复提交登录表单造成重复登录记录。这个知识点面试特别喜欢问你一定要彻底搞清楚。5.3 入住登记模块的前后端串联入住登记是整个系统里最具完整业务链感觉的功能。页面上的表单需要选择客户或者输入客户信息、选择房间类型、选择具体房间、填写入住天数然后提交。这里的交互设计是房间的可用列表要和房间类型联动也就是客人先选类型数据库里该类型的空闲房间列表就异步加载到房间下拉框里。后端CheckinServlet接收到入住请求后先做一个参数校验然后计算退房时间和预估费用再调Service层的checkin方法。Service方法里做一个关键校验根据roomId查询房间当前状态如果状态不为空闲直接抛业务异常提示该房间已被入住请刷新页面。这个校验非常重要因为可能出现两个前台同时操作的情况一个人刚提交了101房间的入住另一个还停留在旧页面上也提交同一房间如果不校验就会造成同一房间重复入住。业务异常和系统异常要区分处理业务异常的错误提示可以直接展示给用户系统异常则记录日志后统一跳转到错误页面。退房结算流程和入住是反向的。前台点击退房按钮输入房间号系统查出这个人住了多久根据房价乘天数计算出总金额展示确认。确认后在一个事务里完成两步把t_checkin记录的状态改成已退房并填上实际退房时间和总金额把t_room的房间状态改回空闲。结算金额的计算要注意头尾日期的处理比如当天中午入住第二天中午退房算一天精准到小时的计算逻辑要写清楚否则金额会有分歧。5.4 web.xml与部署配置的几个关键点虽然Servlet 3.0之后可以用注解不用在web.xml里一个一个配置Servlet了但web.xml里仍然有几个东西要设置好。首先是欢迎页welcome-file-list指定访问项目根路径时默认打开哪个页面一般配置成login.jsp或者一个跳转到登录页的servlet映射。不配置的话你输入http://localhost:8080/hotel会显示404或者目录结构体验很不好。其次是session超时时间的配置在web.xml里加一段session-config session-timeout30/session-timeout /session-config单位是分钟30分钟算是比较合理的值。登录状态存在session里如果用户长时间不操作session过30分钟后自动失效再次访问需要重新登录。如果不配置session超时时间Tomcat默认是30分钟大多数场景是够用的但了解这个配置可以让你在需要调整时知道去哪里改。还有编码过滤器。虽然每个Servlet里都设置了请求编码response编码但这种方式容易漏。更好的方案是写一个Filter给所有请求统一设置UTF-8编码再在web.xml里配置filter映射到/*。这也是JavaWeb里面Filter最经典的应用场景。你这套项目做下来对Filter的理解会比看十遍教材都深。6. 常见问题与排查技巧实录6.1 HTTP 404和500到底怎么快速定位404是资源找不到500是服务器内部错误。这两种报错在JavaWeb项目中几乎每天都会遇到但定位思路完全不同。404首先检查URL路径是否拼写正确包括大小写Servlet映射路径是区分大小写的再看Servlet类上是否有WebServlet注解注解路径是否和访问路径一致然后确认Tomcat是否成功部署了项目看控制台有没有deploy相关的日志有没有报错。如果用的是Eclipse或者IDEA部署还要检查项目是否被加入到了Tomcat的部署列表中。500错误最常见的两层原因一层是Java代码异常比如空指针NPE、SQL语法错误、ClassNotFoundException另一层是JSP页面编译失败比如页面里引用了不存在的属性、JSTL标签写错。排查500的第一步不是看代码而是打开浏览器的开发者工具看Network面板里这个请求的ResponseTomcat会把完整的异常堆栈信息返回给浏览器这里面会明确告诉你异常是哪一行抛出来的。如果你嫌浏览器显示不完整直接去Tomcat的logs目录看localhost.log和catalina.out日志文件最原始的堆栈都在那个文件中。看到堆栈信息后按图索骥找到代码对应行大多数问题都能在几分钟内定位到。6.2 中文乱码问题的系统性解决方案中文乱码是整个JavaWeb新手期最折磨人的问题没有之一。乱码产生的原因只有一个数据在不同环节之间传输时用了不同的编码。比如页面用UTF-8提交表单Tomcat默认按ISO-8859-1解码数据到Servlet里就已经是乱码了再比如数据库表字段是latin1编码你往里面存中文必然变成问号。我总结了一套三层编码统一的方案。第一层是Tomcat层面如果使用Tomcat 8.5及以上版本POST请求参数的编码默认已经是UTF-8但GET请求参数在server.xml的Connector上需要配置URIEncodingUTF-8。第二层是Servlet层面每个Servlet处理POST请求前都加request.setCharacterEncoding(UTF-8)或者像前面说的用一个全局Filter统一设置。第三层是数据库层面建库的时候指定utf8mb4字符集连接URL里带上characterEncodingutf8两者缺一不可。页面顶部的meta标签也要设置charsetUTF-8且JSP文件本身要保存成UTF-8编码。这三层都对了项目里基本不会再有乱码出现。这里有一个人容易忽略的点如果你用上面的三层方案都设置好了但是旧数据已经以错误编码写入数据库了你改完代码之后查出来的旧数据还是乱码。这种情况没办法靠编码配置修复只能重新导入数据或者在读取时做转码。所以数据库一开始就设置utf8mb4字符集特别重要中途改编码是很痛苦的。6.3 无法连接MySQL和处理时区报错刚把项目部署到Tomcat启动后运行登录功能控制台突然报一个类似The server time zone value UTC is unrecognized的错误。这个就是MySQL 8.0和JDBC驱动之间的时区兼容性问题。解决方式很简单连接URL里加上serverTimezoneAsia/Shanghai。如果还有人用老版本的mysql-connector-java驱动还要在驱动类名上注意MySQL 8.0对应的驱动类是com.mysql.cj.jdbc.Driver旧的com.mysql.jdbc.Driver在新版本驱动里可能已经被移除了。另外一个典型的连接数据库问题是在新电脑上搭建环境时经常遇到的MySQL服务没有启动或者密码输入错误或者MySQL的端口被占用。排查顺序是先用命令行或者MySQL Workbench确认能正常连接数据库排除数据库本身的问题再检查项目里druid.properties中的用户名密码和实际MySQL的一致最后看防火墙是否拦了3306端口。这套排查流程里最快的一步其实是把druid.properties里的配置和实际环境一条一条对比大多数时候问题就出在配置和实际环境不一致。6.4 前端静态资源加载和路径问题排查页面打开之后发现完全没有样式图片全部裂开这基本可以判断是静态资源路径错了。打开浏览器F12开发者工具切到Network面板刷新页面找到bootstrap.min.css这条请求看它的URL是不是404了。常见原因是页面里的引用路径少了项目上下文路径比如我前面说的写死了/css/bootstrap.min.css。解决办法是使用${pageContext.request.contextPath}动态拼接项目路径。还有一点是在JSP里用相对路径时要注意当前页面的位置。比如你访问的是/hotel/admin/room_list.jsp页面里一个相对路径是css/bootstrap.min.css那它实际请求的是/hotel/admin/css/bootstrap.min.css这个目录下当然没有文件。无论是绝对路径还是相对路径都要心里清楚最终拼出来的URL到底是什么。经验法则就是JSP页面里统一用开头是斜杠的绝对路径再配合contextPath动态前缀基本不会再踩路径坑。6.5 常见问题排查速查表问题现象可能原因解决方案访问Servlet返回404WebServlet路径写错或者项目未部署成功检查注解路径和访问URL查看Tomcat部署日志页面报500且堆栈有空指针异常查询结果是null但没有判空查看堆栈定位到具体行增加null判断中文表单提交后台乱码Servlet没设置请求编码统一增加request.setCharacterEncoding(UTF-8)数据库中文显示为问号表或字段字符集不是utf8mb4建库时指定utf8mb4修改已有表的字符集页面没有样式静态资源路径少了contextPath使用${pageContext.request.contextPath}拼接路径时间比本地时间差8小时MySQL时区和本地不一致连接URL添加serverTimezoneAsia/Shanghai刷新页面重复提交表单用了forward而非sendRedirect写成功后重定向到列表页JSP显示找不到JSTL标签缺少jstl.jar和standard.jar导入JSTL依赖到WEB-INF/lib目录7. 项目做完之后的复盘与扩展方向7.1 一个小细节登录状态拦截器的实现思路项目功能全部做完之后我发现在一个很小的安全点上可以进一步加深对Filter的理解。现在管理员登录后直接访问某个JSP页面比如在地址栏输入http://localhost:8080/hotel/admin/room_list.jsp是可以直接访问的因为JSP本身也是一种资源它不会自动判断用户有没有登录。解决这个问题的方式就是再增加一个登录拦截过滤器LoginFilter。在doFilter方法里先判断当前session里有没有用户信息如果没有就重定向到登录页拦截所有admin目录下的请求。写这个过滤器的时候你会深刻体会到什么是横切关注点这就是Spring Security和Shiro这类安全框架在Servlet层面的底层逻辑。7.2 如何从这套项目平滑过渡到主流框架做完这个jspservlet项目之后你已经亲手实现了Web开发的完整底层链路这时候去学Spring Boot会轻松非常多。你先回头看看项目里写的那些Servlet、DAO、Service你会发现Spring MVC做的事情就是把Servlet那一层封装掉了Controller就是ServletRequestMapping就是WebServlet映射DispatcherServlet就是统一入口的中央控制器。Spring Boot又解决了项目配置繁琐的问题把Tomcat嵌入进来连部署都不用单独做了。DAO那一层可以用MyBatis或者JPA替换连接池可以从Druid换成HikariCP但核心思路完全一致。所以这个老项目不是过时的东西而是你学习主流框架最好的铺垫。我自己的体会是先写一个原生的Servlet项目再学Spring Boot你会知道框架帮你做了什么也知道框架背后发生了什么出了问题不会一头雾水。如果你现在正卡在学完JavaSE和MySQL不知道下一步怎么走这个阶段我非常推荐就把这个酒店管理系统完整地敲一遍。不要只看文章一定要自己动手建项目、建表、写代码、部署、测试把每个功能都跑通。踩坑的过程就是你真正理解JavaWeb的过程。
返回列表