简介一份基于SSM框架开发的产品管理系统Java Web项目源码面向高校Java课程设计、毕业设计以及后台管理入门开发者可帮助读者理解传统Web项目从环境配置、数据库设计到前后端联调的整体流程。系统覆盖用户信息、图片素材、视频素材等典型管理模块前端采用Vue、ElementUI、Bootstrap搭配HTML/CSS/JS后端基于SpringSpringMVCMyBatisPlus底层使用MySQL5.7整体为B/S架构便于理解分层结构与请求处理过程。资源包内文件总数1342个核心包括Java源码、JSP页面、XML配置和SQL数据库脚本另有约400个JS文件、187个PNG、150个CSS以及GIF、JPG等静态素材压缩包整体仅17.39MB传输和导入IDE都很轻便。目前已有86人学习浏览目录结构完整包含绪论、相关技术介绍、系统分析、具体实现等内容样式备份和前端组件文件也一应俱全读者可直接运行演示、对照SSM配置流程排查问题也可在此基础上快速扩展产品管理功能完成课程设计或论文验证。1. 基于SSM的产品管理系统为什么这套源码还值得照着做把 Spring、SpringMVC、MyBatis 组合起来做一套产品管理系统听起来像是很多年前的老方案。但真实情况是直到今天产品管理这类中小型后台系统在交付时仍然大量采用 SSM 结构不少公司内部维护的库存台账、商品样例管理后台跑的就是这套代码。对开发者来说基于 SSM 的产品系统源码也是理解 Java Web 调用链最直接的一条路径它没有 Spring Boot 那种自动配置的黑匣子Bean 怎么创建、请求怎么转发、SQL 怎么执行全部显式写在配置和代码里。你拿到一套 SSM 产品管理系统源码核心能验证的事只有四件产品信息的增删改查、产品分类的维护、库存和上下架状态的管理、以及简单的下单记录。这套东西能解决的问题很明确给一个没有现成后台的中小业务快速搭出可用的管理界面同时让维护的人能看懂每一行代码的归属。适合正在做课程设计、毕业设计或者刚入职需要快速读懂老系统的开发者。接下来从项目结构、数据库、Mapper、Service、Controller 一路拆到前端再给你几组能直接用的配置和踩坑记录。2. 搭建项目骨架Maven依赖与三份核心配置文件的对应关系2.1 SSM项目源码目录三层分包与配置文件的对应位置拿到任何一套产品管理系统的 SSM 源码先不要急着开 IDE 运行第一步是看清目录结构。SSM 项目虽然是 Web 应用但工程结构基本是固定的源代码、资源配置文件、前端页面三块分开。典型结构如下product-manage ├── pom.xml └── src/main ├── java │ └── com/example/product │ ├── controller │ ├── service │ ├── dao │ ├── entity │ └── common ├── resources │ ├── jdbc.properties │ ├── spring-mybatis.xml │ ├── spring-mvc.xml │ └── log4j.properties └── webapp ├── WEB-INF │ └── web.xml └── jsp ├── product ├── category └── commoncontroller、service、dao、entity 四层分包是 SSM 的默认约定也是这套代码最核心的价值所在。dao 层只负责跟数据库打交道service 层处理业务规则controller 层接收请求和返回视图。resources 下的 spring-mybatis.xml 和 spring-mvc.xml 决定了 Spring 容器怎么初始化、MyBatis 的 SqlSession 怎么产生web.xml 则是整个 Web 应用的入口。我一般建议拿到源码后先看 web.xml因为这里定义了 Spring 配置文件的位置和 DispatcherServlet 的映射路径。看懂了加载顺序后面遇到 Bean 找不到、请求 404 这类问题就有排查方向。不要一上来就点运行按钮SSM 项目跑不起来有九成是配置文件之间的路径对应关系没搞清楚。2.2 pom.xml 中必须有的四组依赖版本选择与引入理由SSM 项目不像 Spring Boot 那样一个 starter 解决问题pom.xml 里的每个依赖都要手动声明。一个能正常编译运行的产品管理系统最少需要四组依赖Spring 核心与 Web MVC、MyBatis 与 MyBatis-Spring 整合包、数据库驱动、连接池。下面这份依赖清单是常见做法版本选取以稳定为原则。properties spring.version5.3.23/spring.version mybatis.version3.5.13/mybatis.version /properties dependencies !-- Spring MVC 与 Spring JDBC包含 spring-core、spring-context 等传递依赖 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis 与 Spring 整合包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.18/version /dependency /dependenciesspring-webmvc 会传递引入 spring-context、spring-beans、spring-core所以不需要逐个声明。spring-jdbc 是为了让 Spring 管理数据源和事务搭配 MyBatis-Spring 整合包时几乎必加。mybatis-spring 这个包的版本要和 MyBatis 主版本匹配常见教训是 MyBatis 3.5.x 配了 mybatis-spring 1.x启动时直接报类找不到。一般我习惯用 mybatis-spring 2.1.x 搭配 MyBatis 3.5.x这套组合经过的验证最多。另外要注意 Servlet 和 JSP 相关依赖。如果源码里用到了 JSP 和 JSTL通常还要加 javax.servlet-api 和 jstlscope 设为 provided。有些简化的源码会省略这部分本地跑的时候 Tomcat 能补上但编译期会报错收到代码后先编译一次再动业务这个习惯能帮你区分是环境问题还是代码问题。2.3 加载顺序与冲突规避web.xml、spring-mybatis.xml、spring-mvc.xmlSSM 的配置文件经常让新手看得头疼实际上只需要抓住两条线一条是 Spring 容器怎么建一条是 SpringMVC 怎么接管请求。web.xml 是总入口ContextLoaderListener 负责创建 Spring 的根容器DispatcherServlet 创建的是 SpringMVC 子容器。子容器能看见父容器的 Bean父容器看不见子容器的 Bean。!-- web.xml 关键配置 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mybatis.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingCommon practice是spring-mybatis.xml 管理数据源、SqlSessionFactory、Mapper 扫描和事务管理器spring-mvc.xml 管理 Controller 扫描、视图解析器和静态资源。注意 spring-mvc.xml 里不能再去扫描 Service 和 Dao否则会出现事务失效这种翻车现场。这个拆分原则值得记住因为它直接决定了你的 Transactional 能不能生效。spring-mybatis.xml 的核心是 SqlSessionFactoryBean它要把 Mapper XML 的位置、实体类的包名和别名的关系全部告诉 MyBatis。下面是一份可以直接用的模板!-- spring-mybatis.xml -- context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.product.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.product.dao/ /beanmapperLocations 参数指定 XML 映射文件的位置写成 classpath:mapper/*.xml 就意味着 resources/mapper 目录下所有 XML 都会被加载。typeAliasesPackage 是实体类所在的包配好之后在 Mapper XML 里写 resultType 时可以直接用类名不用带全限定名。MapperScannerConfigurer 的 basePackage 要精确到 dao 接口所在的包扫描范围错了就会报无法注入 Mapper 的错。这些配置文件改起来很直接但坑往往藏在细节里。比如 Spring 版本和 Druid 版本的兼容性XML 头部的 schema 地址以及 properties 文件里的 key 名称是否和 ${} 占位符一致。跑不起来的时候先把这三份配置从头到尾读一遍大多数问题都能定位。3. 产品数据模型数据库表设计、Mapper接口与XML映射的落地写法3.1 分类表、产品表和订单表的字段设计产品管理系统的业务核心就是产品本身但纯粹一张产品表撑不起一个系统。围绕产品会延伸出分类、库存、订单记录这几张表。设计一套能够支撑增删改查的 SSM 产品表结构字段不需要多但每一个字段都要有明确作用。下面是常见的设计。-- 产品分类表 CREATE TABLE category ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 分类ID, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort INT NOT NULL DEFAULT 0 COMMENT 排序权重越小越靠前 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT产品分类表; -- 产品表 CREATE TABLE product ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 产品ID, category_id INT NOT NULL COMMENT 所属分类ID, name VARCHAR(120) NOT NULL COMMENT 产品名称, price DECIMAL(10,2) NOT NULL COMMENT 售价单位元, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, status TINYINT NOT NULL DEFAULT 1 COMMENT 上下架状态1上架0下架, image_url VARCHAR(255) DEFAULT NULL COMMENT 产品图片地址, description TEXT COMMENT 产品描述, create_time DATETIME NOT NULL COMMENT 创建时间, update_time DATETIME DEFAULT NULL COMMENT 更新时间, KEY idx_category_id (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT产品表; -- 订单记录表 CREATE TABLE order_item ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 订单ID, product_id INT NOT NULL COMMENT 产品ID, product_name VARCHAR(120) NOT NULL COMMENT 产品名称快照, price DECIMAL(10,2) NOT NULL COMMENT 成交单价快照, quantity INT NOT NULL COMMENT 购买数量, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额, create_time DATETIME NOT NULL COMMENT 下单时间, KEY idx_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单记录表;价格字段用 DECIMAL(10,2) 而不是 FLOAT 或 DOUBLE是因为浮点数在运算中会丢失精度产品金额一旦对不上账排查成本非常高。库存用 INT上下架状态用 TINYINT 并且只取 0 和 1这些决策都是照着实际业务来的。order_item 里存 product_name 和 price 快照也是一种常见做法因为产品名称和价格后续可能改但历史订单必须保留下单当时的信息。这三张表的关系是category 一对多 productproduct 一对多 order_item。外键约束在中小型管理系统里经常不建原因很现实——删除产品时会被外键卡住业务上又需要允许删除产品同时保留订单记录。所以一般只在产品表上建普通索引不建物理外键。这个取舍在面试里也经常被问到值得理解背后的逻辑。3.2 Mapper接口方法定义与XML中的SQL映射MyBatis 的 Mapper 由接口和 XML 两部分组成。接口里定义方法XML 里写 SQL。在 SSM 源码里一个产品模块的 Mapper 通常包含增删改查和几个特定业务方法。接口的每个方法签名要和 XML 中的 statement id 一一对应。public interface ProductMapper { // 多条件查询产品列表 ListProduct selectList(ProductQuery query); // 根据主键查询产品 Product selectById(Integer id); // 新增产品 int insert(Product product); // 更新产品信息 int update(Product product); // 删除产品 int deleteById(Integer id); // 扣减库存 int updateStock(Param(id) Integer id, Param(stock) Integer stock); }对应到 ProductMapper.xmlnamespace 必须是接口的全限定名这是 MyBatis 定位 Mapper 的关键。select 语句的 resultType 可以直接写 Product因为前面在 spring-mybatis.xml 里已经配置了 typeAliasesPackage。mapper namespacecom.example.product.dao.ProductMapper select idselectList resultTypeProduct SELECT id, category_id, name, price, stock, status, image_url, description, create_time FROM product where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if teststatus ! null AND status #{status} /if /where ORDER BY id DESC /select insert idinsert parameterTypeProduct useGeneratedKeystrue keyPropertyid INSERT INTO product (category_id, name, price, stock, status, image_url, description, create_time) VALUES (#{categoryId}, #{name}, #{price}, #{stock}, #{status}, #{imageUrl}, #{description}, #{createTime}) /insert update idupdate parameterTypeProduct UPDATE product SET category_id #{categoryId}, name #{name}, price #{price}, stock #{stock}, status #{status}, image_url #{imageUrl}, description #{description}, update_time #{updateTime} WHERE id #{id} /update delete iddeleteById parameterTypeint DELETE FROM product WHERE id #{id} /delete /mapperselectList 里的 标签是 MyBatis 的动态 SQL 核心能力它会自动处理第一个条件前面的 AND避免出现 WHERE 后面直接跟 AND 的语法错误。 标签的 test 表达式里判断的是 ProductQuery 对象的属性。注意字符串判断要写成 name ! null and name ! 只判断 null 会导致空字符串也进查询条件。insert 的 useGeneratedKeys 和 keyProperty 配合使用插入后把自增主键回填到 Product 对象里后续如果要用到新产品的 id 就不需要再查一次。3.3 多条件查询、库存扣减与排序的SQL封装产品列表页面通常需要按名称模糊搜索、按分类筛选、按状态筛选还要控制排序。这套 SQL 在真实系统里天天被调用写法的优劣直接影响使用体验。查询条件可以单独封装成一个 ProductQuery 对象继承或包含分页参数这样 Controller 接收参数后直接传给 Service再透传到 Mapper。public class ProductQuery { private String name; // 产品名称模糊搜索 private Integer categoryId; // 分类筛选 private Integer status; // 上下架状态 private Integer pageNum 1; // 当前页码 private Integer pageSize 10; // 每页条数 // getter / setter 省略 }库存扣减是产品系统里最容易写错的一段 SQL。常见的错误写法是先在 Java 里查出当前库存判断够不够再做减法更新。这种写法在并发场景下会超卖因为两个请求同时读到库存 10同时扣减 1最后库存可能变成 9 而不是 8。正确的做法是把扣减条件写进 UPDATE 语句让数据库来判断。-- 扣减库存库存不足时影响行数为0由Service层判断是否失败 UPDATE product SET stock stock - #{quantity}, update_time NOW() WHERE id #{id} AND stock #{quantity}这条 SQL 的关键在于 WHERE 里加了 stock #{quantity}。MySQL 执行 UPDATE 时会对满足条件的行加锁所以两个并发请求只有一个能成功更新另一个影响行数为 0。Service 层拿到影响行数后可以做回滚或提示。这是用数据库行锁来保证库存正确比 Java 层加锁更轻量也更容易维护。排序方面产品列表一般默认按创建时间倒序也可以提供按价格升序或降序的排序参数。在 XML 里使用 ${} 拼接排序字段时要格外小心因为 ${} 是字符串替换而不是预编译占位如果直接把前端传来的字段名拼进去存在注入风险。常见的做法是在 Java 层做白名单校验只允许传入 id、price、create_time再映射成固定的排序片段。4. 把业务链路串起来Controller、Service到前端页面的完整请求流4.1 Controller层请求入口、参数绑定与跳转Controller 是整个请求链路的入口。拿到源码后看一个完整的请求从点击按钮到数据回显能最快理解 SSM 的工作方式。产品管理系统的 Controller 通常负责两件事返回页面、处理 Ajax 请求。前者返回视图名称后者返回 JSON 数据。Controller RequestMapping(/product) public class ProductController { Autowired private ProductService productService; // 打开产品列表页面 RequestMapping(/list) public String list(ProductQuery query, Model model) { ListProduct list productService.queryList(query); model.addAttribute(list, list); return product/list; } // 新增或更新产品Ajax 请求 RequestMapping(/save) ResponseBody public Result save(RequestBody Product product) { productService.save(product); return Result.ok(); } // 删除产品Ajax 请求 RequestMapping(/delete) ResponseBody public Result delete(Integer id) { productService.delete(id); return Result.ok(); } }list 方法接收 ProductQuery 作为查询参数。SpringMVC 会把请求参数按照属性名自动绑定到 ProductQuery 对象的字段上比如请求参数 name手机就会绑定到 query.name。Model 对象用来携带数据到 JSP 页面返回的字符串 product/list 会被视图解析器解析成 /WEB-INF/jsp/product/list.jsp。save 方法使用 RequestBody 接收 JSON 数据所以前端提交时要用 JSON 格式发送如果前端用普通的表单序列化提交这里会报 HttpMessageNotReadableException。delete 方法直接接收 Integer idSpringMVC 会自动做类型转换。这里有个细节方法名 delete 和 HTTP 的 DELETE 方法没有关系URL 是 /product/delete任何请求方式都可以访问。如果要做更严格的 REST 风格需要再加 method 属性限制。4.2 Service层事务边界与增删改查的编排Service 层是业务逻辑的所在地也是一个 SSM 产品系统源码里最值得读的部分。Controller 不做业务判断Mapper 只做单表操作剩下的规则都放在 Service。比如新增产品时校验分类是否存在、删除产品时统计订单引用、保存时判断是新增还是更新这些都在 Service 层完成。Service public class ProductServiceImpl implements ProductService { Autowired private ProductMapper productMapper; Autowired private OrderItemMapper orderItemMapper; Override Transactional public void save(Product product) { if (product.getId() null) { // 新增补全创建时间 product.setCreateTime(new Date()); product.setStatus(product.getStatus() null ? 1 : product.getStatus()); productMapper.insert(product); } else { // 更新只更新非关键字段 product.setUpdateTime(new Date()); productMapper.update(product); } } Override Transactional public void delete(Integer id) { // 删除前检查是否被订单引用避免留下孤儿数据 int count orderItemMapper.countByProductId(id); if (count 0) { throw new BusinessException(该产品已有订单记录不能直接删除); } productMapper.deleteById(id); } Override public ListProduct queryList(ProductQuery query) { return productMapper.selectList(query); } }save 方法上加了 Transactional表示这个操作拥有事务边界。如果 insert 或 update 过程中抛异常事务会回滚数据库不会留在半更新状态。delete 方法里先查询订单引用次数有引用就抛出业务异常这是常见的防呆设计——避免删掉产品后订单里出现查不到产品信息的局面。事务注解默认只在抛出 RuntimeException 时回滚受检异常不会触发回滚。如果业务里手动 try catch 了异常但没有重新抛出事务同样会失效。这套规则是 SSM 开发里很容易翻车的地方后面避坑章会详细展开。值得注意的还有 queryList 方法没有加 Transactional因为查询操作只需要只读事务不加反而更高效。4.3 前端JSP页面与Ajax交互列表渲染和删除操作产品管理系统的前端在 SSM 项目里一般是 JSP 加 jQuery这套组合虽然老但胜在简单直接。列表页用 JSTL 的 c:forEach 循环渲染表格删除操作通过 jQuery 发 Ajax 请求。产品列表页的核心代码通常长这样c:forEach items${list} varp tr td${p.id}/td td${p.categoryName}/td td${p.name}/td td classprice${p.price}/td td${p.stock}/td td c:choose c:when test${p.status 1}上架中/c:when c:otherwise已下架/c:otherwise /c:choose /td td a hrefjavascript:void(0) classedit-btn>$(document).on(click, .del-btn, function () { var id $(this).data(id); if (!confirm(确认要删除这个产品吗)) { return; } $.ajax({ url: /product/delete, type: POST, data: {id: id}, dataType: json, success: function (res) { if (res.code 200) { location.reload(); } else { alert(res.msg); } } }); });事件用 $(document).on 做委托而不是直接给按钮绑定 click是因为列表刷新后旧的事件绑定会丢失。确认框 confirm 是删除类操作的基本保护防止误点。Ajax 返回的 res.code 对应后端 Result 对象里的 code 字段code 为 200 表示成功。整套交互不复杂但包含了 SSM 产品系统里最常用的一套前端套路后端返回状态码、前端统一处理这个模式在后来的前后端分离项目里也一直被延续。5. SSM产品系统避坑实录配置、事务、乱码与映射的5个翻车点5.1 MySQL 8.x驱动时区配置不对启动就报连接失败现象项目启动时控制台报错日志里出现 Communications link failure 或 The server time zone value йʱ is unrecognized数据库连接建立不起来。这个错误在高版本 MySQL 驱动下非常常见因为 MySQL Connector/J 8.x 默认要求显式指定时区。原因MySQL 8.x 的驱动类名从 com.mysql.jdbc.Driver 改成了 com.mysql.cj.jdbc.Driver同时连接 URL 必须带 serverTimezone 参数。旧项目源码里的 jdbc.properties 往往还是老驱动的配置复制到新环境后直接翻车。解决jdbc.properties 里统一使用下面的配置驱动类名和时区一起改掉。如果服务器时间用北京时间就写 Asia/Shanghai。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/product_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyour_password另外注意 useSSLfalse 这个参数本地连接时如果不加MySQL 8.x 会输出 SSL 警告虽然不影响运行但日志里的大批警告会干扰排查。把 useSSLfalse 也加进 URL日志会干净很多。5.2 Mapper接口扫描不到报NoSuchBeanDefinitionException现象Spring 容器启动后调用 Service 时抛出 org.springframework.beans.factory.NoSuchBeanDefinitionException提示找不到 ProductMapper 类型的 Bean。代码看起来没少任何注解但 Mapper 就是注入不进去。原因最常见的原因有三个。第一MapperScannerConfigurer 的 basePackage 配置的包名和 Mapper 接口实际所在包不一致。第二spring-mybatis.xml 文件里配置了 Mapper 扫描但 web.xml 的 contextConfigLocation 加载的是另一个配置文件导致根容器里根本没有 SqlSessionFactory。第三Mapper XML 的 namespace 和接口全限定名不一致MyBatis 绑定接口失败。解决按照发生的概率依次检查。先确认接口所在的包路径再核对 spring-mybatis.xml 中 basePackage 的配置。然后检查 web.xml 里 context-param 指向的文件路径确定 Mapper 扫描配置确实被加载。最后打开 Mapper XML验证 namespace 是否和接口全限定名完全一致包括大小写。这三处对齐后这个错基本不会再出现。5.3 新增产品中文乱码页面显示问号现象表单里输入产品名称“无线鼠标”点击保存后列表页显示“”。数据库表里也能查到问号重启项目问题依旧。原因乱码链路有三个环节任何一个断了都会出问题。第一浏览器提交的请求体编码不是 UTF-8SpringMVC 默认按 ISO-8859-1 解码。第二数据库连接 URL 里没有 characterEncodingutf8MySQL 驱动使用了默认编码。第三数据库表本身的字符集不是 utf8mb4。解决三个环节一次到位。SpringMVC 配置里加 CharacterEncodingFilterJSP 页面加 pageEncoding。数据库连接 URL 带上 characterEncodingutf8建表语句用 utf8mb4。下面这段过滤器需要放到 web.xml 的最前面确保请求进入业务代码之前编码已经被处理。!-- web.xml 编码过滤器放在所有过滤器最前面 -- filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping排查乱码有个铁律先确认数据库里存的已经是问号还是只有页面显示问号。如果数据库里是正常中文问题在页面展示如果数据库里就是问号问题在请求解码或连接编码。定位到具体环节再改效率最高。5.4 Transactional没有生效删除后数据还在现象Service 方法中先删产品再插入一条日志日志插入失败抛异常但产品已经被删掉了事务没有回滚。也就是 Transactional 注解写了却完全不生效。原因这是一个典型的 SSM 事务失效案例常见诱因有三个。第一spring-mybatis.xml 中只配置了 SqlSessionFactory没有配置 DataSourceTransactionManagerSpring 不知道该由谁管理事务。第二Transactional 加在了 private 方法上Spring 默认使用 JDK 动态代理或 CGLIB 代理private 方法无法被代理。第三ServiceImpl 类没有被 Spring 扫描到类上的 Service 注解包路径没被 context:component-scan 覆盖。解决在 spring-mybatis.xml 中配置事务管理器并开启注解驱动。同时确认事务注解加在 public 方法上ServiceImpl 类能被组件扫描找到。!-- spring-mybatis.xml 追加事务配置 -- bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/检查 Transactional 是否生效有一个快速方法在 Service 方法里故意抛 RuntimeException观察数据库数据是否回滚。回滚了说明配置正常没回滚就按上面三个原因逐个排查。注意 MyBatis 的自动提交默认是关闭的事务由 Spring 管理如果事务管理器没有配置即使前面的 insert 执行了数据也不一定立即提交这个状态会让排查变得更混乱。5.5 数据库下划线字段映射不上查出来全是null现象数据库表字段是 category_id、create_time实体类属性是 categoryId、createTime。查询结果里其他字段正常但带下划线的字段全部是 null。直接执行 SQL 能查到数据说明 SQL 没问题。原因MyBatis 默认的自动映射要求数据库列名和 Java 属性名完全一致。category_id 和 categoryId 对不上MyBatis 不会自动转换。很多 SSM 项目会在 SQL 里写别名但如果 Mapper XML 漏写了某个字段的别名对应的属性就是 null。另外用 resultType 自动映射时依赖的是 mapUnderscoreToCamelCase 全局开关。解决在 spring-mybatis.xml 的 SqlSessionFactoryBean 里开启驼峰映射配置这是最简单也最不容易漏的方式。bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.product.entity/ property nameconfiguration bean classorg.apache.ibatis.session.Configuration property namemapUnderscoreToCamelCase valuetrue/ /bean /property /bean开启这个开关后category_id 会自动映射到 categoryId。但要注意查询结果里如果某个字段在实体类中不存在MyBatis 不会报错只是忽略掉。反之如果要手动指定复杂映射还是应该用 resultMap特别是在 JOIN 查询时resultType 的自动映射会丢失关联对象信息。产品列表的分页查询如果需要显示分类名称一般就是 JOIN 分类表然后通过 resultMap 或扩展属性来接收。6. 让源码跑通后再进一步分页、统一返回结构与交付演示6.1 给产品列表加真正的分页产品数据一旦超过几十条一次性查询全部数据会导致页面卡顿。SSM 产品系统最常见的改进就是加分页。不引入 PageHelper 也能实现手写 LIMIT 分页更直观。在 ProductQuery 中已经有 pageNum 和 pageSize只需要在 SQL 里计算偏移量再单独查一次总数。!-- ProductMapper.xml 新增 -- select idcountByQuery parameterTypeProductQuery resultTypelong SELECT COUNT(*) FROM product where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if /where /select select idselectPage parameterTypeProductQuery resultTypeProduct SELECT id, category_id, name, price, stock, status, create_time FROM product where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if /where ORDER BY id DESC LIMIT #{offset}, #{pageSize} /selectoffset 在 Java 层计算填入((pageNum - 1) * pageSize)。注意 LIMIT 的参数用 #{} 预编译不要用 ${} 拼接。count 查询和分页查询共用同一个 where 条件只是查询字段不同维护时改一个地方就行。首页、上一页、下一页、总页数这些信息可以封装到 Page 对象里前端就能渲染完整的翻页栏。6.2 用统一返回结构收口Ajax结果Controller 里每个方法都 return Result.ok()这套写法在小型管理系统里很够用。Result 类的设计直接影响前端判断逻辑是否一致。一个好的 Result 至少包含 code、msg、data 三个字段提供一个静态工厂方法。public class Result { private int code; private String msg; private Object data; public static Result ok() { return ok(null); } public static Result ok(Object data) { Result r new Result(); r.code 200; r.msg success; r.data data; return r; } public static Result fail(String msg) { Result r new Result(); r.code 500; r.msg msg; return r; } // getter / setter 省略 }在 Service 层处理业务异常时可以抛出自定义的 BusinessExceptionController 中通过 ExceptionHandler 统一捕获并返回 Result.fail。这样每个方法里不需要重复写 try catch代码简洁前端只要统一判断 code 是否等于 200 即可。这个模式是从 SSM 到 Spring Boot 一直延续的做法学会了之后迁移到新框架也完全能用。6.3 本地演示前建议检查的三件事产品系统源码跑通之后如果要拿去做演示或者交付我每次都会花二十分钟检查三件事。第一件是数据库初始化脚本是否完整包括建库、建表、初始分类和几个示例产品。一个空列表的演示页面没有说服力有数据才能展示查询和筛选功能。第二件是 Tomcat 的启动端口和数据库账号密码是否写在 README 里换一台机器能不能照着步骤跑起来。第三件是资源文件的编码统一确认所有 JSP、properties、XML 都是 UTF-8避免换环境后乱码。演示用的数据也有讲究比如产品名称用贴近实际的“无线键盘”“USB扩展坞”价格保留两位小数库存设置几个接近零的数值。这样演示时既能展示列表又能展示库存预警的逻辑。我在交付这类项目前会顺手把删除按钮的二次确认加上把编辑页面的必填校验补上这些细节往往比功能本身更能体现工程的完成度。最后提醒一句SSM 源码不是跑通就结束了真正提升是把配置文件逐行弄明白把事务和动态 SQL 的原理搞清楚。这套东西虽然老但其中的分层思想和 MyBatis 的机制换到 Spring Boot 里依然成立。等你能不看任何资料独立把一套产品管理系统从建表写到页面再回头看 Spring Boot 自动配置的那些内容会觉得一切都顺理成章。希望帮到你。本文还有配套的精品资源点击获取