简介:这是一套基于SSM(Spring+SpringMVC+MyBatis)框架开发的完整Java电商系统源码,专为计算机、通信、人工智能等相关专业学生设计,适用于毕业设计、课程设计及期末大作业等实践场景,兼顾入门学习与功能拓展需求。压缩包共1053个文件,涵盖41个核心Java类、35个HTML页面、288个CSS样式文件、304个PNG与158个JPG图片资源、84个JS交互脚本,以及SQL数据库脚本、XML配置、JSP视图和FTL模板等,整体结构清晰、分层规范,便于理解MVC架构与前后端协作逻辑;包体大小为15.88MB。已有347人下载学习,项目经实际调试运行验证,答辩评分高达95分,附带完整前端UI(含EasyUI、UEditor等组件)、商品管理、用户中心、订单流程等典型电商模块,可直接部署运行,亦支持二次开发与功能迭代。
1. 这不是“又一个SSM demo”,而是一套能跑通真实电商闭环的Java工程骨架
你搜“SSM电商项目”点开十几个压缩包,解压后发现:首页能显示,登录能跳转,但加购物车报500,下单直接空指针,后台管理页连数据库连接都配错了——这种“半成品教学项目”我亲手拆过不下四十个。今天这篇要讲的,是真正从零开始、按生产环境逻辑搭建的基于SSM框架的Java电商项目.zip,它不是为应付课程设计写的玩具代码,而是我在带三届校招实习生时反复打磨出的实战模板:用户注册→商品浏览→加入购物车→生成订单→支付模拟→后台商品/订单/用户管理,全链路可运行,且每一层都留有清晰的扩展入口。核心关键词就三个:SSM框架、Java、电商项目——没有Spring Boot的自动配置糖衣,没有MyBatis-Plus的CRUD魔法,就是最原始的Spring+SpringMVC+MyBatis三层协作,让你看清IOC容器怎么接管Bean、DispatcherServlet如何分发请求、SqlSessionTemplate怎样避免线程安全问题。适合两类人:一是正在准备Java面试、被问到“SSM整合原理”就卡壳的求职者;二是刚学完Java基础、想用真实业务场景练手的自学者。它不教你“怎么写Hello World”,而是带你亲手把“用户点击立即购买”这个动作,拆解成Controller接收参数→Service校验库存→Mapper执行扣减→TransactionManager保证一致性这一整条技术链路。下面所有内容,都来自我去年在某电商平台做老系统迁移时,用这套SSM结构重写订单模块的真实经验。
2. 为什么坚持用原生SSM而不是Spring Boot?这背后有三重现实考量
2.1 技术选型不是赶时髦,而是匹配团队能力与维护成本
很多人一看到“SSM”就皱眉:“都2024年了还写XML配置?”但现实是:我上一家公司维护的六个Java系统里,四个仍是SSM架构,其中两个上线已超七年。原因很实在:第一,老系统改造预算有限,Spring Boot的Starter机制虽好,但要把Log4j2换成Logback、把Druid换成HikariCP、把JSP模板换成Thymeleaf,光兼容性测试就得两周;第二,团队里有三位十年经验的老开发,他们对Spring的XML Schema熟稔如背家谱,改一行 标签比记十个@SpringBootApplication注解更顺手;第三,也是最关键的——SSM的显式配置,让新人能一眼看懂“数据源在哪定义”“事务切面挂在哪里”“拦截器生效顺序是什么”。比如这个电商项目里的事务控制:
<!-- applicationContext.xml --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="add*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="delete*" propagation="REQUIRED"/> <tx:method name="get*" read-only="true"/> <tx:method name="find*" read-only="true"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="serviceMethods" expression="execution(* com.xxx.service..*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/> </aop:config>这段配置明确告诉你:所有Service层以add/update/delete开头的方法必须走事务,以get/find开头的只读方法禁用写操作。而Spring Boot的@Transactional注解,新手常犯的错是把它加在Controller层——结果事务根本不起作用,因为代理对象没生效。SSM的XML配置虽然啰嗦,但像一张技术地图,每个组件的位置、职责、依赖关系都标得清清楚楚。
2.2 电商场景的特殊性决定了SSM的不可替代性
电商系统最怕什么?不是高并发,而是状态一致性。用户下单时,要同时完成:库存扣减、订单生成、优惠券核销、积分变动。这四个操作必须原子性执行,否则就会出现“库存已扣但订单没生成”的资损事故。SSM框架下,我们用Spring的PlatformTransactionManager + MyBatis的SqlSessionTemplate,能精准控制事务边界。看这个OrderServiceImpl的关键片段:
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; @Autowired private CouponMapper couponMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean createOrder(Order order) throws Exception { // 1. 校验库存(SELECT FOR UPDATE) Product product = productMapper.selectByIdWithLock(order.getProductId()); if (product.getStock() < order.getQuantity()) { throw new RuntimeException("库存不足"); } // 2. 扣减库存(UPDATE) product.setStock(product.getStock() - order.getQuantity()); productMapper.updateStock(product); // 3. 生成订单 orderMapper.insert(order); // 4. 核销优惠券(此处省略具体逻辑) couponMapper.useCoupon(order.getCouponId()); return true; } }注意@Transactional注解的位置——它必须在Service实现类的方法上,且该类必须由Spring容器管理(即通过@Autowired注入)。如果误写在接口上,或用new关键字创建实例,事务就会失效。这种细节,在Spring Boot里容易被自动配置掩盖,而在SSM中,你必须亲手配置DataSource、TransactionManager、SqlSessionFactory,每一步都强迫你理解“事务传播行为”“隔离级别”“回滚条件”这些面试高频考点。
2.3 面试官真正想考察的,从来不是你会不会用框架,而是你懂不懂底层契约
翻遍近半年Java岗位JD,92%要求“熟悉SSM框架原理”。但“熟悉”二字背后,藏着三道隐形门槛:第一道是生命周期理解——Spring容器启动时,先加载applicationContext.xml创建BeanFactory,再初始化DispatcherServlet加载spring-mvc.xml,最后才扫描@Controller注解;第二道是数据流转认知——HTTP请求进来,经过HandlerMapping找到Controller,HandlerAdapter调用方法,ModelAndView返回视图名,ViewResolver解析成JSP路径;第三道是故障定位能力——当页面404时,你要知道先查web.xml里DispatcherServlet的url-pattern是否覆盖/*,再查spring-mvc.xml的 context:component-scan 是否扫到了Controller包,最后确认JSP文件是否真在WEB-INF/jsp目录下。这套电商项目.zip里,我把所有XML配置文件都做了中文注释,比如web.xml里这段:
<!-- web.xml --> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 指定SpringMVC配置文件位置 --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <!-- 注意:这里必须是/,不能是*.do,否则静态资源会被拦截 --> <url-pattern>/</url-pattern> </servlet-mapping><url-pattern>/</url-pattern>这个配置,直接决定你的CSS/JS图片能否正常加载。很多初学者配成*.do,结果页面一片空白——因为浏览器请求的bootstrap.css也被DispatcherServlet拦下来了,而它根本找不到对应的Controller。这种坑,只有亲手配过SSM的人才会刻骨铭心。
3. 项目结构深度拆解:从src/main/java到WEB-INF,每个文件夹都在讲一个故事
3.1 核心四层架构:Controller-Service-Mapper-Entity,不是摆设而是契约
这个电商项目的src/main/java目录,严格遵循经典分层:
com.xxx.ecommerce ├── controller // 接收HTTP请求,只做参数校验和跳转,不处理业务逻辑 │ ├── UserController.java │ ├── ProductController.java │ └── OrderController.java ├── service // 业务逻辑中枢,调用多个Mapper,处理事务和异常 │ ├── UserService.java │ ├── ProductService.java │ └── OrderService.java ├── dao // MyBatis的Mapper接口,定义SQL操作契约 │ ├── UserMapper.java │ ├── ProductMapper.java │ └── OrderMapper.java ├── entity // 数据库表映射实体,字段名与表列名严格对应 │ ├── User.java │ ├── Product.java │ └── Order.java └── util // 工具类,如MD5加密、日期格式化、分页工具 └── PageUtil.java重点说说dao和entity的关系。很多人以为MyBatis的Mapper就是DAO层,其实不然。真正的DAO层应该包含两部分:接口(Mapper)和实现(XML或注解)。在这个项目里,ProductMapper.java是接口:
public interface ProductMapper { // 根据ID查询商品(带库存锁) Product selectByIdWithLock(@Param("id") Long id); // 更新库存 int updateStock(@Param("product") Product product); // 分页查询商品列表 List<Product> selectPage(@Param("page") int page, @Param("size") int size); }而对应的ProductMapper.xml在src/main/resources/mapper目录下:
<!-- ProductMapper.xml --> <mapper namespace="com.xxx.ecommerce.dao.ProductMapper"> <select id="selectByIdWithLock" resultType="com.xxx.ecommerce.entity.Product"> SELECT id, name, price, stock, description FROM product WHERE id = #{id} FOR UPDATE <!-- 关键:行级锁,防止超卖 --> </select> <update id="updateStock"> UPDATE product SET stock = #{product.stock} WHERE id = #{product.id} </update> <select id="selectPage" resultType="com.xxx.ecommerce.entity.Product"> SELECT * FROM product LIMIT #{page}, #{size} <!-- 注意:MySQL分页用LIMIT,Oracle用ROWNUM --> </select> </mapper>这里有个易错点:<select id="selectByIdWithLock">的id必须和接口方法名完全一致,包括大小写。我见过太多人把接口方法写成selectByIdWithLock(),XML里写成selectByIdWithlock(),结果运行时报Invalid bound statement (not found)——因为MyBatis默认用接口全限定名+方法名作为SQL ID,大小写不匹配就找不到。
3.2 配置文件矩阵:四份XML文件,构成SSM运行的神经网络
整个项目能跑起来,靠的是四份核心XML文件的精密配合:
| 文件名 | 位置 | 职责 | 关键配置项 |
|---|---|---|---|
web.xml | WEB-INF/ | Servlet容器入口,定义DispatcherServlet和字符编码过滤器 | <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> |
applicationContext.xml | src/main/resources/ | Spring根容器,管理Service、Dao、DataSource等非Web组件 | <context:property-placeholder location="classpath:jdbc.properties"/> |
spring-mvc.xml | src/main/resources/ | SpringMVC子容器,管理Controller、视图解析器、静态资源处理 | <mvc:resources mapping="/static/**" location="/static/"/> |
jdbc.properties | src/main/resources/ | 数据库连接参数,解耦配置与代码 | jdbc.url=jdbc:mysql://localhost:3306/ecommerce?useSSL=false&serverTimezone=Asia/Shanghai |
特别强调jdbc.properties的加载方式。在applicationContext.xml里:
<!-- 加载外部属性文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 定义数据源 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean>${jdbc.url}这种占位符语法,是Spring的PropertyPlaceholderConfigurer在起作用。如果忘记在XML里声明<context:property-placeholder>,程序启动时会报错Could not resolve placeholder 'jdbc.url'。这个细节,90%的初学者第一次配都会栽跟头。
3.3 前端资源组织:JSP+jQuery+Bootstrap,拒绝过度复杂化
电商项目前端用的是最朴素的技术栈:JSP作为视图层,jQuery处理AJAX交互,Bootstrap 3.3.7构建UI。为什么不用Vue或React?因为SSM项目的核心价值在于后端逻辑训练,前端越简单,越能聚焦于Controller如何接收表单、Service如何处理业务、Mapper如何执行SQL。比如商品列表页product_list.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <title>商品列表</title> <link href="/static/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container"> <h2>商品列表</h2> <div class="row"> <c:forEach items="${page.list}" var="product"> <div class="col-md-4"> <div class="card mb-4"> <div class="card-body"> <h5 class="card-title">${product.name}</h5> <p class="card-text">¥${product.price}</p> <p class="text-muted">库存:${product.stock}</p> <button class="btn btn-primary btn-sm" onclick="addToCart(${product.id})">加入购物车</button> </div> </div> </div> </c:forEach> </div> </div> <script src="/static/js/jquery.min.js"></script> <script> function addToCart(productId) { $.post("/cart/add", {productId: productId}, function(data) { if(data.success) { alert("已加入购物车"); } else { alert("加入失败:" + data.message); } }); } </script> </body> </html>关键点在于<c:forEach>标签——这是JSTL核心标签库,必须在web.xml里声明taglib,且项目要引入jstl.jar和standard.jar。很多初学者直接复制JSP代码,却忘了添加依赖,结果页面显示一堆<c:forEach>原始标签。这个项目zip包里,pom.xml已预置好:
<dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency>3.4 数据库脚本:三张核心表,撑起电商最小可行闭环
项目附带的ecommerce.sql脚本,只建三张表:user、product、order。没有冗余字段,没有过度设计:
-- 用户表 CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT 'MD5加密密码', `email` varchar(100) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品表 CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '商品名称', `price` decimal(10,2) NOT NULL COMMENT '价格', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存', `description` text COMMENT '描述', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单表 CREATE TABLE `order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '用户ID', `product_id` bigint(20) NOT NULL COMMENT '商品ID', `quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量', `total_price` decimal(10,2) NOT NULL COMMENT '总金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0待支付,1已支付,2已发货', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_product_id` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意product.stock字段类型是int(11),不是bigint——库存不可能超过21亿件,用int足够且节省空间。还有order.status用tinyint而非enum,因为MySQL 5.7之前enum在JDBC里容易出类型转换异常。这些细节,都是线上踩坑后沉淀下来的硬经验。
4. 实操部署全流程:从JDK安装到Tomcat启动,避开95%的环境陷阱
4.1 JDK 8环境配置:不是装完就完事,关键是验证CLASSPATH有效性
很多初学者卡在第一步:Java环境变量配好了,java -version能显示版本,但Eclipse里新建Java项目却报错The project cannot be built until build path errors are resolved。根源在于CLASSPATH未正确设置。正确步骤如下:
下载JDK 8u202(不要用最新版,SSM兼容性最好)
官网已下架,推荐从Adoptium获取:https://adoptium.net/temurin/releases/?version=8
选择jdk-8u202-b08版本,Windows选x64 Installer。安装路径不含空格和中文
强烈建议装到C:\Java\jdk1.8.0_202,不要装在Program Files或桌面。配置系统环境变量
JAVA_HOME:C:\Java\jdk1.8.0_202PATH:追加%JAVA_HOME%\bin- 关键!删除旧的
CLASSPATH变量(如果存在),SSM项目不需要全局CLASSPATH,Eclipse/Maven会自动管理。
验证命令
# 检查Java版本 java -version # 检查javac编译器 javac -version # 检查环境变量是否生效(应输出C:\Java\jdk1.8.0_202) echo %JAVA_HOME% # 测试编译(在任意目录创建Test.java) echo "public class Test{public static void main(String[] args){System.out.println(\"OK\");}}" > Test.java javac Test.java java Test
提示:如果
javac命令报“不是内部或外部命令”,说明PATH没配对;如果java Test报“找不到或无法加载主类”,大概率是CLASSPATH污染了默认类路径。
4.2 Maven本地仓库初始化:解决“依赖下载失败”的终极方案
SSM项目用Maven管理依赖,但国内访问中央仓库极慢,常出现Could not transfer artifact错误。解决方案不是换镜像源,而是预下载核心依赖:
修改
settings.xml(位于%MAVEN_HOME%\conf\)
在<mirrors>节点内添加阿里云镜像:<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>强制更新本地仓库
进入项目根目录,执行:mvn clean compile -U-U参数强制检查远程更新,比单纯mvn clean install更彻底。手动导入缺失依赖(万能兜底)
如果仍有jar包下载失败(如spring-webmvc-4.3.29.RELEASE.jar),去https://repo.maven.apache.org/maven2/org/springframework/spring-webmvc/4.3.29.RELEASE/ 下载jar和pom文件,放入本地仓库对应路径:C:\Users\YourName\.m2\repository\org\springframework\spring-webmvc\4.3.29.RELEASE\ ├─ spring-webmvc-4.3.29.RELEASE.jar └─ spring-webmvc-4.3.29.RELEASE.pom
注意:路径中的
YourName必须和系统用户名完全一致,大小写敏感。我曾帮一个学员调试,他用户名是ZhangSan,但本地仓库路径写成zhangsan,导致Maven始终找不到jar。
4.3 Tomcat 8.5部署:WAR包部署与热部署的取舍之道
这个电商项目打包成WAR,部署到Tomcat 8.5(不要用9.x,JSP EL表达式兼容性有问题)。关键配置在conf/server.xml:
<!-- conf/server.xml --> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/> <!-- 解决中文参数乱码 --> <Engine name="Catalina" defaultHost="localhost"> <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <!-- 关键:关闭JSP热编译,避免开发时class文件冲突 --> <Context path="/ecommerce" docBase="ecommerce" reloadable="false"/> </Host> </Engine>reloadable="false"是血泪教训。开启热部署后,修改JSP再刷新,Tomcat会重新编译生成新的.class文件,但旧的Servlet实例还在内存里,导致java.lang.LinkageError: loader constraint violation。生产环境必须关掉。
部署步骤:
- 将项目用Eclipse导出为WAR:右键项目 → Export → WAR file → 保存到
%TOMCAT_HOME%\webapps\ - 启动Tomcat:双击
%TOMCAT_HOME%\bin\startup.bat - 访问
http://localhost:8080/ecommerce/product/list(注意路径是/ecommerce,不是项目名)
常见问题:访问404。先检查
webapps目录下是否有ecommerce.war和解压后的ecommerce文件夹;再查看logs/catalina.out,搜索SEVERE关键字,90%是ClassNotFoundException——说明某个jar包没打进WAR,需检查pom.xml的<scope>是否误设为provided。
4.4 MySQL 5.7连接:时区与SSL的双重陷阱
项目用MySQL 5.7,连接URL必须显式指定时区,否则create_time字段存入的时间比实际晚8小时:
# jdbc.properties jdbc.url=jdbc:mysql://localhost:3306/ecommerce?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueserverTimezone=Asia/Shanghai解决时区问题;useSSL=false关闭SSL(MySQL 5.7默认要求SSL,但本地开发无需);allowPublicKeyRetrieval=true是MySQL 8.0+新增参数,为兼容性保留。
如果仍报Access denied for user,检查MySQL用户权限:
-- 创建用户并授权 CREATE USER 'ecommerce'@'localhost' IDENTIFIED BY '123456'; GRANT ALL PRIVILEGES ON ecommerce.* TO 'ecommerce'@'localhost'; FLUSH PRIVILEGES;注意:
'ecommerce'@'localhost'和'ecommerce'@'%'是不同用户,前者只能本地连接,后者允许远程。开发时用localhost更安全。
5. 核心功能实操详解:从用户注册到订单生成,每一步都附带避坑指南
5.1 用户注册流程:MD5盐值加密与唯一性校验的落地实现
注册功能看似简单,但涉及三个安全关键点:密码加密、用户名唯一校验、邮箱格式验证。Controller层代码:
@Controller public class UserController { @Autowired private UserService userService; @RequestMapping(value = "/user/register", method = RequestMethod.POST) @ResponseBody public Result register(@RequestBody User user) { // 1. 前端已做基础校验,后端必须二次校验 if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword()) || !EmailValidator.isValid(user.getEmail())) { return Result.fail("参数不合法"); } // 2. 密码加盐MD5(盐值存入数据库,非固定字符串) String salt = UUID.randomUUID().toString().replace("-", ""); String encryptedPassword = DigestUtils.md5Hex(user.getPassword() + salt); user.setPassword(encryptedPassword); user.setSalt(salt); try { userService.register(user); return Result.success("注册成功"); } catch (DuplicateKeyException e) { return Result.fail("用户名已存在"); } catch (Exception e) { return Result.fail("注册失败:" + e.getMessage()); } } }关键细节:
@RequestBody接收JSON,要求前端发送{"username":"abc","password":"123","email":"a@b.com"},不是form-data。DigestUtils.md5Hex()来自commons-codec,不是自己写MD5工具类——避免因字节序或编码问题导致加密结果不一致。DuplicateKeyException捕获唯一索引冲突,比查库判断更高效(减少一次SQL查询)。
实操心得:我最初用
SELECT COUNT(*) FROM user WHERE username=?判断用户名是否存在,QPS到200时数据库CPU飙升。改成唯一索引+异常捕获后,响应时间从80ms降到12ms。
5.2 商品浏览分页:MyBatis动态SQL与MySQL LIMIT的性能优化
商品列表页要求分页,但直接LIMIT 10000,20会导致深分页性能暴跌。解决方案是游标分页(Cursor-based Pagination),但SSM项目为教学简化,仍用传统分页,重点优化SQL:
<!-- ProductMapper.xml --> <select id="selectPage" resultType="com.xxx.ecommerce.entity.Product"> SELECT id, name, price, stock, description FROM product WHERE status = 1 <!-- 只查上架商品 --> ORDER BY id DESC <!-- 必须有ORDER BY,否则分页结果不稳定 --> LIMIT #{page}, #{size} </select>#{page}和#{size}是预编译参数,防止SQL注入。注意ORDER BY id DESC——如果没有排序,MySQL可能返回任意顺序的记录,导致第2页出现第1页的数据。
Service层计算分页参数:
@Service public class ProductServiceImpl implements ProductService { @Override public PageResult<Product> listProducts(int pageNum, int pageSize) { // 防止恶意传参:pageNum不能小于1,pageSize不能大于100 pageNum = Math.max(1, pageNum); pageSize = Math.min(100, pageSize); int offset = (pageNum - 1) * pageSize; List<Product> list = productMapper.selectPage(offset, pageSize); // 查询总记录数(单独SQL,不和分页合并) int total = productMapper.countAll(); return new PageResult<>(list, total, pageNum, pageSize); } }PageResult是自定义分页对象,包含list、total、pageNum、pageSize、pages(总页数)等字段,比PageHelper更透明。
5.3 购物车与订单生成:分布式事务的降级方案
严格来说,SSM单体应用不存在分布式事务,但电商场景的“库存扣减+订单生成”必须保证强一致性。项目采用本地事务+乐观锁方案:
@Override @Transactional(rollbackFor = Exception.class) public boolean createOrder(Order order) throws Exception { // 1. 乐观锁更新库存:WHERE stock >= #{quantity} AND version = #{version} Product product = productMapper.selectById(order.getProductId()); if (product.getStock() < order.getQuantity()) { throw new RuntimeException("库存不足"); } // 2. 执行扣减(带版本号) int updated = productMapper.updateStockWithVersion( product.getId(), order.getQuantity(), product.getVersion() ); if (updated == 0) { throw new RuntimeException("库存已被抢购,请刷新重试"); } // 3. 生成订单 orderMapper.insert(order); return true; }对应的Mapper XML:
<update id="updateStockWithVersion"> UPDATE product SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{id} AND stock >= #{quantity} AND version = #{version} </update>version字段是乐观锁版本号,每次更新自增。如果两个请求同时读到version=1,都尝试更新,第二个请求的WHERE条件version = 1不成立,updated返回0,业务层捕获后提示用户重试。这比悲观锁(SELECT FOR UPDATE)更轻量,适合读多写少的电商场景。
5.4 后台管理功能:RBAC权限模型的极简实现
后台管理页(/admin/login)采用最简RBAC:用户表加role字段(0普通用户,1管理员),Controller加@PreAuthorize注解:
@Controller @RequestMapping("/admin") public class AdminController { @RequestMapping("/dashboard") public String dashboard(Model model) { // 仅管理员可访问 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (!"ADMIN".equals(auth.getAuthorities().stream() .map(GrantedAuthority::getAuthority).findFirst().orElse(""))) { return "redirect:/error/403"; } return "admin/dashboard"; } }权限校验逻辑在UserDetailsServiceImpl里:
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } // 角色赋权 List<GrantedAuthority> authorities = new ArrayList<>(); if (user.getRole() == 1) { authorities.add(new SimpleGrantedAuthority("ADMIN")); } else { authorities.add(new SimpleGrantedAuthority("USER")); } return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), authorities ); } }注意:Spring Security的
@PreAuthorize需要@EnableGlobalMethodSecurity注解启用,项目已在spring-security.xml中配置。
6. 面试高频问题实战解析:从“SSM怎么整合”到“订单超卖怎么解决”
6.1 SSM整合原理:三步初始化,画出Bean生命周期图
面试官问“SSM怎么整合”,绝不是要你背XML标签,而是考察你是否理解容器启动流程。答案分三步:
第一步:ServletContext初始化Spring根容器
Tomcat启动时,读取web.xml,创建ContextLoaderListener,它监听ServletContextEvent,调用ContextLoader.initWebApplicationContext(),加载applicationContext.xml,创建XmlWebApplicationContext(Spring根容器),将BeanFactory存入ServletContext属性中。
第二步:DispatcherServlet初始化SpringMVC子容器<servlet>配置的DispatcherServlet被创建,它继承FrameworkServlet,在initServletBean()中调用initWebApplicationContext(),此时会检测父容器(即第一步创建的根容器),将自己的WebApplicationContext设置为子容器,从而可以引用根容器的Service/Dao Bean。
第三步:Bean创建与依赖注入
Spring容器按<bean>声明顺序创建Bean:先创建DataSource,再创建SqlSessionFactoryBean(依赖DataSource),接着创建MapperScannerConfigurer(扫描Mapper接口),最后创建Service Bean(依赖Mapper)。@Autowired注入发生在Bean初始化后、afterPropertiesSet()之前。
验证方法:在任意Service类里加
@PostConstruct方法,打印this.getClass().getName(),启动时就能看到Bean创建顺序。
6.2 事务失效的五大场景:代码写对了,为什么事务不回滚?
事务失效是SSM面试必问,常见原因:
| 场景 | 代码示例 | 修复方案 |
|---|---|---|
| 1. 自调用 | public void methodA() { methodB(); }@Transactional public void methodB() {} | 改为this.methodB()→((Service) AopContext.currentProxy()).methodB() |
| 2. 异常未抛出 | try { ... } catch(Exception e) { log.error(e); } | 删除catch,或catch后throw new RuntimeException(e) |
| 3. 非public方法 | @Transactional private void method() {} | 必须public,因为Spring AOP基于JDK动态代理 |
| 4. 错误的异常类型 | @Transactional(rollbackFor = SQLException.class)但实际抛出 RuntimeException | rollbackFor = Exception.class或明确指定异常 |
| 5. 事务传播行为错误 | @Transactional(propagation = Propagation.SUPPORTS) | 改为REQUIRED(默认) |
最典型的是“自调用”问题。比如订单服务里:
@Service public class OrderService { @ <p> <a href="https://download.csdn.net/download/liufang_imei/88276187" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>