简介:这是一份面向Java开发学习者的SSM框架入门课件,聚焦Spring注解开发与Spring整合MyBatis两大主题,适合正在学习Spring核心容器、依赖注入及持久层框架整合的读者。讲解脉络清晰,从IoC/DI基本思想、BeanFactory与ApplicationContext容器层次,到注解定义bean、纯注解开发、第三方bean管理和依赖注入,再延伸到Spring整合MyBatis与JUnit,层层递进,便于系统建立SSM整合知识框架。压缩包内共1个pptx文件,约2.99MB,彩色排版,通过结构图、示例代码和XML配置对照展示知识点,既可用作课堂同步讲义,也可作为考前复习提纲。目前已有181人学习使用,内容覆盖Spring概述、核心容器总结、注解开发、整合MyBatis与JUnit等章节,适合希望快速掌握Spring注解风格开发和SSM整合配置的初学者进阶参考。 看到这个标题我就知道,这应该是不少Java开发入行时都会遇到的一份PPT——讲注解开发,也讲SSM里最核心的Spring和Mybatis整合。老实说,这类材料网上多得是,但绝大多数要么只贴代码不讲为什么,要么上来就甩配置把人看懵。这篇我就以做过的项目经验为底,把注解开发和两者整合这条线从头捋一遍,不光是能用,还得让你知道它是怎么跑起来的。
1. Spring注解开发的核心思路:从XML到注解,到底省了什么
1.1 为什么注解能取代一堆XML配置
早些年做Spring项目,光一个applicationContext.xml就能写上好几百行,里面全是<bean>标签,定义一个对象就得写一段,对象多了以后维护成本直接爆炸。后来Spring从2.5开始引入注解,到了Spring Boot时代基本全面转向注解和自动配置,这背后其实是三层逻辑。
第一,注解把“定义”和“使用”放在了一起。你在类上写一个@Service,这个类就是业务层组件,不用再跑到配置文件里找对应的bean定义。类本身即文档,别人看代码就知道这个类在容器里扮演什么角色,而不是把代码和配置来回切换。第二,注解开发能大幅减少样板代码。比如依赖注入,你只要用@Autowired声明一下,Spring容器启动时就会把对应的bean塞进来,不需要写setter注入或构造器注入的XML。第三,注解让项目的启动和构建流程更轻,尤其在微服务拆分的场景下,一个服务一个模块,各自用注解完成装配,互不干扰。
但有一点我得提醒你:注解不是银弹。它把配置从XML搬到了代码里,虽然直观,但如果你想全局查一下某个bean被哪些地方引用,还是会比搜XML麻烦一点。这也是为什么Spring Boot后来引入了@ConfigurationProperties这类配置绑定方式,把外部配置和Java对象做映射,尽量把容易变的东西留在配置文件里,把稳定的装配逻辑留在代码里。我的理解是,注解解决的是“怎么装配的问题”,配置文件解决的是“参数是什么的问题”,两者要搭配用,别走极端。
1.2 实操里最高频的注解组合,逐个说透
@Component系列最基础,@Service、@Repository、@Controller其实都是它的衍生,本质都是把类注册成Spring容器中的bean,只是语义层面区分了层次。@Autowired是按类型注入,@Resource是按名称注入,这俩面试经常会问,实际开发里如果你的容器里只有一个实现类,两者用起来没区别,一旦有多个实现,@Autowired需要配合@Qualifier指定名字,@Resource自己就能指定name属性。我个人的习惯是,能用一个实现类就直接@Autowired,有多实现时优先@Resource(name = "xxx"),写起来短,也不容易出错。
@Configuration和@Bean是用来定义配置类的,简单说就是让你用Java代码代替XML里的<bean>定义。比如你要注入一个第三方库的对象,没法在类上直接加@Component,那就在配置类里写一个方法,方法上标注@Bean,方法的返回值就是容器里注册的对象。这个方法名字默认就是bean名称,也可以显式指定@Bean("name")。
@ComponentScan负责扫描包路径,Spring Boot里默认扫描启动类所在包及其子包。这里有个坑,有时候你把新写的@Service放在启动类包外面的目录里,结果启动报了NoSuchBeanDefinitionException,多半就是扫描路径没覆盖到。处理方式要么调整包结构,要么在启动类上显式加上@ComponentScan(basePackages = "com.xxx")。
还有一个容易被忽略的是@Scope,默认单例,这个对于绝大多数场景没问题,但你在多线程环境下往类成员变量里塞状态,就要小心了。单例bean在多线程下是共享的,成员变量容易出并发问题。需要原型作用域时,在@Bean或@Component上标注@Scope("prototype")即可。说实话,我工作这些年用到prototype的次数很少,大多数情况都是把无状态的服务类设计成单例,有状态的数据往方法参数和数据库里放。
@Value用来注入配置文件里的值,@PropertySource指定外部配置文件位置,比如@PropertySource("classpath:jdbc.properties")。@Value("${jdbc.url}")这样就能读出来,但这个方案在字段多的时候很啰嗦,Spring Boot时代基本被@ConfigurationProperties取代了。
1.3 注解开发的底层逻辑:Bean生命周期是怎么回事
很多人面试被问到Spring三级缓存,其实这玩意儿跟注解开发也有关系。Spring IoC容器启动的时候,会先扫描所有加了注解的类,生成BeanDefinition,然后按照依赖关系实例化bean。实例化并不等于初始化完成,Spring把对象创建分成了好几步。三级缓存解决的是循环依赖问题,比如A依赖B、B依赖A,没有三级缓存的话,两个对象互相引用就会死锁。一级缓存是成品对象,二级缓存是早期暴露的半成品,三级缓存放的是对象工厂。注解注入基本都是在对象创建完成后通过后置处理器来完成的。
我在实际工作中直接遇到循环依赖的次数不多,但确实有一次加了@Transactional后发现启动报错,定位下来是A依赖B、B又依赖A,当时就把其中一个依赖改成@Lazy延迟加载解决了。后来我换了个思路,直接从设计上把循环依赖干掉,该拆的方法拆出去,该抽的类抽出来,反而代码结构更清爽。所以我的建议是,三级缓存这东西面试要知道,但工程项目里碰到循环依赖,优先想怎么重构,而不是依赖缓存机制硬扛。
2. Spring与Mybatis整合:为什么要整合,以及整合时发生了什么
2.1 原生Mybatis的痛点与整合思路
Mybatis本身是个半自动ORM框架,SQL由你控制,执行结果自动映射成Java对象,灵活性很高。但如果你不用Spring,就得自己手动创建SqlSessionFactory,自己管理SqlSession,每个Mapper接口还得手动写实现类去调用SqlSession的方法,或者用Mybatis官方提供的Mapper动态代理。这在简单项目里还能接受,项目一复杂就是灾难。
真正接手一个项目后你会发现,纯Mybatis的使用方式有四个致命伤:第一,SqlSession线程不安全,你不能把它定义为单例,每个线程都要独立的SqlSession;第二,事务控制全靠自己写commit/rollback,稍不留神就出现数据不一致;第三,Mapper的获取和生命周期管理非常繁琐,每个业务代码里都得写“获取session、调用mapper、关闭session”的样板代码;第四,跟Spring的声明式事务@Transactional没法直接集成,事务边界只能靠人肉维护。
所以Spring与Mybatis整合,本质就是让Spring容器接管Mybatis的核心对象。SqlSessionFactory被定义成单例,SqlSession的生命周期交给Spring的SqlSessionTemplate统一管理,事务交给Spring的TransactionManager统一处理,Mapper接口的代理生成也交给Spring扫描注册。你这边的开发,就只需要定义一个接口,加几个注解或者写个XML映射文件,方法调用时自动拿到代理对象去执行SQL。
2.2 SqlSessionFactoryBean和MapperScan是怎么回事
你用Spring Boot的时候,一般只需要在启动类上加@MapperScan("com.xxx.mapper"),然后Mybatis的配置类就自动干了一堆活。但如果你理解了底层,无非就是几件事。
首先,SqlSessionFactoryBean实现了Spring的FactoryBean接口,它的作用是把Mybatis的SqlSessionFactory创建过程封装起来,交给Spring容器管理。这里面会读取Mybatis全局配置,注册typeAliases、typeHandlers、Mapper XML映射文件的位置等。只有这个factory创建好了,后续的Mapper代理才有基础。
其次,@MapperScan这个注解的作用是扫描指定包下的所有接口,把它们注册成MapperFactoryBean。注意,这里不是直接注册接口本身,而是注册了一个工厂bean,每次获取的时候,工厂bean会调用SqlSession.getMapper(Class)来创建接口的动态代理。所以你在业务代码里能直接@Autowired注入Mapper接口,注入的实际对象就是代理对象。
还有一点,整合后的事务。Mybatis本身只提供对数据库的操作能力,但它没有事务管理器。Spring通过DataSourceTransactionManager管理事务,整合的时候SqlSessionFactoryBean用的是Spring事务管理下的数据源,这样@Transactional才能生效。当方法调用MybatisTemplate执行SQL时,Spring的事务管理器会保证同一个方法里的SQL都在同一个事务里。这里有个常见问题,如果你在Spring配置里自己new了一个DataSource,没有交给Spring管理,那事务注解就会失效,操作一条数据提交一条,根本不走事务。排查的时候先看DataSource是不是容器里的单例bean。
3. 手把手搭建一个注解开发+Spring与Mybatis整合的项目
3.1 基础依赖和项目结构
理解原理之外还得能跑起来。这里我给一个完整的精简版项目示例,不依赖Spring Boot,用传统的Spring + Mybatis整合方式,把底层链路看得明明白白。项目用Maven管理,先看pom.xml里最核心的依赖:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.23</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.23</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>这里一定要加上spring-jdbc,因为事务管理器要用它。mybatis-spring是两者的桥接包,专门负责把Mybatis的对象接入Spring容器。
项目结构就按标准的Java工程来:
src/main/java ├── com/demo/config │ └── SpringConfig.java ├── com/demo/entity │ └── User.java ├── com/demo/mapper │ └── UserMapper.java ├── com/demo/service │ ├── UserService.java │ └── UserServiceImpl.java ├── com/demo/App.java src/main/resources └── mapper └── UserMapper.xml3.2 核心配置类和数据源
既然用的是注解开发,整个Spring配置就交给Java配置类。先看数据源这块,我们这里手动配置一个简单的HikariCP或者直接用DriverManagerDataSource都行,生产环境建议用HikariCP,测试就怎么简单怎么来:
@Configuration @ComponentScan("com.demo") @EnableTransactionManagement @MapperScan("com.demo.mapper") @PropertySource("classpath:db.properties") public class SpringConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource() { HikariConfig config = new HikariConfig(); config.setDriverClassName(driver); config.setJdbcUrl(url); config.setUsername(username); config.setPassword(password); config.setMaximumPoolSize(10); return new HikariDataSource(config); } @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class); factoryBean.setConfiguration(configuration); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); return factoryBean.getObject(); } @Bean public DataSourceTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }几个细节说一下。setMapUnderscoreToCamelCase(true)这个非常关键,数据库字段是create_time,Java属性是createTime,不开启驼峰映射的话,这个字段就注入不进去,查出来全是null。setMapperLocations是告诉Spring去哪里加载Mapper的XML文件,如果你用的是纯注解SQL,也就是@Select、@Insert这些,那这行可以不写,但XML和注解混合的项目必须配置。@EnableTransactionManagement开启注解事务,没有这行,@Transactional不生效。
3.3 Mapper接口与XML映射的写法
Mapper接口本身很简单:
public interface UserMapper { User selectById(@Param("id") Long id); List<User> selectList(); int insert(User user); int update(User user); int deleteById(@Param("id") Long id); }对应的XML文件放在src/main/resources/mapper/UserMapper.xml里:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.demo.mapper.UserMapper"> <select id="selectById" resultType="com.demo.entity.User"> SELECT id, name, age, create_time FROM t_user WHERE id = #{id} </select> <select id="selectList" resultType="com.demo.entity.User"> SELECT id, name, age, create_time FROM t_user </select> <insert id="insert" parameterType="com.demo.entity.User" useGeneratedKeys="true" keyProperty="id"> INSERT INTO t_user(name, age) VALUES (#{name}, #{age}) </insert> <update id="update" parameterType="com.demo.entity.User"> UPDATE t_user SET name = #{name}, age = #{age} WHERE id = #{id} </update> <delete id="deleteById"> DELETE FROM t_user WHERE id = #{id} </delete> </mapper>有两点值得注意。namespace必须写接口的全限定名,不能写错,写错的话运行时报错还不好查。resultType如果你开了typeAliases可以写别名,但我建议起手阶段就写全限定名,清晰不背锅。useGeneratedKeys="true" keyProperty="id"是插入后回填自增主键到实体对象,这个在新增后马上要用到主键的场景(比如插入订单后要关联订单明细)特别方便,省得再查一次数据库。
3.4 Service层的使用和事务
Service层直接注入Mapper接口,加@Transactional来控制事务:
@Service public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; @Override @Transactional(rollbackFor = Exception.class) public int createUser(User user) { userMapper.insert(user); if (user.getAge() != null && user.getAge() < 0) { throw new RuntimeException("年龄不能为负数"); } return user.getId(); } }@Transactional的rollbackFor = Exception.class这个一定要写,因为Spring默认只在遇到RuntimeException时才回滚,如果你代码里抛的是一个自定义CheckedException,不加这个属性的话事务照样提交,数据就脏了。这个坑我印象很深,有一次导入接口处理Excel解析,里面抛了业务异常但事务没回滚,排查了半天发现就是没加rollbackFor。
启动类就一行:
public class App { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); UserService userService = context.getBean(UserService.class); // 业务调用... context.close(); } }用AnnotationConfigApplicationContext加载配置类,容器启动时会自动扫描com.demo包,创建所有bean,生成Mapper代理,注册事务管理器。跑起来你会在控制台看到Mybatis打印的SQL日志,==> Preparing: ...就是真正执行的语句,排查SQL问题全靠它。
4. 整合过程中的常见问题与排查技巧
4.1 启动报错NoSuchBeanDefinitionException、Invalid bound statement
这是两个出现频率最高的错误。NoSuchBeanDefinitionException一般是扫描路径没覆盖到接口,或者@MapperScan没生效,检查启动类和配置类上的@ComponentScan路径。Invalid bound statement (not found)说明Mapper接口找到了,但接口方法和XML里的id对不上,要么XML文件没被加载,要么namespace写错,要么方法名和XML id不一致。
排查这两类问题,我建议先看启动日志。Spring启动时会打印bean初始化情况,你看有没有类似userMapper这样的bean名称,如果没有,说明根本没扫描到。有的话再看Mybatis加载了多少个Mapper XML,这些日志都会明确打印。把问题定位到具体层级,比瞎猜强太多。
4.2 事务不生效的排查思路
事务不生效的常见原因,除了前面说的DataSource不是同一个、没写rollbackFor,还有一个容易被忽略的点:同一个类的内部调用。@Transactional是基于Spring AOP动态代理实现的,如果你在UserServiceImpl里直接写一个方法内部调用另一个带有@Transactional的方法,事务是不生效的,因为内部调用没有走代理对象。这个冷知识导致的现象就是,外部调用方法A,A内部调用B(B有事务),B抛异常了,但A的事务没回滚。解决办法一是把B挪到另一个Service里,二是自己注入自身代理,三是用AopContext.currentProxy()获取当前代理再调用。
我遇到过一个奇葩case,一开始以为是事务没生效,后来发现是DataSource被C3P0和Druid两个连接池配置了双份,Spring容器里有两个DataSource,事务管理器管的那个和Mybatis用的那个根本不是同一个。你说这种问题怎么排查?看日志基本没用,得把所有关于数据源的配置全部搜一遍,把多余的那份注释掉,世界就清净了。
4.3 Mybatis属性映射和SQL日志打印的实用技巧
数据库字段是下划线风格、Java属性是驼峰风格,这个前面说了,开启mapUnderscoreToCamelCase就行。但如果你用的是@Select注解直接在接口里写SQL,不想建XML文件,那么SQL可以这样写:
@Select("SELECT * FROM t_user WHERE id = #{id}") @Results(id = "userMap", value = { @Result(column = "create_time", property = "createTime") }) User selectById(Long id);SQL日志这块,开发阶段我喜欢在Mybatis配置里加上StdOutImpl,就是上面配置类里那行configuration.setLogImpl(...),SQL会自动打印到控制台。但生产环境别这么干,日志量太大,而且会打印完整参数,有数据泄露风险。线上的做法是用p6spy这类监控组件,或者把Mybatis日志级别调到DEBUG,交给日志框架统一管理。用idea mybatis log free插件也可以把参数替换进SQL里,直接显示可执行的完整语句,排错更快。
4.4 关于逻辑删除和Mybatis Plus的一些补充
提到Mybatis,绕不开Mybatis Plus,现在新项目用Mybatis Plus的很多。它有一个内置的逻辑删除功能,用起来很爽,但有个坑:你写了自定义SQL时,如果没注意它自动拼接的deleted=0条件,查出来的数据可能不符合预期。比如你写一个自定义JOIN查询,没有显式加逻辑删除过滤条件,Mybatis Plus的插件可能在某些场景下不生效。我的经验是,不管是Mybatis还是Mybatis Plus,自定义SQL务必自己把控过滤条件,别把逻辑依赖在框架的自动行为上。另外Mybatis Plus的saveOrUpdateBatch在多数据源环境下有兼容问题,如果项目里配了多数据源、分库分表组件,使用这类批量方法之前一定提前测试,特别是主键策略和分布式ID生成器冲突的场景。
5. 从学习角度给读者的进阶建议
5.1 面试常考的点要连成知识网
结合这份PPT的主题,Spring注解开发和Spring与Mybatis整合,面试官大概率会这么问:@Autowired和@Resource的区别是什么?@Component和@Bean有什么区别?Spring容器启动流程是怎样的?Mybatis的#{}和${}有什么区别?Mapper接口没有实现类,Spring怎么代理的?
这些问题看似零散,但核心都指向一个东西:你有没有真正理解IoC和DI。我建议学习的时候把知识串起来:先理解容器做了什么,再理解注解如何驱动容器干活,最后理解Mybatis如何把自己扮演的角色塞进容器里。这样面试答起来才不是背答案,而是层层递进地讲设计思路。
5.2 读一点点源码,胜过看十篇博客
说真的,做Java开发绕不开Spring系源码。你不用全读,抓住一条主线就行:Spring容器的刷新过程(refresh方法)、Bean的实例化和初始化流程、BeanPostProcessor的扩展点、Mybatis的MapperScan如何通过ImportBeanDefinitionRegistrar把Mapper注入容器的。这几个点走一遍,你立刻能解释很多实际开发中的灵异现象。
我当年就是被一次Invalid bound statement折磨了两天,才开始硬着头皮去看MapperRegistry和MapperAnnotationBuilder的源码,看完才发现Mybatis的XML和注解最终都会被解析成MapperAnnotationBuilder里的方法定义,怪不得id对不上就找不到。从那以后,遇到问题我第一反应不是搜报错信息,而是想这个报错是在哪个环节抛出来的,链路走一遍基本能定位个八九不离十。
5.3 建议动手把项目从零写一遍
看再多文档,不如自己敲一遍。建议你用Maven从零搭一个这种传统Spring + Mybatis的项目,把下面这些事一样一样做完,做完基本就通透了:
- 用
@Configuration定义配置类,替代原来的XML。 - 手动配
DataSource和SqlSessionFactoryBean,不依赖Spring Boot自动配置。 - 写一个带
@Transactional的Service,验证事务是否真的回滚。 - 分别用XML和注解两种方式写Mapper查询,体会两者的风格差异。
- 故意把XML里的id写错,观察报错信息,提高定位问题的速度。
- 尝试开启驼峰映射和不开启,对比实体类字段为null的原因。
尤其是第一项,很多人在Spring Boot里用得顺手,但根本不知道Spring容器是怎么初始化Bean的。回到原生环境写一遍,boot里那层自动配置的神秘面纱就彻底没了。这一步走了,你对Spring的理解会比绝大部分同龄人深一个档次。
最后说个自己的体会吧。从最开始照着文档抄XML配置,到后来能用注解一行搞定,再到后面读懂容器源码,这个过程中最爽的不是学会某个API,而是踩坑后明白每个设计背后的约束条件。Spring和Mybatis能统治Java后端这么多年,靠的就是它们把复杂留给自己,把简单留给开发者的妥帖设计。希望这篇整理能帮你少走点弯路,以后写代码时遇到奇怪问题,能想起链路背后的那一步。
本文还有配套的精品资源,点击获取