ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaWeb购物平台实战:IDEA+Tomcat+Servlet+MySQL全链路解析

JavaWeb购物平台实战:IDEA+Tomcat+Servlet+MySQL全链路解析 简介基于IDEA环境和JavaWeb技术实现的多角色网络购物平台项目面向JavaWeb初学者及需要完成期末作业的高校学生。项目覆盖登录注册、按身份跳转不同界面用户、商家、管理员、用户地址与密码修改、订单查看、热销商品展示、购物车管理和结算等关键流程商家可提交商品改动申请管理员审核后更新商品表适合用来理解权限设计和业务分层。压缩包共1083个文件、约26.06MB前端以Vue、JS、TS、CSS等页面样式文件为主后端包括Java、JSP、Class等逻辑文件另有XML、Properties配置文件及少量图片、文档和Git工程文件目录与模块划分清晰整体结构便于直接导入IDEA学习运行。已有1959人学习下载可作为期末项目参考或课程设计二次开发底稿便于对照搭建环境、排查部署问题对于JavaWeb初学者还能借助源码理解登录注册、权限分发、购物车与结算等功能的实现思路。1. 用IDEA把JavaWeb购物平台跑通先要认清楚这三件事用IDEA开发基于JavaWeb的网络购物平台不是让你去研究某个前端框架也不是让你背Spring Boot的注解它练的是Web应用最原始的一条链路浏览器发请求Tomcat把请求交给ServletServlet调Java代码查数据库再把结果渲染成JSP页面返回。很多人简历上写熟悉JavaWeb真拿到一个空IDEA要新建项目时连Tomcat和Servlet依赖都配不明白或者抄来的项目一运行就是一连串红色报错。这篇文章把用IDEA从零搭购物平台的路径讲透JDK/Tomcat版本怎么选分层结构和四张核心表怎么设计登录、分页、下单这几条Servlet链路怎么落代码以及项目跑起来之后在IDEA里怎么热部署、怎么排查异常。适合拿它做课设的在校同学也适合框架用了好几年、想回头把基础链路补齐的一线开发。2. IDEA里配置JavaWeb购物项目版本选型与Tomcat部署2.1 JDK、Tomcat、Servlet版本匹配表配置错了代码跟着全错IDEA本身不限制JavaWeb的版本组合但Tomcat和Servlet API的版本是绑死的。初学者最容易踩的坑是装了Tomcat 10然后从旧博客抄了一段javax.servlet.http.HttpServlet的代码编译时发现所有Servlet相关类都不见了。原因很简单Tomcat 10开始把包名从javax.*迁移到了jakarta.*旧代码全部失效。建议用下面这套组合组合方案JDKTomcatServlet APIJSP API适用场景方案AJDK 8Tomcat 9.xjavax.servlet-api 4.0.1JSP 内置课程设计、旧项目复现网上案例最多方案BJDK 11Tomcat 9.xjavax.servlet-api 4.0.1JSP 内置需要较新JDK特性且兼容旧依赖方案CJDK 17Tomcat 10.1.xjakarta.servlet-api 5.0jakarta.servlet.jsp-api想用新Tomcat但代码要改包名大多数JavaWeb购物平台案例、黑马的笔记数据、网上的课设源码都是基于javax和Tomcat 8/9所以课程设计阶段最稳的是方案A。如果后续要切到Tomcat 10主要工作就是把所有import javax.servlet.*改成import jakarta.servlet.*业务代码本身几乎不用动。IDEA社区版和Ultimate版都能跑JavaWeb区别在于Ultimate对Jakarta EE、CDI等企业级技术有专门支持而社区版做JSPServlet项目完全够用。装好IDEA之后先确认Project Structure里的Project SDK选到了JDK 8Language level也设置成8不然编译时会报无效的源发行版之类的错误。2.2 在IDEA里配置Tomcatwar exploded模式才是调试首选下载Tomcat 9压缩包后建议放到一个没有空格、没有中文的路径下比如D:\apache-tomcat-9.0.89。这是因为Tomcat启动脚本对路径里的空格处理历史上有过坑IDEA集成Tomcat时也经常因路径中文导致日志乱码。在IDEA里配置Tomcat的常见做法是打开Run菜单 →Edit Configurations→ 左侧加号 → 选Tomcat Server→Local。在Application server处点击Configure选择Tomcat主目录IDEA会自动识别版本号。切到Deployment页签点加号添加Artifact选择war exploded结尾的那一项。回到Server页签把On frame deactivation设置成Update classes and resources。这里war exploded值得多说一句。普通war包模式每次改动代码都要重新构建war包再发布相当于每次启动都做一遍打包动作war exploded是解压后的文件夹Tomcat直接加载这个目录配合IDEA的编译输出目录改完代码按CtrlF9编译classes和资源文件就更新了刚改完的Servlet下一次请求就能用。Community版在较新版本里默认没有Tomcat Server的运行配置项但可以到Settings → Plugins搜索Smart Tomcat插件补上插件支持选择Tomcat主目录和Deployment目录效果和官方集成类似社区版用户用这个方案很普遍。2.3 pom.xml基础依赖servlet和jsp的scope必须填provided如果是Maven工程pom.xml里的依赖配置会直接决定项目能不能编译、war包里有什么。先看一个可以直接复用的依赖清单dependencies !-- Servlet APITomcat 自带打包时不需要打进去 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同上 -- dependency groupIdjavax.servlet/groupId artifactIdjsp-api/artifactId version2.0/version scopeprovided/scope /dependency !-- JSTL 标签库JSP 页面里的 c:forEach 依赖它 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 驱动MySQL 8.x 必须用 8.x 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependenciesprovided的作用是告诉Maven这个依赖编译时需要但不要打包进最后的war包因为Tomcat的lib目录里已经有Servlet和JSP的实现类了。如果scope写成了默认的compilewar包里会出现两份Servlet API部署后可能引发类加载器层面的冲突具体表现就是诡异的NoClassDefFoundError。MySQL驱动的版本选择有个容易踩的坑如果连接的是MySQL 5.7用8.0.33驱动没问题连接MySQL 8.x必须用8.x的驱动否则连接时会直接报CLIENT_PLUGIN_AUTH is required。Druid版本建议选1.2.x1.1.x在某些JDK 8版本下会报警告日志不影响运行但看着很闹心。3. JavaWeb购物平台的分层脚手架与MySQL表设计3.1 实体层到Servlet层的职责划分四层各守边界购物平台的代码结构一旦混乱最常见的表现是JSP里写JDBC查询Servlet里做金额计算DAO里处理转发跳转。这种写法在小项目里能跑但每加一个功能就要牵动一堆文件改到后面自己都不想看。按JavaWeb项目完整案例里最常见的做法把代码分成四层层次常见类名职责不该做的事entityUser.java / Goods.java一张表对应一个JavaBean字段和表结构一致不写任何业务逻辑daoUserDao.java / GoodsDao.java只写SQL和JDBC操作返回实体或列表不判断密码对不对、不处理库存serviceUserService.java / OrderService.java参数校验、事务控制、库存判断不持有HttpSession不写跳转servletLoginServlet.java / GoodsListServlet.java解析请求参数、调用service、跳转页面不写SQL、不算订单金额service这一层在很多课设里被省略直接在Servlet里调DAO。放在一个订单流程里问题会立刻暴露下单时既要扣库存又要拆订单明细如果Servlet里直接两个DAO操作事务就没法控制——第一个DAO执行成功第二个DAO失败库存扣了但订单没生成。有了service层才能把多个DAO放进同一个事务方法里。3.2 建表SQL用户表、商品表先落到MySQL购物平台的所有功能最终都落在数据库上。在创建表之前先在MySQL里建一个独立数据库比如shop字符集选utf8mb4。注意不是utf8因为utf8mb4才是真正的完整UTF-8能存下表情符号和生僻字商品名里一旦出现特殊字符用老字符集可能直接写入失败。用户表和商品表可以直接用这段SQLCREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(32) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT MD5后的密码, nickname VARCHAR(32) DEFAULT COMMENT 昵称, phone VARCHAR(20) DEFAULT COMMENT 手机号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_goods ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 商品ID, goods_name VARCHAR(128) NOT NULL COMMENT 商品名称, price DECIMAL(10,2) NOT NULL COMMENT 单价, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, image_path VARCHAR(255) DEFAULT COMMENT 商品图片路径, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 上架时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;用户表的username字段建议加UNIQUE约束这样注册时不用先查一遍再插入直接捕获数据库的唯一键冲突就能判断用户名已被占用。password字段长度定64是因为MD5摘要值是32位十六进制字符再加盐之后最长会到60多位64留了安全余量。商品表的price必须用DECIMAL(10,2)而不是FLOAT或DOUBLE后两者是浮点数计算0.1 0.2会出现精度偏差订单金额一旦算错直接影响到账。3.3 购物车与订单表外键约束到底加不加购物车和订单是购物平台里最核心的两张关联表。购物车可以单独建一张表也可以存在Session里这个选择放在第4章讲但订单表必须设计好因为它要支撑整个下单链路。CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(64) NOT NULL COMMENT 订单编号, user_id INT NOT NULL COMMENT 下单用户ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表; CREATE TABLE t_order_item ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 明细ID, order_id INT NOT NULL COMMENT 所属订单ID, goods_id INT NOT NULL COMMENT 商品ID, goods_name VARCHAR(128) NOT NULL COMMENT 下单时的商品名快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时的单价快照, quantity INT NOT NULL COMMENT 购买数量 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;外键约束课设里建议不建物理外键只在逻辑上维护关联关系。理由有两点一是物理外键会在插入、删除时多做一致性检查下单高频场景下有一定性能损耗二是如果建了外键后面想删测试数据或者批量改数据会频繁撞上外键约束报错。真正的线上系统一般也不挂物理外键靠应用层保证一致性。订单明细里的goods_name和price是特意做的快照金额和商品名在下单那一刻被写死。商品表里的价格之后可以改但历史订单必须保留当时的成交价这也是表设计里一个容易忽略的细节。3.4 用Druid配合DButil统一管理数据库连接项目里每个DAO都要获取数据库连接如果不做统一封装每个类里都写一遍DriverManager.getConnection代码重复不说连接用完不关还会把数据库连接池耗尽。常见做法是用Druid连接池加一个静态工具类。在src/main/resources下创建druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue usernameroot password123456 initialSize5 maxActive20 maxWait3000再写一个DButil工具类import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DButil { private static DataSource dataSource; static { try (InputStream in DButil.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(连接池初始化失败 e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static DataSource getDataSource() { return dataSource; } }URL里的参数逐个说明serverTimezoneAsia/Shanghai解决MySQL 8驱动与本地时区不一致导致的报错useSSLfalse是因为本地开发环境不需要加密连接不写可能多一次SSL握手低版本驱动甚至会直接失败allowPublicKeyRetrievaltrue是MySQL 8必须的否则连接时可能抛Public Key Retrieval is not allowed。连接池参数里initialSize是启动时预创建的连接数maxActive是最大连接数maxWait是拿不到连接时的等待时间单位毫秒开发环境maxActive设10到20足够。4. 网络购物平台的核心Servlet链路登录、分页、购物车、下单4.1 注册登录的会话保持与过滤器拦截注册登录是购物平台的入口。密码不能明文存库至少做一次MD5加盐可以写一个MD5Util工具类在service层对原始密码加固定盐再做摘要数据库里只存密文。登录成功后把用户对象放进Session而不是只放一个用户名因为后续下单要拿user.id写订单表。WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password MD5Util.md5(req.getParameter(password)); User user userService.login(username, password); if (user null) { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } else { req.getSession().setAttribute(user, user); resp.sendRedirect(req.getContextPath() /goods/list); } } }这段代码里最值得留意的是失败和成功走了两个不同方向失败用forward转发回登录页msg属性能在JSP上用${msg}直接显示成功用sendRedirect重定向到商品列表页。区别在于转发是一次请求内的跳转地址栏不变刷新会再次提交表单造成重复登录重定向是浏览器发第二次请求地址栏变成了商品列表URL天然规避了表单重复提交。登录拦截用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 user req.getSession().getAttribute(user); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }注意WebFilter(/cart/*)这种写法在Servlet 3.0规范里叫通配符匹配所有以/cart/开头的请求都会先经过这个过滤器不需要在web.xml里再配一遍。如果项目用的是web.xml配置风格则需要在xml里声明filter-mapping。4.2 商品列表分页Page对象与LIMIT子句商品列表不能一次把全表数据查出来需要分页。通用的做法是封装一个Page对象包含当前页码、每页条数、总记录数和当前页数据再配合数据库LIMIT实现物理分页。public class PageT { private int pageNo; // 当前页码从1开始 private int pageSize; // 每页条数 private int totalCount; // 总记录数 private int totalPage; // 总页数 private ListT data; // 当前页数据 }DAO层分页方法的SQL如下public ListGoods findByPage(int pageNo, int pageSize) throws SQLException { QueryRunner runner new QueryRunner(DButil.getDataSource()); String sql SELECT * FROM t_goods WHERE status 1 ORDER BY id DESC LIMIT ?, ?; return runner.query(sql, new BeanListHandler(Goods.class), (pageNo - 1) * pageSize, pageSize); }LIMIT子句有两个参数第一个是偏移量第二个是取几条。pageNo是前端传上来的当前页码用户看到的第1页对应数据库里偏移0条所以偏移量要写成(pageNo - 1) * pageSize。总页数的计算公式是(totalCount pageSize - 1) / pageSize这个写法能保证totalCount正好整除时不会多出一页空数据。4.3 购物车放Session还是放数据库要按项目规模选购物车实现有两种风格。课程设计和演示项目里最常用的是把购物车放在Session里因为不需要建表也不需要处理购物车数据持久化但用户关闭浏览器购物车就丢了。真实电商平台会把购物车放数据库用户在手机和电脑之间切换能看到同一份购物车。Session购物车可以用一个MapGoods, Integerkey是商品对象value是购买数量。加购时判断Map里是否已有该商品有则数量加1没有则put进去。JSP页面上用c:forEach遍历Map展示商品列表和数量。如果决定购物车入库需要一张t_cart表结构参考第3章的t_order_item多加一个user_id字段。加购操作就变成INSERT INTO t_cart(user_id, goods_id, quantity) VALUES(?, ?, 1) ON DUPLICATE KEY UPDATE quantity quantity 1配合user_id和goods_id的唯一索引一次SQL既处理首次加购又处理重复加购不需要先select再update。选型建议是功能偏好简单、重点是演示购物完整流程用Session就够了想在项目里展示更完整的事务处理和复杂SQL能力就把购物车落库这样整个购物平台的复杂度会上一个档次。4.4 下单事务扣库存、生成订单、写明细的三个动作必须同生共死下单是整个购物平台里最考验代码质量的一环。它要完成三件事扣减库存、插入订单主表、插入订单明细。这三件事任何一个失败前面的操作都不能生效必须包在同一个数据库事务里。事务代码的骨架如下public Order createOrder(int userId, MapInteger, Integer cart) throws SQLException { Connection conn DButil.getConnection(); try { conn.setAutoCommit(false); // 1. 扣减库存带条件的UPDATE返回0说明库存不够 for (Map.EntryInteger, Integer entry : cart.entrySet()) { String sql UPDATE t_goods SET stock stock - ? WHERE id ? AND stock ?; int rows runner.update(conn, sql, entry.getValue(), entry.getKey(), entry.getValue()); if (rows 0) { throw new SQLException(商品库存不足ID entry.getKey()); } } // 2. 生成订单主表取得自增ID // 3. 根据订单ID批量插入订单明细 conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }注意UPDATE t_goods SET stock stock - ? WHERE id ? AND stock ?这个写法它把库存判断和扣减合并到了同一条SQL里利用的是数据库的行锁同一秒内多个用户同时买同一个商品只有一个事务能更新成功另一个会因rows 0拿到库存不足的结论。如果先SELECT stock再UPDATE两次操作之间商品可能已经被别人买了这就是典型的超卖隐患。5. IDEA调试JavaWeb购物平台的三个实战技巧热部署、断点、异常定位5.1 开启IDEA热部署省掉反复重启Tomcat的时间IDEA做JavaWeb开发最烦的事情是改一行println就要重启Tomcat。多数人的习惯是restart server但正确操作是配置热部署的三个选项在Edit Configurations的Server页签里On frame deactivation选Update classes and resourcesOn update action选Update classes and resources。改动Java类文件后按CtrlF9重新编译Tomcat会增量加载新的class改JSP页面连编译都不用Tomcat默认对JSP有修改检测保存后刷新浏览器即可看到新页面。需要重启的场景只剩两种情况改了web.xml、加了新的WebServlet注解或者改了pom.xml依赖。5.2 断点打在Servlet的doPost第一行看request参数比看页面快排查表单传参问题时与其在JSP和Servlet之间来回猜不如直接在Servlet方法开头打一个断点用Debug模式启动Tomcat访问页面后在断点处用IDEA的Evaluate表达式查看req.getParameterMap()一次性列出所有前端提交的参数名和值。购物平台的注册和登录表单字段多经常出现username和user_name不一致前端传了A名字后端取B名字导致空值这一步可以最快定位。Debug模式下还能直接修改变量的值再继续走比如临时改Session里的用户ID不用重新填表单。5.3 404、500、中文乱码的定位顺序这三个异常是购物平台开发里出现频率最高的排查顺序可以固定成一套思路报错现象检查顺序具体操作404urlPatterns、访问路径、资源位置先看WebServlet注解的路径是否包含项目上下文再对比浏览器地址和注解路径是否完全一致500日志Caused by、依赖、SQL往IDEA控制台最下方翻Caused by那一行其他堆栈都是陪衬中文乱码页面、请求、数据库三段JSP加pageEncodingUTF-8Servlet加req.setCharacterEncoding(UTF-8)连接URL带characterEncodingutf8见第3章Druid配置乱码问题里最常见的坑是请求参数乱码POST请求必须调用req.setCharacterEncoding(UTF-8)而且要在第一次读取参数之前调用GET请求的参数编码由Tomcat的URIEncodingUTF-8控制需要在server.xml的Connector里配置。数据库侧如果表已经建成了latin1即使JDBC配置正确中文写入后仍然显示问号那时候只能改表字符集所以建表时一定要用utf8mb4这一步后续想补票的成本很高。本文还有配套的精品资源点击获取
返回列表