☰
JavaWeb商城项目全解析:三层架构、部署流程与踩坑指南
2026/10/20 4:11:18 网站建设 项目流程

简介:基于JavaWeb技术打造的网上商城系统实例,面向Java初、中级学习者及课程设计/毕业设计场景。系统以Servlet处理业务逻辑、JSP构建动态页面、JavaBean封装数据,搭配MySQL完成持久化,覆盖用户注册登录、商品展示、购物车、订单管理等电商核心模块,有助于理解典型Web应用的分层架构与请求响应流程。压缩包共2个文件,rar为项目完整源码,含Java类、配置文件、JSP页面、静态资源及WEB-INF目录结构;sql为数据库初始化脚本,用于建表并填充演示数据,方便直接导入搭建环境。整体大小58.24MB,结构紧凑且分类明确。已有962人学习下载,实践参考价值较高;运行项目可系统梳理JavaWeb与MySQL的联动方式,掌握JSP标签库用法与数据交互技巧,也能作为实训项目二次开发的基础模板。

1. 那种“满屏都是代码”的JavaWeb商城项目,到底在解决什么问题

如果你正在做JavaWeb课设,或者在面试前想找一个能讲清楚“请求从浏览器到数据库再回来”的完整链路,那么这种“品牌风格电子商城模板”类的zip资源,是比啃SSH框架文档更快的路径。这类项目多半是JSP + Servlet + MySQL的老牌三层结构,没有Spring,没有MyBatis,反而把Servlet生命周期、Session会话、JDBC事务这些JavaWeb最核心的机制暴露得明明白白。适合三类人:准备课程设计答辩的学生、想补JavaWeb基础的在职开发者、以及需要给企业老项目做维护而被迫回头啃JSP的人。拆完这个包你会发现,它解决的不是“写一个商城”这个业务问题,而是“理解JavaWeb请求响应全链路”这个认知问题。

2. JavaWeb商城项目的结构与选型:拿到zip后先看清这六个目录

2.1 三层架构的实体映射:从web.xml到Dao层是怎么串起来的

解压之后,不要急着点运行。先在IDE里展开src目录,对照着web.xml走一遍请求路径。这个商城项目的典型分包是com.xxx.servlet、com.xxx.service、com.xxx.dao、com.xxx.entity、com.xxx.util。一整套链路是:浏览器访问index.jsp,表单提交到LoginServlet,Servlet调用UserService,Service调UserDao,Dao通过JDBC访问MySQL,返回结果再一层层传回JSP页面。这个过程里没有框架帮你做对象转换,每条数据都是手动set进去的,所以读代码时重点看两个地方:DAO层的ResultSet取值逻辑,以及Service层是否有事务控制。

我一般拿到包后会先看web.xml里的servlet-mapping,因为这里的URL映射方式决定了你后面改代码时动哪里。多数课设项目用的是 /login、/register、/cart 这样的精确映射,配合@WebServlet注解混用。如果web.xml里同时存在Servlet声明和注解,注意容器会以web.xml为准,改注解不生效是常见问题。

<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.shop.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>

这个配置的意思是:所有访问 /login 的POST或GET请求,都交给com.shop.servlet.LoginServlet处理。注意url-pattern必须以/开头,不能写成“login”这种不带斜杠的形式,否则容器启动时直接报错。另外,如果你在LoginServlet类上还加了@WebServlet("/login"),而web.xml也配了这个映射,实际生效的是web.xml,排查问题时先确认你是不是被这个双写搞晕了。

实体类方面,记得对照数据库表结构看一遍。User、Product、Order、OrderItem四个类是这套电商的基础。字段类型要和数据库列严格对应,尤其是Decimal类型对应BigDecimal,DateTime对应Timestamp。常见错误是订单金额用double接收,导致金额精度丢失,页面上出现 99.999999 这种数,这种问题不是代码逻辑错,是类型选错。

2.2 数据库脚本与连接池配置:改三个参数就能连上你的MySQL

数据库是这个项目能跑起来的命门。压缩包里一般会带一个shop.sql文件,里面是建库、建表、插入测试数据的完整脚本。先打开它确认数据库名、表名前缀、以及默认的字符集。注意建表语句里的ENGINE=InnoDB和CHARSET=utf8mb4这两项,InnoDB是事务支持的前提,utf8mb4是表情符号和生僻字不乱码的前提,缺一不可。

连接数据库的配置集中在src的jdbc.properties文件里。拿到项目后,先把这里的地址、用户名、密码改成你自己的。常见坑是MySQL版本不一样,驱动类也不一样。MySQL 5.7及以下用com.mysql.jdbc.Driver,MySQL 8.0以上要用com.mysql.cj.jdbc.Driver,并且URL后要带serverTimezone和useSSL参数。我建议直接把驱动jar换成mysql-connector-java 8.0系列,然后统一用下面这个配置,不管对方用的MySQL版本是5.7还是8.0都能跑。

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/shop?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=root jdbc.password=你的数据库密码

这段配置里最关键的是url。useSSL=false是关掉SSL握手,本地开发能少一层加密协商,速度快且不会出现SSL警告刷屏。serverTimezone=Asia/Shanghai是解决MySQL 8.0的时区报错,如果不加,连接时会出现“The server time zone value”异常。characterEncoding=utf8是强制字符集,配合JSP页面的UTF-8声明,能避免中文乱码。密码那行,建议不要用root的超级密码,单独建一个账号授权,避免项目里写死密码的代码泄露后影响整个数据库。

连接池方面,这个项目如果用的是c3p0,配置在c3p0-config.xml;如果用的是Druid,配置在druid.properties。我个人更推荐Druid,监控页面能直接看到SQL执行次数和慢查询。下面这段是以Druid方式获取连接的工具类代码,很多课设模板里都有类似实现:

public class DbUtil { private static DataSource dataSource = null; static { try { Properties props = new Properties(); props.load(DbUtil.class.getClassLoader() .getResourceAsStream("druid.properties")); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() { try { Connection conn = dataSource.getConnection(); // 事务操作时自行开启,默认自动提交 return conn; } catch (SQLException e) { throw new RuntimeException("数据库连接获取失败", e); } } }

这个工具类的逻辑很直白:静态代码块在类加载时执行一次,读取druid.properties并创建连接池;所有DAO通过DbUtil.getConnection()拿连接。注意注释里标的那行,默认情况下Connection是自动提交事务的,如果你想在订单创建这种多步操作里手动控制事务,必须调用conn.setAutoCommit(false)。连接池的初始大小、最大连接数由druid.properties里的initialSize和maxActive控制,一般本地开发设initialSize=5、maxActive=20就够了。

3. 部署到Tomcat的完整流程:从导入工程到浏览器看到首页的六个关键设置

3.1 在IDEA里导入项目并绑定本机Tomcat的细节

这个zip解压出来,如果是Eclipse风格的目录结构(包含.classpath和.project文件),IDEA不能直接Open,要选Import Project,然后选Eclipse模式导入。如果是Maven结构(有pom.xml),那就直接用IDEA的Maven导入。判断方法很简单:看根目录下有没有pom.xml,有就是Maven工程,没有就是传统Web工程。

导入后第一件事是配置Project Structure。SDK选JDK 8,Language Level选8。然后看Libraries里有没有引入lib目录下的jar包,尤其是servlet-api.jar、mysql-connector-java.jar、jstl.jar。如果这些jar没有引入,代码里import javax.servlet.*的部分会标红,编译都过不去。顺便说一句,如果里面没有这些jar,而是用了Maven的pom.xml,就到本地仓库确认下依赖是否下载完整。

接下来是配置Tomcat。点Run菜单的Edit Configurations,添加Tomcat Server Local。在Server标签页的Application server里选你本机的Tomcat,然后切到Deployment标签,点加号选Artifact,如果你的工程名是shop_war_exploded,说明这是war包展开模式,调试时改JSP不用重启。注意:Deployment里Application context最好设成 /shop,这样浏览器访问地址就是 http://localhost:8080/shop/index.jsp,和项目里的base路径尽量保持一致,避免CSS和JS因为上下文路径不对全部404。

3.2 数据库初始化与账号授权:比执行SQL多一步

打开MySQL命令行或Navicat,先执行建库脚本。但执行前改两个东西:一个是把脚本开头的CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARACTER SET utf8mb4;确认存在的编码是utf8mb4;另一个是检查脚本里的表是否用了外键约束。如果表之间用了FOREIGN KEY,初始化时注意执行顺序,先建父表再建子表,否则会出现外键关联失败。

mysql -u root -p source /解压目录/sql/shop.sql;

执行source命令时,路径如果包含中文或空格,必须先进入sql文件所在目录再执行,否则MySQL会提示找不到文件。执行完后用SHOW TABLES;确认至少能看到user、product、orders、order_item这几张表。然后建议顺手查一下商品表的数据,看图片字段存的是相对路径还是完整URL。很多模板项目里商品图片存的是 images/product/1.jpg 这种相对路径,图片在webapp的images目录下,部署后是能访问的。如果存的是C盘绝对路径,那部署到别的机器后图片全部裂开,这个后面说。

3.3 启动运行并处理首次访问的三类典型问题

配置完启动Tomcat,控制台出现“INFO: Server startup in xxx ms”就是启动成功。这时候不要急着点index.jsp,先做一件事:直接访问 http://localhost:8080/shop/,看能不能跳到首页。

如果页面显示404,先看控制台报错是“Unable to compile class for JSP”还是“The requested resource is not available”。前者是JSP编译错误,一般是页面里有语法错误或者依赖的Java类没编译进去;后者是上下文路径不对,检查IDEA里Deployment的Application context是否与访问路径一致。

如果页面显示500,重点看异常堆栈。最典型的三种:第一种是java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,说明驱动jar没放到WEB-INF/lib或者Tomcat的lib目录,注意IDEA里Libraries引入和部署到Tomcat是两回事,必须在Artifact的Output Layout里确认jar被打进了WEB-INF/lib;第二种是CommunicationsException: Communications link failure,说明数据库没启动或URL里的端口不对,确认MySQL服务是否在运行、端口是3306还是别的;第三种是Unknown database 'shop',说明建库没成功,回到命令行重新执行一次建库脚本。

4. 核心功能拆解:登录、购物车与订单模块的代码走读

4.1 登录与Session会话:看懂这三十行就能给面试官讲清楚

登录逻辑是这个项目里最值得花时间读的部分。它把请求参数获取、调用Service、Session保存用户状态、页面跳转四个动作完整串起来。下面这段代码是典型的LoginServlet实现,不同模板里的字段名可能略有差别,但骨架基本一致:

@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.login(username, password); if (user != null) { request.getSession().setAttribute("user", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp") .forward(request, response); } } }

这里有两个关键点值得说。第一,doPost开头必须setCharacterEncoding("UTF-8"),不写这一行,表单提交过来的中文用户名在后续取值时就已经乱码了,后面的编码过滤器形同虚设。第二,登录成功后用sendRedirect,失败后用forward。区别很微妙但重要:sendRedirect是浏览器重新发起一次请求,地址栏会变化,刷新页面不会重复提交表单;forward是服务端内部转发,地址栏不变,刷新时会再次提交表单导致重复登录。这个区别也是面试官最爱问的考点之一。

再看Session部分。request.getSession().setAttribute("user", user)这段代码把整个User对象放进了会话,之后所有JSP页面通过${sessionScope.user.username}取值。这里注意一个坑:如果你在登录后修改了数据库里的用户昵称,Session里存的还是旧数据,页面头像和昵称没变化,这不是bug,是会话缓存。解决办法是在用户修改资料后重新从数据库取一次并覆盖Session中的user对象。

4.2 购物车数据结构:为什么用Map而不是List

打开cart相关的代码,你会发现购物车不是数据库表,而是一个Java对象,存储在Session里。这是JavaWeb项目的经典设计思路,也体现了“有状态会话”这个概念。购物车类Cart的核心结构如下:

public class Cart { private Map<Integer, CartItem> items = new HashMap<>(); public void addItem(CartItem item) { Integer productId = item.getProduct().getId(); if (items.containsKey(productId)) { CartItem old = items.get(productId); old.setQuantity(old.getQuantity() + item.getQuantity()); } else { items.put(productId, item); } } public double getTotalPrice() { double total = 0.0; for (CartItem item : items.values()) { // 单价 * 数量,累加所有条目 total += item.getProduct().getPrice() * item.getQuantity(); } return total; } }

为什么用HashMap而不是ArrayList?因为购物车是“以商品ID为索引”的键值关系。addItem时如果商品已经存在,只需要在原数量上加1,不需要遍历整个列表。HashMap的containsKey和put操作时间复杂度是O(1),而ArrayList要O(n),虽然购物车最多几十条数据感受不到性能差异,但数据结构选型体现的是设计思路。另外,商品ID作为Integer类型key时,HashMap自动装箱和equals策略都没问题;如果你用String类型的ID,要注意大小写不敏感的需求,那就得用TreeMap配合自定义Comparator。

这里还需要留意CartItem和Product的关系。CartItem内部持有Product对象,持有quantity数量字段。整套设计里没有把商品名、价格import进CartItem,而是直接引用Product实体,这就是对象组合。后面如果要做购物车数量更新,只需要拿到productId后调cart.updateQuantity(productId, newQuantity),不需要操作数据库。这种纯内存操作非常快,刷新页面也没有额外SQL开销。

4.3 订单生成的SQL事务边界:扣库存和加订单必须同一事务

订单模块是整个项目里唯一出现了“数据库事务”概念的地方。生成订单涉及三步操作:向orders表插入一条订单记录、向order_item表插入商品明细、减少商品表的库存数量。这三步必须要么全成功,要么全失败,所以需要手动控制事务。课设模板的OrderDao里通常是这样写的:

public boolean createOrder(Order order, List<OrderItem> itemList) throws SQLException { Connection conn = DbUtil.getConnection(); PreparedStatement ps = null; boolean result = false; try { conn.setAutoCommit(false); String sql = "INSERT INTO orders (user_id, total_price, status, create_time) " + "VALUES (?, ?, 1, NOW())"; ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setInt(1, order.getUserId()); ps.setBigDecimal(2, order.getTotalPrice()); ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); int orderId = 0; if (rs.next()) { orderId = rs.getInt(1); } for (OrderItem item : itemList) { String itemSql = "INSERT INTO order_item " + "(order_id, product_id, quantity, price) " + "VALUES (?, ?, ?, ?)"; PreparedStatement ps2 = conn.prepareStatement(itemSql); ps2.setInt(1, orderId); ps2.setInt(2, item.getProduct().getId()); ps2.setInt(3, item.getQuantity()); ps2.setBigDecimal(4, item.getProduct().getPrice()); ps2.executeUpdate(); ps2.close(); // 扣减库存 String stockSql = "UPDATE product SET stock = stock - ? WHERE id = ?"; PreparedStatement ps3 = conn.prepareStatement(stockSql); ps3.setInt(1, item.getQuantity()); ps3.setInt(2, item.getProduct().getId()); ps3.executeUpdate(); ps3.close(); } conn.commit(); result = true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } return result; }

这个事务有三个边界要点。第一,conn.setAutoCommit(false)必须放在try开始的位置,确保三个操作都在同一事务里。第二,Statement.RETURN_GENERATED_KEYS配合getGeneratedKeys()获得数据库自增主键,order_item表的外键order_id靠这个值填充。第三,任何一个SQL执行失败都会触发rollback回滚,所有操作恢复到事务开始前的状态。你可以手动在扣库存的SQL后面抛一个RuntimeException来验证回滚机制,订单表和库存表都会保持原样。

这个实现里有个性能隐患:循环内每次new一个PreparedStatement,1件商品相当于对order_item表执行2次SQL(插入+扣库存)。少量数据没问题,如果订单里有20件不同的商品,交互次数会明显增加。更好的做法是用addBatch批量执行,这个作为进阶改造点,后面再展开。

5. 避坑排查:JavaWeb商城部署中高频翻车的五个根因

5.1 Tomcat 10 启动失败且 ClassNotFoundException: javax.servlet.Filter

现象:把项目部署到Tomcat 10后,启动直接报错,控制台打出大量NoClassDefFoundError,类名是javax.servlet.Filter或者javax.servlet.http.HttpServlet。

原因:Tomcat 10是第一个把Servlet规范从javax.*迁移到jakarta.*的版本。老项目里所有的javax.servlet引入都会失效,这与代码本身无关,是容器版本和代码版本不匹配。

解决:换成Tomcat 8.5或者9.0跑这个项目。如果非要用Tomcat 10,只能把代码里的javax全部批量替换为jakarta,注意web.xml头部声明也要同步改成jakarta的命名空间。对于一个课设项目,直接换Tomcat 8.5是最省事的路径。从那以后,我拿到JavaWeb项目先问一句Tomcat版本,再谈下一步。

5.2 JDBC连接报 Communications link failure

现象:启动Tomcat后打开登录页,输入账号密码点击登录,页面500,控制台堆栈里有CommunicationsException: Communications link failure。

原因:数据库服务没启动,或者jdbc.url里写的端口与实际MySQL端口不一致。很多本子代码里写的是3307,但本机MySQL用的是3306。

解决:先用命令行确认数据库状态:netstat -ano | findstr 3306,如果端口不存在,说明MySQL没启动或者端口被改了。确认后同步修改jdbc.properties里的URL,端口改成实际的。另外检查一个渗透率很高的坑:MySQL 8.0默认的认证插件是caching_sha2_password,老的mysql-connector-java 5.x驱动不支持,表现为连接时Authentication plugin问题,换成8.0驱动或把MySQL用户认证改回mysql_native_password都行。

5.3 登录框输入中文出现乱码,用户名显示成“???”

现象:表单输入“张三”,登录后耀眼的问号出现在数据库里,页面显示乱码。

原因:三层出现了断裂。JSP页面编码是UTF-8,Servlet取参数时没有request.setCharacterEncoding("UTF-8"),数据库连接URL没带characterEncoding=utf8,导致字节流在传递过程中被错误解码。

解决:逐个加。JSP头部保证pageEncoding="UTF-8"和contentType="text/html; charset=UTF-8"两项都写;Servlet的doPost第一行加上setCharacterEncoding("UTF-8");jdbc.url里加characterEncoding=utf8;另外在Tomcat的server.xml的Connector标签上加URIEncoding="UTF-8",护住GET请求的URL参数。这四层做完,中文就稳了。

5.4 首页能打开,但CSS、JS、图片全部 404

现象:浏览器访问首页,框架和文字全出来了,但页面没有样式,控制台显示一堆.css和.js请求返回404。

原因:JSP里引用了 /css/style.css 这种以/开头的绝对路径。在Tomcat部署后,应用上下文是 /shop,正确的路径应该是 /shop/css/style.css,而JSP里的硬编码路径直接定位到了Tomcat根目录,所以404。

解决:把所有静态资源引用都改成动态获取上下文路径。JSP里用${pageContext.request.contextPath}拼前缀,或者用<c:url>标签自动加上下文。示例:

<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css">

这段代码里${pageContext.request.contextPath}会输出 /shop,拼接后就是正确的 /shop/css/style.css。检查所有img、a、form、script标签里的路径,一个都别漏。form的action也要拼上下文,否则登录表单的提交地址也是错的。

5.5 商品图片显示正常但后台修改后图片全裂

现象:第一次导入sql脚本后商品图片显示正常,过几天在后台管理里修改了商品信息,重新上传图片,前端商品图全部裂开。

原因:模板里商品图片要么存在磁盘绝对路径,要么存在项目的某个临时目录,重启Tomcat后路径变化或文件被清理。上传功能写入的路径与读取时拼接的路径不匹配,导致图片无法展示。

解决:先找到项目里的FileUploadServlet,看它把文件写到什么位置。正确做法是把图片重命名后写到webapp目录下的upload文件夹,路径存成相对路径,比如 upload/product_20250101.jpg。读取时在页面拼${pageContext.request.contextPath}/ + 相对路径。千万别存“D:\xxx\upload\1.jpg”这种绝对路径,换个机器就是死路。代码里统一用一个常量保存上传根目录,比如Constants.UPLOAD_DIR,改路径时只改一处。

6. 把课设项目变成面试作品:三个验证动作与一处改造

6.1 做一次并发登录压测:让面试官看不出你只跑过单用户测试

单线程登录正常不等于项目能扛住真实场景。至少在本地装个JMeter,建一个线程组,100个线程同时请求 /login,循环5次,观察错误率和响应时间。如果出现大量的Connection or Statement Closed异常,说明连接池的maxActive设置太小,或者DAO层没有正确关闭连接。顺带说一句,很多课设模板的DAO在finally里没有做出站关闭,连接池耗尽只是时间问题。压测发现异常后,优先检查所有DAO的finally块是否调用了资源关闭。

6.2 把密码从明文改成加盐哈希:安全设计是最便宜的加分项

打开UserDao会发现,密码字段直接存的是明文。这在课设答辩时是送命题。最简单的改造是引入JDK自带的消息摘要功能,完全不需要额外jar:

public static String md5Salt(String password, String salt) { String input = salt + password; try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : hash) { // 每个字节转成两位十六进制,拼成64位字符串 sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("加密算法不存在", e); } }

改造时记住逻辑:注册时用随机生成的salt拼上密码做SHA-256,数据库同时存salt和hash值;登录时取出该用户对应的salt,用同样算法算一次,比对hash是否相等。盐不要用固定的字符串,每个用户注册时UUID生成一段,这样即使两个用户密码相同,库里的hash结果也不一样。演示完这个改造环节再用一句话点出“数据库泄露时攻击者拿不到明文”,就足以把这个课设项目拔高到“有安全意识”的档次。

6.3 顺手做一遍代码清理:删掉这三处明显冗余再打包

第一个冗余是Service层里多余的System.out.println。第二个是Dao层里明明select了id,实体类setId却漏写了,导致后续外键关联全是null。第三个是orders表status字段的值在代码里被写死成1,没有走常量定义,整个项目里散落着“1”“2”“3”这种魔法数字。把它提出来定义成OrderStatus.PENDING这类常量,读代码的人一下子就看懂了。这三个清理动作做完,再把改造前后的对比截图放进答辩PPT,比单纯讲功能强很多。

最后说一条个人习惯:我每次拿到一个JavaWeb项目包,不会先打开IDE,而是先翻压缩包里的sql脚本、jdbc.properties和web.xml三份文件。这三份文件能在一个小时内暴露这个项目的数据库版本、连接方式、以及映射结构的问题。顺序看完了,再启动项目。这个习惯帮我避开了至少十次“项目跑不起来”的尴尬,都是从前的血泪经验。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询