简介一套完整的 JavaWeb 在线商城开发学习资源基于 JSP、Servlet、JDBC 与 MySQL 构建并搭配 CSS、JavaScript、jQuery 前端技术适合初学 JavaWeb 的开发者作为综合实战项目参考。项目中实现了商品浏览、商品详情查看、购物车添加与价格计算等核心电商流程从数据库表设计到后端业务处理均有覆盖可帮助理解分层开发思路、JDBC 数据访问方式以及前后端交互机制。压缩包共 2 个文件包含 JavaWeb 商城完整工程源码压缩包与配套的 MySQL 数据库脚本整体大小约 39.94MB导入数据库后即可配合工程运行调试。目前已有 28045 人学习下载适合想通过真实商城案例串联 JSP、Servlet、MySQL 等技术点并完成课程设计或毕业设计的读者。1. 仿小米商城这个JavaWeb项目到底在练什么如果你在自学JavaWeb或者正在为课程设计找选题大概率见过“JavaWeb仿小米在线商城ShoppingMall”这个标题。它不是什么商业项目而是国内JavaWeb教学里最经典的一类综合练习用Servlet、JSP、MySQL和Maven把一个带用户注册登录、商品展示、购物车、下单流程的完整商城从零写出来。我当初做它的时候最大的感受是JSP和Servlet单看都不难但把它们和数据库、会话状态、事务串在一起才是真正的分水岭。这个项目适合谁一是想搞懂传统JavaWeb请求链路的人二是需要交一个能完整演示的课设的人。它不解决高并发也不解决微服务它解决的是“你懂不懂HTTP请求是怎么变成一行INSERT语句的”。2. 搭建ShoppingMall技术选型、目录结构与建表SQL2.1 为什么用ServletJSP而不是直接Spring Boot现在的培训机构一上来就教Spring Boot导致很多人写商城项目时直接甩一个Controller加MyBatis但项目标题既然叫“JavaWeb”最稳妥、最容易过答辩的技术栈反而是经典的Servlet JSP JDBC。原因有三个第一你能在答辩时把请求处理流程说清楚从web.xml里的Servlet映射到doGet和doPost再到转发到JSP页面每一步都是可见的评委问不倒你第二纯JavaWeb项目部署简单一个Tomcat就能跑不需要搞Spring容器那一堆间接层第三很多学校的数据结构、Java程序设计课程考核点就是Java基础语法和JDBCSpring Boot的自动配置反而掩盖了你真正的代码能力。当然如果你已经熟练Spring Boot也可以用spring-boot-starter-web Thymeleaf做一版但那样项目名通常不会叫“JavaWeb仿小米商城”。我见过最尴尬的答辩场面是学生说用了Spring Boot老师问“那你的DispatcherServlet是怎么注册的”学生答不上来。用Servlet JSP至少每个映射都是自己写的被问住了也能翻代码讲。前端部分我建议直接用JSPCSS少量JavaScript不要上Vue。理由很实际JSP页面可以直接用EL表达式和JSTL遍历购物车后端把数据塞进request或session页面就能渲染。如果强行加Vue还得处理跨域、JSON序列化、前端路由工作量直接翻倍对课设来说不划算。2.2 用Maven构建的工程目录长什么样搭建项目我习惯用Maven而不是直接建普通Web项目。Maven能把依赖版本统一管理比如Servlet API、JSTL、MySQL驱动、Druid连接池都写在pom.xml里换机器部署时不会出现“缺一个jar包”的尴尬。最标准的目录结构如下ShoppingMall/ ├── pom.xml ├── src/main/java │ ├── com/mall/entity │ │ ├── User.java │ │ ├── Product.java │ │ ├── CartItem.java │ │ └── Order.java │ ├── com/mall/dao │ │ ├── UserDao.java │ │ ├── ProductDao.java │ │ └── OrderDao.java │ ├── com/mall/service │ │ ├── UserService.java │ │ ├── ProductService.java │ │ └── OrderService.java │ ├── com/mall/servlet │ │ ├── LoginServlet.java │ │ ├── RegisterServlet.java │ │ ├── ProductListServlet.java │ │ ├── CartServlet.java │ │ └── OrderServlet.java │ └── com/mall/util │ └── DBUtil.java └── src/main/webapp ├── index.jsp ├── login.jsp ├── register.jsp ├── product_list.jsp ├── cart.jsp ├── order_confirm.jsp ├── order_success.jsp └── WEB-INF/web.xml这个分层是JavaWeb最经典的Model-View-Controller变体entity装数据载体dao负责SQLservice处理业务规则servlet充当控制器JSP只做展示。我写单元测试时喜欢直接对dao层用main方法跑SQL确认返回结果后再往上调service和servlet这样出问题时能快速定位是SQL写错还是转发路径写错。pom.xml里最少要有这几个依赖javax.servlet-apiprovided作用域因为Tomcat自带、javax.servlet.jsp-api、jstl、mysql-connector-java、druid。其中druid不是必须的但用连接池能避免每次都手动DriverManager.getConnection()尤其商城项目里频繁查商品列表没有连接池的话数据库连接会占用大量内存。2.3 数据库设计商品、用户、订单、购物车四张核心表仿小米商城虽然是“小”项目但表设计直接决定功能上限。我会建四张核心表再加一张订单商品明细表。先说明购物车不一定要建表可以把购物车数据放在Session里但为了练习事务和持久化我建议存库这样下次登录购物车还在。以下是建表SQLCREATE DATABASE shopping_mall DEFAULT CHARSET utf8mb4; USE shopping_mall; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, subtitle VARCHAR(200), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, image_url VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE cart_item ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, UNIQUE KEY uk_user_product (user_id, product_id), CONSTRAINT fk_cart_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT fk_cart_product FOREIGN KEY (product_id) REFERENCES product(id) ); CREATE TABLE order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, product_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, CONSTRAINT fk_order FOREIGN KEY (order_id) REFERENCES order(id) );参数说明price用DECIMAL(10,2)而不是FLOAT因为浮点数算总价会出“0.10.20.30000000000000004”这种问题DECIMAL用于金额是行业惯例stock用无符号INT后面扣库存时要判断stock 0cart_item的UNIQUE KEY保证了同一用户对同一商品只有一条购物车记录这样“加入购物车”就变成INSERT ... ON DUPLICATE KEY UPDATE quantity quantity 1很省事。order表名要加反引号因为ORDER是SQL关键字。我一开始没注意直接用create table orderMySQL直接报语法错误排查了半天。另外order_no建议用yyyyMMddHHmmss 随机数生成不要用自增主键当订单号因为订单号会暴露订单量而且容易被人猜接口遍历。3. 从注册到下单核心业务链路的代码落地3.1 注册登录Session管理和拦截器用户的登录状态我放在Session里属性名统一叫loginUser。注册Servlet处理POST请求时先校验用户名是否重复再用PreparedStatement插入数据库密码用MD5摘要后存储。虽然MD5不算安全但课设够用如果不想被老师吐槽可以用BCrypt这里保持可读性用MD5。WebServlet(/register) public class RegisterServlet extends HttpServlet { private UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); String phone req.getParameter(phone); User user new User(); user.setUsername(username); user.setPassword(md5(password)); user.setPhone(phone); boolean ok userService.register(user); if (ok) { resp.sendRedirect(req.getContextPath() /login.jsp); } else { req.setAttribute(msg, 用户名已经存在); req.getRequestDispatcher(/register.jsp).forward(req, resp); } } }req.setCharacterEncoding(UTF-8)这一行必须放在读取任何参数之前否则中文用户名直接乱码。sendRedirect和forward的区别也要讲清楚注册成功后用重定向避免用户按F5刷新时重复提交表单注册失败用转发这样可以在register.jsp上显示错误消息。很多初学同学在这两个方法之间乱选导致刷新后数据库里多了好几条重复数据。登录逻辑注册逻辑几乎一样唯一需要注意的是登录成功后要把User对象放进Session并且重定向到首页。紧接着要做“拦截未登录用户”的检查不能等用户点到购物车才报错。最传统的做法是写一个Filter把需要登录的路径包起来WebFilter(/cart/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; Object loginUser req.getSession().getAttribute(loginUser); if (loginUser null) { resp.sendRedirect(req.getContextPath() /login.jsp); } else { chain.doFilter(request, response); } } }WebFilter(/cart/*)表示只拦截以/cart/开头的URL我这里把购物车、结算的Servlet路径都设计成/cart/list、/cart/add这样命中规则简单明了。注意/login、/register、商品列表这些路径不能拦否则首页都进不去。3.2 商品列表与分页JSP页面怎么拿数据商品列表页是商城门面仿小米的首页强调大图、价格、标题两行。但技术核心是分页与查询参数。我在ProductServlet里接收page和keyword两个参数调用DAO查询把结果放进request然后转发到product_list.jsp。WebServlet(/product/list) public class ProductListServlet extends HttpServlet { private ProductService productService new ProductService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page 1; int pageSize 12; String pageStr req.getParameter(page); if (pageStr ! null pageStr.matches(\\d)) { page Integer.parseInt(pageStr); } String keyword req.getParameter(keyword); PageResultProduct result productService.pageQuery(page, pageSize, keyword); req.setAttribute(result, result); req.getRequestDispatcher(/product_list.jsp).forward(req, resp); } }pageStr.matches(\\d)是为了防止用户传pageabc导致NumberFormatException。我见过很多项目直接用Integer.parseInt(req.getParameter(page))结果爬虫扫一下URL整站报500。PageResult是一个简单的DTO包含list、totalCount、totalPage、currentPage四个字段。在JSP里遍历数据是这一章的核心用JSTL能少写10倍Java代码% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:forEach items${result.list} varp div classcard img src${p.imageUrl} alt${p.name} h3${p.name}/h3 p${p.subtitle}/p span classprice${p.price}/span a hrefcart/add?id${p.id}加入购物车/a /div /c:forEachEL表达式里${p.price}会自动调用getPrice()DECIMAL类型会被渲染成字符串。如果页面显示${p.name}原样不解析说明web.xml或JSP头里没开EL后面避坑章节会专门讲。分页导航可以直接在JSP里拼链接注意带上keyword不然翻到第二页关键词就丢了c:if test${result.currentPage 1} a hrefproduct/list?page${result.currentPage - 1}keyword${param.keyword}上一页/a /c:if这里用${param.keyword}取请求参数比在Servlet里反复传参更省事。3.3 购物车与订单事务和库存扣减购物车加购逻辑遵循前面说的INSERT ... ON DUPLICATE KEY UPDATE。下订单是全场最需要严谨的地方因为涉及多张表写入往order表插入订单、往order_item批量插入明细、扣减product.stock、清空cart_item。这四个操作必须在一个事务里否则“订单生成了但库存没扣”这种脏数据一旦出现只能人工改库。事务控制要放在Service层DAO层只负责单条SQL。下面是一个典型的下单Service写法public boolean createOrder(User user, ListCartItem cartItems) { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 生成订单 String orderNo generateOrderNo(); Order order new Order(); order.setOrderNo(orderNo); order.setUserId(user.getId()); BigDecimal total BigDecimal.ZERO; for (CartItem item : cartItems) { total total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } order.setTotalPrice(total); orderDao.insert(conn, order); // 2. 插入订单明细 for (CartItem item : cartItems) { orderItemDao.insert(conn, order.getId(), item); } // 3. 扣库存注意这里的WHERE stock quantity for (CartItem item : cartItems) { int updated productDao.decreaseStock(conn, item.getProductId(), item.getQuantity()); if (updated 0) { conn.rollback(); return false; } } // 4. 清空购物车 cartDao.clear(conn, user.getId()); conn.commit(); return true; } catch (Exception e) { try { conn.rollback(); } catch (SQLException ignored) {} e.printStackTrace(); return false; } finally { DBUtil.release(conn); } }扣库存的SQL我会写成UPDATE product SET stock stock - ? WHERE id ? AND stock ?返回影响行数。如果返回0说明库存不足立刻回滚。这个写法的妙处在于利用数据库行锁保证了不会超卖——即使两个人同时下单也只有一个能扣成功。BigDecimal做乘法时不要用double否则算总价有精度问题。DBUtil.getConnection()应该从DruidDataSource获取而不是DriverManager这样连接池能复用连接避免频繁建立TCP连接的开销。4. 让项目在IDEA里跑起来Tomcat配置与三个必调参数4.1 用IDEA配置Tomcat 9与Artifact很多同学代码写好了但跑不起来问题多半出在IDEA的部署配置上。我用的组合是IDEA 2023 Tomcat 9 JDK 1.8 MySQL 8.0。IDEA里跑JavaWeb项目不是直接点运行而是需要先配置一个“Tomcat Server”运行环境。具体步骤如下打开Run/Debug Configurations选择号找到Tomcat Server Local在Server选项卡的Application server里选择本地Tomcat安装目录然后切到Deployment选项卡点选择Artifact...这里必须选war exploded也就是展开目录形式而不是war包。选war的话每次改代码都要重新打包调试效率极低。配置好之后Tomcat默认会打开一个http://localhost:8080/的地址。如果你把Application context设成/ShoppingMall那么访问首页是http://localhost:8080/ShoppingMall/index.jsp。但这个上下文路径要和你的Servlet路径区分开很多同学把WebServlet(/ShoppingMall/login)写成这样结果访问时变成两层前缀直接404。一个容易忽略的点IDEA里web.xml文件位置要在WEB-INF下IDEA新建项目时会自动生成一个空的web.xml。如果你用了WebServlet注解web.xml可以几乎留空但需要至少有一个web-app根元素。否则启动时Tomcat会报“错误: 至少有一个JAR被扫描用于TLD但尚未包含TLD”之类的一堆警告虽然不是致命错误但不干净。4.2 三个必调参数编码、连接池、JSP路径第一个必调参数是JSP页面编码。每个JSP文件头都要写这两行% page contentTypetext/html;charsetUTF-8 languagejava % % page pageEncodingUTF-8 %同时web.xml里建议加一个编码过滤器对所有请求强制设置为UTF-8。很多同学只在Servlet里写了req.setCharacterEncoding但过滤静态资源和JSP转发时还是会乱码不如一次性在过滤器里做掉。第二个必调参数是Druid连接池配置。在pom.xml引入druid后在项目里创建一个druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/shopping_mall?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai usernameroot password你的密码 initialSize5 maxActive20 maxWait60000这里的serverTimezoneAsia/Shanghai是针对MySQL 8.0的。如果你用MySQL 5.7这个参数不写也能跑但MySQL 8.0不带时区会直接报The server time zone value Öйú±ê׼ʱ¼ä。连接池的maxActive不要设太大课设项目并发撑死几十个设20足够设成200反而会浪费数据库资源。第三个必调参数是web.xml里的Session超时时间。不加的情况下Tomcat默认Session超时是30分钟用户逛着逛着就要重新登录。设置成60分钟比较合适session-config session-timeout60/session-timeout /session-config如果商城有“记住我”功能需要把登录状态放到Cookie里那另说。对于课设项目Session超时时间太短是用户体验的坑太长则是安全的坑60分钟是一个中庸值。4.3 静态资源与动态页面混排的注意点仿小米商城的页面里有大量图片、CSS和JS文件。这些静态资源统一放在webapp/static目录下JSP页面用绝对路径引用不能写相对路径。因为相对路径会随着Servlet转发路径变化而错乱比如你在/product/list这个URL下引用static/css/style.css浏览器会去请求/product/static/css/style.css然后404。我一般会在JSP页面顶部用JSTL定义一个basePath变量c:set varctx value${pageContext.request.contextPath} / link relstylesheet href${ctx}/static/css/style.css这样无论在哪个页面${ctx}都会展开成/ShoppingMall所有静态资源请求都能正确落盘。另外Tomcat默认会为webapp根目录下的静态资源做映射但如果你把CSS文件放到WEB-INF下Tomcat为了保护安全会拒绝访问所有样式全丢。这个问题我遇到过一次改了半天代码发现是文件放错位置了。另一个混排细节JSP页面里的${ctx}在JS脚本中也可用但要小心字符串拼接。比如AJAX请求时var ctx ${ctx}; fetch(ctx /cart/add, { method: POST });注意JSP会被服务端先解析所以${ctx}在响应给浏览器之前已经被替换成真实路径这段JS是能正常工作的。如果你把它单独放在.js外部文件里JSP标签不会解析就会变成字符串${ctx}。所以涉及路径的JS代码最好内联到JSP页面不要外链。5. ShoppingMall避坑指南我踩过的5个坑5.1 中文乱码提交表单后用户名变成????现象注册用户名填“小明”数据库里存的是“???”页面回显也是乱码。原因有三层编码没统一。浏览器提交表单默认按页面编码如果页面是UTF-8但Tomcat没有设置请求编码getParameter拿到的是ISO-8859-1解码的字节同时数据库连接URL里没有characterEncodingUTF-8JDBC驱动返回的字符串也是错的。解决第一步确保每个JSP文件头部有pageEncodingUTF-8第二步写一个CharacterEncodingFilter强制设置请求和响应编码为UTF-8第三步在JDBC连接URL中加入characterEncodingUTF-8。三步都做了之后我打赌不会再出现中文乱码。如果你已经存了乱码数据可以用UPDATE user SET username CONVERT(CAST(CONVERT(username USING latin1) AS BINARY) USING utf8mb4)抢救但大概率救不回来还是删了重建干净。5.2 Servlet路径404或405现象点击“加入购物车”链接地址栏URL没问题但页面报404或者明明是POST请求却返回405错误。原因404通常是WebServlet注解里的路径和表单/链接里的路径不一致比如注解写了/cart/add表单action却写了cart/add少了前导斜杠。405是Servlet只重写了doGet但表单请求方式是POST或者反过来。解决所有Servlet路径统一以/开头且要和WebServlet的字符串完全一致。如果页面既要GET又要POST就用protected void service(HttpServletRequest req, HttpServletResponse resp)方法统一接管或者同时重写doGet和doPost里面调同一个业务方法。我用IDEA的Find功能全局搜索action和WebServlet逐个核对一般五分钟能找出来。5.3 数据库连接不上时区与驱动版本现象启动项目后第一次访问商品列表页面等了很久然后报Communications link failure或Unknown initial character set index。原因MySQL服务端编码或时区设置与驱动不兼容。MySQL 8.0之后driverClassName如果还写com.mysql.jdbc.Driver会直接报驱动类找不到必须用com.mysql.cj.jdbc.Driver。解决在pom.xml里使用8.0.33或更高版本的mysql-connector-java然后druid.properties里的URL改成上面我写的那一长串。另外检查MySQL服务是否启动Windows下服务名字可能是MySQL80在命令行跑sc query mysql80确认状态。如果连接还慢检查druid的initialSize连接池初始化时会尝试建立连接这个次数不要设太大。5.4 并发下单导致库存负数现象用两个浏览器同时登录两个账号买同一件只剩一件库存的商品结果两个订单都创建成功库存变成-1。原因下单逻辑里没有对stock做条件更新。如果代码是select stock from product where id ?在Java里判断stock 0再update product set stock stock - 1那么两个线程可能同时读到stock1同时通过判断再先后更新最终结果不对。解决把判断和更新合并成一条原子SQLUPDATE product SET stock stock - ? WHERE id ? AND stock ?然后用返回值判断。这个坑在课设阶段不常被触发但答辩老师很爱问“如何防止超卖”答出这一条能加不少分。更进一步的话题是select ... for update但在单机商城项目里用条件更新足够。5.5 JSP的EL表达式不显示值现象页面源码里直接看到${p.name}浏览器没有渲染成商品名JSP被当成了纯文本。原因web.xml里web-app的版本太低或者JSP页面里设置了% page isELIgnoredtrue %。Tomcat从7开始默认支持EL但如果你用旧版web.xml模板可能没开启。解决检查web.xml的文件头version改成4.0或至少3.1并且不要加isELIgnored属性。另外确保JSP文件后缀是.jsp不是.htmlHTML文件不会被Tomcat的JSP引擎解析。我曾经图方便把页面命名成index.html然后只有静态效果换成.jsp并加头后一切正常。6. 给仿小米商城加点含金量三个进阶改造方向如果课设评分还要更高或者你想把项目写进简历这三个方向按“性价比”排序本地缓存、模拟支付、前端单页优化。先从最简单的开始商品列表做本地缓存。用ConcurrentHashMap模拟一个缓存把ProductService.pageQuery的结果存进内存设置5分钟过期。这个改动只有20行代码但答辩时你能说出“减少数据库压力”这句话。更标准一点可以用Caffeine但课设项目引入一个本地缓存框架反而显得大材小用不如手写一个简单版本还能展示对并发集合的理解。然后是模拟支付。下单成功后不要立刻清空购物车并把订单状态置为“已支付”而是生成订单后跳转到一个支付确认页用户点“模拟支付”按钮后后台把order.status从0改成1。这能让你额外练习“更新操作”和状态机思维也能演示完整的用户流程。实现时只需要一个PayServlet接收orderId调用orderDao.updateStatus。最后是前端优化。仿小米商城的原有页面比较朴素你可以用原生JavaScript给商品列表加一个“快速预览”弹窗或者把价格排序做在前端。预算有限的话优先保证图片有固定宽高避免页面加载时布局抖动。这里说一个我自己的教训不要为了炫技引入Bootstrap轮播图轮播图JS和JSP标签混在一起时图片路径经常写错。老老实实写一个简单的CSS轮播反而更稳。这个项目做完之后最大的收获不是商城本身而是你理解了从浏览器请求到数据库返回的完整路径。以后学Spring Boot时你会知道RequestMapping背后其实就是Servlet的映射MyBatis的Mapper就是在帮你自己拼PreparedStatement。我后来面试时被问“JDBC和ORM的关系”直接拿这个项目的DAO层举例面试官点头认可。如果你正在卡在某个环境问题上不要急着怀疑自己写代码的能力。先按第4章的三个必调参数排查一遍再按第5章逐个对症状。大多数JavaWeb项目的翻车都不是代码逻辑错而是编码、路径、连接池配置这些看起来不重要的细节。希望帮到你。本文还有配套的精品资源点击获取