简介:面向Java Web零基础或入门阶段学习者,这份网上购物商城系统完整演示了JSP+Servlet+JDBC的原生开发流程,覆盖用户登录、商品展示、购物车管理等核心模块,前端引入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等框架提前铺垫。 说实话,现在再回头折腾 jsp+servlet+jdbc 这套组合,很多人第一反应是“都什么年代了还在用传统手艺”。但你要是问我,我反而觉得这套“老三样”恰恰是理解 Java Web 底层逻辑最扎实的一条路。这次我拿一个带 sql 脚本的网上购物商城系统当练手项目,不是因为它多新潮,而是因为它足够典型——用户、商品、购物车、订单、会话管理、增删改查,一个电商系统该有的核心链路全都有。把这个项目从头到尾跑通,你对 Java Web 后端请求处理流程、数据库交互、页面动态渲染的理解会清晰很多。
这篇文章我打算从项目设计思路、数据库脚本落地、核心代码实现到踩坑排查,完整拆一遍。适合正在学 Java Web 的学生党、准备课程设计/毕业设计的同学,以及想回头巩固基础的开发老人。我会尽量把每一步的前因后果讲清楚,不光是给你贴代码,更想让你知道为什么这么写、换另一种写法会有什么坑。
1. 项目整体设计与技术选型思路
1.1 为什么选 jsp+servlet+jdbc,而不是 Spring Boot
网上购物系统用 Spring Boot 写,三小时能撸一个出来。那为什么还要用 jsp+servlet+jdbc?原因很简单: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、price(DECIMAL(10,2))、stock(INT)、description、image_url、category、create_time。价格用 DECIMAL 而不是 FLOAT/DOUBLE,这是个特别容易踩坑的点,浮点数算金额会出大问题,后面我会展开。
订单表:id、order_no(唯一订单编号)、user_id、total_amount、status(0 待支付 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,你又不了解企业里真实项目是怎么玩的。
连接参数里有两个必须加:useUnicode=true&characterEncoding=utf8,不然中文会乱码;useSSL=false,本地调试时避免 SSL 握手告警刷屏。至于serverTimezone=Asia/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作用域、最后转发或重定向到 JSP;Service 层处理业务逻辑(比如注册时检查用户名是否已存在、下单时检查库存并开启事务);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 denied
java.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 'root'@'localhost'是另一类常见问题,大概率是密码不对,或者是 MySQL 8.0 以上版本默认用caching_sha2_password加密方式,而连接驱动版本太老不兼容。解决办法是把驱动升级到 8.x,顺便在连接 URL 中加上&allowPublicKeyRetrieval=true。
4.2 JSP 页面中文乱码的三个层面
乱码问题每个初学者都会遇到,但很多人不知道乱码其实分三个层面。第一层是页面输出乱码,JSP 文件顶部要写<%@ page contentType="text/html;charset=UTF-8" language="java" %>,同时文件本身要用 UTF-8 编码保存。第二层是请求参数乱码,在 Servlet 的doPost里必须调用request.setCharacterEncoding("UTF-8"),很多同学只设置了响应编码忘了请求编码。第三层是数据库存取乱码,连接 URL 必须加characterEncoding=utf8,建表时也要指定DEFAULT CHARSET=utf8mb4。
我排查乱码时的经验是:先看服务器控制台有没有乱码(IDEA 里要设置-Dfile.encoding=UTF-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 这类封装框架,你会发现自己终于能“看穿”它们了。
本文还有配套的精品资源,点击获取