
简介一套基于 Idea 2021 从零整合 Spring、SpringMVC 与 MyBatis 三大框架的源码工程配套 Tomcat 9.0 与 MySQL 8.0.25 环境面向初学 JavaWeb 的读者以及需要快速搭建项目骨架的开发者重点解决框架整合、依赖冲突和基础配置等常见问题。资源包共包含 105 个文件以 52 个 jar 依赖库、28 个 xml 配置、10 个编译后的 class 文件、5 个 Java 源码和 4 个 JSP 页面为主同时保留 properties、gitignore 等辅助文件整体大小约 22.34MB目录按控制层、业务层、持久层划分结构清晰便于逐模块阅读和复用。累计已有三千一百三十五人学习使用是较常见的 SSM 入门参考。整个示例以图书管理为业务场景实体类、数据访问接口、业务服务与控制器的职责划分清晰数据库访问映射、业务逻辑封装与页面交互均有体现关键配置文件也一并给出可帮助读者绕开版本兼容问题快速获得一套可运行、可扩展的 SSM 项目雏形。无论是课程设计、毕业设计还是日常练习都能从中获得可直接参考的项目结构。1. 从零搭SSM框架不是背配置是理解三个容器怎么协同很多人拿到一套SSM源码第一反应是打开百度搜“IDEA 2021 SSM搭建教程”然后跟着教程把applicationContext.xml、spring-mvc.xml、web.xml三个文件复制粘贴一遍项目跑起来就觉得自己会了。结果一旦遇到包名改动、Mapper找不到、Controller 404立刻傻眼因为不知道这三个配置文件之间到底是什么关系。这套源码拆解笔记就是围绕“根容器、子容器、Mapper代理”三条线来写的先搞清楚Spring和SpringMVC各自管哪些Bean再理解MyBatis是怎么被挂进Spring的最后用一份完整可跑通的工程源码作为对照。适合刚过JavaWeb、准备做毕设或第一次接触企业级项目结构的人也适合想快速搭一套可复用骨架的从业者。2. IDEA 2021里初始化工程版本选型与Maven Web骨架的落地细节2.1 JDK、Maven、Tomcat版本怎么选定SSM不是新技术但它对版本组合非常敏感。我见过有人用JDK 17去跑Spring 5的工程结果CGLIB代理直接报错也见过用Tomcat 10搭配javax.servlet写的代码启动就报ClassNotFound。搭建前先把版本边界划清楚能少踩一半坑。这套源码基于的是经典组合JDK 1.8、Maven 3.6.x、Tomcat 8.5或9.0、Spring 5.x、MyBatis 3.5.x。这个组合经过大量项目验证兼容性最稳。组件推荐版本关键说明JDK1.8SSM经典组合CGLIB代理和JSP编译都最稳Maven3.6.xIDEA 2021内置Maven 3建议用自带的减少配置干扰Tomcat8.5 / 9.08.5以下不支持Servlet 4.09.0和javax.servlet命名兼容Spring5.2.x / 5.3.x5.3以下更稳避免Lombok和JDK编译器的边际问题MyBatis3.5.x3.5以上才支持mapperLocations通配符的规范写法MySQL驱动8.0.xdriverClassName和URL都要换成cj前缀后面细说为什么不推荐JDK 11以上因为IDEA 2021里Tomcat 8.5默认的运行参数还是基于JDK 8的高版本JDK会有模块化限制比如java.xml.bind模块缺失这类问题对新手来说排查成本太高。JDK 1.8 Tomcat 8.5这套组合虽然“老”但企业里存量项目大量还在用学它不吃亏。2.2 新建Maven Web工程的两种姿势IDEA 2021里新建Maven Web工程有两条路。一条是用archetype-webapp骨架IDEA会自动生成一个标准的Web目录结构另一条是建立一个普通Maven工程然后手动在src/main下补出webapp目录和WEB-INF/web.xml。我一般推荐第二条因为archetype-webapp生成的web.xml是2.3版本的DOCTYPE还是Sun的旧写法要么手动替换要么得改servlet版本怎么看都绕路。直接建普通Maven工程再补目录反而每一步都看得见。手动补目录之后web.xml需要手动改成Servlet 4.0的规范写法?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-nameSSM Demo/display-name /web-app这里的重点是namespace从java.sun.com换成了xmlns.jcp.org这是Servlet 4.0之后的新规范。如果你用archetype-webapp骨架IDEA生成的是web-app_2_3.xsd的旧版头启动时Tomcat虽然能兼容但部分注解扫描行为会有差异。我一般会直接删掉旧文件重新写省得后面排查时怀疑人生。2.3 pom.xml依赖清单与scope边界依赖配置是整个工程的基石。SSM的依赖不算多但每个依赖的scope和传递关系都有讲究。下面这一组是能跑通的最小集合properties spring.version5.3.9/spring.version mybatis.version3.5.7/mybatis.version mysql.version8.0.26/mysql.version /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency !-- SpringMVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- Spring JDBC - 事务管理依赖 -- dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis核心 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency !-- MyBatis与Spring桥接 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency !-- MySQL 8驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.version}/version scoperuntime/scope /dependency !-- Servlet API编译期需要运行时容器提供 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSTLJSP页面里用c:forEach时依赖 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies这里有一个新手最容易犯的错误javax.servlet-api的scope必须写成provided意思是编译和测试时用最终打包不进去因为Tomcat自己带了Servlet容器实现。如果写成默认的compile打包出的war会把servlet-api也塞进去和Tomcat自带的冲突轻则报警告重则启动时方法签名对不上。mysql-connector-java用runtime就够因为代码里不直接引用驱动类只在运行期通过DriverManager加载。另外spring-jdbc这个依赖很多人会漏掉。搭建SSM时事务管理器用的是DataSourceTransactionManager这个类在spring-jdbc里只引spring-context和spring-webmvc是不够的。我第一次搭的时候就是漏了这个Spring容器启动报BeanDefinitionStoreException找了半天才想起来事务相关类不归spring-context管。3. Spring与SpringMVC容器分工请求是从哪一步真正进到Controller的3.1 web.xml里配置两个容器SSM体系里有两个Spring容器这是理解整套配置文件的关键。一个是ContextLoaderListener创建的根容器负责加载Service、Dao这些业务组件另一个是DispatcherServlet创建的SpringMVC子容器只负责Controller和Web层组件。两个容器是父子关系子容器能看见父容器里的Bean但父容器看不见子容器的Bean。!-- 根容器加载applicationContext.xml -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener !-- SpringMVC子容器 -- 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-mappingdispatcher的url-pattern这里是单个斜杠表示除了JSP之外的所有请求都交给SpringMVC处理。load-on-startup设为1意思是Tomcat启动时立即初始化这个Servlet而不是等第一个请求进来再初始化。如果不写这一行第一次访问页面会明显卡顿因为DispatcherServlet的初始化动作被推迟到了运行时。逻辑说明ApplicationContext.xml是根容器spring-mvc.xml是子容器。DispatcherServlet启动时先检查有没有已存在的根容器有就把它设为自己的父容器。这样Controller里注入Service时Spring会先在子容器找找不到再从父容器里取。3.2 根容器配置只扫Service和Dao?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:contexthttp://www.springframework.org/schema/context xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd !-- 根容器扫描Service和Dao排除Controller -- context:component-scan base-packagecom.demo.ssm context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan /beans这段配置的核心是exclude-filter。根容器把整个业务包都扫了但明确排除了标注Controller的类。为什么要排除如果根容器把Controller也注册了SpringMVC子容器再扫一遍就会出现同一类型两个Bean定义的情况某些AOP代理逻辑会变得非常诡异。另外如果根容器扫描了Controller而事务AOP切在Service上有时候会出现代理对象可见性异常。所以这个排除动作不是可有可无而是必须写。3.3 子容器配置注解驱动、Controller扫描与静态资源放行spring-mvc.xml是SpringMVC的核心也是这个框架里配置项最多的地方?xml version1.0 encodingUTF-8? beans xmlnshttp://www.springframework.org/schema/beans xmlns:contexthttp://www.springframework.org/schema/context xmlns:mvchttp://www.springframework.org/schema/mvc xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd !-- 子容器只扫描Controller -- context:component-scan base-packagecom.demo.ssm.controller/ !-- 开启注解驱动ResponseBody、RequestBody等 -- mvc:annotation-driven/ !-- 静态资源放行 -- mvc:default-servlet-handler/ !-- 视图解析器 -- bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean /beansmvc:annotation-driven/ 是这一节里最值钱的一行配置。它默认注册了RequestMappingHandlerAdapter和RequestMappingHandlerMapping没有它RequestMapping注解是不生效的。很多从旧教程抄代码的人会漏掉这一行然后Controller无论如何都进不去。mvc:default-servlet-handler/ 的作用是把那些没匹配到Controller的请求交给Tomcat默认的Servlet处理。因为DispatcherServlet拦截了/如果请求的是一个图片或者静态CSS文件而你没有这个配置就会404。加了它SpringMVC在自身Handler映射找不到时自动放行给容器默认Servlet。视图解析器里的prefix指向/WEB-INF/views/把JSP页面放在WEB-INF下这是安全习惯。WEB-INF下的文件不能通过浏览器直接URL访问必须由Controller转发打开这样可以防止有人绕过权限直接看JSP源码。这里有一个经典坑很多人把视图解析器配成/和.jsp但如果请求路径里有目录层级比如/user/list视图解析器会拼出/WEB-INF/views//user/list.jsp。路径双斜杠在Windows上没问题Linux上Tomcat的某些版本会报404。我习惯在prefix末尾加/同时保证跳转代码里不要手写前导斜杠的视图名。4. 把MyBatis挂进Spring数据源、Mapper代理与事务三条线的拼装顺序4.1 数据源与连接池参数MyBatis本身不管数据库连接它需要一个DataSource提供物理连接。SSM里最常见的搭配是Alibaba Druid连接池因为它带监控页面参数语义也清楚。先把数据库连接信息抽到外部properties文件里# jdbc.properties jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/ssm_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456jdbc.url这段有两个参数值得展开说。serverTimezoneAsia/Shanghai是因为MySQL 8的时区默认是UTC不加这个Java里的日期和数据库里的日期对不上差8个小时。allowPublicKeyRetrievaltrue是MySQL 8.0.4之后的驱动行为变化不使用SSL连接时如果没有这个参数首次连接会报Public Key Retrieval is not allowed。这两个参数是MySQL 8连接特有的坑用MySQL 5.x驱动根本不会遇到所以网上旧教程里没有。在applicationContext.xml里加载这个properties并配置Druid数据源context:property-placeholder locationclasspath:jdbc.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}/ /beancontext:property-placeholder负责把jdbc.properties里的键值对加载进Spring上下文之后用${}占位符引用。init-method和destroy-method分别指定连接池初始化和关闭时调用的方法。Druid自己维护了一组连接在Spring容器启动时预创建部分连接销毁时主动关闭避免连接泄漏。4.2 SqlSessionFactory与Mapper代理扫描MyBatis要接入Spring关键人物是SqlSessionFactoryBean。它替代了原生MyBatis里SqlSessionFactoryBuilder的工作把数据源和Mapper映射文件揉进同一个工厂里bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.demo.ssm.pojo/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.demo.ssm.dao/ property namesqlSessionFactoryBeanName valuesqlSessionFactory/ /beantypeAliasesPackage配置了一个包路径MyBatis会自动把这个包下的POJO类注册为别名默认别名是类名的首字母小写形式。这样在Mapper XML文件里写resultTypeUser就够了不用写全限定名。mapperLocations指向了classpath:mapper/目录所有Mapper XML文件放这个目录下。MapperScannerConfigurer是MyBatis-Spring桥接包里最精巧的设计。它扫描basePackage包下所有接口然后为每个接口生成一个动态代理对象注册进Spring容器。这意味着你不需要自己写Dao实现类。它还有一个细节就是用sqlSessionFactoryBeanName属性而不是sqlSessionFactory的ref引用。如果用refSpring会因为Bean创建顺序问题在某些场景下提前初始化sqlSessionFactory导致property-placeholder引用失效。用BeanName字符串延迟查找是官方推荐的做法也能避开启动时序的玄学问题。4.3 事务管理的两种落地方式SSM里事务管理的标准做法是声明式事务把事务边界和业务代码解耦。有两种配置路径一种是tx:advice aop:config切面织入一种是直接在Service类或方法上打Transactional注解。我倾向于用aop:config切面方式因为事务规则集中在一处业务代码里不需要满屏注解bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /bean tx:advice idtxAdvice transaction-managertransactionManager tx:attributes tx:method nameget* read-onlytrue propagationSUPPORTS/ tx:method nameselect* read-onlytrue propagationSUPPORTS/ tx:method nameinsert* propagationREQUIRED/ tx:method nameupdate* propagationREQUIRED/ tx:method namedelete* propagationREQUIRED/ tx:method name* propagationREQUIRED/ /tx:attributes /tx:advice aop:config aop:pointcut idservicePointcut expressionexecution(* com.demo.ssm.service..*.*(..))/ aop:advisor advice-reftxAdvice pointcut-refservicePointcut/ /aop:configDataSourceTransactionManager需要注入同一个dataSource对象因为事务的核心是Connection绑定如果事务管理器和SessionFactory用的数据源不是同一个事务会静默失效数据照常写入但回滚不生效。这也是SSM里一个很隐蔽的坑。tx:method的配置规则是按方法名匹配。get和select开头的方法设为read-onlytruepropagationSUPPORTS意思是已有事务就加入没有就不开新事务。insert、update、delete用REQUIRED当前没有事务就新建一个。aop:pointcut的expression表达式里com.demo.ssm.service...前两个点代表service包下所有子包后一个*是方法名通配括号里的两个点代表任意参数。这套表达式是AspectJ切点语法的基础换包名时必须同步修改这里。5. 踩坑排查搭建SSM时最容易翻车的五个典型故障5.1 Controller配了注解还是404DispatcherServlet拦截路径写错了现象Controller类加了Controller和RequestMapping启动日志里能看到SpringMVC的RequestMappingHandlerMapping注册了路径但浏览器访问时Tomcat返回404而且控制台没有任何报错。原因看web.xml里的servlet-mapping十有八九写成了/*。/*和/的区别在于/会交给SpringMVC处理但不会拦截JSP/*会拦截所有请求包括JSP的请求。当视图解析器返回一个JSP路径时请求会被DispatcherServlet再次拦截它找不到这个JSP对应的Handler直接404。解决把servlet-mapping的url-pattern改回/。改完后请求流程变成Controller返回逻辑视图名视图解析器拼出物理路径Tomcat直接处理JSP资源不再经过DispatcherServlet。5.2 注解扫描范围重叠导致MVC失效现象Controller里的ResponseBody返回JSON数据浏览器拿到的是中文乱码或者干脆报406错误。还有一种表现是Controller方法正常执行但所有RequestMapping都失效。原因有的教程会在applicationContext.xml里不加排除两个容器都扫了Controller。根容器注册的Controller使用的是根容器里的消息转换器它没有加载SpringMVC的注解驱动配置所以ResponseBody的工作不正常。这种“双重注册”问题特别难查因为系统不报错只是行为不对。解决严格遵循第3章的配置边界。根容器排除Controller子容器只扫Controller包。如果你发现项目里已经有重叠扫描把applicationContext.xml里的component-scan补上exclude-filter重启后重新测试。从那以后我每次新起工程都会检查一遍两个文件的扫描路径这个习惯救过我好几次。5.3 Mapper接口报绑定异常类型不匹配的经典报错现象项目启动正常但第一次调用某个Mapper方法时抛出org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)提示某个接口找不到对应的SQL语句。原因三层原因按频率排序。第一Mapper接口和Mapper XML文件的namespace不匹配接口的全限定名和namespace必须完全一致少一个点都不行。第二Mapper XML文件没有放在mapperLocations指定目录下很多新手把UserMapper.xml放在java源码目录里打包后目标目录下根本没有这个文件。第三Mapper接口里的方法名和XML里statement的id对不上。解决先确认applicationContext.xml里mapperLocations的路径是classpath:mapper/*.xml然后检查target/classes/mapper目录下有没有对应的xml文件。如果文件不在去pom.xml里的build配置里看是否排除了resources目录下的xml。IDEA里Maven默认不打包src/main/java下的xml所以一个稳妥做法是把Mapper XML放在src/main/resources/mapper目录下而不是和接口放一起。namespace、id、接口名三项逐一比对这个检查习惯我从那以后一直保留着。5.4 本地启动正常部署后报ClassNotFound现象IDEA里跑Tomcat一切正常但把war包丢到独立部署目录里启动报NoClassDefFoundError或者某个类找不到。原因IDEA里运行项目时它用的是自己合成Artifacts会把编译输出目录和依赖jar包都加入classpath。但如果你打包war时IDEA的Artifacts输出布局里没有正确包含所有lib那么部署后就会缺依赖。我自己遇到过Druid和mybatis-spring两个jar包莫名缺失的情况因为Artifacts的Output Layout里lib目录只手动加了一部分依赖。解决在Project Structure里找到Artifacts选中war exploded打开Output Layout查看WEB-INF/lib下是否所有依赖都在。通常做法是选中Available Elements里的所有jar包右键Put into /WEB-INF/lib。避免这个问题的方法是不要手动管理lib而是让IDEA自动把所有依赖都打包进去尽量别在pom里使用system scope的本地jar这类依赖IDEA有时不会自动加入war。5.5 MySQL 8.0连接失败驱动类名与URL参数换了现象启动时Druid连接池报错Caused by: java.sql.SQLException: Unable to load authentication plugin caching_sha2_password或者Access denied for user。原因MySQL 8.0的默认认证插件是caching_sha2_password而MySQL 5.x时代的驱动和连接方式都不认。很多人还在用5.1.46版本的驱动类com.mysql.jdbc.Driver这款驱动和8.0服务端握手时会崩。另外就是之前提到的serverTimezone参数缺失会报时区异常。解决把驱动类换成com.mysql.cj.jdbc.DriverMySQL Connector版本升到8.0.x。URL里加上serverTimezoneAsia/Shanghai和useSSLfalse。如果认证还报错检查allowPublicKeyRetrievaltrue是否加上这是客户端首次连接时获取RSA公钥的开关。这五个参数是MySQL 8连接的标准配置组合少了任何一个都会在特定环境下翻车。6. 把SSM骨架沉淀成脚手架改包名、抽参数、留文档三件套搭建完一次SSM工程之后最有价值的产出不是跑通的Demo而是一套能反复使用的骨架。我一般的做法是把这套源码整理成脚手架目录下次开新项目时直接复制过去然后做三件事改包名、抽参数、补充项目说明。改包名这一步很多人用IDE的重构功能但IDEA对全包名替换有时会漏resources目录下的配置。我习惯写一个简单的bash脚本把java目录、resources目录里所有出现旧包名的地方一次性替换#!/bin/bash # 用法: ./replace-package.sh 旧包名.路径 新包名.路径 项目根目录 OLD_PKG$1 NEW_PKG$2 ROOT$3 # 把包名中的点转换为路径斜杠 OLD_PATH$(echo $OLD_PKG | tr . /) NEW_PATH$(echo $NEW_PKG | tr . /) # 遍历java与resources目录下所有文件 find $ROOT/src -type f \( -name *.java -o -name *.xml -o -name *.properties \) | while read FILE; do # 先替换文本内容 sed -i s/$OLD_PKG/$NEW_PKG/g $FILE # 再处理目录结构 DIR$(dirname $FILE) if echo $DIR | grep -q $OLD_PATH; then NEW_DIR$(echo $DIR | sed s|$OLD_PATH|$NEW_PATH|g) mkdir -p $NEW_DIR mv $FILE $NEW_DIR/ fi done echo 包名替换完成请检查git diff确认改动范围逻辑说明这个脚本先处理文件内容里的全限定名再把文件移动到新路径对应的目录里。关键点是内容替换和目录移动必须顺序执行如果先移动目录find的路径会失效。参数说明第一个参数是旧包名第二个是新包名第三个是项目根目录。如果包名出现在JSP页面的page import标签里这个脚本覆盖不到需要手动补一遍。抽参数这件事更有价值。把多个配置文件里反复出现的值统一抽到properties里比如数据库地址、上传路径、日志级别。下次切换环境时只需要改一个文件。预留一份参数对照表在项目根目录写清楚每个配置项在哪个文件里生效这是我被环境切换折磨过几次后形成的习惯。比如Druid的initialSize、maxActive这些参数不同环境有不同的最优值没有对照表就等于黑匣子。从那以后我每次开新工程都强制走一遍“复制骨架、替换包名、跑通冒烟接口”这三步不跑通不许往下写业务。某个同事曾经跳过第一步直接改包名结果配置文件里的Mapper扫描路径全部失效花了整整一个下午排查。这个过程让我养成了凡事先把地基打牢的习惯希望帮到你。本文还有配套的精品资源点击获取