上一节课的内容:Spring 核心基础总结(容器 & 注解开发)

📚 目录(点击跳转对应章节)

1. Spring 整合 MyBatis —— 为什么要这样设计?
2. Spring 整合 MyBatis 的核心对象深度解析
3. XML 中整合 MySQL 的配置分析
4. Spring 方式配置 MyBatis
5. MapperScannerConfigurer —— Mapper 为什么不用写实现类?
6. Spring 整合 JUnit —— 环境一致性设计
7. AOP —— Spring 最“灵魂”的部分
8. AOP 核心概念深挖
9. AOP 的完整执行流程
10. 代理机制深度解析
11. ProceedingJoinPoint —— AOP 的控制中枢
12. AOP 获取参数 & 返回值(实战)
13. Spring 事务管理 —— AOP 的终极应用
14. 事务传播行为
15. 事务失效的经典场景
16. AOP 在实际项目中的应用
17. 全文总结


一、Spring 整合 MyBatis —— 为什么要这样设计?

1. MyBatis 原生使用的痛点分析

不使用 Spring的情况下,MyBatis 的使用流程大致如下:

在这里插入图片描述

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

🚨 存在的问题:

  1. SqlSession 生命周期需要手动维护

    • 需要手动获取SqlSession
    • 需要手动管理SqlSession的关闭
    • 容易造成资源泄露
  2. 事务提交/回滚全靠程序员自觉

    • 每个操作都需要手动控制事务
    • 容易出现事务管理不一致的问题
  3. DAO 与 MyBatis 强耦合

    • DAO层直接依赖MyBatis API
    • 难以进行单元测试
  4. 无法和 Spring 的事务体系统一

    • 无法利用Spring强大的事务管理功能
    • 与Spring生态不兼容

👉 这和 Spring 的核心理念 “IOC + AOP” 完全冲突


2. Spring 整合 MyBatis 的总体目标

Spring 整合 MyBatis 并不是"替代 MyBatis",而是:

让 MyBatis 的核心对象完全受 Spring 容器控制

整合后你能得到什么?
  • SqlSessionFactory 由 Spring 创建
  • SqlSession 与事务自动绑定
  • Mapper 自动生成代理对象
  • 事务由 Spring AOP 统一管理
  • 统一的异常处理机制
  • 更好的测试支持

二、Spring 整合 MyBatis 的核心对象深度解析


1️⃣ SqlSessionFactory —— MyBatis 的发动机

它的职责包括:

  • 读取 MyBatis 配置
  • 创建 SqlSession
  • 管理 Mapper 映射关系
  • 与数据库交互
  • 管理 MyBatis 运行时环境

关键点:

Spring 整合 MyBatis,本质是"如何把 SqlSessionFactory 交给 Spring 管"


三、XML 中整合 MySQL 的配置分析

传统 mybatis-config.xml 中:

<environments>
  <environment>
    <transactionManager type="JDBC"/>
    <dataSource type="POOLED">
      <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
      <property name="url" value="jdbc:mysql://localhost:3306/test"/>
      <property name="username" value="root"/>
      <property name="password" value="password"/>
    </dataSource>
  </environment>
</environments>

❌ 问题:

  • 数据源由 MyBatis 管理
  • Spring 事务无法介入
  • 与 Spring IOC 体系割裂

四、Spring 方式配置 MyBatis


1️⃣ SqlSessionFactoryBean 的真实作用

你表面看到的是:

<bean class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>

或者使用Java配置:

@Configuration
public class MyBatisConfig {
    
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
            .getResources("classpath:mapper/*.xml"));
        return factoryBean.getObject();
    }
}

实际发生了什么?

Spring 容器启动
    ↓
创建 DataSource
    ↓
SqlSessionFactoryBean 被实例化
    ↓
内部调用 build() 创建 SqlSessionFactory
    ↓
SqlSessionFactory 注册到 Spring 容器

⚠️ SqlSessionFactoryBean 本身不是 SqlSessionFactory
✔ 它是一个 FactoryBean,用来"生产" SqlSessionFactory


2️⃣ DataSource 为什么必须由 Spring 注入?

因为:

  • Spring 事务管理器(DataSourceTransactionManager)
  • 依赖同一个 DataSource
  • 才能做到事务同步

📌 这是 Spring 能统一事务的根本原因


3️⃣ SqlSessionTemplate 与 SqlSessionDaoSupport

SqlSessionTemplate 的作用:

  • 线程安全的SqlSession
  • 自动完成SqlSession的关闭
  • 处理异常转换
@Component
public class UserDao {
    
    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;
    
    public User selectById(Long id) {
        return sqlSessionTemplate.selectOne("com.example.mapper.UserMapper.selectById", id);
    }
}

SqlSessionDaoSupport 的使用:

public class UserDao extends SqlSessionDaoSupport {
    
    public User selectById(Long id) {
        return getSqlSession().selectOne("com.example.mapper.UserMapper.selectById", id);
    }
}

五、MapperScannerConfigurer —— Mapper 为什么不用写实现类?


1️⃣ 它到底做了什么?

@Mapper
public interface UserMapper {
    User selectById(int id);
    
    @Select("SELECT * FROM users WHERE name = #{name}")
    List<User> selectByName(@Param("name") String name);
    
    int insert(User user);
    
    int update(User user);
    
    int delete(int id);
}

没有写实现类,却能直接注入使用:

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    public User getUserById(int id) {
        return userMapper.selectById(id);
    }
    
    @Transactional
    public void saveUser(User user) {
        userMapper.insert(user);
    }
}

原因只有一个:

Spring + MyBatis 在运行期为 Mapper 接口生成了代理对象


2️⃣ 代理对象的本质

UserMapper(接口)
    ↓
JDK 动态代理
    ↓
SqlSession.getMapper()
    ↓
最终执行 XML / 注解 SQL

配置Mapper扫描器:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.example.mapper"/>
</bean>

或者使用注解:

@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
    // 配置内容
}

效果如下:

在这里插入图片描述

在这里插入图片描述


六、Spring 整合 JUnit —— 环境一致性设计


1️⃣ 为什么普通 JUnit 不够?

普通 JUnit:

  • 没有 Spring 容器
  • Bean 无法注入
  • 事务不生效
  • 需要手动创建测试环境

2️⃣ SpringJUnit4ClassRunner 的本质

@RunWith(SpringJUnit4ClassRunner.class)

等价于:

JUnit 在启动前,先启动 Spring 容器

现代写法(Spring 4.3+):

@SpringBootTest
public class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @Test
    public void testGetUserById() {
        User user = userService.getUserById(1);
        assertNotNull(user);
    }
}

3️⃣ @ContextConfiguration 的真正含义

@ContextConfiguration(classes = SpringConfig.class)

等价于:

ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);

完整的测试配置示例:

@SpringBootTest
@ContextConfiguration(classes = {SpringConfig.class, MyBatisConfig.class})
@Transactional  // 测试后自动回滚
@Rollback        // 确保测试数据不影响数据库
public class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private UserMapper userMapper;
    
    @Test
    @DisplayName("测试根据ID查询用户")
    public void testGetUserById() {
        User user = userService.getUserById(1);
        assertThat(user).isNotNull();
        assertThat(user.getId()).isEqualTo(1);
    }
}

七、AOP —— Spring 最"灵魂"的部分


1️⃣ 为什么需要 AOP?

如果没有 AOP:

  • 日志要写在每个方法
  • 事务要写 try-catch-finally
  • 性能监控代码污染业务
  • 安全检查重复代码

AOP 的目标是:

横切关注点 → 统一处理 → 与业务解耦


八、AOP 核心概念深挖


JoinPoint(连接点)

程序执行过程中可被拦截的位置

在 Spring AOP 中:

  • 只能是 方法执行
  • 可以获取方法的参数、签名、目标对象等信息

Pointcut(切入点)

定义哪些 JoinPoint 要被拦截

本质是:一个方法匹配规则

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}

@Pointcut("execution(* *..*Service.*(..))")
public void serviceLayer2() {}

在这里插入图片描述


Advice(通知)

在拦截点上要执行的动作

1. @Before — 前置通知
  • 作用:在目标方法执行之前执行。
  • 特点
    • 无法阻止目标方法的执行(除非抛出异常)。
    • 可用于参数校验、日志记录等前置操作。
  • 示例
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
    System.out.println("方法执行前:" + joinPoint.getSignature().getName());
    System.out.println("参数:" + Arrays.toString(joinPoint.getArgs()));
}
2. @After — 后置通知(最终通知)
  • 作用:在目标方法执行之后执行,无论是否发生异常
  • 特点
    • 类似于 finally 块,总是会执行。
    • 不能访问方法返回值(因为可能因异常而无返回值)。
  • 示例
@After("execution(* com.example.service.*.*(..))")
public void cleanUp(JoinPoint joinPoint) {
    System.out.println("方法执行结束(无论成功或异常)");
}
3. @AfterReturning — 返回通知
  • 作用:在目标方法成功执行并返回结果后执行。
  • 特点
    • 仅在方法正常返回时触发(不包括抛出异常的情况)。
    • 可通过 returning 属性获取返回值。
  • 示例
@AfterReturning(
    pointcut = "execution(* com.example.service.*.*(..))",
    returning = "result"
)
public void logReturn(Object result) {
    System.out.println("方法返回值:" + result);
}
4. @AfterThrowing — 异常通知
  • 作用:在目标方法抛出异常后执行。
  • 特点
    • 仅在方法抛出异常时触发。
    • 可通过 throwing 属性捕获异常对象。
  • 示例
@AfterThrowing(
    pointcut = "execution(* com.example.service.*.*(..))",
    throwing = "ex"
)
public void logException(Exception ex) {
    System.out.println("方法抛出异常:" + ex.getMessage());
}
5. @Around — 环绕通知
  • 作用包围目标方法的执行,可控制是否执行、何时执行、如何执行。
  • 特点
    • 最强大、最灵活的通知类型。
    • 必须显式调用 ProceedingJoinPoint.proceed() 来执行目标方法。
    • 可修改入参、返回值,甚至完全跳过目标方法。
  • 示例
@Around("execution(* com.example.service.*.*(..))")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();
    try {
        Object result = pjp.proceed(); // 执行目标方法
        long end = System.currentTimeMillis();
        System.out.println("方法执行耗时:" + (end - start) + "ms");
        return result;
    } catch (Throwable t) {
        long end = System.currentTimeMillis();
        System.out.println("方法执行异常,耗时:" + (end - start) + "ms");
        throw t;
    }
}

Aspect(切面)

Aspect = Pointcut + Advice

完整的切面示例:

@Aspect
@Component
public class LoggingAspect {
    
    @Pointcut("@annotation(com.example.annotation.Loggable)")
    public void loggableMethod() {}
    
    @Around("loggableMethod()")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result;
        
        try {
            result = joinPoint.proceed();
            return result;
        } finally {
            long endTime = System.currentTimeMillis();
            String methodName = joinPoint.getSignature().getName();
            System.out.println("方法 " + methodName + " 执行时间: " + (endTime - startTime) + "ms");
        }
    }
}

九、AOP 的完整执行流程


1️⃣ Bean 创建阶段

  • Spring 创建普通 Bean
  • 扫描是否匹配切点
  • 满足条件 → 创建代理对象

2️⃣ 方法调用阶段

调用代理对象方法
    ↓
执行通知链
    ↓
执行目标方法
    ↓
返回结果

通知执行顺序:

  1. @Around(开始)
  2. @Before
  3. 目标方法执行
  4. @AfterReturning 或 @AfterThrowing
  5. @After
  6. @Around(结束)

十、代理机制深度解析


JDK 动态代理

  • 基于接口
  • 运行期生成实现类
  • 使用 InvocationHandler
  • 限制:只能代理实现了接口的类

CGLIB 代理

  • 基于继承
  • 不能代理 final 类
  • 字节码增强
  • 优势:可以代理没有实现接口的类

Spring 的选择策略

有接口 → JDK
无接口 → CGLIB

代理配置:

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)  // 强制使用CGLIB
public class AopConfig {
    // 配置内容
}

十一、ProceedingJoinPoint —— AOP 的控制中枢


1. 为什么只有 @Around 能用它?

因为:

  • @Before / @After 无法控制方法执行
  • @Around 可以决定是否执行

2. proceed() 的真实含义

pjp.proceed();

=

触发目标方法调用 + 执行后续通知

3. 极端但合法的用法

// pjp.proceed(); // 不调用 → 方法不执行
// pjp.proceed(); // 多次调用 → 方法多次执行

4. 通过 ProceedingJoinPoint(pjp) 获取方法信息:

@Around("execution(* com.example.service.*.*(..))")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
    Signature signature = pjp.getSignature();
    String className = signature.getDeclaringTypeName();
    String methodName = signature.getName();
    Object[] args = pjp.getArgs();
    
    System.out.println("类名:" + className);
    System.out.println("方法名:" + methodName);
    System.out.println("参数:" + Arrays.toString(args));
    
    // 执行目标方法
    Object result = pjp.proceed();
    
    System.out.println("返回值:" + result);
    return result;
}

十二、AOP 获取参数 & 返回值(实战)


参数获取

@Before("execution(* com.example.service.*.*(..))")
public void logArgs(JoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs();
    Signature signature = joinPoint.getSignature();
    String methodName = signature.getName();
    
    System.out.println("方法 " + methodName + " 的参数为:" + Arrays.toString(args));
}

获取特定参数

@Before("execution(* com.example.service.UserService.createUser(..)) && args(user,..)")
public void logUserCreation(User user) {
    System.out.println("创建用户:" + user.getName());
}

返回值获取

@AfterReturning(
    pointcut = "execution(* com.example.service.*.*(..))",
    returning = "result"
)
public void logReturn(Object result) {
    System.out.println("方法返回值:" + result);
}

十三、Spring 事务管理 —— AOP 的终极应用


1️⃣ Spring 事务的本质一句话版

事务 = AOP + DataSource + 规则定义


2️⃣ @Transactional 到底做了什么?

@Transactional
public void save() {}

背后发生的是:

创建代理对象
    ↓
方法前:开启事务
    ↓
执行目标方法
    ↓
异常?回滚 : 提交

事务配置详解:

@Transactional(
    propagation = Propagation.REQUIRED,      // 传播行为
    isolation = Isolation.DEFAULT,          // 隔离级别
    timeout = 30,                          // 超时时间
    readOnly = false,                      // 只读
    rollbackFor = {Exception.class},       // 回滚异常
    noRollbackFor = {RuntimeException.class} // 不回滚异常
)
public void saveUser(User user) {
    userMapper.insert(user);
}

3️⃣ 为什么事务一般写在 Service 层?

  • DAO 太细:DAO层方法过于基础,不适合复杂的事务逻辑
  • Controller 不稳定:Controller层主要负责请求处理,业务逻辑不应该放在这里
  • Service 是业务边界:Service层代表一个完整的业务操作,是事务的自然边界

十四、事务传播行为

在这里插入图片描述

REQUIRED(默认)

  • 有事务就加入
  • 没有就新建

REQUIRES_NEW

  • 强制新事务
  • 挂起外层事务

SUPPORTS

  • 有就用
  • 没有就不用

NOT_SUPPORTED

  • 不支持事务
  • 挂起当前事务

MANDATORY

  • 必须在事务内运行
  • 否则抛异常

NEVER

  • 不能在事务中运行
  • 否则抛异常

NESTED

  • 嵌套事务
  • 外层回滚,内层也回滚

十五、事务失效的经典场景


❌ 同类方法内部调用

@Service
public class UserService {
    
    @Transactional
    public void methodA() {
        // 事务有效
    }
    
    public void methodB() {
        // 事务失效!this.methodA() 不经过代理
        this.methodA();
    }
}

❌ 方法不是 public

@Transactional
private void privateMethod() {
    // 事务失效!私有方法无法被代理
}

❌ 异常被 catch 且未抛出

@Transactional
public void saveUser() {
    try {
        // 业务逻辑
    } catch (Exception e) {
        // 异常被捕获但未抛出,事务不会回滚
        e.printStackTrace();
    }
}

❌ 异常类型错误

@Transactional
public void saveUser() {
    // 运行时异常才会回滚,检查异常需要指定
    throw new IOException();  // 事务不会回滚
}

十六、AOP 在实际项目中的应用


1. 日志记录切面

@Aspect
@Component
public class LogAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
    
    @Around("@annotation(log)")
    public Object logExecution(ProceedingJoinPoint joinPoint, Loggable log) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        
        logger.info("方法 {} 执行时间: {}ms, 返回值: {}", 
            joinPoint.getSignature().getName(), 
            endTime - startTime, 
            result);
        
        return result;
    }
}

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String value() default "";
}

2. 缓存切面

@Aspect
@Component
public class CacheAspect {
    
    @Around("@annotation(cacheable)")
    public Object cacheAround(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
        String key = generateKey(pjp);
        // 尝试从缓存获取
        Object cached = getFromCache(key);
        if (cached != null) {
            return cached;
        }
        
        // 执行目标方法
        Object result = pjp.proceed();
        // 存入缓存
        putToCache(key, result);
        return result;
    }
}

十七、全文总结


Spring 的三大核心能力

能力 技术 作用
对象管理 IOC 统一管理对象生命周期
功能增强 AOP 横切关注点的统一处理
事务统一 AOP + DataSource 声明式事务管理,简化事务操作

Spring 整合 MyBatis 的核心优势

  1. 配置简化:通过Spring配置管理MyBatis组件
  2. 事务统一:与Spring事务体系完全整合
  3. 依赖注入:Mapper可以直接注入使用
  4. 测试友好:便于进行单元测试和集成测试
  5. 异常处理:统一的异常转换机制
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐