SSM+JSP老框架毕设:百货供应链系统源码解析与实战

发布时间:2026/10/26 22:40:55
SSM+JSP老框架毕设:百货供应链系统源码解析与实战

简介:面向百货零售行业的信息化供应链管理项目,基于Java与SSM框架构建,前端结合Vue与JSP技术,可帮助开发者学习企业级后台系统的分层设计、权限管理与数据交互实现。压缩包共924个文件、约9.97MB,其中136个Java源文件对应业务逻辑与Controller层,224个JS及102个CSS用于前端交互与页面样式,72个JSP页面承载动态视图,另有2个SQL脚本为数据库初始化脚本,并附项目功能介绍文档。系统使用MySQL 5.7与Tomcat 7+运行环境,源码经过调试可直接部署,适合作为毕业设计或课程综合项目参考。目前已有3524人学习下载,配套材料包括完整项目源码、数据库脚本与说明文档,可按目录结构快速定位各模块并在此基础上做二次开发。

1. 为什么这份 SSM + JSP 的老框架毕设源码值得跑通

解压 ssm685 这个压缩包时,第一眼看到的往往不是 src 目录里的 Java 源码,而是一排 .bak 后缀的备份文件:styles.css.bak、index.jsp.bak、topNav.jsp.bak、setMenu.js.bak。接过足够多毕设源码的人都会明白,这反而是个好消息——它说明项目在交付前经历过真实改版和调试,不是那种生成一次就没再动过的"死代码"。这份资源的本质,是一个典型 SSM 脚手架加完整业务页面的百货中心供应链系统:后端 Java + Spring + SpringMVC + MyBatis,前端 JSP 配合 Vue,运行在 JDK 1.8、Tomcat 7+、MySQL 5.7+ 这套高校实验室最常见的环境上,处理的业务正好是零售百货的供应商、商品、采购、库存这条主干链路。它适合三类人:急着交一份能跑起来、能讲解的完整毕设的学生;想把 SSM 整合和 JSP 渲染逻辑彻底弄懂的初级开发者;以及在维护老项目、需要快速判断一个新接手的代码库怎么改版的人。

2. 从 .bak 文件还原改版轨迹:SSM 三层的真实分工与代码结构

2.1 备份文件是免费改版地图:先读资源列表再动代码

接过这个包的时候,别急着解压导入 IDE。先把文件列表扫一遍,这里面藏着的项目历史信息比 README 还密集。

资源包里有 .classpath 和 org.eclipse.wst.common.component,这两样东西直接暴露了项目的出生背景:这是一个用 Eclipse 开发的传统 Web 项目,而不是 Maven 标准目录结构。也就是说,你导入 IDEA 或者直接用命令行跑的时候,不能指望一个 pom.xml 帮你把依赖拉齐,而是要看 lib 目录下有没有打好的 jar 包。很多新手在这第一步就翻车,拿 IDEA 打开后报一堆红叉,其实是 maven 工程和传统 web 工程的加载逻辑不一样导致的。

再往下看,bootstrap.css 和 bootstrap.min.css 同时存在,外加 style.css。这个组合说明前端样式表有过一轮压缩处理,bootstrap.min.css 是压缩版,给正式环境用,bootstrap.css 是未压缩版,调试时更好定位问题。style.css 是项目自定义样式,通常也叠加了一些覆盖逻辑。研究页面样式的时候,优先改 style.css,而不是去动 bootstrap 的源码,否则后面遇到样式冲突会很头疼。

最有意思的是一排 .bak 文件:styles.css.bak、index.jsp.bak、setMenu.js.bak、topNav.jsp.bak。bak 是老派程序员在上线前留后悔药的习惯——把当前版本复制一份带上 bak 后缀,再动手改新版本。所以这一套 .bak 文件拼起来,恰好能还原项目 UI 层的原始状态:首页改过、顶部导航改过、菜单脚本改过、全局样式改过。用 diff 命令对比备份和当前版本的差异,能直接看到上一轮改版动了哪些地方。

# 对比顶部导航 JSP 的备份版与现役版,只看前 50 行差异 diff topNav.jsp.bak topNav.jsp | head -50

diff 输出里,<开头的是备份文件里的行,>开头的是当前版本里的行。这个对比能告诉你两件事:这一版改了哪些功能入口,以及作者当时的改版幅度大不大。如果差异集中在几个链接和权限判断上,说明是业务需求变更;如果差异遍布整个文件,说明是前端方案重构,比如从纯 JSP 后端渲染换成了部分 Vue 接管。这个信息对接手项目的人来说,比看需求文档还直观。

有一点要提醒:.bak 文件不会被 Tomcat 当成 JSP 执行,Tomcat 的 JSP 编译器只认 .jsp 后缀,所以它们留在项目里不影响运行。但会干扰 IDE 的搜索结果——你搜"登录"的时候,index.jsp.bak 和 index.jsp 可能同时命中,如果不注意后缀,会被旧版代码带偏,半夜排查都查不出问题。我一般拿到项目第一件事就是把 .bak 文件从编译路径里排除掉,或者看完 diff 之后整体移到项目外的备份目录。

2.2 一次请求的完整旅程:从 @Controller 到 MyBatis Mapper

SSM 三个框架在项目里的分工,用一个请求就能讲清楚。假设现在要展示采购订单列表,浏览器输入purchase/list,SpringMVC 的 DispatcherServlet 先根据 @RequestMapping 找到对应的 Controller 方法,Controller 调 Service 层,Service 调 Mapper 接口,Mapper 的 XML 里定义的实际 SQL 去查数据库,查完结果一层层返回,最终渲染到 JSP 页面上。

Controller 层的典型写法是这个样子:

// 采购订单模块的控制层:负责接收 URL 请求并绑定视图 @Controller @RequestMapping("/purchase") public class PurchaseController { @Autowired private PurchaseService purchaseService; // 列表页:查询全部采购订单并渲染到 purchase/list.jsp @RequestMapping("/list") public String list(Model model) { List<Purchase> purchaseList = purchaseService.findAll(); model.addAttribute("purchaseList", purchaseList); return "purchase/list"; } }

@Controller注解让 Spring 容器把当前类注册成一个 Bean,同时标记它在 MVC 架构里的控制层身份;@RequestMapping("/purchase")定义的是类级别的 URL 前缀,方法上的@RequestMapping("/list")再拼一层路径,最终完整访问路径是/purchase/list。返回值"purchase/list"是逻辑视图名,SpringMVC 会配合视图解析器把它映射到 WEB-INF 下的 purchase/list.jsp。Model对象的作用是携带数据跨层传递,这里把从 Service 层拿到的订单列表塞进 model,JSP 那边就能用 EL 表达式直接读取。

Service 层的作用是承上启下,不直接碰 SQL,而是封装业务规则,比如新增采购订单时校验供应商是否存在、订单金额不能为负。接口和实现类分离是 SSM 项目最常见的写法,接口定义方法签名,实现类加@Service注解,再通过@Autowired注入 Controller。

MyBatis 这边核心是 Mapper 接口和 XML 的对应关系:

// Mapper 接口:只写方法签名,不写实现 public interface PurchaseMapper { List<Purchase> findAll(); }
<!-- 对应 Mapper XML:id 必须与接口方法名一致 --> <select id="findAll" resultType="Purchase"> SELECT id, supplier_name, order_amount, create_time FROM purchase_order WHERE del_flag = 0 ORDER BY create_time DESC </select>

这里的关键点是 Mapper 接口和 XML 文件必须放在同一个包路径下,XML 的 namespace 写成接口的全限定名,<select>标签的 id 和接口方法名一字不差。很多二次开发的人在这里踩坑:在接口里加了一个方法但忘了在 XML 里写对应的 SQL,或者写了 SQL 但 id 对不上,一运行就报 Invalid bound statement 错误。

resultType的值写的是Purchase,这是实体类的别名,通常由 MyBatis 的 typeAliases 配置扫描包名来指定,或者在配置里写全限定类名。别名配置能省不少事,但如果项目里两个同名实体类在不同包,就会因为别名冲突报错,老项目里更稳妥的做法是写全限定类名。

2.3 前端渲染的两种模式:JSP 与 Vue 在同一页面里的共存方式

这个项目的前端是 JSP 和 Vue 混着用的,很多 SSM 毕设源码都是这个状态。JSP 负责服务端渲染,Java 代码可以直接写在页面里,通过 EL 表达式和 JSTL 标签把后端传过来的数据铺到 HTML 上。Vue 则以局部组件的形式介入,负责交互性强的部分,比如表单的联动校验、动态增删行。

JSP 里最常见的取数方式是 EL 表达式,配合 JSTL 的c:forEach遍历集合。一个典型的列表片段长这样:

<!-- JSP 页面遍历采购订单列表:items 对应 Model 里的键名 --> <c:forEach items="${purchaseList}" var="order"> <tr> <td>${order.supplierName}</td> <td>${order.orderAmount}</td> <td>${order.createTime}</td> </tr> </c:forEach>

这段代码看着简单,但里面有两个容易踩的细节。第一,${purchaseList}这个名字必须和 Controller 里model.addAttribute("purchaseList", purchaseList)的第一个参数完全一致,改了一边另一边就取不到值,页面表现为表格空白但不报错。第二,${order.supplierName}这种写法本质是调用实体类的 getter 方法,实体类字段命名必须严格遵守驼峰规则,数据库字段supplier_name转实体属性supplierName的映射做不对,这里就输出空字符串。

Vue 在这类项目里通常以独立 JS 文件的形式存在,页面引入 vue.min.js 后在某个容器 div 上挂载实例,数据通过 ajax 请求后端接口获取。这种混用模式在改造时有个容易犯的错:JSP 已经用 EL 表达式渲染了一份数据,Vue 又要用 ajax 拉同一份数据,两套渲染逻辑并存,稍不注意就出现数据重复展示或者权限判断失效。接手这种项目时,先分辨每个页面用的是哪种渲染方式,别急着改后端接口返回格式,否则一边好了另一边必然挂掉。

还有一个在 JSP 页面里经常被问到的场景是图片处理。百货系统的商品管理页里,商品图、供应商资质照片都会涉及图片路径的拼接和展示,JSP 里写<img src="${product.imagePath}">是最常见的方式,但这里图片路径要区分是相对路径还是绝对路径,如果 imagePath 存的是uploads/xxx.jpg,页面又在二级目录下,不加上下文路径就会 404。这类个人信息展示页面和图片定位的问题,后端接口返回的数据结构设计得越简单,JSP 这边的踩坑概率就越低。

看完整套结构,这个项目的骨架已经清楚了:典型三层架构,JSP 为主 Vue 为辅的前端组合,MyBatis 半自动 ORM 把 SQL 控制权留给开发者。接下来就是把环境搭起来,让它跑通。

3. 本地复现的一次性配置:JDK 1.8、Tomcat 7、MySQL 5.7 的参数与启动细节

3.1 数据库初始化:导入 .sql 脚本之前的三个确认

这类毕设项目通常附带一个 .sql 文件,建库建表、插入初始数据都准备好了。但执行导入之前有三个参数必须先确认,不然导到一半报错,还得回头查是哪一步出了问题。

第一是 MySQL 版本。项目要求的 MySQL 5.7+ 和现在很多人电脑上装的 MySQL 8.x 大体兼容,但 JDBC 驱动在 8.x 下变化了,旧驱动连不上新版本。具体来说,5.x 用的驱动类是com.mysql.jdbc.Driver,8.x 要换成com.mysql.cj.jdbc.Driver,连接串里也多了时区设置。如果项目生成时代是 5.7,个人建议直接装 5.7 版本最省事,不用折腾驱动兼容问题。

第二是字符集。百货供应链系统这种有大量中文业务数据的项目,数据库脚本建表语句一般会带DEFAULT CHARSET=utf8mb4,如果你的 MySQL 服务端默认字符集是 latin1,导进去之后中文全变乱码。导入前看一眼 my.cnf 里character-set-server的值,不是 utf8mb4 就先改掉再重启 MySQL,否则后面 JSP 页面上显示的供应商名称和商品名称全是一串问号。

第三是文件编码。很多 .sql 文件是 UTF-8 编码,但 Windows 下命令行或图形化工具的默认编码可能不是 UTF-8,直接导入容易把带中文注释的内容导乱。稳妥的做法是命令行指定编码导入:

# 先建库:指定默认字符集,避免后续中文乱码 mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS ssm685 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 再导入:--default-character-set 与建库字符集保持一致 mysql -uroot -p ssm685 --default-character-set=utf8mb4 < ssm685.sql

-e参数让 mysql 客户端直接执行一段 SQL,不进入交互模式。DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci分别指定数据库字符集和排序规则,utf8mb4 是 MySQL 5.7+ 推荐的中文支持方案,比老式 utf8 完整,能存下生僻字和 emoji。第二条命令把 ssm685.sql 文件的内容重定向给 mysql 客户端,导入到 ssm685 库中。--default-character-set=utf8mb4是告诉客户端,脚本文件本身是 utf8mb4 编码,解析时直接按这个编码读取。

库名不一定是 ssm685,如果数据库脚本自带CREATE DATABASE语句,就不用先建库,直接导入整个脚本就行。判断方式是用文本编辑器打开 .sql 文件看开头几十行,有没有建库语句一看便知。

导入完成后做个验证:执行SHOW TABLES;看列表是否完整,再SELECT COUNT(*)看关键表有没有初始数据。这一步能提前暴露脚本截断和字符集问题,比启动项目之后才发现页面空白省事得多。

3.2 数据源配置:db.properties 里的每一项都要改对

SSM 项目的数据源配置一般集中在 db.properties 或 jdbc.properties 里,Spring 的配置文件通过<context:property-placeholder>读取它。拿到项目后,这个文件是首先要改的。

一个典型的配置长这样:

# 数据库连接配置:改密码、改库名,其余按环境核对 jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ssm685?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=123456

jdbc.driver是驱动类全限定名,MySQL 5.x 用com.mysql.jdbc.Driver,8.x 用com.mysql.cj.jdbc.Driver,版本对不上启动就会报 ClassNotFoundException。jdbc.url里localhost:3306是 MySQL 地址和端口,ssm685是上一步建的数据库名,?后面是连接参数,useUnicode=true&characterEncoding=utf8保证传输编码统一为 UTF-8,useSSL=false关闭 SSL 证书验证,本地开发时避免旧驱动遇到 SSL 握手报错。

jdbc.username和jdbc.password换成你本机 MySQL 的实际账号密码。有个容易被忽略的细节:如果 MySQL 密码里含有&或#这类特殊字符,properties 文件里的值需要转义,否则 Spring 读取时会截断,导致密码解析错误。最简单的办法是把密码设为纯数字字母组合,省去一堆麻烦。

改完这个文件,还要确认 Spring 配置文件确实引用了它。看 applicationContext.xml 或 spring-mybatis.xml 里有没有这样一段:

<!-- 读取数据库配置文件的占位符配置 --> <context:property-placeholder location="classpath:db.properties" ignore-unresolvable="true"/>

这段配置的意思是让 Spring 去找 classpath 根目录下的 db.properties 文件,并把它里面的键值对作为占位符使用。如果文件位置不对,Spring 找不到占位符来源,数据源创建失败,Tomcat 启动会直接抛异常。

3.3 Tomcat 端口与部署:两个改动点解决 80% 的启动问题

Tomcat 7+ 很适合这套技术栈。这里的"7+"意味着 7.0.x 到 8.5.x 都能跑,但按项目依赖 jar 的版本来看,Tomcat 7.x 是最稳妥的选择。下载最新版解压到本地,不需要配置额外环境变量,直接在 IDE 里关联,或者把项目打成 war 包丢到 webapps 目录。

打 war 包的方式更适合命令行用户,生成的 .war 文件复制到 webapps 目录,启动 Tomcat 会自动解压部署。这里有个关键细节:war 包文件名就是应用上下文路径。如果你的包叫ssm685.war,访问地址是http://localhost:8080/ssm685/,想通过根路径访问就把 war 包改名为ROOT.war。

端口冲突是最常见的启动问题。本机装多个中间件时,8080 被占是常态。修改位置在 Tomcat 的 conf/server.xml,找到 Connector 那一段:

<!-- Tomcat 默认 HTTP 端口:被占用时改这里的 connectorPort 即可 --> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

port="8080"是 HTTP 访问端口,改成 8081 后访问地址就要跟着换成http://localhost:8081/。connectionTimeout="20000"单位是毫秒,默认 20 秒没收到请求就断开,本地调试不用动它。redirectPort="8443"是请求需要跳转 HTTPS 时用的端口,一般也不用改。

改完端口重启,去 logs 目录看 catalina.out 或 localhost 日志。看到Server startup in xxx ms才算启动成功;如果抛了异常,看堆栈里第一行Caused by,排错优先级是:数据源没配上、端口被占用、jar 包没加载全,就这三类,占了启动失败的九成。

所有底层配置就位后,浏览器打开http://localhost:8080/ssm685/看到登录页,说明整个环路的最后一公里已经通了。这时候如果页面白屏或样式全丢,别急着怀疑代码,很可能就是静态资源被拦截了,具体排查放第五章。

4. 供应链主流程拆解:从供应商维护到库存出库的链路怎么走的

4.1 基础档案:供应商与商品信息的管理逻辑

百货中心的供应链系统,起点是基础档案。供应商、商品分类、商品信息这三类数据的维护是日常使用频率最高的功能。这类模块在代码上长得很像:一个列表页面、一个新增表单、一个编辑弹窗、一个删除按钮,加上分页和条件查询。SSM 项目里每个模块基本是 Controller、Service、Mapper 三层各加三个方法,深入掌握了一个模块,其他模块就是复制粘贴再改字段名的事。

以供应商维护为例,Controller 层和前端之间有个约定:列表页用 GET 加载,新增和修改用 POST 提交,删除也走 POST。这个约定在 SSM 项目里贯彻得很彻底,如果代码里写了method = RequestMethod.POST,用 GET 去访问就会报 405 错误。

新增供应商的时候,用户在页面上填完表单提交,后端执行这样一段逻辑:

// 新增供应商:先校验必填字段,再调用业务层落库 @RequestMapping(value = "/save", method = RequestMethod.POST) public String save(Supplier supplier) { if (supplier.getSupplierName() == null || "".equals(supplier.getSupplierName())) { return "redirect:/supplier/list?error=name"; } supplierService.insert(supplier); return "redirect:/supplier/list"; }

这里有个设计细节:Supplier supplier作为方法参数,SpringMVC 会自动把请求参数绑定到实体类同名属性上,所以前端表单的 name 属性必须和实体类字段名一一对应,比如<input name="supplierName">就映射到supplier.getSupplierName()。字段名对不上,绑定的值就是 null,这是初学者最容易困惑的"前端传了值但后端收到的是 null"问题。

校验逻辑放在 Controller 层算是项目里的简化处理。正规企业级项目会把校验下沉到 Service 层或加 JSR-303 注解,但毕设项目的复杂度用 Controller 层校验足够。返回值redirect:/supplier/list是重定向写法,提交完数据回到列表页刷新最新数据,避免表单重复提交。

商品信息管理比供应商多一个维度:分类。商品表通常会带一个category_id外键关联分类表,JSP 页面上新增商品时,分类是一个下拉框,选项来自分类表查询。这里典型坑是:分类删除时如果还有商品引用,删除会报外键约束错误,很多项目直接在功能层面禁止删除有引用的分类,前端把删除按钮置灰,后端再校验一遍。

4.2 采购入库:从采购单到库存增加的联动过程

供应链系统的核心增量操作是采购入库。业务流程一般是:采购员建采购单,填入供应商和采购明细,主管审核通过后仓库人员执行入库,入库成功库存表库存增加,同时产生一条库存流水。

这套流程在 SSM 项目里的实现是典型的"先写主表再写子表再更新库存"三步。采购单主表存供应商、总金额、状态这些汇总信息;采购明细子表每一行是一条商品的数量和单价;库存动账是第三步。

这里最需要关注的是事务。三步操作任何一步失败,前面成功的数据必须回滚,否则会出现"采购单建了但库存没加"的数据不一致。Spring 声明式事务处理这个场景很直接:

// 采购入库核心逻辑:主表、子表、库存流水三个操作必须在同一事务里 @Transactional(rollbackFor = Exception.class) public void doPurchase(PurchaseOrder order, List<PurchaseItem> items) { purchaseOrderMapper.insert(order); // 1. 插入采购单主表 for (PurchaseItem item : items) { purchaseItemMapper.insert(item); // 2. 插入采购明细 stockMapper.increaseStock(item.getProductId(), item.getQuantity()); // 3. 库存增加 } }

@Transactional注解是 Spring 声明式事务的开关,加在 Service 层方法上,rollbackFor = Exception.class表示任何异常都触发回滚,包括运行时异常和检查异常。如果不加rollbackFor参数,Spring 默认只对 RuntimeException 回滚,碰到受检异常时事务不回滚,数据会出现半完成状态。这是毕设项目里隐藏最深的问题之一,看起来功能正常,但某天一个 SQL 抛了受检异常,一半数据写进去了,一半没写进去。

库存增加这条 SQL 的写法有个值得展开的细节。正确做法是直接在数据库层做数量累加,而不是先查出来再算好数字再更新回去:

<!-- 库存增加:数据库层面原子操作,避免并发超卖 --> <update id="increaseStock"> UPDATE stock_info SET stock_count = stock_count + #{quantity} WHERE product_id = #{productId} </update>

为什么不先查再改?因为供应链场景下多个采购单同时入库很常见,先SELECT stock_count拿回来再UPDATE stock_count = 新值这种两步操作,一旦并发,两次查询读到同一个旧值,后一次更新覆盖前一次增量,库存数目对不上。直接在 SQL 里写stock_count = stock_count + #{quantity},让数据库自己处理行锁,就不存在这个问题。#{}是 MyBatis 的预编译参数占位符,天然防 SQL 注入。

4.3 销售出库与库存联动:一个 UPDATE 语句里的边界保护

出库是采购的反向操作。销售订单确认后,系统扣减库存,同时写销售流水。库存扣减和采购入库正好镜像,但 SQL 层多了一个边界判断:库存不能扣成负数。

很多第一次写这个功能的人会写出这样的逻辑:

// 出库前先查库存,够则扣减,不够则报错 Stock stock = stockMapper.selectByProductId(productId); if (stock.getStockCount() < quantity) { throw new BusinessException("库存不足"); } stockMapper.decreaseStock(productId, quantity);

这段逻辑单独跑没问题,但放到并发场景里就会翻车。两个销售单同时对一个商品出库,两个线程都查到库存是 5,都判断"够扣",然后都执行扣减,最后库存变成 -5。毕设项目一般不会有真实并发压力,但数据正确性是硬指标,答辩被问到"怎么避免超卖"时,这个优化点值得答出来。

更稳的写法和采购入库镜像,把条件判断直接并进 UPDATE 语句:

<!-- 带库存阈值判断的扣减:影响行数为 0 说明库存不足 --> <update id="decreaseStockWithCheck"> UPDATE stock_info SET stock_count = stock_count - #{quantity} WHERE product_id = #{productId} AND stock_count >= #{quantity} </update>

这段 SQL 的逻辑:WHERE限定stock_count >= #{quantity},库存不够时影响行数是 0。Service 层判断update 返回值 == 0就知道库存不足,不需要先查再改。即使多个事务并发执行,数据库行锁也会让第二个 UPDATE 等第一个提交后才能执行,此时库存已变,第二个的判断自然失败,从机制上避免负库存。

调用端配合写法:

// 扣减库存:返回 0 行表示库存不足,直接抛业务异常中断销售流程 int rows = stockMapper.decreaseStockWithCheck(productId, quantity); if (rows == 0) { throw new BusinessException("库存不足,无法出库"); }

这种"影响行数即结果"的模式在电商和供应链系统里非常常见。拿到源码后可以搜一下它 Mapper XML 里有没有类似的写法,如果有,说明写项目的人确实考虑了并发边界;如果只是简单 UPDATE,这也是一个很好的改造切入点,答辩时可以作为"我优化了库存扣减逻辑"的素材。

5. 部署与踩坑排查:SSM + JSP 最常见的五个翻车现场

5.1 现象一:Tomcat 启动报端口占用,页面一直转圈

现象:双击 Tomcat 启动脚本,控制台报java.net.BindException: Address already in use,或者浏览器访问localhost:8080一直转圈打不开。

原因:8080 端口被其他进程占用。常见占用者包括:之前启动过没关干净的另一个 Tomcat 实例、Oracle 或 MySQL 自带的 HTTP 服务、各类开发工具内置的 web 服务器。

解决:先确认占用者。Windows 上执行netstat -ano | findstr 8080看 PID,到任务管理器找到进程杀掉。如果这个端口要留给其他服务,就改 Tomcat 的 server.xml 里 Connector 的 port,改成 8081 或 9090,改完重启。另一个隐蔽场景是:同时启动了两个 Tomcat,第二个启动时日志显示Server startup in xxx ms,但实际访问的还是第一个实例,页面内容全是旧的,"改了代码页面不变"的奇怪问题,十有八九是访问错了实例。

5.2 现象二:页面能打开但 CSS 和 JS 全部 404,样式全丢

现象:JSP 页面渲染出完整 HTML,但没有任何样式,浏览器控制台一堆Failed to load resource,指向 bootstrap.min.css 和 style.css 的请求全部 404。

原因:SpringMVC 的前端控制器 DispatcherServlet 默认拦截了所有请求,包括静态资源。web.xml 里如果配置了<url-pattern>/</url-pattern>,静态文件会进入 Controller 查找流程,找不到对应 handler,直接返回 404。

解决:在 SpringMVC 配置文件里放行静态资源:

<!-- 放行静态资源:mapping 是 URL 路径,location 是服务器上的物理目录 --> <mvc:resources mapping="/css/**" location="/css/" /> <mvc:resources mapping="/js/**" location="/js/" /> <mvc:resources mapping="/images/**" location="/images/" />

mapping="/css/**"表示拦截所有以/css/开头的外部请求并转发到静态资源处理器,location="/css/"表示去 Web 应用根目录的 css 文件夹找对应文件。依次把 css、js、images 目录配上。另一个做法是在 web.xml 里给静态文件单独配一个 Servlet 映射,用 Tomcat 默认 DefaultServlet 处理,省事但写法较老。这个坑高发,是因为很多项目在 IDE 托管环境下没暴露,部署到独立 Tomcat 才现形。

5.3 现象三:数据库连接失败,报 ClassNotFoundException: com.mysql.jdbc.Driver

现象:Tomcat 启动时抛java.lang.ClassNotFoundException: com.mysql.jdbc.Driver,或后端接口一调数据库就报No suitable driver found for jdbc:mysql://...。

原因:两种情况。第一,mysql-connector-java 的 jar 包没随项目发布到 Tomcat 的 WEB-INF/lib;第二,本地用的是 MySQL 8.x,但配置里还是 5.x 的老驱动类名。

解决:先确认项目里有没有 mysql 驱动 jar,在 IDE 的 Deployment Assembly 里确认 WEB-INF/lib 下已经包含。然后看驱动类名,MySQL 8.x 必须写com.mysql.cj.jdbc.Driver,连接串还要追加serverTimezone=Asia/Shanghai,否则报时区错误。如果装的是 MySQL 5.7,确认驱动 jar 版本是 5.1.x 系列。排错顺序:先看 jar 在不在,再看驱动类名对不对,最后看连接串参数全不全。

5.4 现象四:页面中文乱码,供应商名称全变成问号

现象:数据库里中文正常,但 JSP 页面显示出来全是???,或者反过来页面上输入的中文存进数据库后变成乱码。

原因:字符集在链路里没对齐。JSP 页面编码、Tomcat 接收请求的编码、数据库连接串编码、数据库表结构字符集,四个环节只要有一个是 latin1 或默认 ISO-8859-1,中文就保不住。

解决:四个环节统一 UTF-8。JSP 文件头部加<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>;Tomcat 的 server.xml 里给 Connector 加URIEncoding="UTF-8"属性;jdbc.url 连接串保留characterEncoding=utf8;数据库表用 utf8mb4。还有一个经常被忽略的:文件本身要另存为 UTF-8 格式,IDE 默认编码如果不是 UTF-8,代码里写了 charset=UTF-8 但实际文件是 GBK,照样乱码。

5.5 现象五:改了代码重启后页面还是旧内容

现象:在 JSP 里加了一行输出,重启 Tomcat 再访问,页面上看不到新内容,浏览器清缓存也没用。

原因:两个源头。第一,Tomcat 的 work 目录缓存了编译后的 JSP class 文件,没清理;第二,CSS 和 JS 文件被浏览器缓存,服务器新版本没生效。

解决:重启 Tomcat 前手动删除 work/Catalina 目录内容,让 JSP 重新编译。浏览器打开开发者工具,Network 面板勾选 Disable cache,或直接用无痕窗口。更彻底的做法是静态资源 URL 后加版本号参数,比如style.css?v=20250101,每次改版换一个参数,浏览器当新资源重新请求,这个习惯在很多前端开发里是标配。

6. 二次改造前的验证习惯:换库、加接口、看日志的三个动作

源码跑通只是第一步,改造成自己的毕设才是这次下载的核心诉求。我的建议是沿一条固定验证路径走:先换库,再加功能,最后看日志,每一步都对上一环节做交叉确认。

换库这件事不光是改 db.properties 的连接信息,还要做一次数据完整性确认,用 SELECT 数一下关键表的行数,确认供应商、商品、采购订单这些主表都有初始数据可演示。答辩翻车往往翻在"演示的时候列表是空的"——不是功能坏了,是初始数据没导全。

加功能的验证动作,建议从最简单的开始:在已有 Controller 里新加一个返回 JSON 的接口,用来验证请求映射和响应序列化是否通畅:

// 新增 JSON 接口:验证请求映射、Service 注入、Mapper 查询、序列化整条链路 @RequestMapping("/api/count") @ResponseBody public Map<String, Object> count() { Map<String, Object> result = new HashMap<>(); result.put("supplierCount", supplierService.count()); result.put("status", "ok"); return result; }

@ResponseBody注解把返回值直接序列化为 JSON 写进响应体,不走视图解析器。浏览器访问/ssm685/api/count能看到 JSON 字符串,说明 SpringMVC 映射、Service 注入、Mapper 查询、序列化这一整条链路都是通的。如果返回 404 或 500,顺着日志往回查,多半是 Mapper 里少了对应方法绑定。

最后是看日志的习惯。老项目没有完整日志配置时,MyBatis 的 SQL 打印就是最好的调试工具。在 mybatis-config.xml 里把日志级别调到 DEBUG,控制台会输出每条执行的 SQL 语句和参数。每次改动后重启,先看 SQL 有没有按预期执行,再去看页面效果。

从那以后,我每次拿到别人的源码都强制自己走一遍"换库、加接口、看日志"这三步,不跑完这三步绝不开始改页面——这套习惯帮我挡掉了无数"改了代码没反应"和"数据对不上"的隐蔽问题。希望帮到你。

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