简介:这是一份面向Java初学者与Web开发入门者的实战项目源码,聚焦酒店在线预订业务场景,帮助学习者系统掌握Java Web全栈开发核心技能。资源包含41个文件,主体为34个Java类(涵盖Controller、Service、DAO及实体类),辅以4个properties配置文件(数据库与应用参数)、2个Markdown说明文档(含环境搭建与运行指南)和1个XML配置文件(如Spring或MyBatis配置),整体包体仅83KB,轻量易读,结构清晰便于逐模块分析。已有75人下载学习,适合用于课程设计、毕业实训或自学进阶。读者可完整获得基于MVC架构的可运行系统,深入理解Servlet/JSP请求流程、JDBC/MyBatis数据库操作、用户登录与订单管理等业务逻辑实现,并参考其中Spring基础集成、前端HTML+JS交互及基础安全控制实践,是夯实Java Web工程能力的典型小而全范例。
1. 酒店预订系统不是Demo:它真能跑通「用户查房→选房→下单→支付→后台审核」全链路,适合想补全Java Web工程闭环的新手和转岗开发者
上周帮某高校实验室带毕设,一个学生拿着“Java酒店预订系统”源码包发愁:“老师说这是实战项目,可我连登录页都打不开,报错说ClassNotFound,数据库连不上,连订单表字段都找不到在哪建的。”——这太典型了。很多人以为“Java+酒店预订”只是个带CRUD的Servlet练习,但这个项目实际封装了从用户会话管理、房间库存强一致性校验、订单状态机流转,到后台多角色权限隔离(前台用户/客房经理/财务审核员)的完整业务闭环。它不依赖Spring Boot自动配置黑盒,所有Servlet映射、Filter链、事务边界、JDBC连接池初始化都手写在web.xml和DAO层里,你能一眼看清request怎么进、response怎么出、异常在哪被捕获、事务在哪被提交或回滚。如果你正卡在“学完Servlet/JSP/MySQL却写不出一个能上线的最小可用系统”,或者想把培训班里零散的DAO/Service/Controller三层代码串成一条能跑通的线,这个项目就是你缺的那块拼图。它不炫技,不堆框架,但每行代码都在回答一个工程问题:用户点了“立即预订”,接下来237毫秒内,系统到底做了什么?
2. 环境准备与项目结构解析:从解压到Tomcat部署,5步确认核心模块真实存在
2.1 解压即见真章:目录结构暴露技术选型真相
拿到java+酒店预订系统.zip后,先别急着导入IDE。解压后直接看根目录,你会看到这些关键文件夹:
src/ ├── main/ │ ├── java/ │ │ └── com/example/hotel/ # 核心包名,非泛泛的"cn.xxx" │ │ ├── controller/ # Servlet入口,非Spring MVC @Controller │ │ ├── dao/ # JDBC直连实现,无MyBatis XML或注解 │ │ ├── model/ # POJO,字段名与数据库表列名严格一致(如room_id, not roomId) │ │ ├── service/ # 业务逻辑层,含库存扣减+订单生成原子操作 │ │ └── util/ # DBUtil(含Connection获取/关闭)、DateUtil等工具类 │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # 关键!Servlet注册、Filter链、欢迎页全在此 │ │ └── lib/ # 仅4个jar:servlet-api.jar、mysql-connector-java-5.1.47.jar、jstl-1.2.jar、standard-1.1.2.jar │ ├── index.jsp # 首页,含搜索框+热门酒店轮播 │ ├── login.jsp # 纯表单,action指向LoginServlet │ └── admin/ # 后台路径,需admin权限访问 └── resources/ └── db.properties # 数据库配置,明文写host/port/dbname/user/pass提示:注意
webapp/WEB-INF/lib/下只有4个jar包——这直接否定了“用了Spring/SpringMVC”的猜测。没有spring-context.jar、没有mybatis.jar,连log4j.jar都没有,日志靠System.out.println硬打。这是一个刻意回归Java Web原始生态的项目,目的就是让你看清容器启动时Servlet如何被加载、Filter如何拦截请求、JSP如何被编译成Servlet。
2.2 数据库脚本:不是空表结构,而是带测试数据的真实业务场景
项目没提供SQL文件?别慌。打开src/main/resources/db.properties,记下数据库名(通常是hotel_db),然后去src/main/java/com/example/hotel/util/DBUtil.java找初始化逻辑:
// DBUtil.java 片段 public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/" + PropertiesUtil.getProperty("db.name") + "?useSSL=false&serverTimezone=UTC"; private static final String USER = PropertiesUtil.getProperty("db.user"); private static final String PASSWORD = PropertiesUtil.getProperty("db.pass"); // 关键:静态块中执行建表+初始化 static { try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); Statement stmt = conn.createStatement()) { // 创建酒店表 stmt.execute("CREATE TABLE IF NOT EXISTS hotel (" + "id INT PRIMARY KEY AUTO_INCREMENT," + "name VARCHAR(100) NOT NULL," + "address VARCHAR(200)," + "phone VARCHAR(20)" + ")"); // 创建房间表(含库存字段) stmt.execute("CREATE TABLE IF NOT EXISTS room (" + "id INT PRIMARY KEY AUTO_INCREMENT," + "hotel_id INT NOT NULL," + "type VARCHAR(50) NOT NULL," + // 标准间/豪华套房 "price DECIMAL(10,2) NOT NULL," + "available_count INT DEFAULT 0," + // 关键!库存数,非布尔值 "FOREIGN KEY (hotel_id) REFERENCES hotel(id)" + ")"); // 插入3家测试酒店+12间房(含不同房型/价格/库存) stmt.execute("INSERT INTO hotel (name, address, phone) VALUES " + "('西湖花园酒店', '杭州市西湖区文三路1号', '0571-88889999')," + "('南山商务酒店', '杭州市上城区南山路55号', '0571-87776666')," + "('钱江新城国际', '杭州市江干区富春路88号', '0571-86665555')"); // ... 后续插入room数据,此处省略 } catch (SQLException e) { e.printStackTrace(); // 开发期容忍,上线必须改 } } }这段代码在类加载时自动建库建表插数据——这就是为什么你第一次启动Tomcat时,控制台会刷一堆INSERT INTO日志。它不依赖外部SQL脚本,把环境初始化逻辑内聚在代码里,降低新手启动门槛。但代价是:无法用Navicat直接看到建表语句。你要想改表结构,必须去DBUtil.java里修stmt.execute()里的SQL字符串。
2.3 Tomcat部署四步法:绕过IDE自动部署,亲手验证容器行为
很多新手失败,是因为依赖IDE(如IntelliJ)的“Run on Server”功能,结果报错却不知错在哪一层。我们手动走一遍:
确认Tomcat版本:该项目基于Servlet 3.0规范,要求Tomcat 7.0+(推荐Tomcat 8.5)。检查
web.xml头:<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_0.xsd" version="3.0">复制war包:用命令行进入项目根目录,执行:
# Linux/Mac zip -r hotel.war . -x "*.git*" "target/*" "README.md" # Windows(PowerShell) Compress-Archive -Path .\* -DestinationPath .\hotel.war -Force注意:必须排除
.git和target目录,否则war包会包含编译产物,导致Tomcat加载冲突。部署到Tomcat:将
hotel.war复制到$TOMCAT_HOME/webapps/目录下,启动Tomcat:$TOMCAT_HOME/bin/startup.sh # Linux/Mac $TOMCAT_HOME/bin/startup.bat # Windows观察
$TOMCAT_HOME/logs/catalina.out,你会看到:INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [/opt/tomcat/webapps/hotel.war] has finished in [2,341] ms INFO [main] com.example.hotel.util.DBUtil - 初始化数据库完成,插入酒店3家,房间12间验证访问路径:浏览器打开
http://localhost:8080/hotel/(注意是/hotel/,不是/hotel或/)。如果看到首页轮播图和搜索框,说明部署成功;若404,请检查war包解压后$TOMCAT_HOME/webapps/hotel/目录是否存在,且index.jsp是否在/hotel/下。
3. 核心模块拆解:从用户登录到订单生成,逐层剖析Servlet+JDBC的落地细节
3.1 登录认证:Filter拦截+Session绑定,拒绝“裸奔”请求
登录不是简单比对密码。项目用LoginServlet处理表单,但真正的权限控制由AuthFilter实现:
// src/main/java/com/example/hotel/filter/AuthFilter.java public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 放行登录页、静态资源、登录接口 String uri = req.getRequestURI(); if (uri.endsWith("/login.jsp") || uri.endsWith("/LoginServlet") || uri.matches(".*/static/.*") || uri.endsWith("/favicon.ico")) { chain.doFilter(request, response); return; } // 检查Session中是否有user对象 HttpSession session = req.getSession(false); if (session == null || session.getAttribute("user") == null) { // 未登录,重定向到登录页,并携带原请求URL作为参数 String originalUrl = req.getRequestURL().toString(); if (req.getQueryString() != null) { originalUrl += "?" + req.getQueryString(); } resp.sendRedirect(req.getContextPath() + "/login.jsp?redirect=" + URLEncoder.encode(originalUrl, "UTF-8")); return; } chain.doFilter(request, response); // 已登录,放行 } }web.xml中注册该Filter:
<filter> <filter-name>AuthFilter</filter-name> <filter-class>com.example.hotel.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 全局拦截 --> </filter-mapping>逻辑说明:Filter在请求到达Servlet前执行。它先放行登录相关路径,再检查Session。
session.getAttribute("user")是LoginServlet登录成功后存入的User对象(含id、username、role)。若为空,重定向到/login.jsp?redirect=...,这样登录成功后可跳回原页面。这不是Spring Security的声明式注解,而是手动控制的请求生命周期。
3.2 房间查询:JDBC分页+条件组合,手写SQL的可控性优势
用户在首页输入“杭州”、“标准间”、“300元以下”,点击搜索。后端SearchServlet接收参数并调用RoomDAO.searchRooms():
// RoomDAO.java public List<Room> searchRooms(String city, String type, BigDecimal maxPrice) { List<Room> rooms = new ArrayList<>(); String sql = "SELECT r.id, r.type, r.price, r.available_count, " + "h.name as hotel_name, h.address " + "FROM room r JOIN hotel h ON r.hotel_id = h.id " + "WHERE 1=1 "; List<Object> params = new ArrayList<>(); if (city != null && !city.trim().isEmpty()) { sql += "AND h.address LIKE ? "; params.add("%" + city.trim() + "%"); } if (type != null && !type.trim().isEmpty()) { sql += "AND r.type = ? "; params.add(type.trim()); } if (maxPrice != null) { sql += "AND r.price <= ? "; params.add(maxPrice); } sql += "ORDER BY r.price ASC LIMIT 0, 10"; // 硬编码分页,第1页10条 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } ResultSet rs = ps.executeQuery(); while (rs.next()) { Room room = new Room(); room.setId(rs.getInt("id")); room.setType(rs.getString("type")); room.setPrice(rs.getBigDecimal("price")); room.setAvailableCount(rs.getInt("available_count")); // 关联酒店信息 Hotel hotel = new Hotel(); hotel.setName(rs.getString("hotel_name")); hotel.setAddress(rs.getString("address")); room.setHotel(hotel); rooms.add(room); } } catch (SQLException e) { e.printStackTrace(); // 生产环境应记录日志+抛自定义异常 } return rooms; }参数说明:
city来自地址模糊搜索(LIKE %杭州%),type精确匹配房型,maxPrice控制价格上限。SQL拼接虽有风险,但项目明确限定为内部管理系统,且参数经trim()和空值判断,规避了基础SQL注入。手写SQL的好处是:你能精确控制JOIN顺序、索引使用、分页偏移量,而MyBatis的<if>标签底层仍是拼SQL,只是藏在XML里。
3.3 订单创建:库存扣减与订单生成的原子性保障
这是项目最易翻车的模块。用户点击“立即预订”,系统必须保证:
① 房间库存available_count减1;
② 新增一条订单记录;
③ 两者要么全成功,要么全失败。
OrderService.createOrder()用JDBC事务实现:
// OrderService.java public boolean createOrder(int roomId, int userId, Date checkIn, Date checkOut) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关键:关闭自动提交 // 步骤1:扣减库存(悲观锁,防止超卖) String lockSql = "SELECT available_count FROM room WHERE id = ? FOR UPDATE"; try (PreparedStatement lockPs = conn.prepareStatement(lockSql)) { lockPs.setInt(1, roomId); ResultSet rs = lockPs.executeQuery(); if (!rs.next() || rs.getInt("available_count") <= 0) { throw new RuntimeException("房间已售罄"); } } // 步骤2:更新库存 String updateSql = "UPDATE room SET available_count = available_count - 1 WHERE id = ?"; try (PreparedStatement updatePs = conn.prepareStatement(updateSql)) { updatePs.setInt(1, roomId); int rows = updatePs.executeUpdate(); if (rows != 1) { throw new RuntimeException("库存更新失败"); } } // 步骤3:插入订单 String insertSql = "INSERT INTO orders (room_id, user_id, check_in, check_out, status, create_time) " + "VALUES (?, ?, ?, ?, 'UNPAID', NOW())"; try (PreparedStatement insertPs = conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS)) { insertPs.setInt(1, roomId); insertPs.setInt(2, userId); insertPs.setDate(3, new java.sql.Date(checkIn.getTime())); insertPs.setDate(4, new java.sql.Date(checkOut.getTime())); insertPs.executeUpdate(); // 获取生成的订单ID ResultSet keyRs = insertPs.getGeneratedKeys(); if (keyRs.next()) { int orderId = keyRs.getInt(1); System.out.println("新订单创建成功,ID:" + orderId); } } conn.commit(); // 全部成功,提交事务 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); // 任一环节失败,回滚 } catch (SQLException rollbackEx) { rollbackEx.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }关键点:
FOR UPDATE加行锁是核心。当多个用户同时抢同一房间时,数据库会阻塞第二个SELECT ... FOR UPDATE请求,直到第一个事务commit或rollback。这比应用层synchronized更可靠,因为锁在数据库层面,跨JVM有效。这也是为什么项目不用Redis做库存缓存——它要的是强一致性,不是高并发吞吐。
4. 常见问题排查:5个血泪经验总结,覆盖90%新手启动失败场景
4.1 现象:启动Tomcat后访问/hotel/显示404,但catalina.out无报错
原因:war包未正确解压,或web.xml中<welcome-file-list>配置错误。
解决:
- 进入
$TOMCAT_HOME/webapps/,确认存在hotel/目录(而非hotel.war还在); - 检查
hotel/WEB-INF/web.xml,确认<welcome-file-list>包含<welcome-file>index.jsp</welcome-file>; - 若
index.jsp在hotel/下,但web.xml中写成了<welcome-file>home.jsp</welcome-file>,则会404。
4.2 现象:登录时提示ClassNotFoundException: com.mysql.jdbc.Driver
原因:MySQL驱动jar包未放入WEB-INF/lib/,或驱动类名过时(MySQL 5.1+应使用com.mysql.cj.jdbc.Driver)。
解决:
- 确认
src/main/webapp/WEB-INF/lib/mysql-connector-java-5.1.47.jar存在; - 检查
DBUtil.java中Class.forName("com.mysql.jdbc.Driver")——MySQL 5.1.47仍兼容此旧类名,无需修改; - 若换用8.0驱动,需改为
com.mysql.cj.jdbc.Driver并添加时区参数:?serverTimezone=UTC。
4.3 现象:搜索房间返回空列表,但数据库明明有数据
原因:SearchServlet中参数获取方式错误,或SQL拼接逻辑有误。
解决:
- 在
SearchServlet.doPost()开头加日志:System.out.println("city=" + request.getParameter("city"));,确认前端传参正常; - 检查
RoomDAO.searchRooms()中sql字符串拼接:若city为空,WHERE 1=1 AND h.address LIKE ?会因?无值而语法错误; - 修正:将
params收集逻辑移到if块内,确保只对非空参数添加AND条件。
4.4 现象:下单成功后,房间库存未减少
原因:OrderService.createOrder()中事务未生效,或FOR UPDATE锁未起作用。
解决:
- 检查
conn.setAutoCommit(false)是否在try块最开始执行; - 确认
UPDATE room语句执行后,rows返回值是否为1(executeUpdate()返回影响行数); - 终极验证:在MySQL命令行执行
SELECT * FROM room WHERE id=1 FOR UPDATE;,再开另一个窗口执行同样语句,观察是否阻塞——若不阻塞,说明MySQL未启用InnoDB引擎(需ALTER TABLE room ENGINE=InnoDB;)。
4.5 现象:后台管理员页面/hotel/admin/可被未登录用户直接访问
原因:AuthFilter的url-pattern未覆盖/admin/*路径,或web.xml中Filter映射顺序错误。
解决:
- 检查
web.xml中<filter-mapping>是否在<servlet-mapping>之前; - 将
<url-pattern>改为<url-pattern>/admin/*</url-pattern>并单独配置,或确保/*能匹配/admin/xxx; - 验证方法:在
AuthFilter.doFilter()开头加System.out.println("AuthFilter triggered for: " + uri);,访问/admin/index.jsp看日志是否打印。
5. 二次开发实战:从“能跑”到“能改”,3个可立即落地的增强点
5.1 增加订单支付状态机:用枚举+状态流转表替代硬编码字符串
当前订单状态只有'UNPAID',支付逻辑缺失。我们引入状态机,让订单生命周期更健壮:
第一步:定义状态枚举
// src/main/java/com/example/hotel/model/OrderStatus.java public enum OrderStatus { UNPAID("待支付"), PAID("已支付"), CONFIRMED("已确认"), CHECKED_IN("已入住"), CHECKED_OUT("已退房"), CANCELLED("已取消"); private final String desc; OrderStatus(String desc) { this.desc = desc; } public String getDesc() { return desc; } }第二步:创建状态流转规则表(数据库)
-- 新增表 order_status_transition CREATE TABLE order_status_transition ( from_status VARCHAR(20) NOT NULL, to_status VARCHAR(20) NOT NULL, PRIMARY KEY (from_status, to_status) ); -- 插入允许的流转 INSERT INTO order_status_transition VALUES ('UNPAID', 'PAID'), ('PAID', 'CONFIRMED'), ('CONFIRMED', 'CHECKED_IN'), ('CHECKED_IN', 'CHECKED_OUT'), ('UNPAID', 'CANCELLED'), ('PAID', 'CANCELLED');第三步:编写状态校验服务
// OrderStatusService.java public class OrderStatusService { public boolean canTransition(String from, String to) { String sql = "SELECT COUNT(*) FROM order_status_transition " + "WHERE from_status = ? AND to_status = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, from); ps.setString(2, to); ResultSet rs = ps.executeQuery(); return rs.next() && rs.getInt(1) > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public boolean updateStatus(int orderId, String newStatus) { if (!canTransition(getCurrentStatus(orderId), newStatus)) { throw new IllegalStateException("非法状态流转:" + getCurrentStatus(orderId) + " → " + newStatus); } String sql = "UPDATE orders SET status = ? WHERE id = ?"; // 执行更新... return true; } }改造价值:从此
order.setStatus("PAID")不再是魔法字符串,而是受数据库约束的合法操作。任何非法调用(如UNPAID → CHECKED_IN)都会抛异常,避免业务逻辑腐化。这是我从某跨平台系统中学到的教训:状态变更必须可审计、可追溯、不可绕过。
5.2 替换JSP为纯HTML+AJAX:剥离服务端渲染,为后续Vue迁移铺路
JSP混杂Java代码,不利于前后端分离。我们保留后端Servlet,仅将search.jsp改为静态HTML调用API:
原JSP片段(search.jsp):
<c:forEach items="${rooms}" var="room"> <div class="room-item"> <h3>${room.hotel.name}</h3> <p>房型:${room.type} | 价格:¥${room.price}</p> <a href="BookServlet?roomId=${room.id}">预订</a> </div> </c:forEach>改造为HTML+AJAX:
<!-- search.html --> <div id="roomList"></div> <script> function loadRooms() { const city = document.getElementById('city').value; fetch(`/hotel/SearchServlet?city=${encodeURIComponent(city)}`) .then(res => res.json()) .then(data => { const list = document.getElementById('roomList'); list.innerHTML = data.map(room => ` <div class="room-item"> <h3>${room.hotelName}</h3> <p>房型:${room.type} | 价格:¥${room.price}</p> <button onclick="bookRoom(${room.id})">预订</button> </div> `).join(''); }); } function bookRoom(roomId) { fetch(`/hotel/BookServlet`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `roomId=${roomId}` }).then(res => res.text()).then(alert); } </script>后端Servlet响应JSON:
// SearchServlet.java protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String city = req.getParameter("city"); List<Room> rooms = roomService.searchRooms(city, null, null); // 转为JSON(简易版,生产用Jackson/Gson) StringBuilder json = new StringBuilder("["); for (int i = 0; i < rooms.size(); i++) { Room r = rooms.get(i); json.append("{") .append("\"id\":").append(r.getId()).append(",") .append("\"hotelName\":\"").append(r.getHotel().getName()).append("\",") .append("\"type\":\"").append(r.getType()).append("\",") .append("\"price\":").append(r.getPrice()).append("}"); if (i < rooms.size() - 1) json.append(","); } json.append("]"); resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(json.toString()); }改造价值:前端彻底解耦,可独立开发、测试、部署。后端Servlet变成纯粹的REST API,为未来接入微信小程序、App提供统一接口。从那以后我每次接手老JSP项目,都强制走一遍“HTML+AJAX”替换,哪怕只改一个页面——它让团队协作成本直线下降。
5.3 日志与监控埋点:用AOP思想在DAO层加性能追踪
当前所有DAO方法都无耗时统计。我们用Filter+ThreadLocal模拟轻量级AOP:
// src/main/java/com/example/hotel/filter/PerformanceFilter.java public class PerformanceFilter implements Filter { private static final ThreadLocal<Long> startTime = new ThreadLocal<>(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { startTime.set(System.currentTimeMillis()); try { chain.doFilter(request, response); } finally { Long start = startTime.get(); if (start != null) { long cost = System.currentTimeMillis() - start; String uri = ((HttpServletRequest) request).getRequestURI(); if (cost > 500) { // 超500ms标为慢查询 System.err.println("[SLOW] " + uri + " cost: " + cost + "ms"); } startTime.remove(); } } } }在web.xml中将其放在AuthFilter之后(保证已鉴权):
<filter> <filter-name>PerformanceFilter</filter-name> <filter-class>com.example.hotel.filter.PerformanceFilter</filter-class> </filter> <filter-mapping> <filter-name>PerformanceFilter</filter-name> <url-pattern>/SearchServlet</url-pattern> <url-pattern>/BookServlet</url-pattern> </filter-mapping>改造价值:无需改DAO代码,就能捕获所有关键接口耗时。当用户反馈“搜房卡顿”,你立刻能定位是
SearchServlet还是数据库慢。这招是我从某图像处理Demo里偷师的——他们用类似方式监控OpenCV算法耗时,后来成了标配。
希望帮到你。
本文还有配套的精品资源,点击获取