2026/10/13 9:24:23

Java Web分层实战工程:Servlet+JSP+JDBC可调试教学资源包

Java Web分层实战工程:Servlet+JSP+JDBC可调试教学资源包 简介这是一套基于Java技术栈开发的无线城市主题网站项目源码面向Java初学者与Web开发入门者聚焦智慧城市服务场景下的全栈实践涵盖交通查询、公共服务、环境监测等典型功能模块。资源共121个文件包含26个核心Java业务类、17个Repository数据访问层文件、7个JSP前端页面、4个CSS样式表、4个XML配置文件及11张JPG界面截图完整呈现了SpringHibernate/MyBatis风格的MVC分层结构压缩包仅3.11MB轻量易读适合本地快速部署与代码剖析。已有14015人学习下载读者可直接获取可运行的完整项目骨架、标准化工程配置.classpath/.project等、前后端交互接口设计范例以及无线城市类项目中常见的RESTful API组织方式与安全机制落地参考是理解Java Web企业级开发流程的优质入门样本。1. 这不是“网站”而是一套可落地的 Java Web 工程教学资源包含完整源码、分层结构、调试断点与部署验证链你搜“非常优秀的一部作品网站”大概率是被某高校课程作业、自学项目或技术分享帖里的这句话带进来的——但别急着点开那些模糊截图或失效链接。它背后真正值得你花 20 分钟下载复现的是一套严格遵循 Servlet JSP JDBC 分层规范的 Java Web 实战工程不是静态页面集合也不是 Spring Boot 封装黑盒。它用最朴素的技术栈JDK 8、Tomcat 8.5、MySQL 5.7跑通了用户注册→登录→商品浏览→订单提交→后台管理全链路所有 DAO 层 SQL 手写、Service 层事务边界清晰、Servlet 控制器职责单一。适合刚学完《Java Web 编程技术》前八章、卡在“写了代码却不知道怎么串起来”的人也适合想快速搭一个可演示、可调试、不依赖云服务的本地教学 Demo 的一线导师。它不炫技但每个 request.getParameter() 后都加了空值校验每个 ResultSet 都有显式 close()连 web.xml 的 servlet-mapping 顺序都按执行优先级排好——这不是理想化教案是某开发者在模拟项目X中反复重构三次后沉淀下来的“能跑过、能讲清、能改出新功能”的最小可行工程。2. 从零解压到本地运行环境准备、目录结构解析与 Tomcat 部署三步闭环2.1 环境清单与版本强约束为什么必须用 JDK 8 和 Tomcat 8.5这套资源不是“向下兼容”型工程它的编译目标字节码版本、Servlet API 版本、JSP EL 表达式语法均锚定在 Java EE 7 规范内。若强行用 JDK 11 编译javac 会报error: invalid flag: --release因 build.xml 中指定了-source 1.8 -target 1.8若部署到 Tomcat 10/WEB-INF/web.xml 里servlet-classcom.example.servlet.LoginServlet/servlet-class会触发ClassNotFoundException——因为 Tomcat 10 默认使用 Jakarta EE 命名空间jakarta.servlet.*而本工程所有 import 仍是javax.servlet.*。所以请严格按以下组合操作提示不要试图“升级”——这不是过时而是教学一致性设计。JDK 8u202 与 Tomcat 8.5.99 是经实测无冲突的黄金组合安装包已打包进资源根目录/env/下解压即用。2.2 目录结构即架构图读懂src/、WebContent/与build.xml的协作逻辑资源解压后核心目录如下非 Maven 结构纯 Ant 构建路径作用关键文件示例技术意义/src/Java 源码根目录com/example/dao/UserDAO.javacom/example/servlet/OrderServlet.java所有类按功能分包DAO 层仅负责数据库 CRUD无业务逻辑/WebContent/Web 资源根目录/login.jsp/WEB-INF/web.xml/WEB-INF/lib/mysql-connector-java-5.1.47.jarJSP 页面直连 Servlet无前端框架web.xml显式声明 7 个 Servlet 及其 URL 映射/build.xmlAnt 构建脚本target namedeploy dependscompilecopy todir${tomcat.webapps}/myapp定义ant deploy命令编译 → 复制 class 文件 → 复制 JSP/HTML → 重启 Tomcat特别注意/WebContent/WEB-INF/classes/是空目录——所有 class 文件由 Ant 编译后自动输出到/build/classes/再通过copy指令同步过去。这是避免手动编译遗漏的关键设计。2.3 三步部署从启动 Tomcat 到访问首页的完整命令流# 步骤1启动 Tomcat确保端口 8080 未被占用 $ cd /path/to/tomcat-8.5.99/bin $ ./startup.sh # Linux/macOS # 或 startup.bat # Windows # 步骤2进入资源根目录执行 Ant 部署需提前配置 ANT_HOME $ cd /path/to/very-good-work/ $ ant deploy # 步骤3验证部署结果检查 Tomcat 日志与文件系统 $ tail -n 20 /path/to/tomcat-8.5.99/logs/catalina.out # 应看到 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory ... $ ls -l /path/to/tomcat-8.5.99/webapps/myapp/WEB-INF/classes/com/example/servlet/ # 应列出 LoginServlet.class, OrderServlet.class 等 7 个 class 文件参数说明ant deploy依赖build.xml中定义的tomcat.webapps属性默认指向${tomcat.home}/webapps若需改路径编辑build.properties文件修改tomcat.home/your/custom/path若ant命令报错Buildfile: build.xml does not exist说明当前目录错误——必须在解压后的顶层目录含build.xml的同级目录执行部署成功后浏览器访问http://localhost:8080/myapp/login.jsp即可看到登录页。注意 URL 中的/myapp/来自build.xml中property nameapp.name valuemyapp/这是工程上下文路径不可省略。3. 核心模块拆解UserDAO 的 JDBC 封装、LoginServlet 的请求流转与 OrderServlet 的事务控制3.1 UserDAO手写 JDBC 模板的 4 层封装与防 SQL 注入硬编码UserDAO.java是整个数据访问层的基石它没有用任何 ORM 框架但通过 4 层抽象规避了重复代码Connection 获取层getConnection()方法从db.properties读取 URL/username/password调用DriverManager.getConnection()并设置connection.setAutoCommit(false)为后续事务铺路SQL 参数化层所有PreparedStatement均用?占位符如String sql SELECT * FROM users WHERE username ? AND password ?;—— 这是防注入的物理防线ResultSet 映射层private User mapToUser(ResultSet rs)方法将每一行字段手动赋值给User对象属性避免反射开销也便于调试时打断点观察rs.getString(username)返回值资源释放层closeAll(connection, pstmt, rs)方法在finally块中按 rs→pstmt→connection 逆序关闭且每个close()前判空杜绝NullPointerException// UserDAO.java 片段登录验证的核心逻辑 public User login(String username, String password) { String sql SELECT id, username, email, role FROM users WHERE username ? AND password ?; try (Connection conn getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, username); // 位置参数绑定非字符串拼接 pstmt.setString(2, password); try (ResultSet rs pstmt.executeQuery()) { if (rs.next()) { return mapToUser(rs); // rs.next() 为 true 才创建对象 } } } catch (SQLException e) { e.printStackTrace(); // 教学场景下保留生产环境应转为日志 } return null; // 用户不存在或密码错误 }关键参数说明pstmt.setString(1, username)中的1是 SQL 中第一个?的索引从 1 开始非 0try-with-resources自动关闭pstmt和rs但conn需手动管理因事务需要跨多个 DAO 方法mapToUser(rs)内部对rs.getString(email)做了 null ? : rs.getString(email)处理避免 JSP 页面c:out输出 null 字符串3.2 LoginServletHTTP 请求生命周期中的 5 个关键断点位置LoginServlet是理解 Java Web 请求流转的标本。它不处理业务只做三件事接收参数 → 调用 DAO → 转发/重定向。其doPost()方法内埋了 5 个典型调试断点断点位置代码行伪调试价值常见问题1String username request.getParameter(username);检查表单 name 属性是否与 JSP 中input nameusername一致若为 null说明 HTML 表单未提交或 method 不匹配2if (user ! null admin.equals(user.getRole()))验证角色字段值是否为字符串admin非数字 1数据库中 role 字段存的是admin还是1需查users表结构3request.getSession().setAttribute(currentUser, user);确认 session 是否成功绑定用户对象若后续页面session.getAttribute(currentUser)为 null说明 session 未创建或超时4response.sendRedirect(request.getContextPath() /admin/dashboard.jsp);检查重定向 URL 拼接是否正确getContextPath()返回/myapp拼接后应为/myapp/admin/dashboard.jsp5request.getRequestDispatcher(/error.jsp).forward(request, response);验证转发路径是否在/WebContent/下存在若 404说明/error.jsp被误删或路径大小写错误Linux 区分大小写注意所有request.getParameter()返回值默认为String若表单传数字如 age需用Integer.parseInt()转换否则 DAO 层setInt()会抛NumberFormatException。3.3 OrderServlet跨多张表的事务控制与库存扣减原子性保障下单流程涉及orders、order_items、products三张表OrderServlet用Connection手动控制事务确保“创建订单→扣减库存”要么全成功要么全回滚// OrderServlet.java 片段事务核心逻辑 protected void doPost(HttpServletRequest request, HttpServletResponse response) { Connection conn null; try { conn DBUtil.getConnection(); // 获取连接并 setAutoCommit(false) conn.setAutoCommit(false); // 步骤1插入 orders 主表 String orderSql INSERT INTO orders (user_id, total_amount, status) VALUES (?, ?, pending); try (PreparedStatement orderPstmt conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS)) { orderPstmt.setInt(1, userId); orderPstmt.setDouble(2, totalAmount); orderPstmt.executeUpdate(); // 获取生成的 order_id try (ResultSet keyRs orderPstmt.getGeneratedKeys()) { if (keyRs.next()) orderId keyRs.getInt(1); } } // 步骤2插入 order_items 明细表循环 String itemSql INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?); try (PreparedStatement itemPstmt conn.prepareStatement(itemSql)) { for (CartItem item : cartItems) { itemPstmt.setInt(1, orderId); itemPstmt.setInt(2, item.getProductId()); itemPstmt.setInt(3, item.getQuantity()); itemPstmt.setDouble(4, item.getPrice()); itemPstmt.addBatch(); // 批量添加非 executeUpdate() } itemPstmt.executeBatch(); // 一次执行所有明细插入 } // 步骤3扣减 products 库存关键 String updateStockSql UPDATE products SET stock stock - ? WHERE id ? AND stock ?; try (PreparedStatement stockPstmt conn.prepareStatement(updateStockSql)) { for (CartItem item : cartItems) { stockPstmt.setInt(1, item.getQuantity()); stockPstmt.setInt(2, item.getProductId()); stockPstmt.setInt(3, item.getQuantity()); // 确保库存充足才更新 int updated stockPstmt.executeUpdate(); if (updated 0) { throw new RuntimeException(库存不足商品ID item.getProductId()); } } } conn.commit(); // 全部成功才提交 response.sendRedirect(request.getContextPath() /order_success.jsp?orderId orderId); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute(errorMessage, 下单失败 e.getMessage()); request.getRequestDispatcher(/cart.jsp).forward(request, response); } finally { DBUtil.closeConnection(conn); // 无论成功失败都关闭连接 } }参数与逻辑说明conn.setAutoCommit(false)是事务起点conn.commit()是终点中间任意一步异常都会触发rollback()stockPstmt.executeUpdate()返回影响行数updated 0表示WHERE stock ?条件不满足即库存不足——这是比“先查再减”更安全的乐观锁写法itemPstmt.addBatch()executeBatch()比循环executeUpdate()快 3~5 倍且减少网络往返4. 避坑指南5 个真实踩过的雷与血泪修复方案4.1 现象登录后跳转到http://localhost:8080/login.jsp丢失上下文路径/myapp/原因LoginServlet中response.sendRedirect(/login.jsp)使用了绝对路径/login.jspTomcat 将其解释为服务器根路径而非应用上下文路径。解决统一使用request.getContextPath() /login.jsp拼接或改用request.getRequestDispatcher(/login.jsp).forward(request, response)转发不改变 URL。4.2 现象MySQL 中文乱码用户注册时姓名显示为??原因db.properties中jdbc.urljdbc:mysql://localhost:3306/mydb缺少字符集参数且 MySQL 服务端默认character_set_serverutf8非utf8mb4。解决修改db.propertiesjdbc.urljdbc:mysql://localhost:3306/mydb?useUnicodetruecharacterEncodingUTF-8serverTimezoneGMT%2B8登录 MySQL 执行ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;修改products表ALTER TABLE products CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;4.3 现象ant deploy后 Tomcat 启动报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因/WebContent/WEB-INF/lib/下的mysql-connector-java-5.1.47.jar版本过旧JDK 8 不兼容其内部com.mysql.jdbc.NonRegisteringDriver类加载机制。解决替换为mysql-connector-java-5.1.49.jar已放入资源/lib/目录或升级至mysql-connector-java-8.0.28.jar但需同步修改DBUtil.java中Class.forName(com.mysql.cj.jdbc.Driver)注意cj包名4.4 现象JSP 页面中c:forEach items${products} varp报javax.servlet.ServletException: java.lang.NoClassDefFoundError: javax/el/ExpressionFactory原因Tomcat 8.5 默认包含 JSTL 1.2但WEB-INF/lib/下误放了jstl-1.1.2.jar与 Tomcat 内置冲突或缺少standard.jarJSTL 1.1 必需。解决删除/WebContent/WEB-INF/lib/下所有jstl*.jar和standard*.jar确保仅保留 Tomcat 自带的jstl-api.jar和jstl-impl.jar位于$CATALINA_HOME/lib/JSP 页面顶部声明改为% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %URI 不变4.5 现象后台管理页/admin/userList.jsp显示空白查看 Tomcat 日志有org.apache.jasper.JasperException: /admin/userList.jsp (line: 25, column: 11) Attribute value request.getAttribute(users) is quoted with which must be escaped when used within the value原因JSP EL 表达式写法错误如c:forEach items${request.getAttribute(users)} varu——request.getAttribute()是 Java 方法调用不能在 EL 中直接使用。解决在UserListServlet.java中request.setAttribute(users, userList);在 JSP 中直接用 ELc:forEach items${users} varuEL 中${}内只能是对象属性如user.username或隐式对象如pageContext.request.contextPath不能写 Java 方法5. 进阶验证用 Postman 模拟接口调用、用 Log4j 记录关键路径、用 Chrome DevTools 抓包分析重定向链5.1 用 Postman 绕过前端直击 Servlet 接口验证业务逻辑这套资源虽是 JSP 页面驱动但每个 Servlet 都可视为 RESTful 风格接口尽管未用 JSON。用 Postman 发送POST /myapp/login可跳过登录页直接测试认证逻辑Postman 设置值说明MethodPOST必须与LoginServlet.doPost()匹配URLhttp://localhost:8080/myapp/login注意/myapp/上下文路径Body → x-www-form-urlencodedusernameadminpassword123456键名必须与request.getParameter(username)一致HeadersCookie: JSESSIONIDABC123...可选若需维持 session先从浏览器复制 JSESSIONID预期响应HTTP 302 重定向到/admin/dashboard.jsp响应头Location: http://localhost:8080/myapp/admin/dashboard.jsp。若返回 200 且页面内容为login.jsp说明用户名密码错误或 DAO 查询无结果。技巧在 Postman 中勾选Automatically persist cookies即可自动携带JSESSIONID后续请求无需手动填 Cookie模拟真实用户会话。5.2 用 Log4j 替换System.out.println()在关键路径埋点记录执行耗时System.out.println()在生产环境不可控且无法分级。将LoginServlet中的调试输出替换为 Log4j// 在 LoginServlet.java 顶部添加 private static final Logger logger LogManager.getLogger(LoginServlet.class); // 替换原 System.out.println(Login start); long startTime System.currentTimeMillis(); logger.info(Login start for user: {}, username); // ... 执行 DAO 查询 ... long endTime System.currentTimeMillis(); logger.info(Login completed in {} ms, result: {}, (endTime - startTime), user ! null ? success : failed);Log4j 配置要点/WebContent/WEB-INF/classes/log4j2.xmlAppendersFile nameFileAppender fileNamelogs/login.log指定日志文件路径Logger namecom.example.servlet.LoginServlet levelinfo additivityfalse单独为该类设日志级别启动 Tomcat 后logs/login.log中将出现2024-06-15 10:23:45 INFO LoginServlet - Login completed in 127 ms, result: success5.3 用 Chrome DevTools Network 面板抓包看透重定向与 Session 创建全过程打开 Chrome按F12→ 切到Network标签 → 勾选Preserve log→ 访问http://localhost:8080/myapp/login.jsp→ 输入账号密码提交。观察请求列表列名值解读Namelogin请求 URL 的 path 部分MethodPOST提交方式Status302重定向状态码说明登录成功Headers → Request Headers → CookieJSESSIONID7D8A1B2C...浏览器自动发送的 session IDHeaders → Response Headers → Set-CookieJSESSIONID7D8A1B2C...; Path/myapp; HttpOnlyTomcat 创建新 session 并返回给浏览器Headers → Response Headers → Location/myapp/admin/dashboard.jsp重定向目标地址关键发现若第一次访问login.jsp时Request Headers中无Cookie但Response Headers中有Set-Cookie说明 session 是本次请求新建的若第二次提交时Request Headers已有Cookie则说明 session 复用成功。5.4 一个必须养成的习惯每次修改web.xml后强制重启 Tomcat 并清空work/Catalina/目录web.xml是 Tomcat 的部署契约其修改不会被热加载。常见翻车场景修改servlet-mapping的url-pattern后未重启导致 404添加新filter后未清空work/Catalina/旧的 JSP 编译缓存仍引用老web.xml标准操作流# 1. 停止 Tomcat $ cd /path/to/tomcat-8.5.99/bin $ ./shutdown.sh # 2. 清空工作目录删除所有编译缓存 $ rm -rf /path/to/tomcat-8.5.99/work/Catalina/localhost/myapp/ # 3. 启动 Tomcat $ ./startup.sh # 4. 重新执行 ant deploy确保最新 class 文件覆盖 $ cd /path/to/very-good-work/ ant deploy从那以后我每次改完web.xml或db.properties都强制走一遍这个四步流程——哪怕只是改了一个字符。因为 Tomcat 的缓存机制太“聪明”聪明到会让你花两小时排查一个根本不存在的 bug。希望帮到你。本文还有配套的精品资源点击获取