当前位置: 首页 > news >正文

Spring 框架基础教程(Day03)

一. AOP 核心概念

  • 作用:在不修改原始代码的前提下增强方法功能,通过代理模式实现。
  • 核心术语
    • 连接点(JoinPoint):程序执行的任意位置(Spring AOP 中特指方法执行)。
    • 切入点(Pointcut):匹配连接点的表达式(如 execution(* com.itheima.service.*.*(..)))。
    • 通知(Advice):增强逻辑(如前置、后置、环绕等)。
    • 切面(Aspect):通知与切入点的绑定关系。
    • 目标对象(Target):被代理的原始对象。
    • 代理(Proxy):Spring 自动生成的增强对象。

以下将逐步讲解上述代码示例中使用 Spring AOP 统计 SQL 执行时间和接口执行时间的实现原理和代码细节。

1. 开启 AOP 功能

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
public class AspectConfig {

}
  • @Configuration:这是 Spring 的注解,用于将 AspectConfig 类标记为配置类。配置类类似于传统的 XML 配置文件,它可以定义 Bean 以及其他配置信息。
  • @EnableAspectJAutoProxy:该注解用于开启 Spring AOP 的自动代理功能。开启后,Spring 会自动检测并创建代理对象,以便在目标方法执行前后插入切面逻辑。

2. 统计接口执行时间

  • @Aspect:将 InterfaceTimeAspect 类标记为切面类。切面类中定义了切入点和通知,用于在目标方法执行前后插入额外的逻辑。
  • @Component:将该类注册为 Spring 的组件,这样 Spring 容器才能管理它。
  • @Around:这是一个环绕通知注解,它可以在目标方法执行前后都执行额外的逻辑。"execution(* com.example.demo.controller.*.*(..))" 是切入点表达式,表示匹配 com.example.demo.controller 包下的所有类的所有方法。
  • ProceedingJoinPoint:在环绕通知中,ProceedingJoinPoint 用于调用目标方法。pjp.proceed() 会执行目标方法,并返回目标方法的返回值。
  • 统计时间:在调用目标方法前后分别记录时间,计算差值得到执行时间,并使用 Logger 输出日志。

3. 统计 SQL 执行时间

package com.example.demo;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class SqlTimeAspect {
    private static final Logger logger = LoggerFactory.getLogger(SqlTimeAspect.class);

    @Around("execution(* com.example.demo.dao.*.*(..))")
    public Object aroundSqlMethod(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = pjp.proceed();
        long endTime = System.currentTimeMillis();
        logger.info("SQL 方法 {} 执行时间: {} ms", pjp.getSignature().toShortString(), endTime - startTime);
        return result;
    }
}
  • 该类的结构和 InterfaceTimeAspect 类类似,只是切入点表达式不同。"execution(* com.example.demo.dao.*.*(..))" 表示匹配 com.example.demo.dao 包下的所有类的所有方法,用于统计 SQL 方法的执行时间。

4. 示例控制器

package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @GetMapping("/test")
    public String test() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Test Success";
    }
}
  • @RestController:将该类标记为 RESTful 风格的控制器,用于处理 HTTP 请求。
  • @GetMapping("/test"):定义一个 GET 请求的映射路径 /test。当客户端访问该路径时,会执行 test 方法。
  • Thread.sleep(200):模拟接口处理的耗时操作。

5. 示例 DAO 类

  • @Repository:将该类标记为数据访问对象(DAO),Spring 会自动处理该类的异常转换。
  • executeSql 方法:模拟 SQL 执行的耗时操作。

6. 示例服务类

package com.example.demo.service;

import com.example.demo.dao.DemoDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DemoService {
    @Autowired
    private DemoDao demoDao;

    public void callDaoMethod() {
        demoDao.executeSql();
    }
}
  • @Service:将该类标记为服务层组件,用于处理业务逻辑。
  • @Autowired:自动注入 DemoDao 实例,实现依赖注入。
  • callDaoMethod 方法:调用 DemoDao 的 executeSql 方法。

7. Spring Boot 启动类

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@SpringBootApplication:这是一个组合注解,包含了 @Configuration@EnableAutoConfiguration 和 @ComponentScan,用于启动 Spring Boot 应用。

  • SpringApplication.run(DemoApplication.class, args):启动 Spring Boot 应用。

总结

通过上述代码,我们使用 Spring AOP 实现了对接口和 SQL 方法执行时间的统计。核心是定义切面类,使用 @Around 环绕通知在目标方法执行前后记录时间,并输出日志。切入点表达式用于指定要拦截的方法。这种方式可以在不修改原有业务逻辑的前提下,添加额外的统计功能。

2. AOP 配置与实现

2.1 切入点表达式
  • 语法execution(修饰符? 返回值 包名.类名.方法名(参数) 异常?)
  • 通配符
    • *:匹配任意符号(如 * com.itheima.dao.*.*(..))。
    • ..:匹配多级路径或任意参数(如 com..*Service.*(..))。
    • +:匹配子类类型(如 *Service+)。
  • 示例
    @Pointcut("execution(* com.itheima.service.*Service.*(..))")
    
2.2 通知类型
类型注解执行时机参数 / 返回值
前置通知@Before方法执行前JoinPoint 获取参数
后置通知@After方法执行后(无论是否异常)JoinPoint 获取参数
环绕通知@Around方法前后,需调用 proceed()ProceedingJoinPoint 控制原始方法调用
返回后通知@AfterReturning方法正常返回后returning 属性获取返回值
异常后通知@AfterThrowing方法抛出异常后throwing 属性获取异常

查缺补漏

  • 环绕通知必须依赖 ProceedingJoinPoint,否则无法调用原始方法。
  • 返回后通知异常后通知的参数名需与注解属性一致(如 @AfterReturning(returning = "ret"))。
2.3 通知数据获取
  • 参数
    • 非环绕通知:JoinPoint.getArgs()
    • 环绕通知:ProceedingJoinPoint.getArgs()(可修改参数)。
  • 返回值
    • 环绕通知:Object ret = pjp.proceed();
    • 返回后通知:@AfterReturning(returning = "ret")
  • 异常
    • 环绕通知:通过 try-catch 捕获。
    • 异常后通知:@AfterThrowing(throwing = "ex")
3. 事务管理
3.1 核心组件
  • @Transactional:标记业务方法,支持事务属性配置。
  • PlatformTransactionManager:事务管理器(如 DataSourceTransactionManager)。
3.2 事务属性
属性说明示例
readOnly是否为只读事务(默认 false@Transactional(readOnly = true)
timeout超时时间(秒),-1表示永不超时@Transactional(timeout = 5)
isolation隔离级别(如 Isolation.DEFAULTIsolation.REPEATABLE_READ@Transactional(isolation = Isolation.REPEATABLE_READ)
propagation传播行为(如 REQUIRED(默认)、REQUIRES_NEW@Transactional(propagation = Propagation.REQUIRES_NEW)
rollbackFor指定回滚的异常类型(默认仅 RuntimeException 和 Error@Transactional(rollbackFor = IOException.class)
3.3 传播行为
类型说明场景
REQUIRED若当前有事务则加入,否则新建(默认)转账操作(减钱与加钱同一事务)
REQUIRES_NEW强制新建事务,与当前事务独立日志记录(不随转账失败回滚)
NESTED在当前事务中嵌套保存点,回滚到保存点而非完全回滚部分回滚场景

查缺补漏

  • 事务未生效:确保方法被 Spring 管理,且未被 final/private 修饰。
  • 异常回滚:默认仅回滚 RuntimeException 和 Error,需通过 rollbackFor 配置其他异常。
4. 案例应用
4.1 业务层效率监控
  • 实现:使用环绕通知统计万次方法执行时间。
  • 关键代码
    @Around("servicePt()")
    public Object runSpeed(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            pjp.proceed();
        }
        System.out.println("万次执行时间: " + (System.currentTimeMillis() - start) + "ms");
        return null;
    }
    
4.2 密码空格处理
  • 实现:通过环绕通知过滤参数中的空格。
  • 关键代码
    @Around("pt()")
    public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof String) {
                args[i] = ((String) args[i]).trim();
            }
        }
        return pjp.proceed(args);
    }
    
4.3 转账事务管理
  • 实现:通过 @Transactional 确保转账原子性,结合 REQUIRES_NEW 实现日志独立事务。
  • 关键配置
    @Service
    public class AccountServiceImpl {
        @Transactional
        public void transfer(String out, String in, Double money) {
            accountDao.outMoney(out, money);
            accountDao.inMoney(in, money);
        }
    }
    
    @Service
    public class LogServiceImpl {
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void log(String info) {
            logDao.log(info);
        }
    }
    
5. 查缺补漏
  1. AOP 代理机制

    • JDK 动态代理:基于接口,性能较高。
    • CGLIB 代理:基于子类,适用于无接口的类。
    • 默认选择:优先 JDK 动态代理,无接口时使用 CGLIB。
  2. 事务隔离级别

    • READ_UNCOMMITTED:最低级别,可能读到未提交数据。
    • READ_COMMITTED:解决脏读(Oracle 默认)。
    • REPEATABLE_READ:解决不可重复读(MySQL 默认)。
    • SERIALIZABLE:最高级别,完全串行化。
  3. 事务超时

    • 单位为秒,超时后自动回滚。
    • 需根据业务复杂度合理设置(如数据库慢查询阈值)。
  4. 常见问题

    • AOP 不生效:检查 @EnableAspectJAutoProxy 是否开启,包扫描路径是否正确。
    • 事务不回滚:确保异常类型为 RuntimeException 或通过 rollbackFor 显式配置。
6. 总结
  • AOP 通过代理模式实现非侵入式增强,核心是切入点与通知的绑定。
  • 事务管理 通过 @Transactional 和事务管理器确保数据一致性,传播行为和属性配置是关键。
  • 实践 中需结合业务场景选择合适的通知类型和事务策略,避免性能问题和数据不一致。

AOP(面向切面编程)的工作流程

AOP(面向切面编程)的工作流程可以分为以下几个关键步骤,下面为你详细介绍:

1. 定义切面和通知

  • 切面(Aspect):切面是通知和切入点的结合,它定义了在哪些连接点上执行何种通知。在 Spring AOP 中,通常使用 Java 类来定义切面,并使用 @Aspect 注解标记该类。
  • 通知(Advice):通知是切面在特定连接点执行的操作。Spring AOP 支持多种类型的通知,如前置通知(@Before)、后置通知(@After)、环绕通知(@Around)、返回后通知(@AfterReturning)和异常后通知(@AfterThrowing)。
  • 示例代码
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}

    @Before("serviceMethods()")
    public void beforeServiceMethod() {
        System.out.println("Before service method execution");
    }
}

在上述代码中,LoggingAspect 类是一个切面,serviceMethods() 是切入点,beforeServiceMethod() 是前置通知。

2. 定义切入点

  • 切入点(Pointcut):切入点定义了哪些连接点会被拦截。连接点是程序执行过程中的某个特定位置,如方法调用、异常抛出等。在 Spring AOP 中,通常使用切入点表达式来定义切入点。
  • 切入点表达式:常用的切入点表达式类型是 execution,它可以匹配方法的执行。例如,execution(* com.example.service.*.*(..)) 表示匹配 com.example.service 包下所有类的所有方法。
  • 示例

在上述代码中,repositoryMethods() 定义了一个切入点,匹配 com.example.repository 包下所有类的所有方法。

3. Spring 容器启动

  • 创建容器:当 Spring 应用程序启动时,会创建 Spring 容器。容器负责管理所有的 Bean,包括切面和目标对象。
  • 扫描组件:Spring 容器会扫描配置类或 XML 配置文件中指定的包,查找带有 @Component@Service@Repository 等注解的类,并将它们注册为 Bean。
  • 开启 AOP 自动代理:需要在配置类中使用 @EnableAspectJAutoProxy 注解开启 AOP 自动代理功能,这样 Spring 容器会自动为目标对象创建代理。
  • 示例代码
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan(basePackages = "com.example")
@EnableAspectJAutoProxy
public class AppConfig {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        // 其他操作
        context.close();
    }
}

在上述代码中,AppConfig 是配置类,@ComponentScan 用于扫描指定包下的组件,@EnableAspectJAutoProxy 开启 AOP 自动代理。

4. 创建代理对象

  • 代理类型:Spring AOP 支持两种代理类型,JDK 动态代理和 CGLIB 代理。
    • JDK 动态代理:基于接口实现,当目标对象实现了接口时,Spring 会使用 JDK 动态代理。
    • CGLIB 代理:基于继承实现,当目标对象没有实现接口时,Spring 会使用 CGLIB 代理。
  • 创建代理:Spring 容器在创建目标对象时,会根据切入点表达式判断是否需要为该对象创建代理。如果需要,会根据代理类型创建相应的代理对象。
  • 示例:假设 UserService 是一个实现了 UserServiceInterface 的类,Spring 会为 UserService 创建一个 JDK 动态代理对象。

5. 目标方法调用

  • 调用代理对象:当客户端调用目标对象的方法时,实际上调用的是代理对象的方法。
  • 执行通知:代理对象会根据切入点表达式和通知类型,在目标方法执行前后或抛出异常时执行相应的通知。
  • 示例:当调用 UserService 的 addUser 方法时,代理对象会先执行 LoggingAspect 中的前置通知 beforeServiceMethod(),然后再调用目标方法 addUser()

6. 通知执行

  • 前置通知(@Before:在目标方法执行之前执行。
  • 后置通知(@After:在目标方法执行之后执行,无论目标方法是否抛出异常。
  • 环绕通知(@Around:在目标方法执行前后都可以执行额外的逻辑,并且可以控制目标方法的执行。
  • 返回后通知(@AfterReturning:在目标方法正常返回后执行。
  • 异常后通知(@AfterThrowing:在目标方法抛出异常后执行。
  • 示例代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AroundLoggingAspect {
    @Around("execution(* com.example.service.*.*(..))")
    public Object aroundServiceMethod(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("Before method execution");
        Object result = pjp.proceed();
        System.out.println("After method execution");
        return result;
    }
}

在上述代码中,aroundServiceMethod() 是一个环绕通知,在目标方法执行前后分别输出日志。

7. 返回结果

  • 目标方法返回:目标方法执行完毕后,将结果返回给代理对象。
  • 代理对象返回:代理对象将结果返回给客户端。

总结

AOP 的工作流程主要包括定义切面和通知、定义切入点、Spring 容器启动、创建代理对象、目标方法调用、通知执行和返回结果等步骤。通过这些步骤,AOP 可以在不修改原有业务逻辑的前提下,在目标方法的特定位置插入额外的逻辑,实现诸如日志记录、事务管理等功能。

Spring 事务

1. 事务的基本概念
  • 事务(Transaction):数据库操作的最小逻辑单元,确保一系列操作要么全部成功,要么全部失败。
  • ACID 特性
    • 原子性(Atomicity):事务中的操作要么全部完成,要么全部不完成。
    • 一致性(Consistency):事务执行前后,数据的完整性约束未被破坏。
    • 隔离性(Isolation):多个事务并发执行时,彼此不可见(不同隔离级别有不同表现)。
    • 持久性(Durability):事务提交后,数据永久保存到数据库。
2. Spring 事务管理

Spring 提供两种事务管理方式:声明式事务(推荐)和编程式事务

2.1 声明式事务
  • 通过注解实现:使用 @Transactional 注解标记业务方法。
  • 优点
    • 解耦业务逻辑与事务管理。
    • 无需手动编写事务提交 / 回滚代码。
  • 示例
    @Service
    public class AccountService {
        @Autowired
        private AccountDao accountDao;
    
        @Transactional // 标记事务
        public void transfer(String from, String to, double amount) {
            accountDao.withdraw(from, amount);
            accountDao.deposit(to, amount);
        }
    }
    
2.2 编程式事务
  • 通过 API 手动控制:使用 TransactionTemplate 或 PlatformTransactionManager
  • 适用场景:复杂的事务控制逻辑(如嵌套事务)。
  • 示例
    @Service
    public class OrderService {
        @Autowired
        private TransactionTemplate transactionTemplate;
    
        public void processOrder() {
            transactionTemplate.execute(status -> {
                // 业务逻辑
                if (shouldRollback) {
                    status.setRollbackOnly();
                }
                return null;
            });
        }
    }
    
3. 事务管理器(PlatformTransactionManager)
  • 作用:协调事务的提交与回滚。
  • 常用实现类
    • DataSourceTransactionManager:适用于 JDBC/MyBatis 等基于 JDBC 的持久化技术。
    • HibernateTransactionManager:适用于 Hibernate。
  • 配置示例(Spring Boot):
    @Configuration
    @EnableTransactionManagement
    public class TransactionConfig {
        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }
    
4. 事务传播行为(Propagation)

定义事务协调员对事务管理员携带事务的处理态度。

传播行为说明
REQUIRED默认值。若当前有事务则加入,否则新建事务。
REQUIRES_NEW强制新建事务,当前事务挂起。
NESTED在当前事务中嵌套保存点,回滚时仅回滚到保存点。
SUPPORTS若当前有事务则加入,否则以非事务方式执行。
NOT_SUPPORTED以非事务方式执行,挂起当前事务。

示例

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperation() {
    // 日志记录(独立事务)
}
5. 事务隔离级别(Isolation)

定义事务之间的可见性。

隔离级别说明
DEFAULT使用数据库默认隔离级别(MySQL 默认 REPEATABLE_READ)。
READ_UNCOMMITTED允许脏读、不可重复读、幻读。
READ_COMMITTED避免脏读,但仍有不可重复读、幻读。
REPEATABLE_READ避免脏读、不可重复读,但仍有幻读。
SERIALIZABLE最高级别,完全串行化,避免所有并发问题。

示例

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void queryData() {
    // 查询操作
}
6. 事务属性配置
  • timeout:事务超时时间(秒),超时自动回滚(默认 -1 表示永不超时)。
  • readOnly:标记为只读事务(默认 false),可优化数据库性能。
  • rollbackFor:指定触发回滚的异常类型(默认仅 RuntimeException 和 Error)。

示例

@Transactional(timeout = 5, readOnly = true, rollbackFor = IOException.class)
public void readData() throws IOException {
    // 业务逻辑
}
7. 声明式事务实现步骤
  1. 添加依赖
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
    </dependency>
    
  2. 启用事务管理
    @Configuration
    @EnableTransactionManagement
    public class AppConfig {
        // 配置事务管理器
    }
    
  3. 标记业务方法
    @Service
    public class UserService {
        @Transactional
        public void updateUser(User user) {
            // 业务逻辑
        }
    }
    
8. 注意事项
  • 事务未生效场景
    • 方法非 public
    • 方法内部调用(非通过代理对象调用)。
    • 异常被捕获未抛出。
  • 事务范围
    • 避免事务方法过大(降低并发性能)。
    • 避免在事务中执行耗时操作(如文件读写)。
9. 总结
  • 声明式事务是 Spring 事务管理的核心,通过 @Transactional 注解实现解耦。
  • 事务管理器负责协调事务的提交与回滚。
  • 传播行为隔离级别需根据业务场景合理选择,避免数据不一致或性能问题。

Spring 事务角色详解

在 Spring 事务管理中,事务角色分为 事务管理员(Transaction Administrator) 和 事务协调员(Transaction Coordinator),二者通过协作保证数据一致性。

1. 事务管理员
  • 定义:发起事务的方法,通常位于业务层(Service 层)。
  • 职责
    • 开启事务。
    • 控制事务的提交或回滚。
    • 管理事务的传播行为和隔离级别。
  • 示例
    @Service
    public class AccountService {
        @Autowired
        private AccountDao accountDao;
    
        @Transactional // 事务管理员
        public void transfer(String from, String to, double amount) {
            accountDao.withdraw(from, amount); // 事务协调员
            accountDao.deposit(to, amount);    // 事务协调员
        }
    }
    
  • 关键注解@Transactional 标注在业务方法上。
2. 事务协调员
  • 定义:加入事务管理员发起的事务的方法,通常位于数据层(DAO 层)。
  • 职责
    • 参与事务管理员的事务。
    • 执行具体的数据操作(如 SQL 增删改)。
  • 示例
    @Repository
    public class AccountDao {
        @Transactional // 事务协调员(默认传播行为为 REQUIRED)
        public void withdraw(String account, double amount) {
            // 执行扣款 SQL
        }
    }
    
  • 传播行为:默认使用 Propagation.REQUIRED,即加入当前事务。
3. 核心协作流程
  1. 事务管理员开启事务
    • 业务层方法标注 @Transactional,Spring 自动开启事务。
  2. 事务协调员加入事务
    • 数据层方法被调用时,若当前存在事务(管理员开启的),则加入该事务。
  3. 异常处理
    • 若业务层方法抛出 RuntimeException 或 Error,事务管理员触发回滚,所有协调员操作均回滚。
    • 若协调员抛出受检异常且未被捕获,事务管理员也会回滚。
4. 事务传播行为与角色关系
传播行为事务管理员事务协调员示例场景
REQUIRED开启新事务加入当前事务(默认)转账操作(扣款与存款同一事务)
REQUIRES_NEW开启新事务强制新建事务,挂起当前事务日志记录(独立于主事务)
NESTED开启新事务在当前事务中创建保存点,回滚到保存点部分回滚(如订单支付与库存扣减)
5. 事务角色配置
  • 事务管理员
    • 标注 @Transactional 在业务方法上,设置传播行为、隔离级别等。
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
    public void processOrder() {
        // 业务逻辑
    }
    
  • 事务协调员
    • 数据层方法无需显式标注 @Transactional(默认加入当前事务)。
    • 如需独立事务,显式设置 @Transactional(propagation = Propagation.REQUIRES_NEW)
6. 最佳实践
  1. 事务应尽可能放在业务层
    • 业务层方法作为事务管理员,控制多个数据层操作的原子性。
  2. 避免数据层单独开启事务
    • 数据层方法默认加入业务层事务,减少事务嵌套。
  3. 合理选择传播行为
    • 日志记录、缓存更新等非核心操作使用 REQUIRES_NEW,避免主事务回滚影响。
7. 总结
  • 事务管理员是事务的发起者和管理者,位于业务层。
  • 事务协调员是事务的参与者,位于数据层,默认加入管理员的事务。
  • 通过 @Transactional 和传播行为配置,确保复杂业务操作的数据一致性。

Spring 事务失效场景与解决方案

在 Spring 声明式事务中,@Transactional 注解可能因配置或代码逻辑问题导致事务失效。以下是常见失效场景及解决方案:

1. 方法非 public
  • 原因:Spring AOP 默认仅对 public 方法应用事务。
  • 示例
    @Service
    public class UserService {
        // 事务失效(非 public)
        @Transactional
        void updateUser(User user) {
            // 业务逻辑
        }
    }
    
  • 解决方案:将方法声明为 public
2. 异常被捕获未抛出
  • 原因:事务默认仅对 RuntimeException 和 Error 回滚,若异常被捕获且未抛出,事务不会回滚。
  • 示例
    @Transactional
    public void transfer() {
        try {
            // 业务逻辑
            int i = 1 / 0; // 抛出异常
        } catch (Exception e) {
            // 异常被捕获,未抛出
            e.printStackTrace();
        }
    }
    
  • 解决方案
    • 抛出 RuntimeException 或 Error
    • 使用 @Transactional(rollbackFor = Exception.class) 强制回滚所有异常。
3. 传播行为配置错误
  • 原因:若事务协调员配置了 REQUIRES_NEW 但未正确处理异常,可能导致主事务回滚而协调员事务提交。
  • 示例
    @Service
    public class OrderService {
        @Autowired
        private PaymentService paymentService;
    
        @Transactional
        public void createOrder() {
            paymentService.charge(); // 若 charge 异常且未抛出,主事务回滚,但 charge 的事务已提交
        }
    }
    
    @Service
    public class PaymentService {
        @Transactional(propagation = Propagation.REQUIRES_NEW)
        public void charge() {
            // 业务逻辑
            throw new RuntimeException("Payment failed");
        }
    }
    
  • 解决方案:确保异常传播到事务管理员,或在协调员中显式回滚。
4. 类内部方法调用
  • 原因:通过 this 调用类内部方法时,未经过 Spring 代理,事务不生效。
  • 示例
    @Service
    public class UserService {
        public void updateUser() {
            this.saveUser(); // 内部调用,事务失效
        }
    
        @Transactional
        public void saveUser() {
            // 业务逻辑
        }
    }
    
  • 解决方案
    • 通过依赖注入自己(代理对象)。
    • 使用 AopContext.currentProxy() 获取代理对象(需配置 exposeProxy=true)。
5. 未正确配置事务管理器
  • 原因:未在 Spring 容器中注册 PlatformTransactionManager
  • 示例
    @Configuration
    public class TransactionConfig {
        // 缺少 @Bean PlatformTransactionManager
    }
    
  • 解决方案
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    
6. 注解使用错误
  • 原因:错误地在接口或非 Spring 管理的类上使用 @Transactional
  • 示例
    public interface UserService {
        @Transactional // 无效(接口非 Spring 组件)
        void updateUser();
    }
    
  • 解决方案:仅在 Spring 管理的组件(如 @Service@Component)中使用 @Transactional
7. 数据库引擎不支持事务
  • 原因:例如 MySQL 的 MyISAM 引擎不支持事务。
  • 解决方案:使用 InnoDB 引擎。
8. 方法使用 final 或 static
  • 原因:Spring 代理无法覆盖 final 或 static 方法。
  • 示例
    @Service
    public class UserService {
        @Transactional
        public final void updateUser() { // 事务失效
            // 业务逻辑
        }
    }
    
  • 解决方案:移除 final 或 static 修饰符。
9. 事务超时设置不合理
  • 原因:事务执行时间超过 timeout 设置,自动回滚。
  • 示例
    @Transactional(timeout = 1) // 1 秒超时
    public void longRunningMethod() {
        // 执行耗时操作
    }
    
  • 解决方案:根据业务需求调整 timeout 值。
10. 事务未被 Spring 管理
  • 原因:目标对象未被 Spring 容器管理(如手动 new 对象)。
  • 示例
    public class UserService {
        @Transactional
        public void updateUser() {
            // 业务逻辑
        }
    }
    
    // 手动创建对象,事务失效
    UserService userService = new UserService();
    userService.updateUser();
    
  • 解决方案:通过 Spring 容器获取对象(如 @Autowired)。

避免事务失效的最佳实践

  1. 方法声明为 public
  2. 避免捕获异常不抛出,或明确配置 rollbackFor
  3. 合理使用传播行为(如 REQUIREDREQUIRES_NEW)。
  4. 通过代理对象调用方法,避免类内部直接调用。
  5. 正确配置事务管理器
  6. 确保数据库引擎支持事务(如 InnoDB)。
  7. 避免使用 final/static 修饰事务方法

总结

事务失效的核心原因通常是代理机制未生效、异常未正确传播或配置错误。通过遵循最佳实践和仔细检查代码逻辑,可有效避免这些问题。

1. Spring AOP 代理机制的限制

Spring 通过 AOP 实现声明式事务,默认使用 JDK 动态代理 或 CGLIB 代理

  • JDK 动态代理
    • 基于接口实现,只能代理接口中的 public 方法。
    • 非 public 方法(如 protectedprivate)无法被代理,事务注解失效。
  • CGLIB 代理
    • 基于继承,可代理类的 public 方法。
    • 非 public 方法(如 finalstatic)无法被覆盖,事务注解失效。

2. 为什么非 public 方法事务失效?

  • 访问控制限制
    • JDK 代理只能调用接口中声明的 public 方法。
    • CGLIB 代理默认仅处理 public 方法,非 public 方法无法被增强。
  • Spring 设计哲学
    • Spring 推荐面向接口编程,业务逻辑应通过接口暴露,而非直接操作实现类。
    • 非 public 方法违背了这一原则,导致事务无法通过代理生效。

相关文章:

  • JVM 01
  • C++菜鸟教程 - 从入门到精通 第五节
  • 隔空打印,IPP,IPD,HP Jetdirect协议的区别(Mac添加打印机四种协议的区别)
  • 【Unity】合批处理和GPU实例化的底层优化原理(完)
  • Spring 框架中的 BeanUtils
  • AugFPN
  • STM32标准库开发中断流程
  • 编译原理 pl0 词法解析器 使用状态机与状态矩阵,和查找上一步得到分析
  • Windows下rust的安装
  • python 中match...case 和 C switch case区别
  • 数据库联表Sql语句建一个新表(MySQL,Postgresql,SQL server)
  • Linux开机、重启与用户登录注销全解析
  • C++之模板二番战
  • Spring Boot事件机制详解
  • 【STM32】知识点介绍一:硬件知识
  • 画一个分布式系统架构图,标注服务注册、网关、熔断
  • python中的demjson包介绍
  • docker-dockerfile书写
  • 从JVM底层揭开Java方法重载与重写的面纱:原理、区别与高频面试题突破
  • 【性能优化点滴】odygrd/quill 中的冷热属性宏
  • 钟南山谈新冠阳性率升高:可防可治不用慌,高危人群应重点关注
  • 优质文化资源下基层,上海各区优秀群文团队“文化走亲”
  • 新华社千笔楼:地方文旅宣传应走出“魔性尬舞”的流量焦虑
  • 浦江潮涌征帆劲,上海以高质量发展服务全国发展大局
  • 人民网:激发博物馆创新活力,让“过去”拥有“未来”
  • 复旦建校120周年大型义诊举行,百余名专家服务市民超三千人次