资讯详情

资讯详情

J2EE书城源码实战:Servlet+JSP+JDBC分层解析与二次开发改造

简介这是一份面向Java Web初学者与课程设计者的J2EE实战项目源码以「云工厂-网上书城」为主题完整呈现从用户界面到后台数据库的在线图书销售流程适合用于毕业设计参考、框架学习与二次开发练习。压缩包共588个文件约4.09MB其中83个java源文件与82个class文件对应Servlet、DAO、JavaBean等业务逻辑45个jsp页面承担前端展示另有gif、jpg、css等静态资源及jar依赖、sql脚本与db数据库文件构成可部署运行的完整工程。项目覆盖Servlet请求处理、JSP动态页面、JDBC数据库交互、JavaBeans数据封装与EJB分层设计等知识点内容预览中可见BookInfoDAO、SaveOrderServlet、GwcServlet等类便于读者梳理购物车、订单与图书信息管理的实现脉络。目前已有242人学习下载适合按模块研读并对照部署到Tomcat等容器中调试。1. 从一份 J2EE 书城源码说起它到底能跑出什么第一次拿到「云工厂-网上书城」这份源码我下意识先翻的是 web.xml 和 lib 目录而不是 README——因为一个 J2EE 项目能不能跑起来八成答案都藏在这两个地方。翻完我的判断是这是一套典型的 Servlet JSP JDBC 分层书城系统前台做图书浏览、搜索、购物车、下单后台做图书与订单管理数据库用 MySQL构建走 Maven 或手动导 jar容器丢进 Tomcat 就能起。它适合两类人一类是刚学完 Servlet 生命周期、想找一个「有完整业务闭环」的项目练手的同学另一类是接手了老系统、需要摸清 J2EE 分层套路再动手改造的开发者。别指望它有什么微服务、缓存中间件它的价值恰恰在于「朴素」——每一层都摊开给你看改起来不玄学。2. 架构与目录拆解一个请求从 JSP 到 DAO 的完整链路2.1 分层结构与技术选型为什么是这样这套书城走的是经典 MVC 三层JSP 当 ViewServlet 当 ControllerService DAO 当 Model 层。技术栈上没有花活Servlet 3.x、JSP 2.x、JDBC 直连 MySQL、JSTL 标签库渲染列表前端是原生 HTML/CSS/JS 加少量 jQuery。为什么不用 Spring因为这类教学向项目的目标是「让你看清请求怎么流转」一旦上了 Spring 的 IOC 和 AOP新手很容易只记住注解、不知道底下发生了什么。我一般会把这种项目当成「Servlet 容器行为观察器」来用一个/book/list请求进来先过过滤器做编码和登录校验再进 Servlet 拿参数、调 Service、Service 调 DAO 查库、封装成 List 塞进 request 域最后 forward 到 JSP 用 JSTL 遍历输出。整条链路没有黑匣子断点打在哪一层都清楚。分层职责上Servlet 只做参数接收和页面跳转不写 SQLService 负责业务组合比如下单时要同时扣库存、写订单主表、写订单明细这三步必须在一个事务里DAO 只做单表 CRUD返回实体对象。这种划分在真实项目里也站得住脚只是真实项目会把 Service 拆得更细、DAO 换成 MyBatis 或 JPA。理解了这个骨架后面换任何 ORM 都是换零件不是换发动机。2.2 目录结构长什么样源码解压后大致是标准 Maven Web 工程布局我按实际会看到的形态列一下关键路径网上书城/ ├── pom.xml # 依赖与打包配置 ├── src/main/java/ │ ├── com/book/controller/ # Servlet 控制器 │ ├── com/book/service/ # 业务逻辑 │ ├── com/book/dao/ # 数据访问 │ ├── com/book/entity/ # 实体类Book/User/Order... │ └── com/book/util/ # DBUtil、编码工具等 ├── src/main/resources/ │ └── db.properties # 数据库连接参数 ├── src/main/webapp/ │ ├── WEB-INF/web.xml # Servlet 映射与过滤器配置 │ ├── WEB-INF/views/ # JSP 页面 │ ├── css/ js/ images/ # 静态资源 │ └── index.jsp # 入口页 └── sql/bookstore.sql # 建库建表与初始数据看到sql/bookstore.sql就说明作者把初始化脚本一起给了这是好事——省得你对着实体类猜表结构。db.properties单独抽出来也是合格做法至少改数据库密码不用翻 Java 代码。如果你的解压包里没有pom.xml而是lib/下一堆 jar那说明是手动管理依赖的老式工程导入 IDE 时按「Existing Projects into Workspace」走别硬套 Maven 结构。2.3 请求链路的关键代码先看一个最典型的图书列表 Servlet这是理解整条链路的入口// BookListServlet.java —— 图书列表控制器 WebServlet(/book/list) public class BookListServlet extends HttpServlet { private BookService bookService new BookServiceImpl(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 接收分页与搜索参数做空值兜底 String keyword req.getParameter(keyword); int page parseInt(req.getParameter(page), 1); int size 8; // 2. 调 Service 拿分页结果Service 内部再调 DAO PageResultBook result bookService.search(keyword, page, size); // 3. 结果塞进 request 域forward 给 JSP 渲染 req.setAttribute(pageResult, result); req.setAttribute(keyword, keyword); req.getRequestDispatcher(/WEB-INF/views/book_list.jsp) .forward(req, resp); } }这段代码有三个点值得说。第一WebServlet注解替代了 web.xml 里的servlet-mappingServlet 3.0 以后都这么写但如果你在 web.xml 里也配了同名映射会冲突启动失败这是新手常翻的车。第二parseInt兜底很关键page参数被用户手动改成abc时不能直接抛 NumberFormatException得回落到第 1 页。第三JSP 放在WEB-INF/views/下意味着不能通过 URL 直接访问必须经 Servlet forward这是防止用户绕过控制器直接看页面的基本手段。再看 DAO 层怎么把 JDBC 封装得不那么难看// BookDaoImpl.java —— 分页查询实现 public PageResultBook findByKeyword(String keyword, int page, int size) { String sql SELECT * FROM book WHERE name LIKE ? ORDER BY id DESC LIMIT ?, ?; ListBook list new ArrayList(); int total countByKeyword(keyword); // 单独查总数供分页组件用 try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % (keyword null ? : keyword) %); ps.setInt(2, (page - 1) * size); // 偏移量 (页码-1)*每页条数 ps.setInt(3, size); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); // 结果集到实体的手工映射 } } } catch (SQLException e) { throw new RuntimeException(查询图书失败, e); } return new PageResult(list, total, page, size); }参数说明keyword为空时拼成%%等价于全量查询page从 1 开始偏移量公式(page-1)*size是最容易写错的地方写成page*size就会漏掉第一页数据。try-with-resources保证连接和语句自动关闭但注意DBUtil.getConnection()如果每次新建连接而不走连接池并发一上来就会打满 MySQL 的 max_connections这是这类项目最典型的性能短板后面改造章节会讲怎么补。2.4 数据库与配置db.properties一般长这样改成本机参数即可jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的密码注意serverTimezone这个参数MySQL 8 的驱动不配它会报时区异常这是血泪经验。DBUtil里用Class.forName加载驱动、DriverManager.getConnection拿连接属于最原始的写法能跑但不够好改造时优先换成 Druid 或 HikariCP。3. 本地跑通从建库到 Tomcat 起服务的实操步骤3.1 环境准备与建库先把环境对齐JDK 8 或 11别上 17老项目的 JSP 编译容易出兼容问题、Tomcat 8.5 或 9、MySQL 5.7 或 8、Maven 3.6。版本对不上是启动失败的头号原因我见过用 Tomcat 10 跑这套代码直接 404 的——因为 Tomcat 10 把javax.servlet换成了jakarta.servlet包名全变老代码一个都加载不了。建库步骤很直接进 MySQL 命令行执行初始化脚本mysql -u root -p -e CREATE DATABASE bookstore DEFAULT CHARSET utf8mb4; mysql -u root -p bookstore sql/bookstore.sql执行完用SHOW TABLES;确认 book、user、orders、order_item 这几张表都在。如果脚本里带了CREATE DATABASE语句第一条命令可以省掉但重复执行会报库已存在忽略即可。3.2 改配置与打包部署把db.properties里的账号密码改成你本机的然后走 Maven 打包mvn clean package -DskipTests产物在target/下是个 war 包。把它丢进 Tomcat 的webapps/目录启动bin/startup.shWindows 用startup.bat访问http://localhost:8080/网上书城/或按 war 包名访问。如果 IDE 里直接跑IDEA 配一个 Tomcat Server 运行配置Deployment 里加 Artifact 即可改代码热部署更方便。启动后先看日志有没有ClassNotFoundException: com.mysql.cj.jdbc.Driver有就是驱动 jar 没进WEB-INF/lib再看有没有Access denied for user那是密码没改对。这两类错误占了启动失败的绝大多数。3.3 功能自检清单跑起来不等于跑对按这条路径走一遍能覆盖主要业务首页图书列表是否分页正常 → 搜索框输入关键词是否过滤 → 点详情进图书页 → 加入购物车 → 未登录时点结算是否跳登录 → 注册登录 → 提交订单 → 后台管理页能否看到新订单。每一步都对应一个 Servlet哪一步 500 了就去那层的日志里找堆栈定位效率比盲猜高得多。提示首次启动如果页面样式全丢检查静态资源路径是不是写死的绝对路径部署到带 contextPath 的环境下要改成${pageContext.request.contextPath}/css/...。4. 核心模块拆解购物车、下单与登录校验怎么写的4.1 购物车为什么放 Session这套项目的购物车没有落库而是存在HttpSession里结构是MapInteger, CartItemkey 是图书 idvalue 是数量加小计。这么做的好处是未登录用户也能加购实现成本低代价是换浏览器或清 session 就丢且服务端重启非持久化 session 配置下也会清空。教学项目这么选合理真实电商肯定要落库或走 Redis。// CartServlet.java —— 加入购物车 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int bookId Integer.parseInt(req.getParameter(bookId)); int num parseInt(req.getParameter(num), 1); HttpSession session req.getSession(); MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new LinkedHashMap(); // 保持加入顺序展示更自然 session.setAttribute(cart, cart); } CartItem item cart.get(bookId); if (item null) { Book book bookService.findById(bookId); // 查库拿价格和库存 cart.put(bookId, new CartItem(book, num)); } else { item.setNum(item.getNum() num); // 已存在则累加 } resp.sendRedirect(req.getContextPath() /cart/list); }关键点在LinkedHashMap和重定向。用LinkedHashMap是为了购物车列表按加入顺序展示用HashMap顺序会乱末尾用sendRedirect而不是 forward是为了避免用户刷新页面重复提交PRG 模式这是表单提交的标配做法漏了就会出现「刷新一次多一件」的经典 bug。4.2 下单事务三步操作必须绑在一起下单是整个项目里唯一必须用事务的地方扣库存、写订单主表、写订单明细任何一步失败都得回滚否则会出现「库存扣了订单没生成」的对账灾难。// OrderServiceImpl.java —— 创建订单 public int createOrder(int userId, MapInteger, CartItem cart) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 int orderId orderDao.insertOrder(conn, userId, totalAmount(cart)); for (CartItem item : cart.values()) { // 扣库存时带 stock num 条件防止超卖 int affected bookDao.reduceStock(conn, item.getBook().getId(), item.getNum()); if (affected 0) { throw new RuntimeException(库存不足 item.getBook().getName()); } orderDao.insertItem(conn, orderId, item); } conn.commit(); // 全部成功才提交 return orderId; } catch (Exception e) { if (conn ! null) conn.rollback(); // 任一失败整体回滚 throw new RuntimeException(下单失败, e); } finally { DBUtil.close(conn); } }参数与逻辑说明conn.setAutoCommit(false)是事务开关忘了这行后面 commit/rollback 全是摆设。扣库存的 SQL 写成UPDATE book SET stock stock - ? WHERE id ? AND stock ?把库存判断放进 WHERE 里靠数据库行锁保证并发下不超卖——这是不用悲观锁也能挡住大部分超卖场景的实用写法。affected 0说明库存不够主动抛异常触发回滚。注意这里 DAO 方法都接收同一个conn参数保证三步在同一个连接同一个事务里如果每个 DAO 自己getConnection事务就形同虚设这是新手最容易犯的错。4.3 登录校验过滤器后台管理和下单接口都需要登录态逐个 Servlet 判断太啰嗦标准做法是写一个 Filter// AuthFilter.java —— 登录拦截 WebFilter(urlPatterns {/admin/*, /order/*}) public class AuthFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; Object user req.getSession().getAttribute(loginUser); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; // 不放行请求到此为止 } chain.doFilter(request, response); // 已登录继续走后续链路 } }urlPatterns里/admin/*覆盖后台全部路径/order/*覆盖订单相关比在每个 Servlet 里写 if 判断干净得多。注意return之后不能再调chain.doFilter否则拦截等于没拦。过滤器顺序如果和编码过滤器冲突在 web.xml 里用filter-mapping的声明顺序控制注解方式下顺序不保证这点要留意。5. 避坑与排查启动、乱码、事务失效的踩坑记录5.1 启动报 404 或 ClassNotFound现象Tomcat 起来了访问首页 404或日志里ClassNotFoundException。原因通常是两类一是 Tomcat 10 的 jakarta 包名不兼容老代码二是 Maven 依赖没打进 war 的WEB-INF/lib常见于 scope 写成provided的驱动包。解决换 Tomcat 8.5/9检查 pom 里 mysql-connector 的 scope 是否为 runtime 或默认必要时手动把 jar 放进 lib 目录验证。5.2 中文乱码三连现象表单提交到后台变问号、数据库里存进去是乱码、页面输出乱码。原因分别是请求编码、数据库连接编码、响应编码三处没统一。解决加一个CharacterEncodingFilter设request.setCharacterEncoding(UTF-8)JDBC URL 带characterEncodingutf8JSP 页头写% page contentTypetext/html;charsetUTF-8 %建库用 utf8mb4。三处缺一处都会翻车别只改一处就以为好了。5.3 事务不回滚现象下单时库存不足抛了异常但库存还是被扣了。原因多半是 DAO 各自拿连接或者异常被 catch 后没重新抛出事务管理器感知不到失败。解决确认 Service 层统一持有 Connection 并传给 DAOcatch 里必须rollback且把异常往外抛别吞掉。5.4 分页数据错位现象第二页显示的是第一页的后半段或最后一页空白。原因就是偏移量公式写错或者总数查询和列表查询的 WHERE 条件不一致。解决偏移量统一用(page-1)*sizecount SQL 和 list SQL 的过滤条件保持完全一致最好抽成同一个条件拼接方法。5.5 静态资源 404现象页面能开但 CSS/JS 全丢。原因是路径写成了/css/style.css这种根路径部署在非根 contextPath 下就找不到。解决全部改成${pageContext.request.contextPath}/css/style.css或在 JSP 顶部用c:set varctx value${pageContext.request.contextPath}/统一引用。6. 二次开发改造连接池、ORM 替换与接口化的落地思路拿到这套代码想往真实项目靠我建议按「先稳后快」的顺序改别一上来就推翻重写。第一步补连接池这是投入产出比最高的改造引入 Druid 或 HikariCP把DBUtil.getConnection内部换成从DataSource取连接外部调用方一行不用改。改完压一下首页列表并发 50 的情况下响应时间通常能从秒级降到百毫秒级因为省掉了每次建连的 TCP 握手和认证开销。第二步把 JDBC 手工映射换成 MyBatis。这套项目的 DAO 里全是mapRow手工赋值字段一多就是体力活且容易漏。替换路径是保留 Service 层不动把 DAO 接口化用 MyBatis 的 Mapper XML 承接 SQLSqlSession的生命周期交给工具类管理。改造时注意事务边界要从「手动传 Connection」迁移到「SqlSession 提交/回滚」别两套事务机制混用否则会出现一半提交一半回滚的诡异状态。第三步做接口化为前后端分离留口子。现在页面靠 JSP 渲染改造时可以把 Servlet 的 forward 换成 JSON 输出引入 Jackson 序列化前端另起一个静态页调接口。这一步不用全量做先挑图书列表和购物车两个模块试点验证通了再铺开。下面是一个最小改造示例// 把原来的 forward 改成 JSON 响应 resp.setContentType(application/json;charsetUTF-8); ObjectMapper mapper new ObjectMapper(); MapString, Object data new HashMap(); data.put(code, 0); data.put(data, result.getList()); data.put(total, result.getTotal()); resp.getWriter().write(mapper.writeValueAsString(data));验证方法很朴素改造一个模块就跑一遍第 3 章那份功能自检清单确保老功能没退化再对比改造前后的 SQL 执行次数和响应时间。我一般会在 DAO 层加一个简单的计时拦截把每条 SQL 的耗时打到日志里改造前后各跑一轮数据说话比感觉靠谱。最后说个习惯。早年我改这类老项目总想着一次性重构到位结果改到一半新旧逻辑打架回滚都回不干净。从那以后我每次动这种分层项目都强制走一遍「先加测试入口、再小步替换、每步可回滚」的流程哪怕只是换个连接池也先备份一份能跑的 war 包。这套书城代码不复杂但正因为不复杂才适合拿来练这种改造节奏——把它跑通、改顺再去看那些框架封装的东西心里就有底了。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →