JSP+Servlet+JDBC实战:手把手搭建网上购物商城系统

JSP+Servlet+JDBC实战:手把手搭建网上购物商城系统 简介面向Java Web零基础或入门阶段学习者这份网上购物商城系统完整演示了JSPServletJDBC的原生开发流程覆盖用户登录、商品展示、购物车管理等核心模块前端引入layui优化交互并配有SQL脚本可直接搭建数据库。资源共201个文件压缩包5.5MB含31个Java源码与class文件、30个JS脚本、5个CSS样式、4个JSP页面及5个Jar依赖另附大量gif演示便于按步骤复现项目。最大亮点是用layui table完成前后端数据传值通过layui模板实现购物车总价与订单价格实时变化Servlet对数据组装封装使返回值能被layui table直接接收适合理解分层与前后端协作。目前已有1653人学习下载既能巩固基础也能为后续学习MyBatis、Spring Boot等框架提前铺垫。 说实话现在再回头折腾 jspservletjdbc 这套组合很多人第一反应是“都什么年代了还在用传统手艺”。但你要是问我我反而觉得这套“老三样”恰恰是理解 Java Web 底层逻辑最扎实的一条路。这次我拿一个带 sql 脚本的网上购物商城系统当练手项目不是因为它多新潮而是因为它足够典型——用户、商品、购物车、订单、会话管理、增删改查一个电商系统该有的核心链路全都有。把这个项目从头到尾跑通你对 Java Web 后端请求处理流程、数据库交互、页面动态渲染的理解会清晰很多。这篇文章我打算从项目设计思路、数据库脚本落地、核心代码实现到踩坑排查完整拆一遍。适合正在学 Java Web 的学生党、准备课程设计/毕业设计的同学以及想回头巩固基础的开发老人。我会尽量把每一步的前因后果讲清楚不光是给你贴代码更想让你知道为什么这么写、换另一种写法会有什么坑。1. 项目整体设计与技术选型思路1.1 为什么选 jspservletjdbc而不是 Spring Boot网上购物系统用 Spring Boot 写三小时能撸一个出来。那为什么还要用 jspservletjdbc原因很简单Spring Boot 帮你把什么都封装好了你反而看不到请求是怎么进 Servlet 的、Statement 是怎么跟数据库交互的、session 到底是什么时候创建的。这个基础练习的目的就是把“壳”去掉让底层逻辑直接裸露在你面前。Servlet 负责接收 HTTP 请求、调用业务逻辑、转发到 JSP 页面JSP 负责渲染动态页面JDBC 负责跟数据库做最原始的沟通。这套链路走一遍你再看 Spring MVC 的 DispatcherServlet、MyBatis 的 SqlSession就能“哦”一声——原来底层都是这套东西。至于为什么不直接上 MyBatis 或 Hibernate是因为 jdbc 连接池、PreparedStatement、事务手动控制这些基础点如果你没亲手写过一遍后面用 ORM 框架时出了问题会一头雾水。先把基础打牢这个项目的定位就是“基础练习”不是“高并发秒杀系统”所以架构上越简单越好不要过度设计。1.2 网上购物商城的核心模块边界划分我在动手写之前先把这个购物系统拆成了四个核心模块用户模块、商品模块、购物车模块、订单模块。注意我没把支付模块加进来因为真实对接支付宝/微信支付需要商户资质练习项目里用一个“模拟支付”的按钮点一下就算付款成功。四个模块的职责如下用户模块注册、登录、退出登录以及个人信息查看。核心是 session 的运用和密码的简单加密处理。商品模块商品列表展示、商品详情查看以及分页查询。核心是 JDBC 的 ResultSet 数据封装和分页 SQL 拼接。购物车模块加入购物车、修改数量、删除商品、查看购物车。核心是购物车数据是存在 session 里还是数据库里这个决策点值得展开讲讲。订单模块下单结算、生成订单、查看我的订单列表。核心是事务管理——扣库存和生成订单必须要在同一个事务里完成。模块边界划分清楚后你会发现代码结构特别好写每个模块对应一个 Servlet 类、一个 DAO 类、一个 JSP 页面目录。别小看这种组织方式很多人项目写乱就是从一开始没规划好包结构。2. 数据库设计与 SQL 脚本落地2.1 五张核心表的字段设计与关系梳理这个项目的数据库设计我用了五张表用户表t_user、商品表t_goods、购物车表t_cart、订单表t_order、订单明细表t_order_item。其中购物车表我特意设计成了数据库存储而不是 session 存储。为什么因为用 session 存购物车虽然开发简单但用户一清浏览器缓存购物车就没了这在实际项目中是不可接受的。数据库存储的好处是购物车数据持久化换个设备登录购物车还在更贴近真实场景。用户表字段如下id主键自增、username唯一、password不为空、nickname、phone、address、reg_time。密码字段我用的是明文VARCHAR(32)因为这是基础练习但我必须提醒你真实项目里绝对要加盐哈希加密这个后面在常见问题里我再细说。商品表字段id、goods_name、priceDECIMAL(10,2)、stockINT、description、image_url、category、create_time。价格用 DECIMAL 而不是 FLOAT/DOUBLE这是个特别容易踩坑的点浮点数算金额会出大问题后面我会展开。订单表id、order_no唯一订单编号、user_id、total_amount、status0 待支付 1 已支付 2 已取消、create_time。订单明细表id、order_id、goods_id、goods_name、goods_price、quantity、subtotal。这里我把商品名称和价格冗余进了明细表这是故意的——如果商品改名或调价历史订单里的快照信息不能跟着变。2.2 SQL 脚本编写要点与初始化数据设计SQL 脚本是这个项目的亮点之一我建议你养成“脚本即文档”的习惯。这个init.sql脚本里我做了三件事建库、建表、插入测试数据。建库时用了CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARACTER SET utf8mb4;表结构统一用DROP TABLE IF EXISTS开头这样脚本可以重复执行不报错。插入测试数据这块我花了点心思。商品表我插了 8 条商品覆盖四个分类手机数码、电脑办公、家用电器、图书文娱。价格故意设计得有零有整比如 2999.00、58.50 这种方便测试金额计算。用户表插了两个测试账号密码统一方便你快速登录测试。有一点要强调商品表的stock字段我初始化得有差异有的库存多有的库存少这样你测试超卖场景时用库存少的那个商品。下单逻辑里有个“检查库存、扣减库存”的流程如果你测试数据全部库存 999这个问题就不容易被触发。脚本我建议用 Navicat 或者命令行执行都行但如果导入时出现中文乱码八成是客户端编码和脚本编码不一致用 UTF-8 执行能解决。3. 核心代码实现与实操过程3.1 JDBC 连接封装与连接池的取舍JDBC 连接这块很多教程会让你直接写一个DBUtil工具类里面用Class.forName(com.mysql.jdbc.Driver)加载驱动然后用DriverManager.getConnection(url, user, password)获取连接。这个写法没错但你要知道这是最原始的版本——每一次数据库操作都建立一次物理连接开销很大高并发场景下数据库会被打死。我在这个练习项目里分了两步走第一步先把最原始的 JDBC 连接写出来让你理解Class.forName的作用和DriverManager的机制第二步我在工具类里用DBCP连接池改了一版初始化时就创建一批连接放着需要用的时候从池子里拿用完还回去。为什么要演示两遍因为如果你一上来就上连接池你可能都不知道它在帮你解决什么问题但你要是只在作业里写个DriverManager你又不了解企业里真实项目是怎么玩的。连接参数里有两个必须加useUnicodetruecharacterEncodingutf8不然中文会乱码useSSLfalse本地调试时避免 SSL 握手告警刷屏。至于serverTimezoneAsia/Shanghai新版 MySQL 驱动不加会报时间戳相关的错。3.2 Servlet 编写方式web.xml 配置与注解配置对照现在写 Servlet 有两种方式一种是古老的在web.xml里通过servlet和servlet-mapping节点注册另一种是在类上用WebServlet(/login)注解直接映射。我在项目里混用了两种写法不是懒是想让你都见一遍。web.xml 的方式长这样写一个类继承HttpServlet重写doGet和doPost然后在web.xml里配置servlet-name、servlet-class、url-pattern。好处是对 Servlet 的映射关系一目了然坏处是配置文件越写越长。注解方式是在类上加WebServlet(/user/login)代码和配置在一起开发效率高但映射关系分散在各个类里。我个人的建议是练习时两种都写一遍理解url-pattern的匹配规则比记住配置方式更重要。比如/login是精确匹配、/api/*是路径匹配、*.do是后缀匹配它们的优先级完全不一样这也是面试常问的点。另外load-on-startup这个配置如果你希望在项目启动时就加载某个 Servlet比如初始化的定时器就把它设为一个正整数数值正数越小优先级越高。3.3 三层架构实现DAO 层、Service 层、Servlet 层、JSP 页面这个项目我用的是经典三层架构Servlet 层接收请求参数、调用 Service 层、把结果放进request或session作用域、最后转发或重定向到 JSPService 层处理业务逻辑比如注册时检查用户名是否已存在、下单时检查库存并开启事务DAO 层用 JDBC 跟数据库打交道每个表对应一个XxxDao类。以用户登录为例完整链路是这样的JSP 登录表单提交到/loginServlet - Servlet 获取username和password参数 - 调用UserService.login(username, password)- Service 层调用UserDao.findByUsernameAndPassword()- DAO 层执行 SQL 查询并返回User对象 - 如果用户不为空Servlet 将用户信息存入session然后重定向到商品列表页如果为空就回到登录页并带上错误提示。这里有个细节重定向回登录页时我用了request.setAttribute(msg, 用户名或密码错误)配合req.getRequestDispatcher(/login.jsp).forward(req, resp)。为什么不直接用sendRedirect因为重定向是两次请求request作用域里的属性在第二次请求时就没了除非你手动拼接 URL 参数否则错误提示传不过去。这个知识点也是大家经常搞混的。3.4 购物车的实现细节与订单事务管理购物车这块既然决定用数据库存储那核心就在t_cart表的设计上。我在设计表时加了user_id和goods_id联合唯一索引这样同一个用户同一个商品不会出现两条记录。用户点击“加入购物车”时先查这个用户是否已经有这个商品有就UPDATE quantity quantity 1没有就INSERT一条新记录。这里是“先查再改”不是一句INSERT INTO ... ON DUPLICATE KEY UPDATE解决后者更快但前者逻辑更清晰适合讲解。订单模块是这个项目的重头戏因为涉及事务。用户点击“结算”时后端要做的事包括根据用户 ID 查出购物车中所有记录、计算总价、生成一个订单号、往t_order表插入订单记录、往t_order_item表插入所有商品明细、扣减t_goods表中对应商品的库存、清空该用户的购物车。这个链路中只要有一环失败数据就会错乱。比如订单生成了但库存没扣减后面就会出现库存一直不变但订单在增加的问题。所以我用conn.setAutoCommit(false)开启事务所有操作完成后conn.commit()提交任何一个环节抛异常就conn.rollback()回滚。JDBC 连接的事务控制就这三行代码到现在 Spring 的Transactional注解还是同一个原理。4. 典型问题排查与避坑实录4.1 数据库连接报错No suitable driver found 与 Access deniedjava.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/shop这个错在初学者里出现频率极高。原因无非三个驱动 jar 包没导入到WEB-INF/lib目录下、连接字符串拼错、mysql-connector-java 版本太高导致 Driver 类名变了。一个快速检查方法是先写个普通的 Java main 方法在命令行里跑通Class.forName(com.mysql.jdbc.Driver)排除 IDE 部署问题再回到 Web 项目里查。Access denied for user rootlocalhost是另一类常见问题大概率是密码不对或者是 MySQL 8.0 以上版本默认用caching_sha2_password加密方式而连接驱动版本太老不兼容。解决办法是把驱动升级到 8.x顺便在连接 URL 中加上allowPublicKeyRetrievaltrue。4.2 JSP 页面中文乱码的三个层面乱码问题每个初学者都会遇到但很多人不知道乱码其实分三个层面。第一层是页面输出乱码JSP 文件顶部要写% page contentTypetext/html;charsetUTF-8 languagejava %同时文件本身要用 UTF-8 编码保存。第二层是请求参数乱码在 Servlet 的doPost里必须调用request.setCharacterEncoding(UTF-8)很多同学只设置了响应编码忘了请求编码。第三层是数据库存取乱码连接 URL 必须加characterEncodingutf8建表时也要指定DEFAULT CHARSETutf8mb4。我排查乱码时的经验是先看服务器控制台有没有乱码IDEA 里要设置-Dfile.encodingUTF-8再看页面显示乱不乱然后往数据库里插一条数据看库里存的是什么。一层层定位就能找出源头千万别一上来就猜。4.3 请求转发与重定向的区分以及 session 的会话管理技巧很多同学总是弄不清楚forward和sendRedirect的区别我在项目里设计的页面跳转逻辑就是活教材。登录失败时我用了 forward因为要给用户显示错误消息这个消息存在 request 作用域里forward 是同一个请求所以拿得到登录成功跳商品列表我用了 sendRedirect因为商品列表页要从数据库实时拉数据用重定向避免用户按下 F5 刷新时表单重复提交。session 的使用也要讲究。登录成功后我把用户 ID 和用户名存到了 session 里购物车查询和订单生成都从 session 里拿用户 ID不再依赖前端传参——这能有效防止用户修改请求参数访问别人的购物车。退出登录时调用session.invalidate()把整个 session 废掉防止会话复用。这是会话管理里的一个基本安全习惯。4.4 金额计算用整数或 Decimal别用浮点型这个坑我用一条商品数据就测出来了。商品价格 58.50 元数量 3 件如果用double去算结果可能变成 175.49999999999997。你想想购物车里两件商品加起来最后显示 175.49999999999997用户看到不得骂死你。我的做法是数据库字段用DECIMAL(10,2)Java 实体类对应字段用BigDecimal所有乘法和加法都走BigDecimal的 API。new BigDecimal(58.50)和new BigDecimal(58.50)结果完全不同前者才是你要的。这个知识点在 Java 面试里被问烂了但实操中依然有很多人踩坑。5. 项目扩展思路与进阶建议5.1 从传统 JSP 到前后端分离的演进路径做完这个基础练习之后建议你先别急着丢弃 JSP而是想想如果要做成前后端分离改动点在哪里。JSP 的核心是服务端渲染Java 代码写好数据后直接拼成 HTML 返回给浏览器前后端分离则是 Servlet 返回 JSON 数据前端用 Vue/React 等框架去渲染。实际上你只需要在 Servlet 里把原本request.setAttribute换成往外输出 JSON 字符串就行了。这块思路想通了你会发现 Spring Boot MyBatis 学起来特别快。Spring Boot 的RestController就是 Servlet 层RequestMapping就是 url-pattern 的注解版RequestBody就是在帮你解析 JSON 请求体MyBatis 的Mapper接口其实就是你手写的 DAO 层的替身。底层链路一通百通。5.2 可以做哪些功能增强这个项目是基础练习思路完全可以继续扩展。比较实用的几个方向一是文件上传给商品加上图片上传功能学一下commons-fileupload组件这是现在前端必备技能二是搜索功能按商品名称模糊查询就是在商品列表页加个LIKE %keyword%查询三是订单状态流转把下单、支付、发货、收货做成一个状态机这是电商系统的核心硬化能力。如果你时间充裕我建议你再做一个简单的过滤器比如未登录用户不能访问个人中心和订单页面。Filter 这个东西在 Servlet 规范里资历比 Spring MVC 拦截器老多了但到现在 Filter 依然是 Web 安全的头道防线——登录校验、乱码处理、接口计时全都可以在 Filter 里做。6. 写在最后的个人经验这个网上购物商城项目我前前后后带学生跑过很多遍每次都会发现新的细节值得打磨。如果你是从零开始学 Java Web我给你的建议是不要照着别人的代码敲一遍就完事而是合上教程自己从头写哪怕写得很慢很烂。遇到报错是好事报错信息就是你的老师每解决一个 bug 你对整个 Servlet 容器和数据库交互的理解就会深一层。特别是 SQL 脚本执行务必自己动手在命令行里跑一遍不要从来都是点 IDE 按钮自动完成命令行会告诉你更真实的报错信息。最后再分享一个小技巧做这种带数据库的 Web 项目时每写一个模块就单独测试一遍别等所有代码写完了再部署启动。“每写 50 行代码就部署一次”是我个人比较推崇的频率这样出了问题你能立刻定位到最近的改动。等你把这个项目完整跑通回头再看 Spring Boot 这类封装框架你会发现自己终于能“看穿”它们了。本文还有配套的精品资源点击获取