百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

类内部方法调用事务失效的原因

xsobi 2024-12-14 15:45 1 浏览

在spring工程中,一个类内部,a方法使用了@Transactional注解,b方法没有使用@Transactional注解,如果a方法体内调用了b方法,当先调用a方法时,b方法的事务不起作用 或者说 失效,这是为什么了?

首先,看看实验的例子。

依赖包:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.23</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.22</version>
    </dependency>

    <!-- Spring JDBC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.23</version>
    </dependency>
    <!-- MySQL Connector -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.23</version>
    </dependency>

</dependencies>

配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:p="http://www.springframework.org/schema/p"
       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-4.3.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">

    <!--<bean class="com.feng.ioc.model.User" id="userA">
        <property name="id" value="1"></property>
        <property name="name" value="feng"></property>
        <!–<property name="myName" value="feng"></property>–>
    </bean>-->
    
    <!--自动扫包-->
    <context:component-scan base-package="com.feng.aop"></context:component-scan>

    <!--开启自动生成-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/test_json?useSSL=false&serverTimezone=UTC" />
        <property name="username" value="root" />
        <property name="password" value="123456" />
    </bean>

    <!-- 配置 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

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

    <!-- 启用注解驱动的事务管理 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

目标类和接口:

public interface UserService {
    List<User> userList();
    Boolean addUser();
}



@Service("UserService")
public class UserServiceImpl implements UserService{

    @Autowired
    private JdbcTemplate jdbcTempleate;


    @Override
    public List<User> userList() {
        List<User> users = new ArrayList<User>();
        User user1 = new User(1, "aa");
        User user2 = new User(2, "bb");
        users.add(user1);
        users.add(user2);

        addUser();

        return users;
    }

    @Override
    @Transactional
    public Boolean addUser() {
        jdbcTempleate.execute("insert into user ( name) values (\"ccc\")");

        int i = 1/0;

        jdbcTempleate.execute("insert into user ( name) values (\"ddd\")");

        return true;
    }
}

切面类:

@Component
@Aspect
public class TimeAspect {
    @Around("execution(* com.feng.aop.service.UserServiceImpl.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long begin = System.currentTimeMillis();
        System.out.println("开始时间:" + begin);

        Object proceed = joinPoint.proceed();

        long end = System.currentTimeMillis();
        System.out.println("结束时间:" + end);

        System.out.println("使用时间:" + (end - begin));

        return proceed;
    }
}

主类:

public class AopTest {


    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        UserService userService = (UserService)context.getBean("UserService");
//        userService.addUser();
        try {
            userService.userList();
        } catch (RuntimeException e) {

        }

        // 将jdk生成的代理类输出
        byte[] bytes2 = ProxyGenerator.generateProxyClass("$Proxy21",new Class[]{UserService.class});
        FileOutputStream os2 = new FileOutputStream("Proxy21.class");
        os2.write(bytes2);
        os2.close();

    }
}

那么Spring工程启动后,动态创建代理对象的过程:

当spring执行到refresh()方法内的finishBeanFactoryInitialization(beanFactory)方法时,会实例化余下的(前面已经实例化了一些内置的类)所有非延时初始化的单例对象,在使用反射构造方法创建bean对象后,会调用initializeBean()方法初始化bean对象。

在initializeBean()方法内会调用各种初始化方法对bean对象的属性赋值,之后,会调用bean的后置处理器的postProcessAfterInitialization()方法对bean对象再处理,比如生成代理对象。(当然,在创建bean对象前,某些情况下的类也可以通过后置处理器创建代理对象,至于什么类,没做研究)

AnnotationAwareAspectJAutoProxyCreator这个bean后置处理器就是用来生成代理类并创建代理对象的。

在创建代理对象之前,会先查找advisors,哪些对象会成为advisors?

一是被@Aspect注解修饰的类,二是实现Advisor接口的类,除此之外,如果存在被@Aspect注解修饰的类,还会额外增加一个ExposeInvocationInterceptor.ADVISOR,这个类会放到advisors这个列表的第一位。也就是说,在本例中,我们有3个advisor。

查找好advisors后,就会创建ProxyFactory对象,再将advisors赋值给ProxyFactory对象的属性,由于我们的目标类使用了接口,Spring会调用JdkDynamicAopProxy类创建代理对象,在创建JdkDynamicAopProxy类的对象时,会将ProxyFactory对象传递给JdkDynamicAopProxy类的构造函数,所以JdkDynamicAopProxy对象也包含有advisors的信息,然后调用JdkDynamicAopProxy对象生成代理类字节码文件内容,再读入到JVM,创建代理类对象。

这是创建代理类对象的大概过程。

如果一个类有使用动态代理,那它的bean对象就不是由这个类创建的对象,而是由新生成的代理类创建的对象。调用的对象方法也是位于新生成的代理类,而不是原来的原始类。

生成代理类后,我们看看新生成代理类的class文件内容:

public final class $Proxy21 extends Proxy implements UserService {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
    private static Method m4;

    public $Proxy21(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        ....
    }

    public final List userList() throws  {
        try {
            return (List)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        ....
    }

    public final int hashCode() throws  {
        .....
    }

    public final Boolean addUser() throws  {
        try {
            return (Boolean)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.feng.aop.service.UserService").getMethod("userList");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m4 = Class.forName("com.feng.aop.service.UserService").getMethod("addUser");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

为缩小篇幅,我将一些不重要的方法体省略了。

从输出新生成的代理类字节码可以看出,每个方法的方法体主要功能就是调用JdkDynamicAopProxy对象的invoke()方法(属性h保存的就是JdkDynamicAopProxy对象),目标类的方法 和 切面类的方法都是通过这个invoke()方法作为入口去执行。先执行advisors列表第一个元素的invoke(),执行到最后一个元素的invoke()时,会调用目标类的方法。

既然每个方法都使用相同的JdkDynamicAopProxy对象,那么JdkDynamicAopProxy对象里的advisors是不是对每个方法都起作用了?

不是的,在invoke()方法里会根据传递进来的方法反射信息进行筛选,由于目标类的userList方法没有使用@Transactional注解,所以先执行代理类的userList方法时就没有与事务相关的advisor,只有先执行代理类的addUser方法时才有事务相的advisor。

筛选好advisor后,就层层递归调用advisor的方法,最后才调用目标类对象的方法,如果目标类的方法之间有调用,也是目标类对象内部的方法调用,而不是新生成的代理类对象内部的方法调用,所以,在spring工程中,当一个没带@Transactional注解的方法调用一个带@Transactional注解的方法时,事务不起作用。

相关推荐

好用的云函数!后端低代码接口开发,零基础编写API接口

前言在开发项目过程中,经常需要用到API接口,实现对数据库的CURD等操作。不管你是专业的PHP开发工程师,还是客户端开发工程师,或者是不懂编程但懂得数据库SQL查询,又或者是完全不太懂技术的人,通过...

快速上手:Windows 平台上 cURL 命令的使用方法

在工作流程中,为了快速验证API接口有效性,团队成员经常转向直接执行cURL命令的方法。这种做法不仅节省时间,而且促进了团队效率的提升。对于使用Windows系统的用户来说,这里有一套详细...

使用 Golang net/http 包:基础入门与实战

简介Go的net/http包是构建HTTP服务的核心库,功能强大且易于使用。它提供了基本的HTTP客户端和服务端支持,可以快速构建RESTAPI、Web应用等服务。本文将介绍ne...

#小白接口# 使用云函数,人人都能编写和发布自己的API接口

你只需编写简单的云函数,就可以实现自己的业务逻辑,发布后就可以生成自己的接口给客户端调用。果创云支持对云函数进行在线接口编程,进入开放平台我的接口-在线接口编程,设计一个新接口,设计和配置好接口参...

极度精神分裂:我家没有墙面开关,但我虚拟出来了一系列开关

本内容来源于@什么值得买APP,观点仅代表作者本人|作者:iN在之前和大家说过,在iN的家里是没有墙面开关的。...

window使用curl命令的注意事项 curl命令用法

cmd-使用curl命令的注意点前言最近在cmd中使用curl命令来测试restapi,发现有不少问题,这里记录一下。在cmd中使用curl命令的注意事项json不能由单引号包括起来json...

Linux 系统curl命令使用详解 linuxctrl

curl是一个强大的命令行工具,用于在Linux系统中进行数据传输。它支持多种协议,包括HTTP、HTTPS、FTP等,用于下载或上传数据,执行Web请求等。curl命令的常见用法和解...

Tornado 入门:初学者指南 tornados

Tornado是一个功能强大的PythonWeb框架和异步网络库。它最初是为了处理实时Web服务中的数千个同时连接而开发的。它独特的Web服务器和框架功能组合使其成为开发高性能Web...

PHP Curl的简单使用 php curl formdata

本文写给刚入PHP坑不久的新手们,作为工具文档,方便用时查阅。CURL是一个非常强大的开源库,它支持很多种协议,例如,HTTP、HTTPS、FTP、TELENT等。日常开发中,我们经常会需要用到cur...

Rust 服务器、服务和应用程序:7 Rust 中的服务器端 Web 应用简介

本章涵盖使用Actix提供静态网页...

我给 Apache 顶级项目提了个 Bug apache顶级项目有哪些

这篇文章记录了给Apache顶级项目-分库分表中间件ShardingSphere提交Bug的历程。说实话,这是一次比较曲折的Bug跟踪之旅。10月28日,我们在GitHub上提...

linux文件下载、服务器交互(curl)

基础环境curl命令描述...

curl简单使用 curl sh

1.curl--help#查看关键字2.curl-A“(添加user-agent<name>SendUser-Agent<name>toserver)”...

常用linux命令:curl 常用linux命令大全

//获取网页内容//不加任何选项使用curl时,默认会发送GET请求来获取内容到标准输出$curlhttp://www.baidu.com//输出<!DOCTYPEh...

三十七,Web渗透提高班之hack the box在线靶场注册及入门知识

一.注册hacktheboxHackTheBox是一个在线平台,允许测试您的渗透技能和代码,并与其他类似兴趣的成员交流想法和方法。它包含一些不断更新的挑战,并且模拟真实场景,其风格更倾向于CT...