事务拦截器TransactionInterceptor
- 1. 事务拦截器项目配置
- 2. 切面
- 2.1 核心故事:一份油泼面🍽️的旅程
- 2.2 AOP核心概念📚
- 2.3 两种代码💻风格实现“餐厅切面”
- 风格一:基于`@Aspect`注解(现代、声明式、更常用)
- 风格二:基于`Advisor`编程式配置(底层、灵活、更精确)
- 核心关系与选择🔄
- 3. @Transactional解释
- 两种方式的直观对比🔄
- 5. 结论与选择💡
1. 事务拦截器项目配置
在Spirng架构中,如果不想使用 @Transactional注解(有时候会忘记,处写这个注解太麻烦),我们就可以使用@Aspect配置式AOP 来装配 事务拦截器(TransactionInterceptor)的方案,完全摆脱@Transactional注解。这是Spring框架内更原生的编程式事务管理方式。
importorg.aspectj.lang.annotation.Aspect;importorg.springframework.aop.Advisor;importorg.springframework.aop.aspectj.AspectJExpressionPointcut;importorg.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor;importorg.springframework.aop.support.DefaultPointcutAdvisor;importorg.springframework.aop.support.NameMatchMethodPointcut;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.transaction.PlatformTransactionManager;importorg.springframework.transaction.TransactionDefinition;importorg.springframework.transaction.interceptor.*;importjava.util.Collections;importjava.util.HashMap;importjava.util.Map;@Aspect@ConfigurationpublicclassTransactionConfig{// 1. 定义切入点(拦截 com.pro.service 包下所有方法)privatestaticfinalStringAOP_POINTCUT_EXPRESSION="execution(* com.sh.service..*.*(..))";// 2. 定义增删改方法前缀(自动加事务)privatestaticfinalString[]REQUIRED_RULE_TRANSACTION={"insert*","create*","add*","save*","update*","modify*","del*","delete*","remove*"};// 3. 定义查询方法前缀(自动加只读事务)privatestaticfinalString[]READ_RULE_TRANSACTION={"select*","get*","query*","search*","count*","find*","list*","page*"};// 4. 注入事务管理器@AutowiredprivatePlatformTransactionManagertransactionManager;// ========== 5. 核心:配置事务拦截器 (TransactionInterceptor)【这就是个Adivce(通知)】@BeanpublicTransactionInterceptortxAdvice(){// 5.1 创建事务属性源NameMatchTransactionAttributeSourcetas=newNameMatchTransactionAttributeSource();// 5.2 配置增删改事务属性(REQUIRED)RuleBasedTransactionAttributerequiredTx=newRuleBasedTransactionAttribute();requiredTx.setRollbackRules(Collections.singletonList(newRollbackRuleAttribute(Exception.class)));// 发生异常回滚requiredTx.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);// 事务隔离级别:读已提交requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);// 事务传播行为:REQUIREDrequiredTx.setTimeout(30);// 超时30秒// 5.3 配置查询事务属性(只读)RuleBasedTransactionAttributereadOnlyTx=newRuleBasedTransactionAttribute();readOnlyTx.setRollbackRules(Collections.singletonList(newRollbackRuleAttribute(Exception.class)));readOnlyTx.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);// 支持当前事务,不存在也不新建readOnlyTx.setReadOnly(true);// 关键:设置为只读readOnlyTx.setTimeout(20);// 查询超时可设短些// 5.4 将方法名模式映射到事务属性Map<String,TransactionAttribute>txMap=newHashMap<>();for(StringmethodName:REQUIRED_RULE_TRANSACTION){txMap.put(methodName,requiredTx);}for(StringmethodName:READ_RULE_TRANSACTION){txMap.put(methodName,readOnlyTx);}tas.setNameMap(txMap);// 5.5 创建并返回事务拦截器returnnewTransactionInterceptor(transactionManager,tas);}// ========== 6. 核心:配置切面(Advisor),将切入点和拦截器关联@BeanpublicAdvisortxAdviceAdvisor(){// 6.1 创建切入点(使用AspectJ表达式)AspectJExpressionPointcutpointcut=newAspectJExpressionPointcut();pointcut.setExpression(AOP_POINTCUT_EXPRESSION);// 6.2 创建 Advisor(通知器),将切入点 和 事务通知 绑定returnnewDefaultPointcutAdvisor(pointcut,txAdvice());}}在了解AOP之前先看一张图:
2. 切面
用一个生活化的例子——“餐厅点餐服务”解释AOP的相关概念。
2.1 核心故事:一份油泼面🍽️的旅程
想象你去一家面馆点了一份油泼面。它的核心流程是:
你点餐 → 厨师烹饪 → 你享用。
现在,面馆为了提升体验和管理,在核心流程前后加入了一些“通用服务”:
- 点餐前:服务员确认你是VIP会员,以决定是否赠送餐前冰封(饮料)。
- 点餐后、烹饪前:系统自动打印订单日志到后厨。
- 烹饪后、上桌前:厨师检查面有没有煮熟。
- 你吃完后:服务员邀请你进行满意度评价。
核心思想:煮面是“核心业务逻辑”,而身份验证、日志、检查、评价这些都是“横切关注点”。AOP的作用,就是在不改动“煮面”这段核心代码的情况下,优雅地插入这些通用服务。
2.2 AOP核心概念📚
让我们把这个故事映射到AOP的各个概念上:
| AOP 概念 | 专业解释 | 面馆通俗理解 |
|---|---|---|
| 连接点 (Join Point) | 程序执行过程中一个明确的点,如方法调用、异常抛出等。 | 流程中所有可以插入服务的节点。例如:点餐()、烹饪()、上菜()这些方法 被调用的【时刻】。 |
| 切点 (Pointcut) | 一个表达式,用来匹配和筛选你感兴趣的连接点。 | 选择器。例如:“匹配所有点餐()方法”,或“匹配所有以process开头的方法”。它决定了 “通用服务” 要在哪里生效。 |
| 通知 (Advice) | 在切点处执行的动作代码本身。 | 通用服务的具体内容。例如:“验证VIP身份”、“打印日志”这段代码逻辑。 |
| 切面 (Aspect) | 切点 + 通知的完整结合体。它定义了“在何处(切点)执行何种操作(通知)”。 | 一个完整的服务方案。例如:“在点餐()方法执行前(切点),执行验证VIP()操作(通知)”,这就是一个完整的切面。 |
| @Aspect | 一个注解,用来把一个普通的Java类声明为一个切面类。 | 给一个服务方案团队(Java类)挂上牌子,上面写着“我们是提供横切服务的”。 |
| @Before | 一种通知类型注解,表示该通知在目标方法执行之前运行。 | 前置服务。例如:“在厨师烹饪之前,先打印订单日志”。 |
| Advisor | Spring AOP中更原始、更底层的切面表示。一个Advisor通常只包含一个通知 和 一个切点。 | 一个最小化的、不可分割的服务指令。好比一张工单,严格写着:“仅当客户点油泼面时(切点),才在烹饪后做熟度检查(通知)”。它非常精准、直接。 |
2.3 两种代码💻风格实现“餐厅切面”
现在,我们用两种风格来实现上述的“打印日志”切面(在烹饪方法前打印)。
风格一:基于@Aspect注解(现代、声明式、更常用)
这种方式像用高级语言描述需求。
// 1. 声明这是一个切面类,同时它也是一个Spring管理的Bean@Component@AspectpublicclassRestaurantAspect{// 2. 定义切点:匹配所有在 CookingService 类中的 cook 方法@Pointcut("execution(* com.example.service.CookingService.cook(..))")publicvoidcookMethod(){}// 3. 定义通知,并与切点绑定:在烹饪方法执行前@Before("cookMethod()")publicvoidprintOrderLogBeforeCooking(){System.out.println("[日志] 后厨收到订单,开始烹饪...");// 这里可以拿到请求参数、方法名等信息,实现复杂逻辑}// 其他通知@AfterReturning("cookMethod()")publicvoidcheckSteakDoneness(){System.out.println("[质检] 检查油泼面熟了没...");}}看完这个类,可以回过头再看一下上面的事务拦截器AOP。
通俗理解:你写了一个服务手册(RestaurantAspect),在里面用标签(@Before)写明:“在做饭这个环节之前,要执行打印日志这个动作”。Spring 看到@Aspect这个牌子后,会自动帮你安排。
风格二:基于Advisor编程式配置(底层、灵活、更精确)
这种方式更像直接编写部署指令。
@ConfigurationpublicclassRestaurantAdvisorConfig{@BeanpublicAdvisorcookingLogAdvisor(){// 1. 创建通知(Advice):定义要做什么MethodBeforeAdvicelogAdvice=newMethodBeforeAdvice(){@Overridepublicvoidbefore(Methodmethod,Object[]args,Objecttarget){System.out.println("[Advisor日志] 准备烹饪食材: "+args[0]);}};// 2. 创建切点(Pointcut):定义在哪里做(精确到方法名匹配)NameMatchMethodPointcutpointcut=newNameMatchMethodPointcut();pointcut.addMethodName("cook");// 只匹配名为 "cook" 的方法// 也可以用 pointcut.setClassFilter() 来限制类// 3. 将通知和切点组装成最小的切面单元:AdvisorreturnnewDefaultPointcutAdvisor(pointcut,logAdvice);}}通俗理解:你直接发了一张精确的工单(Advisor)给系统。工单上明确指令:“听着,只有当一个方法名严格叫cook时(切点),才在它之前执行这个打印动作(通知)。”这个工单自成一体,非常精准。
核心关系与选择🔄
@Aspect和Advisor是什么关系?
- 最终效果一致:在运行时,Spring都会把
@Aspect注解的切面转换成一个或多个内部的Advisor去执行。 - 抽象层级不同:
@Aspect是声明式的,像写配置清单,更符合人类思维,一个类里可以定义很多相关通知。Advisor是编程式的API,是Spring AOP的工作基石,更底层、更灵活,一个Advisor通常只做一件事。 - 为什么两种都存在?
@Aspect更简洁易用,涵盖了90%的场景。但当需要极其精细的控制(比如根据复杂条件动态决定是否应用通知),或者与一些旧框架集成时,直接使用Advisor这个底层API会更强大。
我们可以通过@Order确定切面的先后顺序,然后在Spring的调度下完美合作,共同增强业务方法,而业务方法本身对此一无所知——这就是AOP的魅力。
3. @Transactional解释
- “定义一个Advisor绑定拦截器” 是一种编程式、集中配置的方式。
- 使用 @Transactional 注解,是另一种声明式、分散标注的方式。
但两者的终点完全相同:都是为了让 TransactionInterceptor 来拦截方法并管理事务。
当你使用 @Transactional 注解时,Spring Boot在启动阶段通过自动配置(TransactionAutoConfiguration)完成了一系列幕后工作,其本质是自动创建了所需的Advisor。在spring中有这样一个源码:
@Configuration(proxyBeanMethods=false)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)@ImportRuntimeHints(TransactionRuntimeHints.class)publicclassProxyTransactionManagementConfigurationextendsAbstractTransactionManagementConfiguration{@Bean(name=TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)publicBeanFactoryTransactionAttributeSourceAdvisortransactionAdvisor(TransactionAttributeSourcetransactionAttributeSource,TransactionInterceptortransactionInterceptor){BeanFactoryTransactionAttributeSourceAdvisoradvisor=newBeanFactoryTransactionAttributeSourceAdvisor();advisor.setTransactionAttributeSource(transactionAttributeSource);advisor.setAdvice(transactionInterceptor);if(this.enableTx!=null){advisor.setOrder(this.enableTx.<Integer>getNumber("order"));}returnadvisor;// 最终还是一个Advisor!}@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)// 创建一个默认的事务拦截器// 这里的入参原本是 TransactionAttributeSource,为了便于理解修改为 AnnotationTransactionAttributeSourcepublicTransactionInterceptortransactionInterceptor(AnnotationTransactionAttributeSourcetransactionAttributeSource){TransactionInterceptorinterceptor=newTransactionInterceptor();interceptor.setTransactionAttributeSource(transactionAttributeSource);if(this.txManager!=null){interceptor.setTransactionManager(this.txManager);}returninterceptor;}}整个流程可以概括为:@Transactional注解 → 被AnnotationTransactionAttributeSource解析为规则 → 与TransactionInterceptor一起被包装进BeanFactoryTransactionAttributeSourceAdvisor→ 该Advisor被应用到所有Bean的创建过程中。
两种方式的直观对比🔄
为了让区别更清晰,我们可以用下面的表格来对比这两种方式:
| 方面 | 编程式Advisor | @Transactional方式(声明式) |
|---|---|---|
| 配置核心 | 在@Configuration类中,显式定义一个AdvisorBean。 | 在方法或类上添加@Transactional注解。 |
| 规则定义 | 在NameMatchTransactionAttributeSource中,通过方法名模式匹配来定义规则。 | 在注解的属性中直接定义规则(如@Transactional(readOnly = true))。 |
| 切点(Pointcut) | 显式指定,如execution(* com.pro.service..*.*(..))。 | 隐式生成,切点逻辑是“匹配所有有@Transactional注解的地方”。 |
| 底层实现 | 直接操控了Spring AOP最底层的Advisor、Advice、Pointcut三大件。 | 利用了Spring的自动配置和@EnableTransactionManagement,在底层帮我们创建了Advisor。 |
| 优点 | 集中管理,规则一目了然;与业务代码完全解耦;适合基于方法名的批量规则。 | 使用简单,贴近业务逻辑;粒度更细,可以针对单个方法特殊配置;是Spring生态的标准做法。 |
| 缺点 | 不够灵活,如果某个方法需要例外规则(如REQUIRES_NEW)很难单独配置。 | 注解分散在代码各处;对业务代码有侵入性;无法根据方法名等动态规则批量处理。 |
5. 结论与选择💡
TransactionInterceptor始终在工作:无论你用哪种方式,最终都是TransactionInterceptor这个“事务引擎”在执行拦截和管理。区别只在于“启动引擎的开关”是编程式装配的Advisor,还是由注解触发的自动装配Advisor。- 可以混合使用(但需谨慎):Spring允许两者共存。如果绝大多数方法遵循统一规则,但极个别方法需要特殊事务行为(比如一个特殊的
handlePayment方法需要REQUIRES_NEW),你可以在那个方法上单独添加@Transactional(propagation = Propagation.REQUIRES_NEW)。 - 编程式配置的规则是全局的,而
@Transactional注解的规则是局部的,局部注解通常会覆盖全局规则。