赞
踩
目录
4、面试官: Spring框架中的单例bean是线程安全的吗?
3、SpringBoot 是如何实现自动装配的?如何实现按需加载?
3.1、@EnableAutoConfiguration:实现自动装配的核心注解
spring框架中的bean是单例的,在默认情况下是singleton模式,即单例模式。如果需要更改则可以在@Scope注解设置为prototype为多例模式。
如上图的代码,在"getById/{id}"请求中,count变量是成员变量,会受到多个用户同时修改,就会出现线程安全问题。但UserService userService不会出现线程安全问题,因为userService是无状态bean,不能被修改。
Spring bean并没有可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。
Spring框架中的单例bean是线程安全的吗?
这种情况可以使用Spring的AOP思想来解决问题。记录日志需要记录当前用户的各种信息到数据库的日志表中去,因此可以在不改变原代码的情况下,使用AOP封装一个模块来对此操作。
大概思路就是创建一个切面类,找到要添加相应功能的类的地址,然后在切面类中对要添加的功能进行封装,可以使用前置通知,后置通知,异常通知,返回通知和环绕通知对次进行添加。一般而言使用环绕通知,获取要添加功能的类的相应信息,比如添加用户,就可以获取添加用户的用户名,请求方式,访问地址等待来将它们添加到数据库的日志表中。
编程式事务
声明式事务(主要)
如上代码,比如要保存一个用户,其中的Object proceed=joinPoint.proceed();方法就是执行保存用户的代码,在代码之前会开启事务,然后在保存之后是提交事务。如果出现异常会在catch代码块执行回滚事务,这是Spring的事务的原理。
如果需要开启事务,就可以使用 @Transactional注解添加到方法上进行开启。
什么是AOP?
你们项目中有没有使用到AOP?
Spring中的事务是如何实现的?
如上图代码,这是正常转帐的情况
如上图,进行模拟异常,那么就会事务回滚
如上图代码,这是将try-catch包裹异常的代码
这是执行后的数据:事务没有被回滚
原因:
这是Spring的事务底层,AOP层无法捕获到异常就不会进行回滚,因为业务层已经将异常捕获了。
解决方法:
在catch块添加throw new RuntimeException(e)抛出
2、抛出检查异常
如上图,代码抛出了 FileNotFoundException异常,这个是检查异常
运行前结果:
运行后结果:事务没有回滚
原因:
解决:
如上代码,访问权限修饰为default,不是public
运行前数据:
运行后数据:事务失效
原因:
解决方法:
Spring中事务失效的场景有哪些
BeanDefinition的作用是保存Bean的相关信息,包括属性、构造函数参数值等等
有三种面向资源的bean定义信息读取方式,常用的方式:
这是BeanDefinition的类的方法
其中比较重要的有:
Bean生命周期主要分为三个大阶段分别为:
首先通过loadBeanDefinitions()方法,用xml、注解等方法将定义的Bean类全部都找出来,并且放到beanDefinitionMap集合中。
通过遍历beanDefinitionMap集合,然后使用creatBean()方法创建一个个Bean对象,创建Bean分为构造对象、填充属性、初始化实例、注册销毁四个步骤。
首先,对于构造对象,通过createBeanInstance()方法进行对象的构造,先用反射机制从beanDefinitionMap中的BeanClass拿到这个类的构造方法。如果有多个构造方法,那么就会造成可读性低,理解和维护困难。对于获取构造方法有响应的规则:
在确定构造方法之后,就需要准备构造方法的参数了,首先在Bean的单例池中根据参数的Class类进行查找 ,如果这个类有多个实例,则会根据参数名进行匹配。如果没有找到,就会认为构造信息不完整之间报错。在参数准备好以后就可以通过反射进行Bean的构造了,即实例化。如果是无参构造,则无需参数之间构造。
接下来是进行属性赋值 ,通过调用populateBean()方法为Bean内部的所需的属性进行复制填充,通常就是@Autowired注解这些变量,通过三级缓存机制进行填充,三级缓存就是依赖注入。
然后是初始化实例。如果实现了其他 *.Aware
接口,就调用相应的方法。
如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor
对象,执行postProcessAfterInitialization()
方法
在Spring内部使用了很多后置处理器,比较典型的是当一个类被增强了使用到了AOP,那么这个类通常使用后置处理器来增强的。
因此到了BeanPostProcessors这一步之后,基本Bean实例已经被创建好了,并且可以使用。
对于销毁,通过实现DisposableBean接口执行destory方法进行销毁。
Spring的bean的生命周期
解释:
对于两个Bean,A引用B,B引用A,当然这是典例,还有以下几种情况:
A依赖于B,B依赖于C,C依赖于A
A依赖于A
首先是实例化A,在堆中开启其内存,目前还是半成品,还没有经过后置处理器和初始化。然后初始化A,但是A中有b属性,然后再Spring容器找B对象,没有找到去实例化B对象。B对象实例化后进行B对象的初始化,B对象有a属性,在Spring容器中没有找到A对象,然后实例化A对象,这样就陷入死循环。
Spring解决循环依赖是通过三级缓存,对应的三级缓存如下所示:
一级缓存作用:限制bean在beanFactory中只存一份,即实现sinleton scope,解决不了循环依赖
在这个图中,没有对象能够走完生命周期,所以无法解决依赖注入
如果需要打破依赖注入,需要一级缓存和二级缓存同时使用才可以
如上图,因为使用二级缓存,就可以将半成品对象放入二级缓存中,因此实例化A后获得A的半成品对象,就是原始对象A,将其放入二级缓存中
如上图,然后A对象有b属性,需要B初始化对象,因此在Spring容器中没有找到b变量的对象即B对象。就开始创建B对象,对其实例化后获得原始对象B,将原始对象B放入二级缓存。这个时候原始对象需要初始化对象B,因为B对象有A对象的变量a,需要在Spring容器找到A对象。结果在二级缓存找到了A对象,然后注入A。
如上图,将半成品A从二级缓存注入到B对象中。
如上图,B对象创建成功,然后存入了单例池中,并将原始对象B从二级缓存删除。
如上图,B对象创建成功,就可以将B对象注入给A对象,A对象也创建成功了,将二级缓存中的A对象删除,在单例池添加A对象。
虽然一级缓存+二级缓存解决了循环依赖,但是只能解决一般对象的循环依赖,无法解决代理对象的循环依赖。如果一个对象被增强了,那么就是代理对象,这个时候就需要借助三级缓存。
首先是实例化A,然后原始对象A会生成A的工厂对象,将其放入三级缓存中。
然后是A对象有B对象的属性b,需要实例化B对象,但是Spring容器没有B对象,因此需要实例化B对象,生成B的原始对象,原始对象B生成B对象的工厂对象,并将其放入三级缓存中。
这个时候B对象有A对象的属性a,然后去Spring容器查找A对象,然后三级缓存有A对象的工厂对象,生产出A对象的代理对象。然后将A对象的代理对象放入二级缓存中,并从三级缓存删除A对象的工厂对象。
然后将A对象的代理对象注入给B对象,B对象创建成功,将B对象(普通对象)放入单例池中
然后将创建成功的B对象注入给A对象,A对象也创建成功,最后将A对象(代理对象)放入单例池中。
三级缓存只能解决初始化依赖问题,无法解决构造器依赖的问题
出现这种情况:
就需要使用延时加载:
延时加载就是什么时候需要对象再进行对象的创建,而不是实例化对象的时候直接注入进去
Spring中的循环引用
构造方法出现了循环依赖怎么解决?
如上图,当前端发送一个请求的时候,首先经过前端控制器 DispatcherServlet ,这个前端控制器是一个调度中心。
然后前端控制器去查询handle,handle就是某一路径对应的方法名,比如/user/getById/1这个路径,找到这个controller中这个路径所对应的方法名就是handle。其中处理器映射器保存着很多handle和一些类名、方法名等。
当从处理器映射器查到了这个handle所对应的value的时候,即找到所对应的方法名,然后返回处理器执行链给前端控制器。因为在执行某个方法的时候可能会遇到拦截器,因此将拦截器和handle一起封装到处理器执行链返回给前端控制器。
然后前端控制器将从处理器映射器获取到的处理器执行链,将其发送到处理器适配器进行处理。在处理器适配器中要处理两个部分,分别为处理参数和处理返回值。因为在请求路径中的请求参数不知道是什么类型,这个类型最终决定为什么类型就由处理器适配器去决定。还有返回值的决定,也是相同的道理由处理器适配器来决定其返回值。
当处理完这两个参数以后将handle发送给处理器handler,用来在对应的controller 找到handler方法名所对应的方法进行执行,如果有拦截器先执行拦截器然后再执行方法。
最后在处理器handler方法添加@ResponseBody,并且转换结果为JSON并响应结果给前端。
在Spring使用的是xml配置来进行装配,某些 Spring 特性或者引入第三方依赖的时候,还是需要用 XML 或java进行显式配置。
没有 Spring Boot 的时候,我们写一个 RestFul Web 服务,还首先需要进行如下配置。
- @Configuration
- public class RESTConfiguration
- {
- @Bean
- public View jsonTemplate() {
- MappingJackson2JsonView view = new MappingJackson2JsonView();
- view.setPrettyPrint(true);
- return view;
- }
-
- @Bean
- public ViewResolver viewResolver() {
- return new BeanNameViewResolver();
- }
- }
spring-servlet.xml
- <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:mvc="http://www.springframework.org/schema/mvc"
- 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/mvc/ http://www.springframework.org/schema/mvc/spring-mvc.xsd">
-
- <context:component-scan base-package="com.howtodoinjava.demo" />
- <mvc:annotation-driven />
-
- <!-- JSON Support -->
- <bean name="viewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
- <bean name="jsonTemplate" class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
-
- </beans>
但是,Spring Boot 项目,我们只需要添加相关依赖,无需配置,通过启动下面的 main
方法即可。
- @SpringBootApplication
- public class DemoApplication {
- public static void main(String[] args) {
- SpringApplication.run(DemoApplication.class, args);
- }
- }
并且,我们通过 Spring Boot 的全局配置文件 application.properties
或application.yml
即可对项目进行设置比如更换端口号,配置 JPA 属性等等。
为什么 Spring Boot 使用起来这么酸爽呢? 这得益于其自动装配。自动装配可以说是 Spring Boot 的核心,那究竟什么是自动装配呢?
SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时会扫描外部引用 jar 包中的
META-INF/spring.factories
文件,将文件中配置的类型信息加载到 Spring 容器(此处涉及到 JVM 类加载机制与 Spring 的容器知识),并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装置进 SpringBoot。
没有 Spring Boot 的情况下,如果我们需要引入第三方依赖,需要手动配置,非常麻烦。但是,Spring Boot 中,我们直接引入一个 starter 即可。比如你想要在项目中使用 redis 的话,直接在项目中引入对应的 starter 即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
引入 starter 之后,我们通过少量注解和一些简单的配置就能使用第三方组件提供的功能了。
自动装配可以简单理解为:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。
我们先看一下 SpringBoot 的核心注解 SpringBootApplication
。
大概可以把 @SpringBootApplication
看作是 @Configuration
、@EnableAutoConfiguration
、@ComponentScan
注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:
@EnableAutoConfiguration
:启用 SpringBoot 的自动配置机制@Configuration
:允许在上下文中注册额外的 bean 或导入其他配置类@ComponentScan
:扫描被@Component
(@Service
,@Controller
)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。如下图所示,容器中将排除TypeExcludeFilter
和AutoConfigurationExcludeFilter
。EnableAutoConfiguration
只是一个简单地注解,自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector
类。
- @Target({ElementType.TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- @Inherited
- @AutoConfigurationPackage //作用:将main包下的所有组件注册到容器中
- @Import({AutoConfigurationImportSelector.class}) //加载自动装配类 xxxAutoconfiguration
- public @interface EnableAutoConfiguration {
- String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
-
- Class<?>[] exclude() default {};
-
- String[] excludeName() default {};
- }
我们现在重点分析下AutoConfigurationImportSelector
类到底做了什么
3.2、AutoConfigurationImportSelector:加载自动装配类
AutoConfigurationImportSelector
类的继承体系如下:
- public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
-
- }
-
- public interface DeferredImportSelector extends ImportSelector {
-
- }
-
- public interface ImportSelector {
- String[] selectImports(AnnotationMetadata var1);
- }
可以看出,AutoConfigurationImportSelector
类实现了 ImportSelector
接口,也就实现了这个接口中的 selectImports
方法,该方法主要用于获取所有符合条件的类的全限定类名,这些类需要被加载到 IoC 容器中。
- private static final String[] NO_IMPORTS = new String[0];
-
- public String[] selectImports(AnnotationMetadata annotationMetadata) {
- // <1>.判断自动装配开关是否打开
- if (!this.isEnabled(annotationMetadata)) {
- return NO_IMPORTS;
- } else {
- //<2>.获取所有需要装配的bean
- AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
- AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
- return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
- }
- }
这里我们需要重点关注一下getAutoConfigurationEntry()
方法,这个方法主要负责加载自动配置类的。
该方法调用链如下:
现在我们结合getAutoConfigurationEntry()
的源码来详细分析一下:
- private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();
-
- AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
- //<1>.判断自动装配开关是否打开
- if (!this.isEnabled(annotationMetadata)) {
- return EMPTY_ENTRY;
- } else {
- //<2>.用于获取EnableAutoConfiguration注解中的 exclude 和 excludeName
- AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
- //<3>.获取需要自动装配的所有配置类,读取META-INF/spring.factories
- List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
- //<4>.获取需要自动装配的所有配置类,读取META-INF/spring.factories
- configurations = this.removeDuplicates(configurations);
- Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
- this.checkExcludedClasses(configurations, exclusions);
- configurations.removeAll(exclusions);
- configurations = this.filter(configurations, autoConfigurationMetadata);
- this.fireAutoConfigurationImportEvents(configurations, exclusions);
- return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
- }
- }
判断自动装配开关是否打开。默认spring.boot.enableautoconfiguration=true
,可在 application.properties
或 application.yml
中设置
用于获取EnableAutoConfiguration
注解中的 exclude
和 excludeName
。
获取需要自动装配的所有配置类,读取META-INF/spring.factories
spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
从下图可以看到这个文件的配置内容都被我们读取到了。XXXAutoConfiguration
的作用就是按需加载组件。
不光是这个依赖下的META-INF/spring.factories
被读取到,所有 Spring Boot Starter 下的META-INF/spring.factories
都会被读取到。
所以,你可以清楚滴看到, druid 数据库连接池的 Spring Boot Starter 就创建了META-INF/spring.factories
文件。
到这里可能面试官会问你:“spring.factories
中这么多配置,每次启动都要全部加载么?”。
很明显,这是不现实的。我们 debug 到后面你会发现,configurations
的值变小了
因为,这一步有经历了一遍筛选,@ConditionalOnXXX
中的所有条件都满足,该类才会生效。
- @Configuration
- // 检查相关的类:RabbitTemplate 和 Channel是否存在
- // 存在才会加载
- @ConditionalOnClass({ RabbitTemplate.class, Channel.class })
- @EnableConfigurationProperties(RabbitProperties.class)
- @Import(RabbitAnnotationDrivenConfiguration.class)
- public class RabbitAutoConfiguration {
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。