一、Spring框架的单例bean是线程安全的吗?
1、Spring框架中的bean是单例的吗?
spring框架中的bean是单例的,在默认情况下是singleton模式,即单例模式。如果需要更改则可以在@Scope注解设置为prototype为多例模式。
- singleton:bean在每个Spring IOC容器中只有一个实例。
- prototype:一个bean的定义可以有多个实例。
2、Spring框架中的单例bean是线程安全的吗?
如上图的代码,在"getById/{id}"请求中,count变量是成员变量,会受到多个用户同时修改,就会出现线程安全问题。但UserService userService不会出现线程安全问题,因为userService是无状态bean,不能被修改。
Spring bean并没有可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。
3、概括
Spring框架中的单例bean是线程安全的吗?
- 不是线程安全的。
- Spring框架中有一个@Scope注解,默认的值就是singleton,单例的。因为一般在spring的bean的中都是注入无状态的对象,没有线程安全问题,如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决 。
4、面试官: Spring框架中的单例bean是线程安全的吗?
- 不是线程安全的,是这样的。
- 当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
- Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。
- 比如:我们通常在项目中使用的Spring bean都是不可可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。
- 如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。
二、AOP相关面试题
1、什么是AOP,你们项目中有没有使用到AOP ?
- AOP称为面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。
2、场景的AOP使用场景
- 记录操作日志
- 缓存处理
- Spring中内置的事务处理
3、记录操作日志思路
这种情况可以使用Spring的AOP思想来解决问题。记录日志需要记录当前用户的各种信息到数据库的日志表中去,因此可以在不改变原代码的情况下,使用AOP封装一个模块来对此操作。
大概思路就是创建一个切面类,找到要添加相应功能的类的地址,然后在切面类中对要添加的功能进行封装,可以使用前置通知,后置通知,异常通知,返回通知和环绕通知对次进行添加。一般而言使用环绕通知,获取要添加功能的类的相应信息,比如添加用户,就可以获取添加用户的用户名,请求方式,访问地址等待来将它们添加到数据库的日志表中。
4、Spring事务是如何实现的?
编程式事务
- 通过TransactionTemplate或TransactionManager手动管理事务,因为是手动,所以实际开发中很少使用。
声明式事务(主要)
- 声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
如上代码,比如要保存一个用户,其中的Object proceed=joinPoint.proceed();方法就是执行保存用户的代码,在代码之前会开启事务,然后在保存之后是提交事务。如果出现异常会在catch代码块执行回滚事务,这是Spring的事务的原理。
如果需要开启事务,就可以使用 @Transactional注解添加到方法上进行开启。
5、概括
什么是AOP?
- 面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合
你们项目中有没有使用到AOP?
- 记录操作日志,缓存,spring实现的事务核心是:使用aop中的环绕通知+切点表达式(找到要记录日志的方法)通过环绕通知的参数获取请求方法的参数(类、方法、注解、请求方式等),获取到这些参数以后,保存到数据库
Spring中的事务是如何实现的?
- 其本质是通过AOP功能,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务
三、事务失效的场景
1、Spring中事务失效的场景有哪些
- 异常捕获处理
- 抛出检查异常
- 非public方法
2、异常捕获处理实例
如上图代码,这是正常转帐的情况
- 这是执行前的数据
- 这是代码执行后的数据
如上图,进行模拟异常,那么就会事务回滚
- 这是执行结果:事务会回滚
如上图代码,这是将try-catch包裹异常的代码
这是执行后的数据:事务没有被回滚
原因:
- 事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉。
这是Spring的事务底层,AOP层无法捕获到异常就不会进行回滚,因为业务层已经将异常捕获了。
解决方法:
在catch块添加throw new RuntimeException(e)抛出
2、抛出检查异常
如上图,代码抛出了 FileNotFoundException异常,这个是检查异常
运行前结果:
运行后结果:事务没有回滚
原因:
- Spring 默认只会回滚非检查异常
解决:
- 配置rollbackFor属性
- @Transactional(rollbackFor=Exception.class)
3、非public方法导致的事务失效
如上代码,访问权限修饰为default,不是public
运行前数据:
运行后数据:事务失效
原因:
- Spring为方法创建代理、添加事务通知、前提条件都是该方法是 public的
解决方法:
- 改为 public方法
4、概括
Spring中事务失效的场景有哪些
- 异常捕获处理,自己处理了异常,没有抛出,解决: 手动抛出
- 抛出检查异常,配置rollbackFor属性为Exception
- 非public方法导致的事务失效,改为public
四、Bean的生命周期
1、BeanDefinition
有三种面向资源的bean定义信息读取方式,常用的方式:
- XML方式配置
- 会将xml配置的<bean>的信息封装成一个BeanDefinition对象,Spring根据BeanDefinition来创建Bean对象,里面有很多的属性用来描述Bean。
- 面向注解
- @component
- @import
- @bean
这是BeanDefinition的类的方法
其中比较重要的有:
- beanClassName: bean 的类名
- initMethodName:初始化方法名称
- properryValues: bean 的属性值
- scope:作用域
- lazylnit:延迟初始化
2、Bean生命周期
Bean生命周期主要分为三个大阶段分别为:
- 生产
- 使用
- 销毁
3、生产阶段
首先通过loadBeanDefinitions()方法,用xml、注解等方法将定义的Bean类全部都找出来,并且放到beanDefinitionMap集合中。
通过遍历beanDefinitionMap集合,然后使用creatBean()方法创建一个个Bean对象,创建Bean分为构造对象、填充属性、初始化实例、注册销毁四个步骤。
首先,对于构造对象,通过createBeanInstance()方法进行对象的构造,先用反射机制从beanDefinitionMap中的BeanClass拿到这个类的构造方法。如果有多个构造方法,那么就会造成可读性低,理解和维护困难。对于获取构造方法有响应的规则:
- 如果Bean只有一个构造方法,那么就使用该构造方法。
- 如果Bean有多个构造方法,那么就会优先选择有@Autowired注解的构造方法,如果多个构造方法都有@Autowired,那么就会报错。
- 如果构造方法没有@Autowired注解,那么会优先使用无参数的构造方法,如果多个构造方法都有参数,那么就会报错。
在确定构造方法之后,就需要准备构造方法的参数了,首先在Bean的单例池中根据参数的Class类进行查找 ,如果这个类有多个实例,则会根据参数名进行匹配。如果没有找到,就会认为构造信息不完整之间报错。在参数准备好以后就可以通过反射进行Bean的构造了,即实例化。如果是无参构造,则无需参数之间构造。
接下来是进行属性填充,通过调用populateBean()方法为Bean内部的所需的属性进行复制填充,通常就是@Autowired注解这些变量,通过三级缓存机制进行填充,三级缓存就是依赖注入。
然后是初始化实例。初始化第一步是初始化容器相关信息,通过invokeAwareMethods方法为实现个各种Aware接口的Bean设置如beanName、beanFactory等容器信息。然后通过invokeInitMethod方法执行Bean的初始化方法,通过实现initializingBean接口而实现的afterPropertiesSet()方法。方法名afterPropertiesSet就代表"Bean填充属性后"执行。最后再执行Bean中自定义的initMethod()方法。
在执行初始化方法之前和之后还需要对Bean后置处理器BeanPostProcessors进行处理。在Spring内部使用了很多后置处理器,比较典型的是当一个类被增强了使用到了AOP,那么这个类通常使用后置处理器来增强的。
4、使用阶段
因此到了BeanPostProcessors这一步之后,基本Bean实例已经被创建好了,并且可以使用。
5、销毁阶段
对于销毁,通过实现DisposableBean接口执行destory方法进行销毁。
6、概括
Spring的bean的生命周期
- 通过BeanDefinition获取bean的定义信息
- 调用构造函数实例化bean
- bean的依赖注入
- 处理Aware接口(BeanNameAware、BeanFactoryAware、ApplicationContextAware)
- Bean的后置处理器BeanPostProcessor-前置
- 初始化方法(lnitializingBean、init-method)
- Bean的后置处理器BeanPostProcessor-后置
- 销毁bean