☰
JSP+SSH水果商城毕设实战:MVC分层、部署与避坑全攻略
2026/10/5 2:51:18 网站建设 项目流程

简介:面向Java初学者的水果销售商城系统完整源码,整合SSH与MVC框架,覆盖J2EE毕业设计常见功能模块,适合正在学习JavaWeb、需要项目练手或准备毕设的开发者。系统区分管理员与普通用户双角色:普通端支持注册登录、首页水果展示、分类浏览、购物车、下单及订单查询;管理端提供水果信息增删改查、分类管理、订单管理、用户管理等完整业务闭环。开发环境为MyEclipse 10、MySQL与Tomcat,代码与数据库脚本均已包含。资源包共139个文件,压缩后仅2.7MB,含25个JSP页面负责前端展示、24个Java类及Class文件构成业务逻辑,另有XML配置、CSS/JS样式脚本、SQL初始化脚本辅助部署运行,目录结构清晰,便于导入调试。已有775人学习下载,适合希望从理论走向实战、独立搭建商城项目或完成毕业设计的开发者。

1. 水果销售商城系统:JSP+SSH 毕业设计源码,为什么我建议你先跑通再改

每年毕业季都会遇到同一类问题:从网上下载的“水果销售商城系统源码 jsp ssh mvc java web j2ee毕业设计.rar”解压后,几十个 java 文件、一堆 xml、一个数据库脚本,新手照着网上的教程导入 Eclipse,不是缺包就是报红,最后连首页都看不到。这个项目是一个典型的 SSH(Struts2 + Spring + Hibernate)分层商城,适合做课程设计和 Java Web 毕业设计,也应该在半小时内启动起来。难点不在代码本身,而在环境匹配、依赖配置和数据库初始化。这篇文章我会以一线调试的视角,把从解压到改造成完整过程拆开,每一个坑都是真实项目里重复出现的,你可以直接照做。

2. 理解 SSH 分层如何分工:Struts2 + Spring + Hibernate 在商城里的职责

拿到一个 SSH 源码包,第一件事不是解压,而是先建立整体认知。因为整段代码的复杂度并不在算法,而在框架之间的协作方式。用一句话概括:Struts2 负责接参数和跳转,Spring 负责管理对象和事务,Hibernate 负责把 Java 对象映射成 MySQL 里的记录。三者叠在同一套 MVC 体系里,就成了很多 J2EE 课设和毕业设计系统的基础骨架。

我习惯把“水果商城”这个系统拆成三层去看:前端 JSP 展示水果列表、分类、购物车;中间层 Action 接收请求并返回页面;底层 Service 和 DAO 操作数据库。只有把这层关系搞清楚,后面每个报错你才能判断它发生在哪一层。

2.1 从 MVC 视角追踪一个请求:JSP、Action、Service、DAO 谁在干活

比如用户访问http://localhost:8080/fruit-shop/goodsList.action,后台发生的事按顺序是:Tomcat 把请求交给 Struts2 过滤器,Struts2 根据struts.xml找到对应的FruitAction;Action 里不写业务逻辑,只是接收页面传过来的分类 id,然后调用 Spring 注入进来的FruitService;Service 再调FruitDAO,由 Hibernate 执行查询;最后把查询结果放进request,forward 到goodsList.jsp。这就是 MVC 里 Controller、View、Model 的标准分工。

很多年后你面试 Java 岗位时,面试官问“MVC 框架怎么理解”,你也可以直接拿这个链路举例。下面是一份典型的struts.xml配置,注意class的写法和devMode参数:

<struts> <constant name="struts.i18n.encoding" value="UTF-8" /> <constant name="struts.devMode" value="true" /> <package name="fruit" extends="struts-default"> <!-- name 是 URL 后缀,class 指向 Spring 容器中的 bean id --> <action name="goodsList" class="fruitAction" method="list"> <result name="success">/fruit/goodsList.jsp</result> <result name="input">/fruit/goodsList.jsp</result> </action> </package> </struts>

这里最容易被忽略的是class="fruitAction"。它不是类的完整路径,而是 Spring 容器里的 bean id。因为 SSH 整合时,Struts2 通过struts2-spring-plugin去 Spring 容器里找 action 实例,如果你把它写成com.fruit.action.FruitAction也能启动,但 action 里的 Service 大概率是 null。struts.devMode在调试阶段一定要设为 true,这样配置改动后不用重启 Tomcat;但答辩演示时建议关掉,避免控制台报一堆开发模式提示。

再看对应的 Action 类,我一般建议用注解注册成 Spring bean,避免每个 action 都要在 XML 里写一遍:

@Controller("fruitAction") @Scope("prototype") public class FruitAction extends ActionSupport { @Resource private FruitService fruitService; private List<Fruit> fruitList; private Integer typeId; public String list() { fruitList = fruitService.findByType(typeId); return SUCCESS; } public List<Fruit> getFruitList() { return fruitList; } public void setTypeId(Integer typeId) { this.typeId = typeId; } }

@Scope("prototype")值得多说一句。Struts2 的 Action 默认每个请求都创建新实例,这样类里的typeId、fruitList这些成员变量不会被并发请求污染。如果你把它配成singleton,用户 A 传入的分类 id 可能被用户 B 覆盖。在毕设项目里可能不明显,但这是工程上的基础常识。

2.2 三个核心配置文件:struts.xml、applicationContext.xml、Hibernate 连接参数

SSH 项目里最让人头晕的是三个配置文件要同时工作:struts.xml管 URL 路由;applicationContext.xml管 bean 和事务;Hibernate 的hibernate.cfg.xml或 Spring 里sessionFactory的hibernateProperties管数据库连接。常见做法是把 Hibernate 相关配置直接并进applicationContext.xml,只保留一份 Spring 配置,运维时更省心。

下面是一份 dataSource 与 sessionFactory 的典型配置,数据库名、用户名、密码都集中在开头:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://127.0.0.1:3306/fruit_shop?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false" /> <property name="username" value="root" /> <property name="password" value="123456" /> <property name="maxActive" value="20" /> </bean> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop> <prop key="hibernate.show_sql">true</prop> <prop key="hibernate.hbm2ddl.auto">update</prop> </props> </property> </bean>

注意 XML 里&必须写成&amp;,不少人把网页上的连接串直接复制进来,结果 Spring 容器解析时报错。characterEncoding=utf8这一项必须保留,否则 insert 到 MySQL 的中文商品名会变成???。show_sql为 true 时,Hibernate 会在控制台打印每次 SQL,调试阶段很有用,但正式部署一定要关掉,否则日志文件会膨胀得很快。

事务配置同样放在 Spring 里,通常用 AOP 把事务边界切到 service 层:

<bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory" /> </bean> <aop:config> <aop:pointcut id="serviceTx" expression="execution(* com.fruit.service.*.*(..))" /> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceTx" /> </aop:config>

为什么事务要放 Service 层而不是 DAO 层?举个例子:用户下单时要做两件事——扣减水果库存、生成订单记录。如果两个操作分别在不同的 DAO 方法里,各自开启事务,那么先扣了库存、后写订单失败,数据就不一致了。只有把它们包在同一个 Service 方法的事务里,才能保证扣库存失败时订单回滚。校园水果商城的代码量不大,但这套分层思想是 J2EE 课程真正想考察的点。

2.3 为什么这个组合被毕设选中,以及它的历史包袱

SSH 在老一代 Java Web 开发里称得上标杆组合,JSP 负责视图展示,Struts2 负责请求分发,Spring 负责对象装配,Hibernate 负责对象关系映射。毕设系统用它,一是学校课程还在按这个路径教,二是源码包数量足够多、资料丰富,遇到问题容易搜到对应解决方案。但放到今天,它也确实有历史包袱。

比较直观的问题是“版本兼容三兄弟”:JDK、Tomcat、MySQL 版本只要有一个偏高,启动时就可能遇到非法反射访问、方言不支持等新问题。这里给一张我常用的版本速查表:

组件推荐版本说明
JDK1.8SSH3 老项目最省心,不要在 JDK17 上死磕
Tomcat8.5兼容 Java EE 7 与 JSP 2.3
Eclipse2021-06 JEE 或之后自带 WTP,支持 Dynamic Web Project
MySQL5.7utf8mb4 表现稳定,8.0 需要检查驱动版本
Struts22.3.x 或 2.5.x必须配对应 struts2-spring-plugin
Spring3.2.x 或 4.xHibernate3 与 Spring4 整合最顺

如果你下载的源码包自带lib目录,优先用包里的 jar;如果自己组装项目,再按这张表选。遇到莫名其妙的ClassNotFoundException,多半是 jar 版本之间互相不认识。这一部分先理解到这里,接下来我们把.rar真正变成能启动的工程。

3. 把 .rar 变成可运行的商城:导入、改库、启动全操作

源码包拿到手后,马上去解压可能有惊喜,也可能有惊吓。我见过很多把源码和数据库脚本分开放的包,也见过直接把 target 目录一起压进去的包。第一步要做的是识别它是什么结构。

3.1 解压并识别项目类型:Maven 项目还是传统 Web 项目

先用命令解压,再确认目录结构:

# 安装 unrar 之后执行 mkdir -p ~/workspace/fruit-shop && unrar x FruitShopSSH.rar ~/workspace/fruit-shop/ cd ~/workspace/fruit-shop ls -l

如果没有 unrar,可以用unzip试试;如果 rar 包带密码,那基本是原作者加了保护,只能换一个包源。解开后重点关注三样东西:src目录、WebRoot或webapp目录、lib目录。用命令快速判断项目类型:

find . -name "pom.xml" -o -name "web.xml" | head -10 find . -path "*WEB-INF/lib*" -name "*.jar" | wc -l

pom.xml存在说明是 Maven 项目;只有web.xml则多半是传统 Web 项目。第二个命令统计WEB-INF/lib下的 jar 数量,如果是 40 到 70 个,说明依赖齐全;如果只有十几个,后面部署大概率缺包。

在 Eclipse 里使用File > Import > Existing Projects into Workspace选择解压后的目录。传统 Web 项目导入后,如果在项目名旁边看到Dynamic Web Module没有被勾选,需要右键项目Properties > Project Facets,勾选Dynamic Web Module 3.1和Java 1.8。这一步不做,后面 Run on Server 运行时可能提示“项目没有部署到 Tomcat”。

3.2 改数据库连接参数:把 root 密码和库名换成自己的

打开工程后,第一步不是启动,而是先找到数据库配置。位置通常在src/jdbc.properties、src/db.properties或WebRoot/WEB-INF/classes/下面。老项目的命名不统一,但不外乎driverClassName、url、username、password这四个键。

先建一个库,再改连接参数。在 MySQL 里执行:

CREATE DATABASE IF NOT EXISTS fruit_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

然后打开 properties 文件:

jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/fruit_shop?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=你的数据库密码

逻辑说明:这里用127.0.0.1而不是localhost,是因为有些机器上localhost会先解析到 IPv6,导致连接超时;直接用127.0.0.1可以跳过这一步。useSSL=false是为了避免 MySQL 5.7 之后联网 SSL 校验带来多余日志。如果你看到的是com.mysql.jdbc.Driver一直报“找不到”,可以改成com.mysql.cj.jdbc.Driver,但前提是mysql-connector-java是 8.x 版本,5.x 驱动不识别这个类名。

如果源码里没有 properties,而是像上一章那样直接写在applicationContext.xml的 dataSource bean 里,那同样改 xml 里的<property>。改完之后做一次字符串检查:密码里如果含有&或<,要么换密码,要么做 XML 转义,否则 Spring 会直接解析报错。

3.3 执行 SQL 初始化脚本:别让 Hibernate 独自建表

大多数毕设源码包会用.sql文件把建表和初始化数据准备好,文件名一般叫fruit_shop.sql、db.sql或fruit.sql。找到后执行:

mysql -h127.0.0.1 -uroot -p fruit_shop --default-character-set=utf8 < fruit_shop.sql

执行完验证一下表和数据是否完整:

USE fruit_shop; SHOW TABLES; SELECT fruit_id, fruit_name, price, stock FROM fruit LIMIT 5;

我见过不少源码包里的 sql 脚本只包含了create table,没有任何 insert 语句。这样登录进去后台列表就是空的,看着像系统坏了,其实只是没有初始数据。可以手动补几条水果记录:

INSERT INTO fruit(fruit_name, type_id, price, stock, photo) VALUES ('红富士苹果', 1, 8.50, 100, '/upload/apple.jpg'), ('海南香蕉', 1, 4.90, 50, '/upload/banana.jpg'), ('赣南脐橙', 2, 9.80, 80, '/upload/orange.jpg');

注意:如果你的fruit表有图片字段,图片路径要对应WebRoot/upload下真实存在的文件。很多毕设项目页面上的图片是死链,原因就是数据库里存了/upload/xx.jpg,但项目里根本没有 upload 目录。可以把项目中自带图片的名称列出来,再对应修改数据。

3.4 部署到 Tomcat 并验证首页

在 Eclipse 里右键项目Run As > Run on Server,选择 Tomcat 8.5。此时重点看控制台日志:

tail -f /opt/tomcat8.5/logs/catalina.out # 看到 INFO: Server startup in [3xxx] milliseconds 才算成功

如果 Eclipse 控制台一闪而过但没有启动完,切到 Server 视图看 Tomcat 状态,然后双击服务器配置,检查Server Locations是否正确。常见错误是把项目部署到 Tomcat 的旧目录,导致新代码没生效。

启动成功后,访问http://127.0.0.1:8080/fruit_shop/。如果你项目名是fruit_shop,Tomcat 默认 context 就是/fruit_shop;如果名字带版本号,URL 也要跟着改。看到首页后,先不要急着浏览功能,直接找“后台登录”入口。因为毕设答辩第一件事往往是登录进去展示管理功能,前台商品列表反而最容易出拼写错误。

如果首页能打开,但点“登录”报 404,大概率是struts.xml里的 action 名没对上。去开发模式下看浏览器地址栏,实际上请求的.action后缀是什么,再回到配置文件里搜索对应 name。此时还不熟悉,尽量别去动 Filter 或 Servlet 配置,那会引入更多变量。

4. 避坑排查:SSH 项目跑不起来的 5 个高频故障与现场修复

跑通了一个 SSH 商城,不代表它永远不会翻车。以下 5 个问题是我处理这类毕设源码时遇到频率最高的,每条都按“现象 → 原因 → 解决”的顺序给出可操作方案。

4.1 中文乱码:登录用户名变成???或 URL 被转义成%E6%9D%8F

现象:前台页面显示正常,但表单提交中文到后台之后,数据库里存的是???;或者地址栏里中文参数变成一长串%开头的编码。

原因:三层字符集没有统一。JSP 的输出编码、Tomcat 接收 GET 请求的 URI 编码、MySQL 连接串的字符编码,任何一层不是 UTF-8,中文就保不住。

解决:先检查所有 JSP 页面顶部是不是都有这一行,如果没有,批量加上:

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>

再打开 Tomcat 的conf/server.xml,找到 Connector,补一个属性:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />

最后确认jdbc:mysql连接串里有characterEncoding=utf8。这三处都对齐,乱码基本绝迹。还有一个小坑:数据库连接串里如果写成utf-8而不是utf8,MySQL 驱动在某些版本下会报Unsupported character encoding,一律用utf8更安全。

4.2 启动时 ClassNotFoundException:jar 包版本像黑匣子

现象:Tomcat 启动到一半,控制台抛出java.lang.NoClassDefFoundError: org/hibernate/cfg/Configuration,或者ClassNotFoundException: org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter。

原因:源码包的WEB-INF/lib目录里缺 jar,或者 jar 之间版本冲突。最常见的是struts2-spring-plugin没放进去,导致 Spring 容器无法给 Action 注入 Service;其次是 Hibernate3 与 Spring3 的兼容包版本不一致。

解决:先列出当前 lib 目录里的关键依赖,确认最小集合:

ls WEB-INF/lib | grep -E "struts|spring|hibernate|mysql" | sort

至少要有struts2-core、struts2-spring-plugin、spring-context、spring-orm、spring-web、hibernate-core、mysql-connector-java。缺哪个就去 Maven 中央仓库下载对应版本。老项目最忌讳“顺手升级到最新版”,Spring 4 和 Struts 2.5 的部分配置和插件版本需要同步调整,如果只是交毕设,严格按源码包原有版本补齐依赖最省时间。

4.3 数据库表没有自动创建:hbm2ddl.auto 不是后悔药

现象:首页能打开,但点击商品列表时报table 'fruit' doesn't exist,或者 Hibernate 打印的 SQL 中表名对不上。

原因:很多人以为 Hibernate 会自动建表,实际上很多源码把hibernate.hbm2ddl.auto设成validate或none。就算设成update,Hibernate 对新增字段有时能补列,但对多个表之间的外键约束处理并不可靠。数据库脚本还是必需品。

解决:回到第 3.3 节,手动执行 sql 脚本。如果脚本执行成功,但 Hibernate 依然报表不存在,看控制台 SQL 里打印出来的表名是什么。比如实体类映射的表名是fruit_info,而 sql 脚本建的是fruit,两者不一致只能改映射或改表名。临时把hbm2ddl.auto改成update可以帮你补建部分字段,但上线前一定要改回validate或者注释掉,避免数据被意外改动。

4.4 Tomcat 端口被占用:8080 起不来

现象:Eclipse 启动 Tomcat 时提示Port 8080 required by Tomcat v8.5 Server at localhost is already in use,而且你自己明明没有手动开过 Tomcat。

原因:本机后台可能驻留了一个旧 Java 进程,比如上一次 Eclipse 强杀留下的 Tomcat 进程,或者某个 Web 服务也占用了 8080 端口。这是开发环境里最容易出现的“端口打架”。

解决:先找到占用进程。Linux 和 macOS:

lsof -i :8080 kill -9 <PID>

Windows:

netstat -ano | findstr :8080 taskkill /F /PID <PID>

如果杀完又立刻出现新进程,检查是不是把 Tomcat 注册成了系统服务。毕设阶段不建议改端口,因为代码里可能有写死的http://localhost:8080资源,改端口后图片或请求全部 404,反而给自己添麻烦。

4.5 登录成功后跳回登录页:session 失效与拦截器组合拳

现象:用户名密码输入正确,点击登录后状态栏出现 302,然后页面又回到 login.jsp,或者被重定向到一个错误页。

原因:有多种。最常见的是 Struts2 默认拦截器把loginAction也拦截了,导致登录请求没有进入login方法;其次是 Tomcat 的 session 默认超时是 30 分钟,开发时频繁重启,session 被清空后前端又没有跳转逻辑;还有一种是登录成功后 result 返回的不是success,而是redirectAction,但 action 名字写错,形成循环跳转。

解决:先放行登录 action。在struts.xml中找到拦截器配置,把 login 方法加入excludeMethods:

<interceptor-ref name="defaultStack"> <param name="defaultStack.excludeMethods">login,logon</param> </interceptor-ref>

然后调整 session 超时时间,在web.xml加:

<session-config> <session-timeout>60</session-timeout> </session-config>

如果是自己调试,用浏览器 F12 观察登录请求:如果每次响应 Set-Cookie 里的 JSESSIONID 都在变,说明 session cookie 未被保存,检查项目路径是否有权限问题,或者页面是否被放置在了不同 context 下。SSH 项目中这个问题的出现频率很高,但解决方案基本都是围绕“排除登录 Action + 确认 session 存储”这两步。

5. 从跑通到答辩:给水果商城加最小改进,并验证主流程

项目跑通后,最怕的就是“看起来一切正常,但深度一问就说不清”。我建议在答辩前,自己动手加一个很小的需求点,哪怕只是给水果列表加排序,也能证明你确实读懂了代码。

5.1 一个最小改造:让商品按价格升降序排列

需求描述:前台水果列表默认按 id 排序,现在希望加一个下拉框,让用户能按价格从低到高或从高到低排序。这个需求足够小,但覆盖了 JSP、Action、Service、DAO、SQL 拼接的全部链路。

先在FruitAction里加一个orderFlag字段,并在list()方法中白名单判断:

private String orderFlag; public String list() { String orderBy = "price asc"; if ("desc".equals(orderFlag)) { orderBy = "price desc"; } fruitList = fruitService.findByTypeWithOrder(typeId, orderBy); return SUCCESS; }

注意不能直接使用用户输入的字符串拼到 HQL 里,比如"from Fruit where typeId=" + typeId是典型的注入漏洞,顺序字段也必须做白名单。然后在 DAO 实现类中写:

String hql = "from Fruit as f where f.typeId = :typeId order by " + orderBy; Query query = getSession().createQuery(hql); query.setParameter("typeId", typeId);

看起来只是加一个字段,但给答辩评审解释时,你可以把 MVC 链路完整说一遍:“用户在 JSP 选择排序方式,Struts2 把参数赋值给 Action 的属性,Service 层把排序字符串传给 DAO,Hibernate 生成 SQL 返回列表。” 这比单纯背原理要有说服力得多。

5.2 验收前必做的三个验证动作

第一,准备一个全新 Tomcat 和空数据库,只导入.sql文件然后部署,走一遍“注册→登录→浏览商品→加入购物车→生成订单→后台看到订单”全流程。这个流程如果顺利,说明项目不依赖任何本地残留垃圾。

第二,在浏览器无痕窗口下测试,避免旧 JSESSIONID 干扰。如果无痕模式下失败,多半是前端没有携带 Cookie。

第三,导出一份数据库备份作为“后悔药”:

mysqldump -h127.0.0.1 -uroot -p fruit_shop > fruit_shop_backup.sql

我在处理这类老源码时,吃过最大的亏是拿到手就大刀阔斧地删配置、改代码,最后出了问题想回到初始版本都做不到。现在无论调试什么项目,都先复制一份带日期的目录,再动任何文件;遇到异常也尽量用控制台日志定位,而不是凭感觉乱试。希望帮到你。

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

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

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

立即咨询