Spring的AOP和事务是如何工作的?

Spring的AOP和事务是如何工作的?,第1张

前期具体描述,可前往查看Spring是如何创建对象的

1、 代码准备

现有如下类:

@Aspect
@Component
public class TestAspect {
    @Before("execution(public void com.test.service.UserService.test())")
    public void testBefore(JoinPoint joinPoint){
        System.out.println("beforeTest");
    }
}
@Component
public class UserService{
	@Autowired
    private OrderService orderService;

    public void test(){
        System.out.println(orderService);
    }
}
@ComponentScan("com.test")
@EnableAspectJAutoProxy
public class AppConfig {

}
public class Test {
    public static void main(String[] args){
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        UserService userService = (UserService)applicationContext.getBean("userService");
        userService.test();
    }
}
2、Spring的AOP是如何工作的

在Spring是如何创建对象一文中,提到了两个对象,一个是普通对象,一个是代理对象。
那就以上代码,getBean获取到的应该是哪个对象呢?
可以运行程序打断点看下

可以看到,这里是一个CGLIB产生的代理对象

在UseService中,注入了OrderService,产生的这个UserService代理对象中,orderService是否有值呢?

OrderService已经加了Autowired注解,为什么没有值呢???
之前提到,创建bean的流程如下:
userService -----> 推断构造方法 -----> 普通对象-----> 依赖注入----->初始化前(@PostConstruct)----->初始化(InitializingBean)----->初始化后(AOP)----->代理对象----->放入单例池map ----->Bean 对象
现在拿到的是userService的代理对象,可以看到代理对象是在初始化后生成的,生成之后spring并没有再根据代理对象进行依赖注入,所以这个时候代理对象的属性(比如orderService)为空

那这样是不是就有问题,拿不到orderService的值呢??,接着刚才的代码继续运行


可以看到,在test方法中,orderService又有值了,为什么会出现这种现象呢?

因为UserService是通过CGLIB产生的代理对象
而CGLIB是基于父子类来进行动态代理的。首先动态生成代理类(代理父类),然后生成代理对象,代理类用伪代码可以表示如下:

class  UserServiceProxy extends UserService{
}

UserServiceProxy 就是UserService的代理对象,从spring中拿到的也是这个对象

当调用代理对象的test方法时,UserServiceProxy.test(),如果不做任何处理的话,会直接执行到父类的test方法(UserServiceProxy是子类,没有显示声明test()方法,则会执行父类的),但是代码中定义了切面,这样执行的话,不会执行到@Before的切面方法。
那需要在代理类中需要做一些文章。

class  UserServiceProxy extends UserService{
	public void test(){
		//@Before切面逻辑
		//父类test方法 super.test()
	}
}

这样处理完成之后就可以了么?
现在还是不符合前面的实际场景,目前执行到test()方法的时候,orderService是空的,但上面运行代码看到的的确确是有值的,

其实在spring的AOP中,会有某些场景使用的是super.test()(后续文章会介绍什么场景会使用,尽请关注),但是大部分情况下都不是用的super.test()

class  UserServiceProxy extends UserService{
	UseService target; //普通对象(userService -----> 推断构造方法 -----> 普通对象-----> 依赖注入.............)
	public void test(){
		//@Before切面逻辑
		//target.test() 生命周期中普通对象的test方法
	}
}

因为普通对象是经过依赖注入的,它的orderService属性是有值的,所以调用target.test就会打印出orderService的值

ps:userService -----> 推断构造方法 -----> 普通对象-----> 依赖注入----->初始化前(@PostConstruct)----->初始化(InitializingBean)----->初始化后(AOP)----->代理对象----->放入单例池map ----->Bean 对象



可以看到,生成的代理对象,target和最终执行test方法的是同一个对象(对象地址是一样的)

3、Spring事务底层是怎么工作的

对上述类稍微进行变动,开启事务,进行JDBC连接

@ComponentScan("com.test")
@EnableTransactionManagement
public class AppConfig {

    @Bean
    public JdbcTemplate jdbcTemplate(){
        return new JdbcTemplate(dateSource());
    }

    @Bean
    public PlatformTransactionManager transactionManager(){
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dateSource());
        return transactionManager;
    }

    @Bean
    public DataSource dateSource(){
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8&useSSL=false");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }
}
@Component
public class UserService{
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void test(){
        //执行SQL语句

    }
}

现在有code_test表,里面没有数据

在代码中执行插入SQL,抛出异常,看下是否可以真正插入数据库

@Component
public class UserService{
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void test(){
        //执行SQL语句
        jdbcTemplate.execute("insert into code_test values(1,1,1,1)");
        throw new NullPointerException();
    }
}

到这里可能有人会认为,抛了异常,数据会回滚,不会插入数据库
运行下程序,然后查询数据


发现数据已经插入到数据库中了。
先不解释为什么,先看下怎么做可以回滚呢?
在APPConfig类上面加上注解:@Configuration(下面有讲解二者关系)

删掉数据库中的内容,再次执行代码。发现没有数据插入到数据库中。

这里先分析下Spring事务是如何工作的。
Spring事务也是基于AOP的,需要在方法上面加了 @Transactional 注解。
上面已经了解到,getBean拿到的是代理对象,在调用test方法的时候,就会进到代理对象的test方法中(代理对象的test方法会有一些自己的逻辑处理)。

这里还是以伪代码的形式来解释下单个Spring事务的逻辑

class  UserServiceProxy extends UserService{
	UseService target; 
	
	public void test(){
		//1、判断有没有Transactional注解
		//2、用配置的事务管理器(PlatformTransactionManager)新建一个数据库连接conn
		//3、conn.autoCommit = false(关闭自动提交)
		//4、target.test() 普通对象.test()
		//5、无异常 conn.commit()  有异常 conn.rollback()
	}
}

为什么要用事务管理器建立连接,而不是在真正执行SQL的时候建立连接呢?
假设在真正执行SQL的时候建立连接,在代码中执行了多次SQL
jdbcTemplate.execute(“insert into code_test values(1,1,1,1)”);
jdbcTemplate.execute(“insert into code_test values(1,1,1,1)”);
jdbcTemplate.execute(“insert into code_test values(1,1,1,1)”);
这种情况下,执行第一条语句,建立一个连接,执行完成,本次调用结束
执行第二条语句,建立一个连接,执行完成,本次调用结束…以此类推。
那么对于这三条SQL,由于对应三个不同的数据库连接,所以就不在同一个事务里面了,很明显,这样并不符合spring事务的需求。同一个方法中的多条SQL应该是共用一个SQL连接的,这样才有可能共用一个事务。

在日常的工作中,会发现,有时spring事务会失效,事务失效的根本原因是什么呢?
下面来举例说明:

@Component
public class UserService{
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void test(){
        //执行SQL语句
        jdbcTemplate.execute("insert into code_test values(1,1,1,1)");
        test2();
    }

    @Transactional(propagation = Propagation.NEVER)
    public void test2(){
        //执行SQL语句
        jdbcTemplate.execute("insert into code_test values(2,2,2,2)");
    }
}

在userService中又增加了test2方法,增加事务注解,传播机制是NEVER(NEVER 事务传播类 表示以非事务的方式执行, 如果当前 *** 作存在事务,则抛出异常。),在test中调用test2(),就目前来理解,因为test()带了事务,而NEVER会抛异常,但是运行之后,正常跑完了。

为什么会出现这种现象呢?
还是下面的逻辑

class  UserServiceProxy extends UserService{
	UseService target; 
	
	public void test(){
		//1、判断有没有Transactional注解
		//2、事务管理器(PlatformTransactionManager)新建一个数据库连接conn
		//3、conn.autoCommit = false(关闭自动提交)
		//4、target.test() 普通对象.test()
		//5、无异常 conn.commit()  有异常 conn.rollback()
	}
}

首先我们是getBean拿到userService,然后调用userService的test方法,此时调用的是代理对象的test方法,然后按照逻辑一步步往下走,在调用到target.test()的时候,由于这个时候调用的是普通对应的test方法,然后调用test2()方法时,目前的写法相当于this.test2(),由于是普通对象,所以并不会判断有没有事务注解,而是直接调用了方法。只有代理对象在执行时代理逻辑才会判断有没有注解,决定走不走事务。
某个方法上面的事务注解(@Transactional)到底有没有用,就去看方法被调用的时候是哪个对象在调,只有代理对象在调用方法的时候,事务注解才会生效

如果希望方法test2上的注解生效,怎么处理呢?
①、可以再重新定义一个类(比如userBaseService),把方法test2迁移到userBaseService里面,在userService中注入userBaseService,使用userBaseService调用方法test2()
②、自己注入自己,在userService中注入userService,使用注入的userService调用方法test2()
两种方法都是为了使用代理对象调用方法

@Configuration注解为什么和Spring的事务有关系呢?

在上面的代码中,没有加@Configuration注解的时候,事务没有生效,而加了@Configuration之后事务就生效了,为什么会这样呢?

从代码表面理解:

结合上面的逻辑

要调用test方法,那么就jdbcTemplate需要先获取dateSource(基于事务管理器建立的连接),然后才能执行SQL

那么怎么获取事务管理器的dateSource连接呢?
      需要借助ThreadLocal,这也是为什么Spring事务多个线程中的SQL无法在同一个事务里面。
      事务管理器建立数据库连接之后,会存到ThreadLocal,由于考虑到存在多数据源的情况,所以存储的是map形式,,key是DateSource为了支持多数据源,value是建立的连接。
      然后jdbcTemplate从ThreadLocal里面获取连接,使用get的方式从map中获取,key是jdbcTemplate建立的dateSource(上面代码截图,jdbcTemplate和事务管理都生成了dateSource)。
      假设jdbcTemplate和事务管理器中的dateSource是同一个,那么就可以正常拿到连接去执行SQL。如果不是同一个,那么就get不到value,jdbcTemplate就会自己建立连接,而自己建立的连接autoCommit是true,执行完SQL就会提交
      没有加@Configuration注解时,就是异常情况,jdbcTemplate和事务管理器中的dateSource是两个对象,jdbcTemplate自己建立了连接,执行完SQL自动提交了。
      加了@Configuration注解之后,jdbcTemplate和事务管理器中的dateSource是同一个对象了,所以可以正常回滚。
      怎么实现的呢?其实和动态代理是有关系的。加了@Configuration后,APPConfig类会生成对应的代理对象,执行方法时是基于代理对象执行的,而代理对象就会有对应的代理逻辑,在获取dateSource时,会先判断Spring容器里面有没有对应的dateSource bean对象,如果有就直接返回,不会去获取连接,没有bean对象才会真正执行获取连接的方法。
      当前也不是绝对的,大家在平常的写代码的时候注意获取dateSource使用的是同一个方法,因为不同的方法名,在spring容器中是不同的对象。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/720481.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存