-手写Spring注解版本&事务传播行为

视频参考C:UsersAdministratorDesktop蚂蚁3期【www.zxit8.com】 0018-(每特教育&每特学院&蚂蚁课堂)-3期-源码分析-手写Spring注解版本&事务传播行为018-(每特教育&每特学院&蚂蚁课堂)-3期-源码分析-手写Spring注解版本&事务传播行为

今天视频要手写一个@Transaction事务注解框架

我们首先来看一个spring 事务的常见配置

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://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/aop
         http://www.springframework.org/schema/aop/spring-aop.xsd
         http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx.xsd">


    <!-- 开启注解 -->
    <context:component-scan base-package="com.itmayiedu"></context:component-scan>
    <!-- 1. 数据源对象: C3P0连接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>

    <!-- 2. JdbcTemplate工具类实例 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 配置事物 -->
    <bean id="dataSourceTransactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!-- 开启注解事物 -->
    <tx:annotation-driven transaction-manager="dataSourceTransactionManager" />
</beans>

 在业务层中@Transaction来使用注解

    @Transactional
    public void add() {
        userDao.add("wangmazi", 27);
        int i = 1 / 0;
        System.out.println("我是add方法");
        userDao.add("zhangsan", 16);
    }

接下来我们要手写@Transactional注解框架

接下来首先学习下java的注解以及利用反射读取注解

@Target(value = { ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface AddAnnotation {

    int userId() default 0;

    String userName() default "默认名称";

    String[]arrays();
}

 接下来利用反射读取注解

package com.itmayiedu.annotation;

import java.lang.reflect.Method;

public class User {

    @AddAnnotation(userName = "张三", userId = 18, arrays = { "1" })
    public void add() {

    }

    public void del() {

    }

    public static void main(String[] args) throws ClassNotFoundException {
        // 怎么样获取到方法上注解信息 反射机制
        Class<?> forName = Class.forName("com.itmayiedu.annotation.User");
        // 获取到当前类(不包含继承)所有的方法
        Method[] declaredMethods = forName.getDeclaredMethods();
        for (Method method : declaredMethods) {
            // 获取该方法上是否存在注解
            System.out.println("####方法名称" + method.getName());
            AddAnnotation addAnnotation = method.getAnnotation(AddAnnotation.class);
            if (addAnnotation == null) {
                // 该方法上没有注解
                System.out.println("该方法上没有加注解..");
                continue;
            }
            // 在该方法上查找到该注解
            System.out.println("userId:" + addAnnotation.userId());
            System.out.println("userName:" + addAnnotation.userName());
            System.out.println("arrays:" + addAnnotation.arrays());
            System.out.println();
        }
    }

}

现在有个User类,类中add方法我们添加了注解,接下来使用反射来操作User类,通过反射获得add方法,然后判断该方法上面是否使用了注解,如果使用了把注解的值打印出来

我们来看下打印的日志

####方法名称main
该方法上没有加注解..
####方法名称add
userId:18
userName:张三
arrays:[Ljava.lang.String;@6607db7d

####方法名称del
该方法上没有加注解..

接下来我们重点讲解手写@Transaction spring的事物注解

例如现在add方法上我们使用@ExtTransactional如何实现注解了。定义一个aop的切面类,在切面类中定义切点可以拦截到controller类的add方法被调用了。在切面中利用反射技术判断add方法上面是否使用了注解,如果使用了注解就开启事务,没有就不开启事务

第一步:需要将spring框架配置文件中的开启事务管理注解去掉

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://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/aop
         http://www.springframework.org/schema/aop/spring-aop.xsd
         http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx.xsd">

    <context:component-scan base-package="com.itmayiedu"></context:component-scan>
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy> <!-- 开启事物注解 -->

    <!-- 1. 数据源对象: C3P0连接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>

    <!-- 2. JdbcTemplate工具类实例 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 3.配置事务 -->
    <bean id="dataSourceTransactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

</beans>

 去掉:

    <!-- 开启注解事务 -->
    <tx:annotation-driven transaction-manager="dataSourceTransactionManager" />
第二步:自定义一个注解
package com.itmayiedu.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 事务注解 设置传播行为
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtTransaction {

}

第三步:自定义一个事务管理工具的实现类,这里使用的是jdbc的事务管理器来实现对事务的管理

package com.itmayiedu.transaction;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;

// 编程事务(需要手动begin 手动回滚 手都提交)
@Component
@Scope("prototype") // 每个事务都是新的实例 目的解决线程安全问题 多例子
public class TransactionUtils {

    // 全局接受事务状态
    private TransactionStatus transactionStatus;
    // 获取事务源
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    // 开启事务
    public TransactionStatus begin() {
        System.out.println("开启事务");
        transactionStatus = dataSourceTransactionManager.getTransaction(new DefaultTransactionAttribute());
        return transactionStatus;
    }

    // 提交事务
    public void commit(TransactionStatus transaction) {
        System.out.println("提交事务");
        dataSourceTransactionManager.commit(transaction);
    }

    // 回滚事务
    public void rollback() {
        System.out.println("回滚事务...");
        dataSourceTransactionManager.rollback(transactionStatus);
    }

}

上面这个类有几个地方需要注意的,dao层使用的是jdbcTemplate,使用jdbc的方式,所以要使用jbdc数据源的事务管理器

DataSourceTransactionManager,其他框架hibernate的事务管理器为

Spring声明式事务管理器类:
Jdbc技术:DataSourceTransactionManager
Hibernate技术:HibernateTransactionManager

第二:对于事务管理工具类一个事务应该对于一个事务管理工具类的实例,spring默认代理对象都是单例模式,如果是单例模式多个事务就是多个线程共享一个对象,会出现线程安全问题

所以@Scope("prototype") // 每个事务都是新的实例 目的解决线程安全问题 多例子,定义为多实例类型

接下来,定位aop实现对调用方法的拦截,实现事务管理

package com.itmayiedu.aop;

import java.lang.reflect.Method;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.TransactionAspectSupport;

import com.itmayiedu.annotation.ExtTransaction;
import com.itmayiedu.transaction.TransactionUtils;

//  自定义事务注解具体实现
@Aspect
@Component
public class AopExtTransaction {
    // 一个事务实例子 针对一个事务
    @Autowired
    private TransactionUtils transactionUtils;

    // 使用异常通知进行 回滚事务
    @AfterThrowing("execution(* com.itmayiedu.service.*.*.*(..))")
    public void afterThrowing() {
        // 获取当前事务进行回滚
        // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        transactionUtils.rollback();
    }

    // 环绕通知 在方法之前和之后处理事情
    @Around("execution(* com.itmayiedu.service.*.*.*(..))")
    public void around(ProceedingJoinPoint pjp) throws Throwable {

        // 1.获取该方法上是否加上注解
        ExtTransaction extTransaction = getMethodExtTransaction(pjp);
        TransactionStatus transactionStatus = begin(extTransaction);
        // 2.调用目标代理对象方法
        pjp.proceed();
        // 3.判断该方法上是否就上注解
        commit(transactionStatus);
    }

    private TransactionStatus begin(ExtTransaction extTransaction) {
        if (extTransaction == null) {
            return null;
        }
        // 2.如果存在事务注解,开启事务
        return transactionUtils.begin();
    }

    private void commit(TransactionStatus transactionStatus) {
        if (transactionStatus != null) {
            // 5.如果存在注解,提交事务
            transactionUtils.commit(transactionStatus);
        }

    }

    // 获取方法上是否存在事务注解
    private ExtTransaction getMethodExtTransaction(ProceedingJoinPoint pjp)
            throws NoSuchMethodException, SecurityException {
        String methodName = pjp.getSignature().getName();
        // 获取目标对象
        Class<?> classTarget = pjp.getTarget().getClass();
        // 获取目标对象类型
        Class<?>[] par = ((MethodSignature) pjp.getSignature()).getParameterTypes();
        // 获取目标对象方法
        Method objMethod = classTarget.getMethod(methodName, par);
        ExtTransaction extTransaction = objMethod.getAnnotation(ExtTransaction.class);
        return extTransaction;
    }

}
package com.itmayiedu.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class UserDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void add(String name, Integer age) {
        String sql = "INSERT INTO t_users(NAME, age) VALUES(?,?);";
        int updateResult = jdbcTemplate.update(sql, name, age);
        System.out.println("updateResult:" + updateResult);
    }

}
package com.itmayiedu.service;

//user 服务层
public interface UserService {

    public void add();

    public void del();
}

接下来在实现类使用

 @ExtTransaction就实现了我们的自定义事务注解
package com.itmayiedu.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.itmayiedu.dao.UserDao;
import com.itmayiedu.service.LogService;
import com.itmayiedu.service.UserService;

//user 服务层
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
@Autowired
    private TransactionUtils transactionUtils;

  
    @ExtTransactionpublic void add() {
        userDao.add("test001", 20);
        // int i = 1 / 0;
        System.out.println("################");
        userDao.add("test002", 21);

    }
    // 方法执行完毕之后,才会提交事务

    public void del() {
        System.out.println("del");
    }

}

 接下来我们分析下事务的传播特性,我们使用spring框架默认自带的注解

我们在配置文件中开启事务管理,然后将上面我们自己编写的AopExtTransaction事务框架代码屏蔽掉,以免和spring默认的框架起冲突

注意:spring的事务传播特性是在两个不同的service中相互调用

我们在新建一个dao

package com.itmayiedu.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class LogDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void add(String name) {
        String sql = "INSERT INTO t_log(log_name) VALUES(?);";
        int updateResult = jdbcTemplate.update(sql, name);
        System.out.println("##LogDao##updateResult:" + updateResult);
    }

}
package com.itmayiedu.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.itmayiedu.dao.LogDao;
import com.itmayiedu.service.LogService;

@Service
public class LogServiceImpl implements LogService {
    @Autowired
    private LogDao logDao;

    @Transactional
    public void addLog() {
        logDao.add("addLog" + System.currentTimeMillis());
        // int i = 1 / 0;
    }

}
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://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/aop
         http://www.springframework.org/schema/aop/spring-aop.xsd
         http://www.springframework.org/schema/tx
          http://www.springframework.org/schema/tx/spring-tx.xsd">

    <context:component-scan base-package="com.itmayiedu"></context:component-scan>
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy> <!-- 开启事物注解 -->

    <!-- 1. 数据源对象: C3P0连接池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>
    </bean>

    <!-- 2. JdbcTemplate工具类实例 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 3.配置事务 -->
    <bean id="dataSourceTransactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!-- 开启注解事务 -->
    <tx:annotation-driven transaction-manager="dataSourceTransactionManager" />
</beans>
 使用spring框架的默认事务管理,这里需要开启注解事务管理的配置

<!-- 开启注解事务 -->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" />

PROPAGATION_REQUIRED—如果当前有事务,就用当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。

对应的就是在addLog()方法上面使用@Transactional注解

 

@Transactional
public void addLog() {
logDao.add("addLog" + System.currentTimeMillis());

}

@Transactional
public void add() {
// 调用接口的时候 接口失败 需要回滚,但是日志记录不需要回滚。
logService.addLog(); // 后面程序发生错误,不能影响到我的回滚### 正常当addLog方法执行完毕,就应该提交事务
userDao.add("test001", 200);
// int i = 1 / 0;
System.out.println("################");
userDao.add("test002", 21);

}

我们来看下运行的情况

第一种情况:正常情况下,事务正常 log日志和人员信息都会添加成功

第二种情况,如果在addLog方法中发送了异常

@Transactional
public void addLog() {
logDao.add("addLog" + System.currentTimeMillis());
int i = 1 / 0;
}

这个时候因为addLog中发送了异常,这个时候就会把异常抛出去给aop框架处理,后续的代码就不会再继续执行

就不会再执行

logService.addLog(); // 后面程序发生错误,不能影响到我的回滚### 正常当addLog方法执行完毕,就应该提交事务
userDao.add("test001", 200);
// int i = 1 / 0;
System.out.println("################");
userDao.add("test002", 21);

因为addLOg方法出现了异常就不会再执行后续的userDao.add("test001", 200);方法,抛出异常的时候aop框架会对事务进行回滚,所以ogDao.add("addLog" + System.currentTimeMillis());不会添加数据到数据库中

第三种情况

如果在addLog方法中发送了异常

@Transactional
public void addLog() {
logDao.add("addLog" + System.currentTimeMillis());
int i = 1 / 0;
}

但是在userService的add方法中使用了try catch进行捕获,我们来看下数据库的情况

@Transactional(rollbackFor=Exception.class)
    public void add() {
        // 调用接口的时候 接口失败 需要回滚,但是日志记录不需要回滚。
        
        try{
            logService.addLog(); 
        }catch(Exception e){
            System.out.println(e.toString());
        }
        
        userDao.add("test001", 200);
        // int i = 1 / 0;
        System.out.println("################");
        userDao.add("test002", 21);

    }

我们来看下这种情况下的运行情况

运行居然抛出了异常

Exception in thread "main" org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:717)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:393)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
at com.sun.proxy.$Proxy13.add(Unknown Source)
at com.itmayiedu.Test001.main(Test001.java:12)

数据库中没有插入任何数据,为啥出现上面这种情况了

##Transaction rolled back because it has been marked as rollback-only spring 具备多种事务传播机制,最常用的是REQUIRED,即如果不存在事务,则新建一个事务;如果存在事务,则加入现存的事务中。 示例代码如下:

public void A() {
querySomething(...);
try {
B()
} catch () {
}
saveSomethinf();
}

public void B() {
throw Exception()
}
此时B会和A存在一个事务中。如果B抛出异常没有捕获,即使在A中捕获并处理,仍会发生异常:Transaction rolled back because it has been marked as rollback-only 因为spring会在A捕获异常之前提前捕获到异常,并将当前事务设置为rollback-only,而A觉得对异常进行了捕获,它仍然继续commit,当TransactionManager发现状态为设置为rollback-only时, 则会抛出UnexpectedRollbackException 相关代码在AbstractPlatformTransactonManager.java中:

public final void commit(TransactionStatus status) throws TransactionException {
if (status.isCompleted()) {
throw new IllegalTransactionStateException(
"Transaction is already completed - do not call commit or rollback more than once per transaction");
}

DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
processRollback(defStatus);
return;
}
if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
}
processRollback(defStatus);
// Throw UnexpectedRollbackException only at outermost transaction boundary
// or if explicitly asked to.
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
return;
}

processCommit(defStatus);
}
解决方法: 在抛出异常的最原始地方处理异常,即在spring捕获到异常之前处理掉

在一个transactional中如果有另一transaction,内层的事务会跟随外层的事务,变为一个整体的事务。

如果内层transaction发生了异常,即使你捕捉了这个异常,那么整体的Transaction也会被定义成RollbackOnly,这也正是事务管理的原则。所以当外层事务想提交整个事务时,出现异常。

解决版本就是在logService中对异常进行try catch处理

@Transactional
    public void addLog() {
        try{
            logDao.add("addLog" + System.currentTimeMillis());
            int i = 1 / 0;
        }catch(Exception e){
            
        }

    }
@Transactional(rollbackFor=Exception.class)
    public void add() {
        // 调用接口的时候 接口失败 需要回滚,但是日志记录不需要回滚。
        
            logService.addLog(); 

        
        userDao.add("test001", 200);
        // int i = 1 / 0;
        System.out.println("################");
        userDao.add("test002", 21);

    }

上面这种情况:log日志和人员信息都会添加到数据库中, int i = 1 / 0;发现了异常但是我们自定义了try catch处理异常,就不会把异常抛出去给aop框架进行处理,对数据进行回滚

因为logService.addLog(); 对外没有抛出异常,后续的 userDao.add("test001", 200);代码也会正常执行,把人员数据添加到数据库中

第五情况在add添加人员方法中抛出了异常

    @Transactional
    public void addLog() {
            logDao.add("addLog" + System.currentTimeMillis());
            int i = 1 / 0;


    }
    @Transactional(rollbackFor=Exception.class)
    public void add() {
        // 调用接口的时候 接口失败 需要回滚,但是日志记录不需要回滚。
        
            logService.addLog(); 
        userDao.add("test001", 200);
        int i = 1 / 0;
        System.out.println("################");
        userDao.add("test002", 21);

    }

在调用add方法中抛出了异常,因为当前事务addLog的事务传播属性是   @Transactional,PROPAGATION_REQUIRED—如果当前有事务,就用当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。

所以logService.addLog();和userDao.add("test001", 200);是在同一个事务中,当发现异常的时候,如果没有对异常使用try catch进行处理,会把异常抛出去给aop框架进行处理

异常前面的

 logService.addLog(); 和 userDao.add("test001", 200);都会回滚


原文地址:https://www.cnblogs.com/kebibuluan/p/10708104.html