简介:这是一套基于SSM框架开发的购物商城系统完整源码,适合正在做Java Web课程设计、毕业设计或想学习SSM整合开发的读者。系统包含前台商城展示与后台管理功能,前后台结构完整。压缩包内共有2000个文件,大小约66.94MB,其中以Java源码、class编译文件、JSP/HTML页面、JS与CSS前端资源为主,同时包含XML配置文件、数据库SQL脚本、导入工具及说明文档,方便对照学习前后端交互与SSM配置。运行前需将JDK、Tomcat调整为本地版本,并修改数据库连接信息(注意代码中有两处配置),即可在本地部署体验。目前已有140人学习下载,适合具备一定Java基础、希望快速获得一个可运行商城项目作为参考的开发者。
1. 从 .rar 解压出来的 SSM 购物商城系统,先看懂框架再跑业务
在技术社区的帖子、培训机构备份盘和企业内部交接文档里,几乎都能翻到一个命名为“ssm框架购物商城系统.rar”的压缩包。解压出来就是一套典型 Java Web 工程:Spring 管理业务对象、Spring MVC 接收请求并分发、MyBatis 负责数据库访问。这套组合虽然已不如 Spring Boot 新,但在中小型电商系统、教育系统和大量内部管理后台里,SSM 依然是实打实的技术底子。对刚接触框架的人,商城是最直观的三层架构样例;对维护老系统的人,理解这套结构直接决定排障效率。不同来源压缩包内容差异不小,有的带 SQL 脚本,有的只有源码,我按业内最标准工程形态讲操作路径:下载后看什么、配置改哪里、核心链路怎么写、部署后排查哪些坑。你手头解压出来的目录以它为准,下文所有路径和包名都是最通用的写法。
2. SSM 框架在购物商城的工程分工,先把三层架构理清
刚拿到压缩包时,先去分清楚 controller、service、dao 三个包的边界,再谈配置。SSM 不是一个大框架,而是 Spring、Spring MVC、MyBatis 三个框架各管一段:Spring 管对象实例和事务,Spring MVC 管 HTTP 请求的接入和响应,MyBatis 管 SQL。商城里“用户下单”这个动作,从页面点击到库表写入,要依次穿过这三层,每一层只做自己的事,层与层之间靠接口隔开。下面按分层职责逐个拆。
2.1 Spring 的 IOC 容器和 AOP 事务,是商城服务层的地基
Spring 在 SSM 工程里承担两件事:其一是 IOC,service 实现类不再自己 new,而是交给容器实例化,业务代码声明字段,容器按类型注入;其二是 AOP,把开启事务、提交、回滚这类横切逻辑统一加到方法前后。商城的下单、扣库存、支付回调都依赖强一致事务,这两块缺一不可。
SSM 项目里的事务配置通常在单独 XML 文件中:
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="create*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="delete*" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <aop:config> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.shop.service.*.*(..))"/> </aop:config>REQUIRED表示当前没有事务就新建,已有事务就加入,适合 create/update/delete 开头的方法。有的事务配置会把select*标成read-only="true",实际读操作不用事务,加上只读反而可能让 MySQL 走旧快照,产生重复读问题,所以我不建议给查询方法强行包只读事务。这里最隐蔽的问题是pointcut只匹配com.shop.service包下一级类,如果实现类在com.shop.service.impl子包里,切点就落空,表现为下单异常时不回滚——这是后面第 5 章要重点排查的现场。
2.2 Spring MVC 的 DispatcherServlet 与商城 URL 路由映射
Spring MVC 的核心是 DispatcherServlet,所有请求先到它这里,再由 HandlerMapping 找到对应 Controller 方法,执行后返回视图名。商城项目里的路由有很强规律:商品列表是/product/list,购物车是/cart/list,提交订单是/order/save,登录是/user/login。如果解压出来的 Controller 里/admin/**和/cart/**混在一个类里,说明作者没有按模块拆路由,维护时改一个接口容易碰到另一个。
登录成功后的跳转是这套路由里比较典型的一段:
@Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @RequestMapping(value = "/login", method = RequestMethod.POST) public String login(String username, String password, HttpSession session, Model model) { User user = userService.login(username, password); if (user == null) { model.addAttribute("msg", "用户名或密码错误"); return "user/login"; } session.setAttribute("loginUser", user); return "redirect:/product/list"; } }@RequestMapping(value = "/login", method = RequestMethod.POST)限定只接 POST 请求,避免用户把密码拼在 URL 上。redirect:/product/list是重定向写法,浏览器地址会变成商品列表,同时刷新 Session 中的用户状态。这里有个容易被忽略的细节:如果 Controller 里既有/login的 GET 又写 POST,两个方法必须用 method 区分开,否则启动报重复映射异常。
2.3 MyBatis 把 SQL 放在 Mapper,商城查询和分页才好维护
MyBatis 的特点是把 SQL 写在 XML 里,由 Mapper 接口和 XML 做一一映射。商城项目最典型的场景是商品检索:按分类查、按关键词模糊查、按价格区间查,再分页。把所有过滤条件写进 XML,用动态 SQL 拼接,比在 Java 里用字符串拼 SQL 安全得多,页面传什么参数就拼什么条件。
<select id="pageQuery" resultType="com.shop.entity.Product"> SELECT id, name, price, stock, category_id, image, create_time FROM product <where> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="minPrice != null"> AND price <![CDATA[ >= ]]> #{minPrice} </if> </where> ORDER BY create_time DESC </select><where>会自动去掉第一个条件前面多余的 AND;#{categoryId}是预编译占位符,能挡住 SQL 注入,比${}拼接安全得多。CONCAT('%', #{keyword}, '%')是标准模糊查询写法,不推荐直接用<if test='keyword != null'>LIKE '%${keyword}%'</if>,那等于把用户输入直接拼进 SQL。价格条件里的大于等于号用 CDATA 包住,避免 XML 转义出问题。这类文件如果启动时报 “Content is not allowed in prolog”,多半是文件头部有 BOM 或声明写错。
2.4 为什么老商城还在用 SSM,以及什么时候该迁移
现在新项目都用 Spring Boot,但存量 SSM 商城还很多。跑得好好的系统没人愿意为换技术而重构,SSM 三层边界清晰,出了问题能直接在 filter、controller、service、mapper 四层里定位。Spring Boot 的自动配置在做一个定制程度很高的商城时,反而要不断排除自动装配项,迁移成本不低。
| 对比项 | SSM 商城 | Spring Boot 商城 |
|---|---|---|
| 配置方式 | XML + 注解混合 | 约定优先,注解为主 |
| 部署形态 | war 包丢 Tomcat | 内置容器,jar 直接跑 |
| 事务管理 | 声明式事务 + AOP 配置 | @Transactional 默认生效 |
| 上手曲线 | 要先懂容器原理 | 快,但排障偏黑盒 |
迁移时机一般出现在两个节点:商城要接入大量第三方 SDK,或者团队决定拆微服务。前者 Boot 的自动装配更省力,后者 SSM 的 war 包在容器编排里部署繁琐。除此之外,就让它在现有 Tomcat 上稳定跑着,别把一个正在盈利的系统当重构试验田。
3. 拿到 ssm 购物商城压缩包后的三个动作:导工程、建库、改配置
解压后的第一件事不是急着启动 IDEA。我见过太多人连数据库表都没建就导入工程,然后被一堆红色报错吓到。正确顺序是固定的:确认 Maven 依赖拉得下来、把 SQL 导进数据库、检查并修正配置。顺序不能乱,因为 Spring 容器启动时就要初始化数据源,数据源连不上,后面全是连环报错。
3.1 工程结构、Maven 依赖和启动入口
标准 SSM 商城源码包解压后,目录基本是这副骨架:
root ├── pom.xml ├── src/main/java │ └── com/shop │ ├── controller │ ├── service │ ├── dao │ └── entity ├── src/main/resources │ ├── applicationContext.xml │ ├── spring-mvc.xml │ ├── mybatis-config.xml │ ├── jdbc.properties │ └── mapper └── src/main/webapp ├── WEB-INF/web.xml └── static如果压缩包里没有 pom.xml,只有 .class 文件或整个.idea目录,说明这不是源码工程,可执行的改动空间很小。以 pom.xml 作为工程入口,用 IDEA 的 Open 选择该目录,IDE 识别成 Maven 项目后会自动拉依赖。pom 里至少要出现这五个坐标:spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java,再加 javax.servlet-api 和 jstl 就够跑通。某些包会直接用 DruidDataSource 替代 DBCP,这时 resources 下还会多一个 druid.properties,这是好事,Druid 自带监控页面,排查连接泄漏时有用。
3.2 商城关系型数据模型,SQL 脚本导入的正确姿势
商城数据库最小也得六张表:用户、分类、商品、购物车、订单、订单明细。多数 SSM 源码包会把 SQL 放在resources/sql/或压缩包根目录的db/文件夹里,文件名形如mall.sql或store.sql。如果包里没有脚本,就需要根据 entity 类和 mapper XML 反推建表语句,工作量不小,这类包一般是只给了部分源码,不建议硬啃。
六张表的职责划分如下:
| 表名 | 作用 | 主要字段 |
|---|---|---|
| user | 用户账号 | id, username, password, nickname, phone |
| category | 商品分类 | id, name, sort |
| product | 商品主体 | id, category_id, name, price, stock |
| cart | 购物车行 | id, user_id, product_id, quantity |
| orders | 订单主表 | id, user_id, order_no, total_price, status |
| order_item | 订单明细 | id, order_id, product_id, quantity, price |
把 SQL 文件导入 MySQL:
mysql -uroot -p shopping < /path/to/shopping.sqlshopping是目标库名,SQL 文件里如果没有CREATE DATABASE语句,就得先手动建库。导入后立刻验证两条语句:
SELECT COUNT(*) FROM product; SELECT COUNT(*) FROM user;商品表和用户表的 count 都大于 0,说明数据初始化成功。如果 product 表为 0,后面商品列表页会空白,查 log 也看不到业务报错。
注意:orders 里的
order是 MySQL 保留字,表名写成orders也可能在某些版本触发语法错误。稳妥做法是建表时叫t_order,或统一加mall_前缀。源码包里的表名是写死的,改表名要连 mapper XML 一起改,所以下载包里如果是orders,建议确认 MySQL 版本后决定要不要动。
3.3 数据库连接参数、上下文路径、JDK 版本,解压必改的三处
改配置的第一优先级是数据库连接,集中在jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/shopping?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=root jdbc.password=123456MySQL 5.7 用com.mysql.jdbc.Driver没问题,MySQL 8.0 必须换成com.mysql.cj.jdbc.Driver,并且要加serverTimezone=Asia/Shanghai,否则启动直接抛The server time zone value异常。characterEncoding=utf8少了,中文商品名在页面显示乱码。这里的密码不要留生产库的真实凭据,练习环境单独建一个低权限账号更符合习惯。
第二处是上下文路径。Tomcat 部署后访问地址是http://localhost:8080/工程名/,JSP 里如果用绝对路径写死了/ssm-shop/,工程名一改页面就 404。正规写法是在 JSP 头部用${pageContext.request.contextPath}拼静态资源路径,遇到用死路径写<link>的旧代码,要全局替换成动态上下文。
第三处是 JDK 版本。SSM 工程大多出生在 JDK 8 时代,本地装 JDK 17 而不动 pom.xml,Maven 默认按新版本编译会直接报错。在 pom 的 properties 里固定编译版本:
<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>编译级别对齐到 1.8,Tomcat 也用 8.5 系列,兼容性最稳妥。这三处处理完再启动,大部分“起不来”的问题就能绕过去。
3.4 在 IDEA + Tomcat 里启动,验证第一个页面
配置全部就位后,IDEA 里添加 Tomcat Server,Deployment 选 war exploded,Application context 填根路径/。这样启动不需要整体打包,改 JSP 还能热更新。点启动前,先确认本地 8080 端口没被占用:
lsof -i :8080占用就换 8081,Tomcat 端口改 server.xml,不然后端一直起不来。启动完成后用 curl 验证:
curl -I http://localhost:8080/index.jsp返回HTTP/1.1 200说明 Tomcat 和工程都起来了。如果首页本身就是登录页,浏览器打开后能看到登录框,下一步就可以进第 4 章的代码链路阅读。
4. 登录拦截、商品分页、购物车与下单,SSM 商城的核心链路实现
工程能跑,开始读核心代码。商城业务主干按“登录 → 浏览商品 → 加入购物车 → 结算下单 → 扣库存”展开。每条链路都横跨三层,下面按业务顺序给出实现要点,每一处代码都可以直接对着你解压出来的包比对。
4.1 登录状态怎么维持,拦截器保护哪些 URL
登录成功后,多数 SSM 商城把 User 对象放进 Session,之后每个请求都从 Session 里取。Controller 层拿到用户名密码去 service 校验,成功就 set 进 session,失败回登录页并带提示。Session 是默认方案,不引入 Redis 的原因也简单:老商城架构里,Session 丢失不外乎服务重启,单体应用重启频率低,用数据库或 Redis 维护会话反而增加链路。
@RequestMapping("/login") public String login(String username, String password, HttpSession session, Model model) { User user = userService.login(username, password); if (user == null) { model.addAttribute("msg", "用户名或密码错误"); return "user/login"; } session.setAttribute("loginUser", user); return "redirect:/product/list"; }这里需要特别留意的业务字段是:service 层做登录校验时,密码比对应该是 MD5 加盐或 BCrypt。很多练习项目直接明文比较,放到公网等于裸奔,只要数据库一泄露,所有账号跟着完。
受保护接口用拦截器统一拦截,而不是在每个 Controller 里重复判断 session。自定义一个 LoginInterceptor:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/user/login"); return false; } return true; } }在 spring-mvc.xml 里注册拦截路径:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/order/**"/> <mvc:mapping path="/cart/**"/> <mvc:exclude-mapping path="/cart/list"/> <bean class="com.shop.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>exclude-mapping不加的话,游客连购物车列表都看不了,连点“加入购物车”都会被踢回登录页,这就过了。下单、支付、后台管理这三类接口必须拦截,商品列表和详情要放行,这是商城权限设计的基本盘。
4.2 商品列表的分页:PageHelper 和手写 LIMIT 怎么选
商品列表最直接的需求是分页。常见做法两种:引入pagehelper依赖,查询前调用PageHelper.startPage(pageNum, pageSize);或者手写 mapper XML,在 SQL 末尾拼接LIMIT #{offset}, #{pageSize}。PageHelper 的优点是业务代码不用改 SQL,一个静态方法解决;缺点是复杂 join 里插件动 SQL 容易出错,排查时得睁大眼睛看生成的 SQL。
public PageInfo<Product> pageQuery(Integer pageNum, Integer pageSize, ProductQuery query) { PageHelper.startPage(pageNum, pageSize); List<Product> list = productMapper.pageQuery(query); return new PageInfo<>(list); }PageHelper.startPage只对紧随其后的第一条 SQL 生效,不能提取成公共变量提前调用。PageInfo里带pageNum、pageSize、total、pages、list等字段,JSP 直接迭代page.list。如果发现没分页,先检查 startPage 和 select 之间是不是插了别的 mapper 调用,一旦有,插件会把分页加到那张表上,结果全然不对。
4.3 购物车存在 Session 还是数据库,差别在登录后能否恢复
这是 SSM 商城代码里最容易引发讨论的点。存 Session,实现简单,Controller 放一个 Map,key 是 productId,value 是数量,不落库,浏览器一关数据就没了。存数据库,每个用户一张 cart 表,加购、改数量、删行都走 mapper,换设备也能恢复,但每次渲染购物车都要查库。我一般这样分:练习项目用 Session,把结构暴露得更清楚;真实上线至少商品价格和 SKU 以数据库为准,Session 只存 key 和数量,页面渲染时再查库对比最新价格,防止用户拿旧价格结算。
SSM 商城最常见的 cart 表实现里,加购方法长这样:
@RequestMapping("/add") public String add(@RequestParam Integer productId, @RequestParam Integer quantity, HttpSession session) { User loginUser = (User) session.getAttribute("loginUser"); CartItem item = new CartItem(); item.setUserId(loginUser.getId()); item.setProductId(productId); item.setQuantity(quantity); cartService.add(item); return "redirect:/cart/list"; }@RequestParam默认参数必传,缺少 productId 直接 400,前端调用时要保证字段名一致。真正容易漏的是业务校验:商品是否在售、quantity 是否超过库存上限。不少商城项目直接 insert 进购物车,不校验库存,等下单减库存时才发现负库存,属于业务层缺陷,到并发压测才会暴露。
4.4 下单闭环的事务边界,扣库存为什么必须用条件 UPDATE
订单创建牵涉三张表,写入顺序我习惯固定为:订单主表 → 订单明细 → 扣减库存 → 清空购物车。后一步失败,前面所有操作都要回滚,所以这四个动作必须包在同一个事务里,@Transactional直接标注在 service 实现类的方法上。
@Override @Transactional(rollbackFor = Exception.class) public Long createOrder(Long userId, List<CartItem> cartItems) { Order order = new Order(); order.setUserId(userId); order.setOrderNo(UUID.randomUUID().toString().replace("-", "")); order.setStatus(0); order.setCreateTime(new Date()); orderMapper.insert(order); for (CartItem item : cartItems) { OrderItem oi = new OrderItem(); oi.setOrderId(order.getId()); oi.setProductId(item.getProductId()); oi.setQuantity(item.getQuantity()); orderItemMapper.insert(oi); int rows = productMapper.decreaseStock(item.getProductId(), item.getQuantity()); if (rows == 0) { throw new StockNotEnoughException("库存不足"); } } cartMapper.deleteByUserId(userId); return order.getId(); }仔细看扣库存那句,它必须是一个带条件的 UPDATE 而不是先查再改:
UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}stock >= #{quantity}是并发防超卖的关键。两个请求同时进来抢同一件库存为 5 的商品,A 买 4,B 买 2,MySQL 的 UPDATE 行锁保证同一时刻只有一个请求能执行成功。A 更新完库存剩 1,B 再执行时条件stock >= 2不成立,影响行数是 0,service 层捕获到 rows == 0 后抛出异常,事务回滚,订单主表和明细都不落库。这个写法是商城防超卖的底线方案,任何绕开它的方案在并发下都会出问题。
5. 把 SSM 商城打成 war 部署到 Tomcat,再把三个坑扫一遍
源码包最终通常要打成 war 交给运维。下面按部署顺序走一遍,每一步对应一个可验证的结果,卡住的地方按技术点排查。
5.1 Maven 打包命令和 Tomcat 部署方式
先清后包,避免 stale class 干扰结果:
mvn clean package -DskipTests跳过测试打包,避免单元测试环境不完整导致构建中断。成功后target/目录生成xxx.war,文件名决定了应用访问上下文。放进 Tomcat 的webapps/,重启:
/opt/tomcat/bin/shutdown.sh /opt/tomcat/bin/startup.shTomcat 第一次启动会自动解压 war。部署完盯日志:
tail -f /opt/tomcat/logs/catalina.out看到Deploying web application archive和has finished两行,Web 应用加载完成。war 包如果要改上下文路径,直接重命名 war 文件即可,Tomcat 按文件名生成 context。
5.2 用 curl 验证核心接口是否接通
部署完用 curl 做四个最小验证:
curl -I http://localhost:8080/ssm-shop/index.jsp curl -I http://localhost:8080/ssm-shop/user/login curl -I http://localhost:8080/ssm-shop/product/list curl -I http://localhost:8080/ssm-shop/cart/listindex.jsp和login返回 200 说明 Tomcat 与 MVC 链路通;cart/list如果 302 跳转登录页,属于正常拦截逻辑。关键是/product/list,它要查数据库并渲染 JSP,返回 500 说明 mapper XML 路径错或数据表没导全。再核对页面内容:
curl -s http://localhost:8080/ssm-shop/login | grep -o "登录"能输出“登录”二字,说明 JSTL 标签库和静态资源引用都好着。
5.3 字符集、依赖缺失、事务切点:部署后最常见的三个坑
第一是中文乱码。Tomcat 老版本默认 URIEncoding 按 ISO-8859-1 解码,URL 里的中文参数会乱,要在server.xml的 Connector 上加URIEncoding="UTF-8",同时保证jdbc.url带characterEncoding=utf8。两边缺一,商品名或收货地址就会变成乱码,页面和数据库各错一半不好查。
第二是运行时缺依赖。压缩包里的 pom.xml 有时只写了 Spring 和 MyBatis,漏掉 jstl 或 commons-fileupload,启动后页面正常但一访问 JSP 就 500,日志报NoClassDefFoundError。按报错的类名去 mvnrepository 搜坐标,补到 pom 里重新打包。
第三是事务不生效,这是最隐蔽的。日志里看不到Creating new transaction信息,说明 Spring AOP 的切点没拦住 service 实现类。看到包名是com.shop.service.impl,而事务 pointcut 写的是execution(* com.shop.service.*.*(..)),就立刻能定位:它只匹配service下一级类,处理impl子包需要把切点改成com.shop.service..*.*(..)。事务失效的经典现象是下单异常后订单主表还在、明细缺一半,出现这个现场先看事务日志,别急着改业务代码。
最后再提一个定位技巧:把 MyBatis 的 SQL 日志输出打开,会极大缩短定位时间。在 mybatis-config.xml 里加:
<settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings>生产环境的连接池 maxActive 至少调到 20,验证并发后把它关掉,回到logImpl为 Slf4j 的正常输出。SQL 日志打开时,每一次 update 影响的行数直接印在控制台,影响行数为 0 时,再往回看 whether 事务切点与 SQL 条件是否匹配,这比加断点调试高效得多。
本文还有配套的精品资源,点击获取