前期具体描述,可前往查看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方法的是同一个对象(对象地址是一样的)
对上述类稍微进行变动,开启事务,进行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注解的时候,事务没有生效,而加了@Configuration之后事务就生效了,为什么会这样呢?
从代码表面理解:
结合上面的逻辑
要调用test方法,那么就jdbcTemplate需要先获取dateSource(基于事务管理器建立的连接),然后才能执行SQL
那么怎么获取事务管理器的dateSource连接呢?
需要借助ThreadLocal,这也是为什么Spring事务多个线程中的SQL无法在同一个事务里面。
事务管理器建立数据库连接之后,会存到ThreadLocal,由于考虑到存在多数据源的情况,所以存储的是map形式,
然后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容器中是不同的对象。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)