简介:这是一套面向高校计算机专业学生与Java初学者的校园二手交易平台实战项目源码,基于SSM(Spring+SpringMVC+MyBatis)框架构建,兼顾Web管理端与移动端协同场景,解决校内闲置物品流通效率低、信息不对称等实际问题。资源共188个文件,含32个核心Java业务类、90个XML配置与映射文件(涵盖Spring容器配置、MyBatis SQL映射及Android布局资源)、50张PNG图标与界面素材,以及JSP后台管理页面、Gradle构建脚本、WAR部署包等完整交付物,压缩包大小为60.92MB。已有345人学习下载,适合课程设计、毕业设计或小型团队快速搭建可运行的二手交易系统。读者可直接导入IDE运行后端服务,配合原生Android客户端调用API,并通过JSP页面完成商品审核、用户管理等后台操作,目录结构清晰,模块划分明确,包含Fragment页面组织、Gradle多模块构建支持及基础权限控制逻辑。 毕业设计选了校园二手交易系统,从网上下了一份SSM源码,结果导入IDEA之后,启动Tomcat直接白屏,控制台报了一堆错——这是我在好几个技术交流群里见过最多的求助。这个项目本身其实不复杂,真正难住人的地方在于:面对一份完整的SSM框架源码,你分不清哪些东西是基础配置、哪些是业务代码、哪些地方必须根据自己电脑环境去改。我自己前前后后帮人排查过几十个类似的报错,也带过几个实习生从零把这个项目跑通,所以把整个系统的设计思路、源码结构、数据库表关系、启动步骤和踩坑点一次性讲透。
这篇博文围绕基于SSM框架的校园二手交易系统展开,覆盖Spring、Spring MVC、MyBatis三个框架的整合方式、前后台功能拆解、数据库表设计、核心功能实现思路,以及拿到源码后如何配置环境、启动项目、解决常见错误。适合正在做毕业设计、准备SSM框架面试、或者想通过一个完整项目练手来理解框架底层原理的读者。不管你是刚学完SSM基础语法,还是已经能写单体接口但没接触过完整JSP+SSM项目,这篇内容都能给你省下不少时间。
1. 为什么校园二手交易系统是最适合练手的SSM完整项目
1.1 从项目复杂度看性价比
我说个很直白的结论:校园二手交易系统是一个“业务量足够、技术栈覆盖足够、代码量适中”的典型CRUD综合体。它不像电商秒杀系统那样需要高并发设计,也不像企业级OA那样有复杂的工作流引擎,但它能把SSM框架三个核心组件全部用到实处,覆盖了注册登录、商品发布、图片上传、搜索筛选、收藏下单、订单状态管理、后台审核这些常见业务场景。
你用这个项目去练手或者做毕设,学的不是某个炫技算法,而是一个真实Web项目应该有完整闭环:前端页面发出请求,Spring MVC负责接收分发,Service层处理业务逻辑,MyBatis通过Mapper接口和SQL语句访问MySQL数据库,最后视图解析返回JSP页面。这个过程恰好是SSM框架中“谁负责什么、谁调用谁、配置怎么串起来”的最直观呈现。
1.2 一份标准SSM校园二手交易源码包含哪些内容
拿到源码后,你首先要知道整个工程里有哪些东西。标准Maven结构的SSM项目通常包含以下内容:
src/main/java:Java源码,按controller、service、mapper、pojo、common等包分层src/main/resources:配置文件,包括Spring配置、Spring MVC配置、MyBatis配置、数据库连接配置、日志配置src/main/webapp:前端资源,JSP页面、JS、CSS、图片、WEB-INF下的web.xmlsql目录:数据库脚本文件,通常是.sql格式,包含建库建表语句和基础测试数据pom.xml:Maven依赖描述文件,锁定项目依赖的三方库版本- 部分源码会附带README或部署文档,但很多网上下载的源码没有,这也是我写这篇文章的直接原因
1.3 这套源码能让你学到什么
我建议你带着这三个目标去读这份源码:第一,理解SSM整合的配置链路,知道为什么需要applicationContext.xml、spring-mvc.xml、mybatis-config.xml,以及它们各自的职责边界;第二,看懂一次完整的请求从JSP页面到数据库再返回页面的整个路径,每条数据是怎么流动的;第三,掌握一个中等规模Web项目DAO层、Service层、Controller层的代码组织方式,这种分层思想以后换到Spring Boot项目同样适用。
2. 为什么选SSM而不是Spring Boot:三个框架的分工逻辑
2.1 Spring、Spring MVC、MyBatis各自解决什么问题
很多人学框架时把SSM当成一个大黑盒,每个框架具体干了什么说不清。打个比方:一个外卖平台要完成一次送餐,Spring就像是平台的调度中心,负责管理所有员工(Bean)的生命周期,员工之间谁依赖谁、谁需要谁,都由调度中心统一分配;Spring MVC是前台接待窗口,用户下单(HTTP请求)先到这个窗口,窗口根据订单内容分发给对应的处理部门(Controller);MyBatis则是和仓库(数据库)打交道的物流组,它把Java方法的调用翻译成仓库能听懂的SQL指令,再把仓库返回的数据装回Java对象。
对应到代码层面:Spring管理Service和Mapper的Bean实例以及事务边界;Spring MVC管理Controller的注册、请求映射、参数绑定、视图解析;MyBatis负责Mapper接口与SQL映射文件之间的绑定关系,执行增删改查并完成结果集到JavaBean的自动映射。
2.2 SSM与Spring Boot的本质差异,以及为什么这些源码用SSM
有人问我:现在新项目都用Spring Boot,学SSM还有意义吗?我的看法是:SSM的价值不在“新”,而在“透明”。Spring Boot把Tomcat内嵌、自动配置、约定大于配置全部做了封装,你很容易写出一个能跑的接口,但你不一定知道Spring容器是什么时候启动的、DispatcherServlet在哪里注册的、MyBatis的Mapper是怎么被扫描进容器的。SSM项目把这一切都摆在你面前,每一段配置都对应一个明确的初始化动作,你对照源码能真正理解框架的工作原理。
此外,大量高校课程设计和毕业设计仍然要求基于SSM框架实现,这类源码的需求量一直很大。理解了SSM的整合方式,后续迁移到Spring Boot非常快——Boot只是把配置变成自动约定,底层核心逻辑并没有变。
2.3 SSM整合时配置文件的职责划分
一个典型的SSM项目至少有三类配置文件,这是我强烈建议你逐个打开去看的:
| 配置文件 | 职责范围 | 关键内容 |
|---|---|---|
applicationContext.xml | Spring根容器配置 | 数据源、SqlSessionFactory、事务管理器、Service层扫描 |
spring-mvc.xml | Spring MVC子容器配置 | Controller扫描、视图解析器、静态资源映射、文件上传解析器 |
mybatis-config.xml | MyBatis全局配置 | 驼峰映射、日志、类型别名,少部分项目还会在这里配Mapper路径 |
这里有一个容易忽略的知识点:Spring根容器和Spring MVC子容器是父子容器关系,Controller在子容器中注册,Service和Mapper在父容器中注册,子容器可以访问父容器的Bean,父容器不能访问子容器的Bean。很多启动报NoSuchBeanDefinitionException的问题,就是扫描包范围配置错了。
3. 功能拆解:前台和后台分别包含什么
3.1 前台用户端核心模块
校园二手交易系统的前台面向普通学生用户,设计时围绕“逛—搜—发—买—管”这条完整链路展开:
- 用户注册与登录:用户名、密码、学号、手机号、邮箱等信息校验,密码加密存储,登录状态通过Session或Cookie维持
- 商品分类浏览:按数码、图书、生活用品、衣物、运动器材等分类展示商品列表,支持分页
- 关键字搜索:根据商品标题、描述做模糊查询,筛选条件通常包括分类、价格区间、新旧程度
- 商品详情查看:展示商品图片、价格、原价、成色描述、卖家信息、发布时间,同时显示浏览量
- 商品发布:登录用户填写标题、分类、描述、价格,上传封面图和细节图,提交后进入待审核或直接上架状态
- 收藏功能:用户可以对感兴趣的商品添加收藏,后续在个人中心查看收藏列表
- 下单购买:针对某个商品生成订单,如果项目包含在线支付模拟,则增加支付状态字段;如果不做支付,通常采用线下见面交易模式,订单状态流转相对简单
- 站内留言或咨询:买家在商品详情页留言提问,卖家可以回复
- 个人中心:修改资料、查看自己发布的商品列表、管理订单状态、查看收藏列表
3.2 后台管理端模块
后台管理端是给系统管理员使用的,很多源码把后台和前台的登录入口分开,通过用户角色字段区分。核心功能主要有:
- 用户管理:查看用户列表、启用或禁用账号、重置密码,管理员账号通常单独在数据库初始化
- 商品管理:审核新发布的商品,下架违规商品,删除垃圾信息
- 订单管理:查看所有订单记录,处理异常订单状态
- 分类管理:新增、编辑、删除商品分类,调整分类排序
- 数据统计(部分项目包含):统计每日新增用户数、新增商品数、成交订单数,展示简单数据图表
3.3 不同版本源码的常见差异
需要提醒你的是,网上的SSM校园二手交易系统源码有多种变体:有的带管理员后台,有的只有前台基础功能;有的用了JSP+JSTL做服务端渲染,有的改成了前后端分离返回JSON;有的集成了Redis缓存,有的没有。下载源码后先花10分钟梳理它的功能列表和页面文件,确认它用的是哪种模式,再决定怎么动手改,避免一上来就瞄着代码改结果越改越乱。
4. 数据库设计:六张表的字段、状态与关联关系
4.1 核心表结构速览
数据库设计决定了业务逻辑的复杂度边界。二手交易系统最核心的实体有用户、商品、分类、订单、收藏、留言,对应的数据表通常如下:
4.2 用户表t_user
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键,自增 |
| username | varchar(50) | 登录用户名,唯一 |
| password | varchar(128) | 加密后的密码 |
| nickname | varchar(50) | 昵称 |
| student_no | varchar(30) | 学号,二手交易场景下常用来增强身份信任 |
| phone | varchar(20) | 手机号 |
| varchar(100) | 邮箱 | |
| avatar | varchar(255) | 头像图片路径 |
| role | tinyint | 角色字段,0普通用户,1管理员 |
| status | tinyint | 状态字段,0禁用,1正常 |
| create_time | datetime | 注册时间 |
4.3 商品表t_goods
这是整个系统中字段最多、状态变更最频繁的表。关键字段及设计思路如下:
title:商品标题,用于列表展示和搜索description:商品详情描述,一般用text类型price/original_price:售价和原价,使用decimal(10,2)category_id:关联分类表的外键cover_image:封面图路径,列表页只取这一张图,减轻页面渲染压力images:细节图,可用逗号分隔的多路径存储,也可单独建一张商品图片表status:商品状态,我见过比较规范的设计是 0下架、1在售、2卖出、3审核中、4审核拒绝 五档view_count:浏览量,商品详情页每次打开自增,这个字段可以以后扩展热度排序seller_id:发布者ID,关联用户表
商品状态是项目中非常容易写错的地方。举个例子:当买家对某商品下单成功后,商品状态应该立刻切成“已卖出”,否则其他用户还能继续下单,同一件商品被重复卖出,这在二手交易场景中是很严重的逻辑漏洞。
4.4 分类表t_category和收藏表t_favorite
分类表字段极简:id、name、sort_order、create_time。sort_order用于控制前台分类展示顺序,后台管理分类时可以修改。收藏表是典型的多对多关系中间表:id、user_id、goods_id、create_time,查询某个用户收藏列表时,用user_id关联goods_id即可。
4.5 订单表t_order与状态机设计
订单表是理解整个系统业务逻辑的重点,字段大致如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| order_no | varchar(64) | 订单编号,通常用时间戳+随机数生成 |
| goods_id | int | 关联商品表 |
| buyer_id | int | 买家ID |
| seller_id | int | 卖家ID |
| price | decimal(10,2) | 成交价 |
| status | tinyint | 订单状态 |
| create_time | datetime | 下单时间 |
| pay_time | datetime | 支付/确认时间 |
| finish_time | datetime | 完成时间 |
订单状态推荐用数字常量管理:0待付款、1已付款待发货、2已发货、3确认完成、4已取消。在实际二手交易项目里,如果走线下当面交易,状态也可以简化为 0待确认、1进行中、2已完成、3已取消,具体取决于你的业务设计。关键是状态之间的流转要有限制:已取消的订单不能再变成已完成,已完成的订单不能再被取消,这些判断逻辑写在Service层而非Controller层。
这里还有一个细节值得学习:order_no之所以要单独生成而不是直接用自增主键,是因为订单号可能出现在短信通知、聊天记录、线下核验等场景,一个唯一且不易重复的订单号比纯数字主键更安全、更合理。
5. 源码包结构与一次请求的完整旅程
5.1 标准分包结构是这样的
拿到一套完整的SSM项目源码,Java包结构通常长这样:
com.secondhand ├── common │ ├── Result.java // 统一返回结果封装 │ ├── PageResult.java // 分页结果封装 │ └── Constant.java // 常量类,定义订单状态、商品状态等 ├── controller │ ├── UserController.java │ ├── GoodsController.java │ ├── OrderController.java │ ├── FavoriteController.java │ └── AdminController.java ├── service │ ├── UserService.java │ ├── GoodsService.java │ ├── OrderService.java │ └── impl/ │ ├── UserServiceImpl.java │ ├── GoodsServiceImpl.java │ └── OrderServiceImpl.java ├── mapper │ ├── UserMapper.java │ ├── GoodsMapper.java │ ├── OrderMapper.java │ └── GoodsMapper.xml └── pojo ├── User.java ├── Goods.java ├── Order.java └── vo/ └── GoodsVO.java // 视图对象,可能包含分类名、卖家昵称等冗余字段这套结构和Spring Boot项目本质上是一样的,区别只是SSM项目多了XML配置文件,Spring Boot项目则用注解把它压缩到极致。建议你先看pojo下的实体类,再看mapper接口和XML,然后顺着service、controller的顺序往上读,这样整个项目的数据流转关系会非常清楚。
5.2 一次“发布商品”请求是怎样从头走到尾的
我以一个实际请求为例,把调用链完整拆开:
- 用户在JSP页面的商品发布表单中填写信息,点击提交,浏览器发起
POST /goods/publish请求,表单数据中包含文件类型的图片 DispatcherServlet(前端控制器)拦截请求,根据@RequestMapping("/goods/publish")找到GoodsController的对应方法- Spring MVC将请求参数自动绑定到
Goods对象中,文件参数绑定到MultipartFile对象 - Controller调用
GoodsService.publish(goods),Service层补充一些Controller层不该关心的逻辑:校验用户是否登录、校验商品标题和价格是否合法、生成默认状态等 - Service层调用
GoodsMapper.insert(goods),MyBatis执行SQL语句把数据写入MySQL - 返回成功结果,Controller将用户重定向或转发到商品列表页,整个请求完成
这里面有一个典型的设计原则问题:Controller不应该写业务逻辑。很多学生能写出来的代码长这样:Controller里直接注入Mapper,然后调用insert,Service层形同虚设。这种写法在小型项目里确实能跑,但一旦业务变得复杂——比如下单时需要校验商品状态、需要更新商品状态、需要生成订单号——Controller就会膨胀得没法维护,面试官考核代码质量时也会重点关注这一点。
5.3 配置文件之间如何互相协作
SSM项目的启动流程可以简单概括为:Tomcat启动时读取web.xml,加载Spring的ContextLoaderListener初始化根容器,加载DispatcherServlet时初始化Spring MVC子容器,两个容器各自扫描不同包下的Bean,最终形成完整的Bean依赖网络。你在源码里找到web.xml,里面会配置这两者的映射关系。
配置文件里面最容易被忽略的是数据库连接配置。很多项目用jdbc.properties保存数据库账号密码,配置内容大概是:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/secondhand?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456特别注意:如果本地安装的是MySQL 8.0以上,驱动类应该写com.mysql.cj.jdbc.Driver而不是com.mysql.jdbc.Driver,URL里面必须加serverTimezone参数,否则会报时区错误。这个问题在第七节还会详细说。
6. 四个核心功能的实现思路与关键代码
6.1 注册登录:MD5加盐与登录状态拦截
用户密码不能明文存储,这是基本的安全底线。在SSM项目中,最简单的做法是在注册时对密码做MD5加密,登录时把用户输入的密码做相同加密后比对:
public String encryptPassword(String password, String salt) { return DigestUtils.md5DigestAsHex((password + salt).getBytes()); }如果想要更安全,可以在原始密码上拼接一个随机盐值,每个用户盐值不同,这样相同密码的密文也不一样。不过大部分课程设计用固定盐值的MD5已经足够,但你要知道这个方案的缺陷:没有加盐的MD5很容易被彩虹表撞库。
登录状态通过拦截器维持是比较合理的方案。Spring MVC的拦截器配置在spring-mvc.xml中,拦截所有需要登录才能访问的URL,在preHandle方法中检查Session中是否存在用户对象,不存在则跳转到登录页:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }拦截器的思路可以扩展到权限控制:后台管理请求除了需要登录,还需要校验role == 1(管理员),否则拒绝访问,这就是一个最简单的基于角色的访问控制模型。
6.2 商品发布与图片上传:文件命名与回显路径
上传图片是二手交易系统最核心的交互功能之一。Controller方法接收MultipartFile参数,然后把文件写入服务器的某个目录:
@PostMapping("/goods/publish") public String publish(@RequestParam("file") MultipartFile file, Goods goods, HttpSession session) { if (!file.isEmpty()) { String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String filename = System.currentTimeMillis() + "_" + new Random().nextInt(1000) + suffix; String realPath = request.getServletContext().getRealPath("/upload"); file.transferTo(new File(realPath + File.separator + filename)); goods.setCoverImage("/upload/" + filename); } goodsService.publish(goods, user); return "redirect:/goods/list"; }这里有两个经验值得记下来。
第一,保存到数据库的路径应该是相对路径/upload/xxx.jpg,而不是绝对路径。因为项目的部署环境可能改变,绝对路径一旦变化,页面上的图片就全部失效。相对路径配合Tomcat的静态资源映射,无论在本地还是服务器上都能正常访问。
第二,文件名必须做重命名。用户上传的文件名可能是“毕业设计封面.jpg”甚至包含特殊字符,直接用原名存储很容易造成路径解析错误。用时间戳+随机数生成新文件名,既避免文件名冲突,也兼容各种字符集。Spring MVC的配置中还需要添加文件上传解析器和大小限制:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="5242880"/> </bean>6.3 商品搜索:MyBatis动态SQL拼接
商品搜索功能适合拿来理解MyBatis动态SQL。用户可能按标题搜,也可能同时勾选分类、价格区间,查询条件是不固定的,如果每个组合都写一条SQL,工作量巨大且不现实。MyBatis的方案是用<where>和<if>节点动态拼接查询条件:
<select id="searchGoods" resultType="com.secondhand.pojo.Goods"> SELECT * FROM t_goods <where> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> AND status = 1 </where> ORDER BY create_time DESC </select>这段SQL里有个小坑:在MyBatis XML中,大于号>和小于号<属于XML转义字符,不能直接写。要么用>和<转义,要么把整段SQL放进<![CDATA[]]>中,否则解析XML时就会报错。你看到别人的源码中写>=时别觉得奇怪,那是XML的合法写法。
6.4 订单状态流转:从下单到完成的状态机控制
订单模块最容易出现的逻辑问题,前面已经提到过——商品被重复下单。这个问题必须在Service层通过事务控制来解决,实现思路是:
- 下单前根据商品ID查询商品记录,判断
status == 1(在售) - 校验通过后,开启事务,更新商品状态为2(已卖出)
- 同时插入订单记录,订单初始状态为0(待付款)或1(待确认)
- 提交事务;任何一步失败则回滚
@Transactional public void createOrder(Order order) { Goods goods = goodsMapper.selectById(order.getGoodsId()); if (goods == null || goods.getStatus() != 1) { throw new RuntimeException("商品不存在或已下架"); } // 更新商品状态为已卖出 goodsMapper.updateStatus(order.getGoodsId(), 2); // 生成订单 order.setOrderNo(generateOrderNo()); order.setStatus(0); orderMapper.insert(order); }@Transactional是Spring声明式事务的体现,它意味着这个方法内的所有数据库操作要么全部成功,要么全部回滚。如果不加事务,更新商品状态成功但插入订单失败,就会出现商品变已卖但订单不存在的脏数据。这是很典型的数据库一致性问题,面试时经常以此为切入点考察候选人对事务的理解。
7. 部署实操:从零把源码跑起来
7.1 环境准备清单
先说结论:一个SSM小项目通常不需要太新的版本,我推荐的搭配是:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 1.8 | 大部分SSM项目都基于Java 8编写,代码中很少用到新版本语法 |
| Maven | 3.6.3 | 稳定版,配置阿里云镜像后依赖下载速度会快很多 |
| Tomcat | 8.5 | 与Servlet 3.1规范兼容良好 |
| MySQL | 5.7 或 8.0 | 5.7和8.0在连接方式上有差异,注意驱动配置 |
| IDEA | 任何较新版本 | Ultimate版自带Tomcat集成 |
7.2 完整启动步骤
第一步,在IDEA中用File → Open打开下载好的源码目录,确认它被识别为Maven项目。如果IDEA没有自动导入依赖,在右侧Maven面板点击刷新按钮,等待依赖下载完成。
第二步,安装并启动MySQL,新建数据库,然后执行SQL脚本。建议直接在Navicat或命令行中执行:
CREATE DATABASE secondhand DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE secondhand; SOURCE /path/to/secondhand.sql;执行后检查一下核心表是否创建成功,确认表名前缀和源码中JDBC配置的表名一致。
第三步,修改jdbc.properties文件。这里有一个高频问题:源码里自带的数据库密码是root或123456,你本地环境不同,如果漏改,启动时不会报错,但一访问数据库相关页面就会出现500错误,所以配好后务必检查一遍。
第四步,配置Tomcat。在IDEA中打开Run → Edit Configurations,添加一个Tomcat Server,在Deployment选项卡中添加Artifact,选择项目war或war exploded。建议选war exploded模式,因为这种模式可以直接加载本地文件,修改JSP页面后刷新浏览器就能看到效果,不需要重新打包。
第五步,启动后访问地址。如果部署时没有修改Application context,访问路径通常是http://localhost:8080/项目名/,例如http://localhost:8080/secondhand/。如果出现404,先确认项目名是否在URL中,这是新手最容易犯的错误——Tomcat的根路径默认不是你的项目。
7.3 启动成功后的验收清单
项目能跑起来只是第一步,我建议启动后按下面几条逐项验证:
- 注册一个新用户,密码加密入库,数据库中密码不是明文
- 登录后进入商品列表,确认图片能正常加载
- 发布一个测试商品,带上图片上传,确认图片存储路径和回显正常
- 用另一个账号查看该商品,确认商品状态显示为“在售”
- 下单后回到卖家账号,确认商品状态已变为“已卖出”,列表不再展示
- 前往后台管理页面,确认管理员账号可以登录并看到商品条目
这套验收流程走完,说明项目主体功能闭环是通的,后面再改动代码就有底气了。
8. 第一次跑SSM项目大概率踩的坑
8.1 Java版本不匹配导致启动失败
源码如果是在旧版JDK下编译的,而你本地用JDK 11或17,Maven编译时可能出现source 1.5 中不支持 diamond 运算符之类的报错。解决办法是在pom.xml中显式声明编译版本:
<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>如果pom.xml里已经写了,IDEA的Project Structure中也要确认Project SDK选择的是1.8,三者保持一致才能避免编译版本冲突。
8.2 Tomcat启动后访问全是404
这个问题的出现概率非常高。第一个可能是项目没有成功部署,在Tomcat部署配置里检查Artifact是否已经添加;第二个可能是URL缺少项目名,也就是把http://localhost:8080/secondhand/login写成了http://localhost:8080/login;第三个可能比较隐蔽——项目结构不规范,JSP文件放到了WEB-INF外面或者缺失欢迎页配置,导致访问根路径找不到默认页面。逐项排查即可,绝大多数404都出在前两种原因。
8.3 MySQL连接失败和时区错误
用MySQL 8.0的童鞋特别容易碰到这个问题。报错一般长这样:
java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized解决办法很明确,在JDBC的URL后面追加serverTimezone=Asia/Shanghai,同时把驱动类改成com.mysql.cj.jdbc.Driver。另外如果MySQL 8.0使用了caching_sha2_password认证插件,而项目用的驱动版本过低,也会连接失败,把mysql-connector-java版本升到8.0以上即可。
8.4 图片上传成功但页面不显示
图片不显示,十有八九是路径访问被Spring MVC拦截了。Spring MVC的DispatcherServlet默认映射为/时,会拦截所有请求,包括静态资源。需要在spring-mvc.xml中配置静态资源映射:
<mvc:resources location="/upload/" mapping="/upload/**"/> <mvc:resources location="/static/" mapping="/static/**"/>否则浏览器请求/upload/xxx.jpg时会被当成一个Controller路径处理,找不到映射自然404。这个问题虽然代码短,但排查起来很隐蔽,很多人以为是保存路径错了,折腾半天才发现是静态资源映射没配。
8.5 部署环境变动后图片全部丢失
如果在本地能显示图片,但发布到服务器后图片全部丢失,多半是因为图片保存到了Tomcat部署目录下。这个目录会随项目重新部署而被清空。严格来说,应该把上传目录单独配置到服务器固定路径,比如/data/upload/,再通过虚拟路径映射对外提供访问。不过对于课程设计和毕设项目而言,本地开发使用相对路径已经足够,知道自己部署到服务器时要注意这个区别即可。
8.6 其他频率很高的坑
除了上面说的几类,还有几个在SSM项目里非常常见:JSP页面中文乱码,排查顺序是页面编码、请求和响应编码过滤器、数据库连接URL中的characterEncoding=utf8;MyBatis的Mapper接口和XML找不到绑定关系,检查命名空间是否和接口全限定名一致,mapper的XML文件是否被Maven打包到classes目录;Controller返回的JSON数据中包含实体类循环引用导致序列化失败,可以用@JsonIgnore注解在关联字段上解决。
这些坑单看都不难,但它们串联起来的排查过程,恰恰是你理解SSM项目运行机制最好的机会。一套源码跑通、读完、改过一遍,你对Spring容器、Spring MVC的请求流程、MyBatis的SQL映射这几个抽象概念的理解,会比看十遍教程都深刻。
最后再分享一个我自己的经验:拿到这类源码,不要一开始就想着改功能。先按数据库脚本把表建好,然后顺着web.xml → spring-mvc.xml → Controller → Service → Mapper.xml的顺序把代码通读一遍,每读到一个模块就在纸上画一下它的请求链路。等你对整个项目有了全局认识,再动手改第一处代码——比如给商品搜索页加一个排序功能,或者给订单列表加一个状态筛选。改完跑通之后,这个项目的源码才真正属于你了。后面无论做毕设答辩,还是面试聊项目,你都能拿出真东西。
本文还有配套的精品资源,点击获取