简介这是一套面向Java Web初学者与需要二次开发者的SSM框架完整项目源码基于Spring、Spring MVC与MyBatis三大框架搭建采用MVC分层架构适合用来理解企业级应用的依赖注入、请求分发与持久层映射机制。压缩包共226个文件约56.11MB其中32个java源文件与32个class文件构成核心代码20个jsp页面与19个js、13个css负责前端展示20个xml与4个properties承载Spring、Spring MVC、MyBatis及数据库连接配置另有26个jar提供运行依赖。项目按DAO、Service、Controller三层组织配合视图解析器完成请求到JSP的跳转并包含AppInfo等初始化信息目录结构清晰便于按模块阅读与调试。目前已有2386人学习下载读者可借此掌握SSM整合流程、配置要点与分层职责并在此基础上扩展功能、积累Java Web实战经验。1. 从“能跑就行”到“能交差”一套 SSM 完整项目该有的样子很多同学做 Java SSM 项目时习惯先写代码再补文档结果到了中期检查被导师问“你的分层依据是什么”“事务边界画在哪”当场卡壳。这套 SSM 完整项目方案要解决的就是“代码能跑但说不清设计”这个通病。它适合正在做毕设或课程设计的学生也适合需要快速交付一个后台管理类项目的初级开发者。整套方案围绕 Spring SpringMVC MyBatis 三层结构展开覆盖从需求拆解、数据库设计、模块编码到验收答辩的完整链路。下面按“需求立住 → 设计落地 → 避坑排查 → 验收交付”的顺序推演每一章都给出可直接抄改的结构和关键实现。2. 需求与选型为什么 SSM 仍是教学项目的稳妥解2.1 先锁定业务边界再谈技术SSM 项目最容易翻车的地方不是框架本身而是需求没锁死就动手。我一般会先画一张业务边界图把系统拆成“必须做”“可以做”“不做”三档。以典型的后台管理系统为例必须做的通常是用户登录、角色权限、单表增删改查可以做的包括文件上传、操作日志、数据导出不做的比如分布式事务、消息队列、微服务拆分。这个划分直接决定了后面数据库表数量和接口数量也决定了你答辩时能不能自圆其说。需求锁定后再倒推数据模型。用户表、角色表、权限表、业务主表这四类基本跑不掉。用户与角色是多对多角色与权限也是多对多所以中间表至少两张。业务主表根据选题不同可能是订单、图书、设备、课程等。表设计阶段就要把外键关系、索引字段、逻辑删除标记想清楚不然后面改表结构会连带改一堆 Mapper 和 Service。提示需求文档不用写几十页但“功能清单 数据字典 接口列表”这三样必须齐。答辩时导师翻的就是这三样。2.2 技术选型版本组合与依赖关系SSM 的版本组合是新手第一个坑。Spring 5.x 配 SpringMVC 5.xMyBatis 3.5.xJDK 用 8 或 11Tomcat 用 8.5 或 9。不要混用 Spring 6 和 JDK 8也不要把 MyBatis-Plus 和原生 MyBatis 的配置文件混在一起。下面是一个经过验证的 Maven 依赖片段版本号可按需微调但大版本要对齐。!-- pom.xml 关键依赖版本对齐是前提 -- properties spring.version5.3.30/spring.version mybatis.version3.5.14/mybatis.version /properties dependencies !-- Spring 核心IoC 容器和事务管理 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency !-- SpringMVC处理 Web 层请求映射 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- MyBatis持久层与 Spring 整合需要 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 !-- 数据库驱动与连接池 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies这段依赖里mybatis-spring是整合的关键它负责把 MyBatis 的 SqlSession 交给 Spring 管理。Druid 连接池不是必须但它的监控页面在调试阶段很好用。版本号不要盲目追新Spring 5.3.x 是长期维护版和 JDK 8/11 兼容性最稳。2.3 分层职责别把 Service 写成大杂烩SSM 的标准分层是 Controller → Service → Mapper。Controller 只做参数接收和结果封装不写业务判断Service 承担事务和业务逻辑Mapper 只负责 SQL 映射。常见错误是把权限校验、数据组装、异常处理全塞进 Controller导致 Service 变成空壳。我的习惯是Controller 里不超过 15 行Service 方法按业务动作命名Mapper 接口一个方法对应一条 SQL。事务边界通常放在 Service 层用Transactional注解。注意默认只对 RuntimeException 回滚如果捕获了异常又没重新抛出事务不会生效。这个点在后面避坑章节会展开。3. 总体架构与数据库设计四层结构与核心表落地3.1 架构分层与请求流转整套系统按四层组织表现层JSP/HTML Controller、业务层Service、持久层Mapper XML、数据层MySQL。请求从浏览器发出经过 web.xml 中的 DispatcherServlet 拦截根据 spring-mvc.xml 里的视图解析器和注解映射找到对应 Controller 方法再调用 ServiceService 通过 Mapper 操作数据库结果逐层返回。这个流转过程听起来简单但配置文件一多就容易乱。我一般把配置拆成三个文件applicationContext.xml管 Spring 核心和事务spring-mvc.xml管 Web 层扫描和视图解析mybatis-config.xml管 MyBatis 全局设置。web.xml 里用 ContextLoaderListener 加载 Spring 容器用 DispatcherServlet 加载 SpringMVC 容器。两个容器扫描的包要分开否则事务注解可能不生效。!-- applicationContext.xml 关键片段数据源与事务 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/ssm_demo?useSSLfalseamp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value你的密码/ /bean !-- 事务管理器绑定数据源 -- bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:annotation-driven transaction-managertransactionManager/ !-- Mapper 接口扫描注意与 SpringMVC 扫描包错开 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.demo.mapper/ /beanbasePackage指向 Mapper 接口所在包Spring 会自动生成代理实现。tx:annotation-driven开启注解事务Service 上的Transactional才会被识别。数据源 URL 里的serverTimezone不写可能报时区错误这是 MySQL 8 的常见问题。3.2 核心表结构与字段说明数据库设计直接决定 Mapper 的复杂度。下面给出四张核心表的字段定义业务主表以“设备信息”为例可替换成你的选题。表名字段类型说明sys_useridbigint主键自增usernamevarchar(50)登录名唯一索引passwordvarchar(100)存储加密后的密码statustinyint1 启用 0 禁用sys_roleidbigint主键role_namevarchar(50)角色名sys_user_roleuser_idbigint联合主键role_idbigint联合主键biz_deviceidbigint主键device_namevarchar(100)设备名称device_codevarchar(50)编号唯一create_timedatetime创建时间is_deletedtinyint逻辑删除标记密码字段不要存明文通常用 MD5 加盐或 BCrypt。逻辑删除标记is_deleted在查询时统一加条件避免物理删除导致关联数据丢失。联合主键在 MyBatis 里需要用Param或 Map 传参后面 Mapper 章节会演示。3.3 接口清单与返回格式接口设计要统一返回结构否则前端处理起来很痛苦。我一般定义一个Result类包含code、msg、data三个字段。code 用 200 表示成功500 表示业务异常401 表示未登录。Controller 方法统一返回Result由 SpringMVC 自动转 JSON。// 统一返回体放在 common 包下 public class ResultT { private int code; private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg 操作成功; r.data data; return r; } public static T ResultT fail(String msg) { ResultT r new Result(); r.code 500; r.msg msg; return r; } // getter/setter 省略 }这个类看起来简单但能省掉大量重复的 JSON 拼接。注意泛型T让 data 可以是任意类型分页数据、单个对象、列表都能装。前端只需要判断 code 是否为 200不用关心 HTTP 状态码。4. 模块实现登录鉴权、分页查询与文件上传4.1 登录鉴权从密码校验到拦截器登录模块是 SSM 项目里最能体现分层思想的模块。Controller 接收用户名密码Service 查库比对成功后把用户信息写入 Session。后续请求通过拦截器判断 Session 是否存在不存在则跳转登录页。// LoginController 核心方法 PostMapping(/login) ResponseBody public ResultUser login(String username, String password, HttpSession session) { User user userService.login(username, password); if (user null) { return Result.fail(用户名或密码错误); } session.setAttribute(loginUser, user); return Result.success(user); }Service 层的login方法先按用户名查用户再用加密工具比对密码。如果用户状态是禁用直接返回 null。这里不要抛异常否则前端拿不到友好提示。拦截器实现HandlerInterceptor接口在preHandle里检查 Session放行登录接口和静态资源。// 登录拦截器注册在 spring-mvc.xml 中 public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); if (session.getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } return true; } }拦截器注册时要排除/login、/css/**、/js/**等路径否则登录页自己都被拦。这个细节在调试时经常被忽略导致页面无限重定向。4.2 分页查询PageHelper 的接入与参数传递分页是后台系统的刚需。手写 limit 不是不行但 PageHelper 插件能省掉 count 查询和分页逻辑。接入方式是在 MyBatis 配置里加插件然后在 Service 里调用PageHelper.startPage。!-- mybatis-config.xml 中配置分页插件 -- plugins plugin interceptorcom.github.pagehelper.PageInterceptor property namehelperDialect valuemysql/ property namereasonable valuetrue/ /plugin /pluginsreasonable设为 true 时页码小于 1 查第一页大于总页数查最后一页避免空结果。Service 里这样写public PageInfoDevice queryByPage(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); ListDevice list deviceMapper.selectByKeyword(keyword); return new PageInfo(list); }PageInfo封装了总记录数、总页数、当前页数据前端直接取用。注意startPage必须紧挨着查询方法中间不要插入其他数据库操作否则分页会作用到错误的 SQL 上。4.3 文件上传与下载路径配置与大小限制文件上传在 SSM 里需要配置MultipartResolver。在 spring-mvc.xml 中定义 bean设置最大文件大小和编码。bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver property namemaxUploadSize value10485760/ property namedefaultEncoding valueUTF-8/ /beanmaxUploadSize单位是字节10485760 即 10MB。Controller 方法用MultipartFile接收保存到服务器磁盘或项目外的上传目录。保存路径不要放在 webapp 下否则重新部署会丢失。我一般配一个upload.path属性指向服务器绝对路径。PostMapping(/upload) ResponseBody public ResultString upload(MultipartFile file) throws IOException { if (file.isEmpty()) { return Result.fail(文件为空); } String originalName file.getOriginalFilename(); String suffix originalName.substring(originalName.lastIndexOf(.)); String newName UUID.randomUUID().toString() suffix; File dest new File(uploadPath newName); file.transferTo(dest); return Result.success(newName); }文件名用 UUID 重命名避免中文名和重名问题。下载时通过ResponseEntitybyte[]返回字节流设置Content-Disposition头。这个模块的坑主要在路径权限和文件类型校验后面避坑章节会提。5. 避坑与排查SSM 项目里最容易翻车的五个点5.1 事务不生效注解加了但回滚没发生现象是 Service 方法抛异常后数据仍然写入。原因通常是三个一是 Service 类没有被 Spring 扫描到Transactional自然无效二是异常被 catch 后没有重新抛出三是事务方法不是 public。解决方式是检查 spring 配置里的component-scan是否包含 Service 包catch 块里用throw new RuntimeException(e)重新抛出确保方法修饰符是 public。5.2 Mapper 绑定失败Invalid bound statement启动时报Invalid bound statement (not found)说明 Mapper 接口和 XML 没对上。原因可能是 XML 的 namespace 写错、方法名不一致、XML 没放在 resources 的对应目录下。解决方式是核对 namespace 是否等于接口全限定名方法名和 id 是否一致并在 pom 的 resources 配置里把**/*.xml包含进去否则 Maven 打包会漏掉 XML。5.3 中文乱码GET 和 POST 表现不同POST 乱码通常是 web.xml 里没配CharacterEncodingFilter或者配了但forceEncoding没设 true。GET 乱码在 Tomcat 8 以后默认 UTF-8但 URL 参数仍可能乱。解决方式是统一在 web.xml 加编码过滤器并在数据库连接 URL 上加characterEncodingutf8。JSP 页面头部也要声明pageEncodingUTF-8。5.4 静态资源被拦截CSS 和 JS 加载 404现象是登录页样式全丢控制台报 404。原因是 DispatcherServlet 映射了/把静态资源请求也当成 Controller 处理了。解决方式是在 spring-mvc.xml 里加mvc:default-servlet-handler/或mvc:resources mapping/static/** location/static//。前者交给容器默认 Servlet 处理后者显式映射目录。5.5 连接池耗尽Too many connections压测或长时间运行后报连接数超限。原因是 Druid 配置的maxActive过大或连接没归还。检查 Service 里是否有手动获取 Connection 没关闭的情况MyBatis 和 Spring 整合后不需要手动关连接。把maxActive设为 20 左右加removeAbandonedtrue和removeAbandonedTimeout180让泄漏连接自动回收。6. 验收与答辩怎么把项目讲成一份合格方案验收前一周我一般会做三件事跑通全流程、整理文档、模拟提问。全流程不是只点一遍页面而是从登录开始把每个模块的正常和异常路径都走一遍记录报错和修复方式。文档按“需求 → 设计 → 实现 → 测试”四段整理每段配一张图或一张表。模拟提问则把导师可能问的问题列出来比如“为什么用 SSM 不用 SpringBoot”“事务边界怎么定的”“分页插件原理是什么”每个问题准备三句话以内的回答。答辩时有一个技巧先讲业务价值再讲技术实现。很多同学一上来就讲 Spring 的 IoC 和 AOP导师听多了会烦。不如先说“这个系统解决了设备信息手工登记效率低的问题”再展开“用 SSM 分层是为了让业务逻辑和 SQL 解耦方便后续维护”。这样导师会觉得你是在做项目不是在背框架。下面这张表是我常用的验收自查清单按模块逐项打勾避免遗漏。检查项通过标准常见问题登录鉴权未登录跳转登录后放行拦截器路径配置错误增删改查每张业务表都有对应页面删除未做逻辑删除分页页码切换正常总数正确startPage 位置错误文件上传大小限制生效路径可配上传目录在 webapp 下事务异常时数据回滚异常被吞掉日志关键操作有记录只打 System.out答辩前把这张表过一遍基本不会出大问题。还有一点代码里不要留TODO和注释掉的调试代码导师翻到会扣印象分。从那以后我每次带项目都强制在验收前三天做一次“陌生人测试”——找一个没参与项目的同学让他按文档从零部署一遍卡在哪就补哪。这个习惯帮我省掉了无数次现场翻车。希望这套结构能帮你把 SSM 项目从“能跑”推到“能交差”需要完整源码和文档的可以按项目名自取。本文还有配套的精品资源点击获取