SSM框架整合实战:Spring MVC、Spring与MyBatis核心配置与事务管理
简介这是一份基于SSMSpring MVC Spring MyBatis框架开发的学生管理管理系统源码包适合正在学习JavaWeb整合开发或需要完成课程设计的读者。资源涵盖学生信息的模糊查询、删除、新增等核心功能并融合了JSP/Servlet/JavaBean/JDBC等经典技术栈可帮助理解从传统JDBC到主流框架的过渡思路。整个压缩包共95个文件约36.14MB包含58个jar依赖、12个xml配置文件含MyBatis映射和Spring配置、5个Java源文件及对应class文件、3个JSP页面、2个properties属性文件同时提供student_db.sql数据库脚本和题目说明PDF方便直接导入运行与对照学习。目前已有2339人学习下载资源目录结构清晰适合作为SSM整合开发的入门实战参考也可为课程设计或毕业设计提供可直接复用的代码基础。1. 这个 zip 里的 SSM 项目核心不是 CRUD 而是框架间的接线这套名为“学生管理系统”的 SSM 工程拆开后值得研究的并不是某个 Controller 里的增删改查而是三套框架之间十几行 XML 接线Spring MVC 把所有前端请求转成方法调用Spring 容器管理 Service 和事务MyBatis 把 SQL 结果映射回 Java 对象。对于要维护后台系统或在老模块里新增一张表的开发者理清“谁扫包、谁创建对象、谁代理事务”这三个约定比背框架文档更有用。下面按我平时维护这类工程项目的方式从依赖搭建写到事务、分页和排错过程中用到 Spring 5.3 和 MyBatis 3.5 这一代常见版本。2. 把 Spring MVC、Spring、MyBatis 拼起来依赖、web.xml 与三份 Spring 配置SSM 工程的搭建顺序通常是先定 Maven 依赖再写 web.xml 指定两个 Spring 容器最后准备 applicationContext.xml、spring-mvc.xml 和 mybatis-config.xml。这套流程在今天已经被 Spring Boot 自动配置替换了大半但老项目的维护、新项目的代码阅读都要求能看懂这几份文件的配合关系。2.1 pom.xml 里容易漏掉的坐标mybatis-spring 和分页插件properties spring.version5.3.24/spring.version mybatis.version3.5.13/mybatis.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.18/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version scoperuntime/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency /dependenciesmybatis-spring 是 MyBatis 与 Spring 容器之间的桥接包没有它Mapper 接口无法生成代理对象事务也无法交给 Spring 管理。pagehelper 单独作为依赖引入它和 mybatis-spring 2.x 配合时不需要额外插件配置只要在 MyBatis 全局配置里注册 PageInterceptor 就能使用。jackson-databind 是为了让 Spring MVC 支持 JSON 请求和 JSON 响应漏掉它时常出现 415 错误排错成本很高。2.2 web.xml 中两个容器各管各的包SSM 工程中 DispatcherServlet 和 ContextLoaderListener 会各自创建一个 Spring 容器。常见做法是根容器放数据源、SqlSessionFactory、Mapper、Service 和事务MVC 容器放 Controller、视图解析器和消息转换器。context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.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-mappingurl-pattern 用/而不是*.do这是让 DispatcherServlet 接管除 JSP 外的所有请求。需要注意的是Controller 不能同时被两个容器扫描到否则某些请求会被同一个 bean 在父容器和子容器中各注册一次事务代理可能出现“看似生效、实则没走代理”的情况。2.3 applicationContext.xml数据源、SqlSessionFactory 与事务管理器context:component-scan base-packagecom.example.ssm context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ context:exclude-filter typeannotation expressionorg.springframework.web.bind.annotation.RestController/ /context:component-scan context:property-placeholder locationclasspath:db.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource init-methodinit destroy-methodclose property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ property nameinitialSize value5/ property namemaxActive value20/ property namevalidationQuery valueSELECT 1/ property nametestWhileIdle valuetrue/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.ssm.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.ssm.mapper/ /bean bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/SqlSessionFactoryBean 的 mapperLocations 指向 classpath 下的 mapper 目录这样 Mapper 接口和 XML 文件可以不在同一个包路径下。typeAliasesPackage 用于给实体类起短名之后在 mapper XML 中可以直接写parameterTypeStudent。MapperScannerConfigurer 会自动扫描指定包下的接口为每个接口生成一个可注入的代理对象不需要手写 Mapper 实现类。2.4 spring-mvc.xml 和 mybatis-config.xml视图解析与 SQL 日志context:component-scan base-packagecom.example.ssm.controller/ mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.StringHttpMessageConverter property namedefaultCharset valueUTF-8/ /bean bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property namedefaultCharset valueUTF-8/ /bean /mvc:message-converters /mvc:annotation-driven bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean mvc:default-servlet-handler/mvc:annotation-driven 负责注册 HandlerMapping 和参数解析器MappingJackson2HttpMessageConverter 负责把 Controller 返回的对象序列化为 JSON。default-servlet-handler 的作用是把静态资源请求放给容器默认 Servlet 处理否则 Controller 没映射到的 CSS、JS 会全部 404。mybatis-config.xml 里我固定会开这几项settings setting namemapUnderscoreToCamelCase valuetrue/ setting namecacheEnabled valuefalse/ setting namelazyLoadingEnabled valuetrue/ setting nameaggressiveLazyLoading valuefalse/ setting namelogImpl valueSTDOUT_LOGGING/ /settingsmapUnderscoreToCamelCase 让数据库的 stu_no 自动映射到 Java 的 stuNo省去大量 resultMap 列别名配置。STDOUT_LOGGING 表示 SQL 直接打到控制台这个配置方便排查 SQL 拼接是否符合预期上线前再换成其它日志实现。3. 学生管理系统的数据层三张业务表、动态 SQL 与事务边界学生管理系统的数据模型通常不会太复杂但至少要包含专业、班级、学生三张表这样 mapper 里能体现多表 JOIN、动态条件、批量更新等常用场景。单表 CRUD 太简单贴不出有参考价值的东西。3.1 用 utf8mb4 建三张业务表索引按查询链设计CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; CREATE TABLE major ( id INT PRIMARY KEY AUTO_INCREMENT, major_name VARCHAR(64) NOT NULL COMMENT 专业名, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE clazz ( id INT PRIMARY KEY AUTO_INCREMENT, clazz_name VARCHAR(64) NOT NULL, major_id INT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_major_id (major_id) ) ENGINEInnoDB; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, stu_no VARCHAR(20) NOT NULL, gender TINYINT NOT NULL DEFAULT 1 COMMENT 1男2女, age INT, clazz_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在读0离校, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_no (stu_no), KEY idx_clazz_id (clazz_id) ) ENGINEInnoDB;学号必须加唯一索引这是管理系统底线的约束。clazz_id 和 major_id 建普通索引是为了让列表页按班级过滤、按专业汇总的 SQL 不触发全表扫描。字符集用 utf8mb4 是为了兼容各种生僻字符老项目里常见的 utf8 在存储某些特殊字符时会直接报 truncated 错误。3.2 StudentMapper.xmlwhere、set 标签处理动态条件public interface StudentMapper { ListStudentVO selectStudentPage(StudentQuery query); StudentVO selectById(Integer id); int insertStudent(Student student); int updateStudent(Student student); }对应的 XML 核心部分resultMap idStudentVOMap typecom.example.ssm.vo.StudentVO id propertyid columnid/ result propertyname columnname/ result propertystuNo columnstu_no/ result propertygender columngender/ result propertyclazzName columnclazz_name/ result propertymajorName columnmajor_name/ /resultMap sql idbaseSelect SELECT s.id, s.name, s.stu_no, s.gender, s.age, c.clazz_name, m.major_name FROM student s LEFT JOIN clazz c ON s.clazz_id c.id LEFT JOIN major m ON c.major_id m.id /sql select idselectStudentPage resultMapStudentVOMap include refidbaseSelect/ where if testkeyword ! null and keyword ! AND s.name LIKE CONCAT(%, #{keyword}, %) /if if testclazzId ! null AND s.clazz_id #{clazzId} /if if teststatus ! null AND s.status #{status} /if /where ORDER BY s.id DESC /select update idupdateStudent parameterTypecom.example.ssm.entity.Student UPDATE student set if testname ! nullname #{name},/if if teststuNo ! nullstu_no #{stuNo},/if if testgender ! nullgender #{gender},/if if testclazzId ! nullclazz_id #{clazzId},/if if teststatus ! nullstatus #{status},/if /set WHERE id #{id} /updatewhere 标签会自动去掉第一个条件前面的 AND而 set 标签会自动处理最后一个字段后的逗号这两个标签是 MyBatis 动态 SQL 里最常用、也最容易写错的点。LIKE 查询用 CONCAT 拼%而不是直接写%${keyword}%避免字符串拼接 SQL 注入。LEFT JOIN 查出来的 clazzName、majorName 通过 resultMap 映射到 VO 上页面或接口直接取字段名不需要在 Java 层二次循环。3.3 Service 里的事务边界一个方法一个事务Service Transactional(rollbackFor Exception.class) public class StudentServiceImpl implements StudentService { private final StudentMapper studentMapper; public StudentServiceImpl(StudentMapper studentMapper) { this.studentMapper studentMapper; } Override public void disableStudent(Integer id) { studentMapper.updateStatus(id, 0); // 这里还可以加一条审计日志插入任何一步抛异常都会整体回滚 } Override public PageInfoStudentVO page(StudentQuery query, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); ListStudentVO list studentMapper.selectStudentPage(query); return new PageInfo(list); } }Transactional 加在类上表示类内所有 public 方法默认开启事务。rollbackFor Exception.class 是因为 Spring 默认只对 RuntimeException 回滚而检查型异常默认不触发回滚在这个系统里比如学号重复、业务状态异常都应该触发回滚所以必须写明。PageHelper.startPage 只对它后面的第一条 mapper 查询生效所以要先调用 startPage再执行 selectStudentPage不能反过来调整顺序。4. Spring MVC 控制层学生管理请求的三种参数绑定与 JSON 出参Controller 是学生管理系统的入口层。这个模块里涉及的请求不外乎三类带查询条件的列表请求、路径上带 id 的详情请求、JSON 格式的保存请求。三种请求对应三种不同的参数拿法混着用时最容易出问题。4.1 列表分页接口RequestParam 加默认值Controller RequestMapping(/student) public class StudentController { private final StudentService studentService; public StudentController(StudentService studentService) { this.studentService studentService; } GetMapping(/list) ResponseBody public ApiResult list(StudentQuery query, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { PageInfoStudentVO pageInfo studentService.page(query, pageNum, pageSize); return ApiResult.ok(pageInfo); } }StudentQuery 直接作为参数对象Spring MVC 会把请求参数 name、clazzId、status 自动绑定到它的字段上。pageNum 和 pageSize 单独用 RequestParam 接收默认值写 1 和 10避免前端没传参数时报 MissingServletRequestParameterException。ResponseBody 表示返回对象直接序列化为 JSON不走 InternalResourceViewResolver 的视图渲染流程。4.2 详情接口PathVariable 与查询参数的使用边界GetMapping(/detail/{id}) ResponseBody public ApiResult detail(PathVariable(id) Integer id) { return ApiResult.ok(studentService.detail(id)); }PathVariable 从 URL 路径中取值适合“详情、删除、编辑回显”这类明确针对单个资源的操作而 RequestParam 适合过滤、分页这类修饰查询条件的参数。二者边界清楚接口风格也稳定。如果 Controller 上再配合 RequestMapping(/student)实际访问路径就是/student/detail/3。4.3 JSON 保存接口RequestBody 与 Content-TypePostMapping(/save) ResponseBody public ApiResult save(RequestBody StudentDTO dto) { int rows studentService.save(dto); return rows 1 ? ApiResult.ok() : ApiResult.fail(保存失败); }RequestBody 要求请求头 Content-Type 为 application/json它把请求体中的 JSON 字符串反序列化成 StudentDTO。如果用普通表单方式提交请求体是 keyvalue 格式反序列化会直接抛 HttpMessageNotReadableException。对应测试命令curl -X POST http://localhost:8080/student/save \ -H Content-Type: application/json \ -d {name:张三,stuNo:2024001,gender:1,age:20,clazzId:3}DTO 和实体分开是必要的学生表字段和页面提交字段并不完全一致比如 DTO 里可以有确认密码、入学年份等表里不存在的字段直接用实体接收会让表结构被迫跟着页面请求改动。四种参数绑定方式区别如下写法使用场景关键注意点RequestParam分页、查询条件可设置 defaultValue 或 requiredfalsePathVariable路径式资源定位Restful 风格id 类型不匹配返回 400RequestBodyJSON 请求体请求头必须是 application/jsonStudentQuery 对象多条件查询字段名与请求参数同名才自动绑定4.4 返回 JSP 页面时视图解析器如何工作如果后台管理界面需要服务端渲染比如编辑页需要回填数据Controller 方法可以返回 StringGetMapping(/edit) public String edit(RequestParam Integer id, Model model) { model.addAttribute(student, studentService.detail(id)); return student/edit; }这里返回的student/edit会被 InternalResourceViewResolver 拼成/WEB-INF/views/student/edit.jsp。Model 中的数据可以通过 JSTL 在页面上读取。注意这个方法和上面的 JSON 返回不一样它不能加 ResponseBody一旦加了就会把字符串本身当成响应写出接口返回的会是 student/edit 而不是页面。5. Spring 事务在 SSM 里的验证、现场排错与最后的方言开关事务配置写完后别急着往下做功能先验证它真的生效。最直接的办法是在一个写操作后面人为抛运行时异常Transactional(rollbackFor Exception.class) public void registerStudent(Student s) { studentMapper.insertStudent(s); throw new RuntimeException(测试回滚); }跑完这个方法后查表如果新纪录不存在说明事务代理正常。如果记录还在优先检查三类问题Service 实现类是否被 component-scan 扫描到、类或方法是否被 final/private 修饰、调用方是否在同一个类内部调用了带 Transactional 的方法。最后一种情况是经典陷阱Spring 事务基于代理同类内部方法调用不会经过代理。SSM 项目在实际运行中排错方向通常比预想中固定。下面是我最常对着看的四类现象现象定位方向最常见处理Invalid bound statement (not found)Mapper XML 的 namespace 与接口全限定名是否一致检查 mapperLocations 路径和 XML id启动报 Mapper 类没有 beanMapperScannerConfigurer 的 basePackage 是否正确确认扫描包包含 Mapper 接口SQL 中文乱码数据库连接串和表字符集URL 加 characterEncodingutf8表改 utf8mb4返回 JSON 时 415 或 NoSuchMethodErrorjson 依赖冲突或缺失确认 jackson-databind 版本一致最后一个值得分享的配置是 databaseIdProvider它让同一套 mapper 在不同数据库下自动切换 SQL 方言。这对那些需要本地跑 H2 测试、生产用 MySQL 的老项目很实用databaseIdProvider typeDB_VENDOR property nameMySQL valuemysql/ property nameH2 valueh2/ /databaseIdProvider然后在 StudentMapper.xml 里对同一个 select id 提供两份 SQL用 databaseId 区分select idselectStudentPage resultMapStudentVOMap databaseIdmysql SELECT s.id, s.name, s.stu_no, s.gender, DATE_FORMAT(s.create_time, %Y-%m-%d) AS create_date FROM student s /select select idselectStudentPage resultMapStudentVOMap databaseIdh2 SELECT s.id, s.name, s.stu_no, s.gender, FORMATDATETIME(s.create_time, yyyy-MM-dd) AS create_date FROM student s /selectMyBatis 根据连接的产品名自动选择对应 databaseId 的 SQL没有匹配时走默认 version。这样一来本地集成测试和线上 MySQL 环境共用同一组 Mapper 接口不需要为测试单独维护 SQL 副本。把这个配置放进 SSM 工程里比单纯改数据源 URL 更彻底也是这套老框架里被绝大多数人忽略的实用能力。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →