一、谈谈你理解的 Spring 是什么?
- Spring是一个生态,包含了23个开源框架,可以构建Java应用所需的一切基础设施
- Spring通常指Spring Framework
核心解释
- Spring是一个开源的、轻量级的容器(包含并管理对象的生命周期)框架
- Spring是为了解决企业级开发中业务逻辑层中对象之间的耦合问题
- Spring的核心是IoC和AOP
二、Spring的优缺点有哪些?
从IoC、AOP、事务管理、JDBC、模板集成(简化开发)、源码方面进行解释
- IoC:集中管理Bean对象,降低了对象之间的耦合度,方便维护对象(比如单例对象和多例对象只需要指定scope即可,
- AOP:在不修改业务逻辑代码的情况下对业务进行增强,方便集成系统级服务,减少重复代码,提高开发效率
- 声明事务:只需要一个注解
@Transactional
就能进行事务声明 - 方便测试:Spring中集成了测试,方便Spring能进行测试
- 集成各种框架:Spring提供了对各种框架的支持,降低了使用难度,拥有非常强大的粘合度,集成能力强大
- 降低Java EE API的使用难度:简化了开发,封装了很多功能性代码
- 源码学习:Spring的源码是一个很值得学习的典范
缺点:
- 轻量级却集成了大量功能
- 简化开发,但是上层封装越简单,底层实现就越复杂
- 本身代码量庞大,深入学习有困难
三、什么是Spring IoC容器?有什么作用?
- 控制反转,将类的产生过程托管给了IoC容器,由程序员创建交给了IoC容器创建。
- 如果要使用对象,要实现依赖注入,一般使用注解
@Autowired
来注入。 - 集中管理对象,方便维护,降低了对象之间的耦合度
四、紧耦合和松耦合是什么?如何实现松耦合?
- 紧耦合:对象之间形成高度的依赖
- 松耦合:利用单一职责、依赖倒置的原则,将依赖程度降低,形成松耦合。
- 单一职责:从整体中剥离出单一职责的接口,并提供具体实现,而不是各种功能全部耦合到一起
- 依赖倒置:接口依赖于主体运转,而不是主体依赖于接口
- 具体模型可以参照电脑的发展史。早期的电脑:所有的零部件以及现在的“外设”都是集中到一起的,一旦发生故障便导致全局瘫痪且难以维护;后期剥离出接口的概念,将鼠标键盘等外设剥离成接口,但是不支持热插拔,电脑还是要依赖于接口上的外设;现在的电脑形式,鼠标键盘可以单独剥离出来,且支持热插拔,不影响电脑主机的运转,方便管理维护。
五、IoC和DI有什么区别?
IoC,Inverse of Control,控制反转。IoC是一种设计思想,你需要什么类型的对象(POJO),就将设计好的创建模板(XML配置文件或者注解)交给Spring IoC容器,在需要使用到的时候由Spring IoC容器创建这个对象(Bean)。在这个过程中,对象的创建由依赖于它的程序主动创建变成了Spring IoC容器来创建,控制权发生反转,所以叫做控制反转。
DI,Dependency Injection,依赖注入。DI是一种行为,组件之间依赖关系由容器在运行期决定,容器动态地将某个依赖关系注入到组件之中。整个依赖的过程,应用程序依赖于IoC容器,需要IoC容器来提供对象所需要的外部资源;注入就是IoC容器将对象所需要的外部资源注入到对象中,某个对象所需要的外部资源包括对象、资源、常量数据等。DI主要是通过反射实现的。
六、IoC的实现机制是什么?
底层是通过工厂 + 反射实现的。简单工厂是一种设计模式,通过传入一个标识然后交由工厂类创建出所需要的对象。在没有利用反射时,传入的是一个对象的名称,工厂的的设计原则也更加的复杂。引入反射之后可以直接传入类路径名,之后动态地生成一个对象。
七、配置Bean的方式有哪些?
- xml配置文件形式:
<bean id = "user" class = "com.company.entity.User"/>
-
@Component
注解:该注解等价于@Service
、@Controller
、@Repository
注解。这种方式使用的时候需要配置扫描包<component-scan>
,是通过反射的形式来利用类创建Bean对象。 -
@Bean
注解:该注解通常使用在一个方法上,使用这个注解区别于@Component
注解可以控制Bean的实例化过程。 -
@Import
注解:有三种方式可以创建Bean对象。
八、什么是Spring Bean?和普通的Java Bean以及对象有什么区别?
- Spring Bean是一个由Spring IoC容器实例化、组装和管理的对象。
- Java Bean是一个Java类:所有属性为private、提供默认构造方法、提供getter和setter、实现serializable接口
- 对象就是普通的Java类实例化的产物,区别于上述Java bean的四点要求。
九、Spring IoC有哪些扩展点?什么时候用到?
- Bean在创建之前会先生成Bean的一些定义,也就是Bean Definition,然后注册到Bean Factory中,之后交由工厂创建B,才能生成Bean对象。BeanDefinition创建好之后,想要修改从xml文件配置好的BeanDefinition,应该使用的扩展为:BeanDefinitionRegistryPostProcessor接口。
- Bean Factory创建之后,想要对Bean Factory进行一些修改,应该使用BeanFactoryPostProcessor接口。
- 在Bean的实例化过程中会调用一些特定的接口实现类,这些接口包括有InstantiationAwareBeanPostProcessor接口【该接口是在Bean实例化之后,设置显式属性或自动装配之前,是一个回调函数】
- 其他扩展点:BeanPostProcessor接口、InitializingBean接口、各种Aware、BeanDefinition入口扩展
十、Spring IoC的加载过程?
Spring IoC的创建,首先会实例化一个ApplicationContext对象。Spring IoC的加载分为四个形态:【可以类比于工厂拿着设计图纸参数去生产产品的过程】
- 概念态:Spring Bean只是进行了配置,编写好Bean的一个配置信息【只是一个概念信息】
- 定义态:将配置信息封装成BeanDefinition
- 纯静态:只是通过BeanDefinition中的信息,得知Bean的路径,调用反射创建早期的Bean,其他资源还没有进行注入,是一个不完整的Bean对象。
- 成熟态:对纯静态的Bean进行外部资源注入,使其成为一个完整的Bean。
概念态需要调用一个Bean工厂的后置处理器invokeBeanFactoryPostProcessors,提供扩展点操作Bean定义。这个扩展点既对内扩展也对外扩展,然后通过这个扩展注册成为一个定义态。简易化的过程就是:扫描src下的com.company.moduleName路径下的所有类,判断是否存在@Component注解,之后将符合条件的类封装成BeanDefinition。
定义态就是Bean已经被封装成BeanDefinition的状态,这个状态下包含Bean的许多信息,比如scope、dependsOn、lazyInit、className、beanClass等。成为定义态之后需要判断是否符合初始化标准:比如是否是单例的,是否懒加载,是否是抽象的。符合标准就会直接进入实例化阶段。实例化成为早期暴露的Bean之后,就进入纯静态了。
纯静态之后的主要工作就是属性赋值,是DI的一种实现。属性赋值之后就进入初始化阶段,这个阶段会进行AOP的一个使用。这个步骤完成之后,就判断Bean的类型回调Aware接口,调用生命周期回调方法;如果需要代理就实现代理。在这之后就会将Bean添加进一个Map<String, Object>
中。这个Map就是BeanDefinitionMap,作用就是缓存好实例化的Bean对象,把它存放在单例池中,Bean的创建就完成了,Bean就存放在Spring Ioc容器中。
成熟态使用的时候就直接从IoC容器中获取所需要的Bean即可。至此IoC加载完成。
十一、BeanFactory和ApplicationContext有什么区别?
- BeanFactory的作用的是生产Bean对象。举例来说,BeanFactory相当于工厂,ApplicationContext相当于4S店。相比于ApplicationContext,它的占用内存更小。
- ApplicationContext实现了BeanFactory,本身不生产Bean,主要作用是通知BeanFactory生产Bean。相比于BeanFactory,它能做的事情更多:可以自动将Bean注册到容器中、加载环境变量、支持多语言、实现事件监听、注册对外扩展点。
- 共同点:都是容器,都能够对Bean进行生命周期的管理。两者都有getBean方法,只不过真正产生Bean的是BeanFactory。
十二、BeanDefinition的作用?
BeanDefinition主要用来存储Bean的定义信息,用来决定Bean的生产方式。
- 通常定义的Bean中,只有singleton、非abstract、非lazy的Bean才会在IoC容器被创建的时候加载。
十三、BeanFactory的作用?
- BeanFactory是Spring中的非常核心的一个顶级接口,也属于一个Spring容器,管理着某个Bean的生命周期,只不过BeanFactory只能算是非常低级的容器,远没有ApplicationContext这样的高级容器这么多的功能
- BeanFactory的作用就是传入一个标识产生一个Bean,利用的是getBean方法
- BeanFactory实现了简单的工厂模式,拥有非常多的实现类,最强大的实现类是DefaultListenableBeanFactory。Spring的底层就是使用该实现工厂产生Bean对象的。
十四、自动注入有哪些需要注意的?
- 一定要声明set方法
- 覆盖:需要用
<constructor-arg>
和<property>
配置来定义依赖,这些配置将始终覆盖自动注入 - 简单的数据类型:不能注入简单的数据类型,比如基本数据类型、String、类(手动注入可以)
十五、什么是Bean的装配?
Bean装配就是将对象注入IoC容器,这个过程就是Bean的装配。
创建应用对象之间协作关系的行为通常称为装配(wiring),这也是依赖注入(DI)的本质。
Spring一般通过两个角度来自动化装配Bean:
- 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。(@Component)
- 自动装配(autowiring):Spring自动满足bean之间的依赖。(@Autowired)
十六、Spring实例化Bean的方法?
- 构造方法实例化:反射的方式,利用类在编译期间产生的.class二进制字节码文件,动态地在运行期间生成Bean实例化对象。一般使用xml配置或者注解实现。
- 静态工厂实例化:使用一个静态类,然后实例化Bean的时候调用factory-method为工厂类,通过静态工厂实例化Bean
- 实例工厂实例化:@Bean注解实现,实际上调用fctory-bean和factory-method一起来实现
- Factory-Bean方法:在类的定义中让POJO类实现FactoryBean接口,之后重写其中的两个方法,就可以返回指定的Bean对象了。
第一种方式,使用构造器,构造过程是Spring来控制的,我们只是配置了一些Bean的定义信息。后面三种方法,Bean的构造过程都是可控的,虽然编写上稍微复杂,但使用上更加灵活。
十七、Spring如何处理线程并发安全问题?
- 将成员变量声明在方法内部,一定程度上可以解决线程安全问题。
- 单例Bean的情况下,如果在类中声明成员变量,并且有读写操作,就有可能发生线程安全问题。
- 将scope配置为多例prototype可以解决。多例情况下,Bean彼此之间是互不影响的,
- 将成员变量存放在ThreadLocal中
- 使用同步锁:在操作成员变量的set方法中加上synchronized关键字,会影响吞吐量
十八、Spring Bean是线程安全的吗?
- Spring中的Bean默认是单例的,如果在类中声明了成员变量,并且会对成员变量进行读写操作(有状态),这样就可能会造成线程安全问题了。
- 但是如果把成员变量声明在方法内部(无状态),就不会造成线程安全问题了。
十九、单例Bean的优势是什么?
单例Bean采用了单例模式,也就是这个类只能创建一个实例。单例模式中,将构造方法进行了私有化,而且单例类必须自己给自己提供这个唯一的实例,而且必须给所有其他实例提供这一对象。
由于不会每次都创建新的对象,所以有下面这些优点:
- 减少了创建Bean的消耗:第一,Spring通过反射或者cglib生成Bean对象中的性能消耗;第二,创建Bean对象的内存消耗
- 减少JVM进行垃圾回收的次数,生成的对象少了,GC的次数自然就降低了
- 可以快速地获取到Bean。因为除了第一次创建Bean之外,其余时候获取Bean都是直接从缓存中去读,所以速度变快
二十、描述BeanDefinition的加载过程?
首先来简略地描述下Bean的加载过程:
假设是以JavaConfig的方式来创建Bean对象:@Bean
指令调用之后,会生成一个AnnotationConfigApplicationContext容器,之后解析配置类,注册成为一个BeanDefinitionMap,然后根据这个Map,由BeanFactory调用getBean方法,生成Bean对象。
那么BeanDefinition的加载,主要就是解析我们所需要传入的配置信息,然后将这些属性信息封装成一个BeanDefinition对象。顺序如下:
1、读取配置:BeanDefinitionReader
2、解析Config:@Bean @Import @Component…
3、配置类的解析器ConfigurationClassParser
4、扫描:ClassPathBeanDefinitionSacnner#doScan
5、根据包路径找到所有的.class文件判断类是不是标准@Component注解
6、排除接口是不是抽象类
7、注册BeanDefintion
二十一、Spring如何避免并发中获取不完整Bean?
在Bean的产生过程中,如果Bean已经被实例化,但是还没有被注入属性值和初始化,这个Bean就是不完整的,对应于纯静态。
假设现在存在多个线程,当第一个线程以微弱的优势将Bean创建之后存放到L3缓存中,但是还没有进行赋值,这个时候被第二个线程直接从三级缓存中获取到这个没有赋值的Bean,就造成了获取到的Bean不完整的情况。
Spring是通过双重检查锁DCL解决这个问题的。
双重检查锁是使用在单例模式中的,简单的DCL如下所示:
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
第一个线程在获取Bean的时候,会调用getSingleton方法来创建Bean。从这个时间点开始一直到创建完成,整个过程都会加锁。一级缓存只会存完整的Bean,创建的时候是在二三级缓存中进行的,二三级缓存在创建的过程中是加锁状态的,创建完成之后会返回一级缓存并清理二三级缓存。
整个过程一级缓存不加锁,第二个线程先访问一次一级缓存,如果没有创建完毕,那么一级缓存中是不会存在Bean的。二三级缓存此时加锁状态,线程就是阻塞的。
第一个线程创建完成之后二三级缓存锁释放并清理二三级缓存,线程二如果此时访问二三级缓存会发现是空状态。此时如果第二个线程直接创建,那么可能造成资源的浪费与单例获取出现问题,此时会进行二次检查一级缓存,会发现一级缓存中存在Bean,直接返回即可。
整个过程中不对一级缓存加锁,是为了提高性能,避免已经创建好的Bean阻塞等待。
二十二、@Component、@Service、@Controller、@Repository有什么区别?
后面三个注解都是调用的@Component注解,实际上都是@Component这一个注解。加上后三者注解是为了标识三层架构,提高代码的可读性。
二十三、Spring是如何解决循环依赖的?
循环依赖:简单分为三种:自身依赖自身;A依赖B,B又依赖A;三者及以上构成闭环的依赖关系。
Spring是通过三级缓存解决循环依赖的,简单来说就是三个Map。解决循环依赖的关键就是一定要有一个缓存保存早期对象,形成一个循环的出口。
- singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
- earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
- singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。
A对应testService1,B对应于testService2。那么这个过程就是:
- A创建的过程发生需要B,于是A将自身存放在三级缓存中,去创建B
- B创建的时候会发现需要A,此时开始在缓存中寻找A,按照一二三的顺寻查找,在三级缓存中发现A之后,会把三级缓存中的A放到二级缓存,并删除三级缓存中的A。
- B初始化顺利完成,B放入一级缓存。之后继续进行A的创建,A创建的时候直接从一级缓存中可以查到B(B中的A仍然处于创建中的状态),完成依赖注入,创建A完成之后直接放入一级缓存,解决循环依赖。
查缓存是在doGetBean方法中进行的,装配属性发现依赖关系是在populateBean方法中进行的。doGetBean方法由getBean方法调用。
二级缓存是为了避免实现了AOP的类重复创建动态代理。三级缓存中使用的是函数式接口,不会立即调用。使用二级缓存,就会避免在循环依赖中重复创建动态代理,这与普通的Bean初始化产生区分。【普通Bean在进行实例化创建,三级缓存中进行;循环依赖的Bean,创建循环依赖Bean的时候,三级缓存会被依赖的对象在创建的时候删除,避免了在三级缓存中创建动态代理(第二条)】
没有发生循环依赖的正常Bean的生命周期中,应该是在初始化的时候创建的动态代理。而由于发生循环依赖,是在第二次创建A的时候才会创建动态代理。
三级缓存的作用:①一级缓存存储完整的Bean;②二级缓存避免重复创建动态代理;③存放ObjectFactory对象,主要调用工厂产生早期Bean。
- 二级缓存能不能解决循环依赖?
- 如果只是死循环的问题,一级缓存就可以解决;只不过无法避免在并发下获取不完整的Bean
- 二级缓存也可以解决循环依赖。只不过如果出现重复循环依赖会多次创建aop的动态代理
- Spring有没有解决多例Bean的循环依赖?
- 多例不会使用缓存进行存储(多例Bean每次使用都需要重新创建)
- 不缓存早期对象就无法解决循环
- Spring有没有解决构造函数参数Bean的循环依赖?
- 构造函数的循环依赖会报错
- 可以通过
@Lazy
解决构造函数的循环依赖
- 使用懒加载不会立即创建依赖的Bean,而是等到用到才通过动态代理进行创建
二十四、JavaConfig是如何代替xml配置文件的?
- XML方式
- 容器:
ClassPathXmlApplicationContext(".xml")
- 配置文件:applicationContext.xml
- 配置方法:
<bean id = "" lazy = "" scope = ""/>
- 包扫描:
<component-scan/>
- 引入外部属性配置方式:
<property-placeHolder resources = "xxx.properties"/>
- 指定其他配置文件:
<import resource = ""/>
- 属性注入:
<property name = "${name}"/>
- Config方式
- 容器:
AnnotationConfigApplicationContext(Config.class)
- 配置类:Config (@Configuration)
- 配置方法:@Bean @Lazy @Scope
- 包扫描:@ComponentScan
- 引入其他配置文件:@PropertySource(“xxx.properties”)
- 指定其他配置文件:@Import
- 属性注入:@Value
两者在实现的时候一般都使用多态的方式创建,利用共同的接口ApplicationContext
以多态的方式创建出不同的容器,之后使用ClassPathXmlApplicationContext
和AnnotationConfigApplicationContext
去调用不同的加载配置类的方法,解析配置信息,之后封装成BeanDefinition
对象。
加载配置注解容器的时候,AnnotationConfigApplicationContext
的过程:①读取配置类:使用AnnotatedBeanDefinitionReader
类的this.reader.register(annotatedClasses);
方法;②解析配置类:使用BeanDefinitionRegistryPostProcess
类,调用ConfigurationClassParser
配置类解析器,解析各种注解如@Bean @Component等,注册为BeanDefinition
对象。
加载xml配置文件的时候,ClassPathXmlApplicationContext
的过程:①加载xml配置文件:读取xml配置文件使用XmlBeanDefinitionReader
类来对配置文件进行读取操作,使用AbstractXmlApplicationContext#loadBeanDefinitions()
方法加载BeanDefinition
的所需信息;②解析配置文件:使用LoadBeanDifinition
和DefaultBeanDefinitionDocumentReader
来解析<bean>
和<import>
等配置标签,注册为BeanDefinition
。
二十五、Spring有哪几种配置方式?
①基于XML文件的配置方式:从Spring诞生就有的方式,使用applicationContext.xml文件和<bean>
标签
②基于注解的配置方式:使用@Component注解标识该类是要注入到IoC容器的类,并且在applicationContext.xml文件中创建<component-scan base-packages = ""/>
标注需要扫描的包路径。需要注入的时候,使用@Autowired
注解完成注入。该方式在Spring 2.5版本之后开始支持。
③基于Java的配置:JavaConfig方式,诞生于Spring 3.0方式之后。使用@Configuration
和 @Bean
注解完成配置。
二十六、Spring Bean的生命周期?
首先,对于prototype的Bean,Spring只负责在使用的时候加载多例的Bean,之后就交给客户端代码管理。对于singleton的Bean,Spring负责跟踪整个Bean的生命周期。
Bean的生命周期:指的是Bean从创建到销毁的整个过程。主要分为四个阶段:实例化、属性赋值、初始化、销毁。
- 实例化:通过反射去推断构造函数进行实例化
- 一般有静态工厂、实例工厂的方式进行实例化
- 属性赋值:解析自动装配(byName、byType、Constructor、@Autowired)
- 是DI的体现,将依赖的对象/属性值注入到需要创建的对象中
- 可能出现循环依赖的情况,具体参考23题
- 初始化:
- 调用Aware回调方法,这个过程是一个渐进过程,只有实现了Aware接口才会去调用,依此如下
- 调用BeanNameAware的
setBeanName()
方法 - 调用BeanFactoryAware的
setBeanFactory()
方法 - 调用ApplicationContextAware的
setApplicationContext()
方法
- Aware接口实现之后,调用BeanPostProcessor的预初始化方法。调用InitializingBean的
afterPropertiesSet()
方法,调用定制的初始化方法,调用BeanPostProcessor的后初始化方法。(调用初始化生命周期回调,有三种方式,此处是其一)初始化生命周期回调另外两种方式:①XML文件中指定<init-method>
;②用注解@PostConstructor实现初始化生命周期回调 - 如果Bean实现了AOP,会在这一步创建动态代理
- 销毁
- Spring容器关闭的时候进行调用
- 调用销毁生命周期回调(三种方式)
- 实现
Dispoable
接口的destroy()
方法 - XML配置文件中配置
<destroy-method>
- 使用注解
@PreDestory
创造销毁前置方法
二十七、Bean的生产顺序由什么决定?
Bean的生产顺序是由BeanDefinition的注册顺序来决定的,其次依赖关系也会决定Bean的注册顺序。对于依赖关系来说,A依赖于B,那么被依赖的B应该在A生产完成之前生产出来。
- BeanDefinition的注册顺序又由什么决定?主要是由注解/配置的解析顺序来决定。
配置相关,注册顺序遵循下面顺序:
- @Configuration
- @Component
- @Import-类
- @Bean
- @Import-ImportBeanDefinitionRegister
扩展点:BeanDefinitionRegistryPostProcessor
- 这个扩展点是使用Bean注册后置处理器,加载的时候应该是在Bean加载完成之后
二十八、Bean在创建的时候有哪几种形态?
- 概念态
-
@Bean
或者<bean>
两种方式定义的Bean,处于配置文件或者注解形态,没有进行解析,处于一个初始阶段,相当于设计图纸。定义了scope、lazy属性等。
- 定义态
- 将配置信息读取到BeanDefinition中,将Bean的生产指标进行封装
- 纯静态
- 二级缓存,早期暴露状态的Bean(创建完成的中间状态是存放在一级缓存中)。纯静态一般是在解决循环依赖的问题的时候才会发挥作用。
- 成熟态
- singletonObjects最终在应用中使用的状态,加载完成的状态应该是存放在一级缓存中。
二十九、Bean的生命周期回调有哪些方法?
- 实现特定接口的方式
- 初始化:实现
InitializingBean
接口 - 销毁:实现
DisposableBean
接口
- 使用XML配置文件的方式:
- 通过@Bean注解的init-method
属性和
destroy-method`属性指定初始化和销毁的回调方法
- 使用JSR-250标准的注解:
- Bean完全初始化后将回调使用
@PostConstruct
标注的方法 - 在销毁
ApplicationContext
前将回调使用@PreDestroy
标注的方法
三十、如何在所有BeanDefinition注册之后进行扩展?
使用BeanFactoryPostProcessor
接口可以对BeanDefinition进行扩展。实现该接口,重写其中的相关方法即可。该方法重写的时候,可以使用其中提供的beanFactory
对象,调用其中的getBeanDefinition("name")
方法,获取相关的BeanDefinition之后可以调用相关的set方法,这就是进行扩展的操作。
三十一、自动装配的方式有几种?
自动装配的方式,指的就是<bean>
标签内部的autowire
属性值,该属性值一共有五种。
- no:默认的方式就是no,不采用任何的自动装配,手动使用ref装配Bean/使用@Autowired来进行手动指定需要自动注入的属性
- byName:根据Bean的名称来进行自动装配。这个名称使用的是set方法中的名称。如
setUser
方法,那么Name应该就是去掉set并将首字母改成小写的user
- byType:根据参数的数据类型来进行自动装配
- constructor:根据构造函数的方式进行自动装配,构造函数的参数通过byType的形式进行装配
- autodetect:自动探测,如果存在构造函数就使用构造函数,否则就是用byType的方式进行装配(Spring 3.0之后弃用)
三十二、如何在Spring所有Bean创建完之后进行扩展?
首先,我们需要知道什么时候才算是所有的Bean都创建完毕。
-
new ApplicationContext()
–>refresh()
–>finishBeanFactoryInitialization()
【这个方法遍历所有的BeanDefinition,并进行解析,调用BeanFactory.getBean()
进行创建Bean】–>这个遍历结束就相当于所有的Bean都创建完毕了
实现扩展存在两种方法:
- 实现
SmartInitializingSingleton
接口,重写其中的afterSingletonsInstantiated()
方法 - 基于Spring的事件监听
- Spring在加载完毕所有的BeanDefinition之后,会调用一个
finishRefresh()
方法【Spring的IoC加载体现在refresh方法中,方法中最后调用了finishRefresh
方法】,监听其中的ContextRefreshedEvent即可 - 使用方法:在方法上加上
@EventListener(ContextRefreshedEvent.class)
三十三、@Bean的方法调用是怎样保证单例的?
这个题和【@Configuration注解添加与否有什么区别】其实是一样的。
- 如果需要@Bean返回的是单例,需要在类上加上@Configuration注解
- Spring会在invokeBeanFactoryPostProcessor中通过内置BeanFactoryPostProcessor生成CGLib动态代理
- 当@Bean方法进行互调的时候,会通过CGLib进行增强,通过调用的方法名作为Bean的名称去IoC中获取,进而保证了@Bean方法的单例
三十四、@Configuration的作用以及解析原理?
@Configuration用来代替applicationContext.xml文件中的<bean>
标签,所有没有@Configuration注解也可以用来配置@Bean,所以@Configuration加与不加有什么区别呢?
- 加了@Configuration会让配置类生成一个CGLib动态代理,保证@Bean调用的是Bean的单例。@Bean的获取就会调用容器的getBean方法进行获取
解析原理:
- 创建Spring上下文容器的时候会注册一个解析配置的处理器
ConfigurationClassPostProcessor
类,它是一个BeanFactoryPostProcessor
的实现类,也是Spring内部对于BeanDefinitionRegistryPostProcessor
接口的唯一实现类。该配置处理器主要用来处理@Configuration
类。 - 前期注册完成要使用的配置处理器之后,在
refresh()
方法内部,会调用一个invokeBeanFactoryPostProcessor()
方法,之后调用ConfigurationClassPostProcessor.PostProcessBeanDefinitionRegistry()
进行配置解析【解析就是对@Bean、@Configuration等等注解进行识别,并注册成BeanDefinition】 - 调用
ConfigurationClassPostProcessor.PostProcessBeanFactory()
去创建cglib动态代理
三十五、@Autowired自动装配的过程是怎样的?
@Autowired注解是通过BeanPostProcessor接口解析的。
- 在Spring创建IoC容器的时候,会在构造函数中注册一个
AutowiredAnnotationBeanPostProcessor
解析类 - 在Bean的创建过程中进行解析,分为两个步骤
- 在实例化之后预解析:解析@Autowired标注的属性、方法等,将这些属性值的元数据缓存起来
- 在属性注入的时候进行真正的解析:拿到预解析的元数据之后,去IoC容器中进行查找【这个查找是根据预解析的元数据类型进行的查找】,并且返回注入
- 如果查找的结果刚好为一个,那么将Bean装配给@Autowired指定的数据
- 如果查找的结果不止一个,那么将会根据名称进行查找
- 如果查找的结果为空,将会抛出异常,解决方法时,使用required=false
- 总的来说,自动装配分为三步:
- 实例化Bean
- 填充属性参数
- 初始化Bean
三十六、@Autowired注解有什么作用?和@Resource有什么区别?@Qualifier注解的作用?
@Autowired是一个自动装配的注解。相比于配置文件提供的byName、byType等方法,这个注解能够实现更加简洁的注入方式。默认是按照类型进行注入,如果有多个结果则按照名字进行注入。
@Resource 注解是一个在语义上被定义为通过其唯一的名称来标识特定的目标组件,其中声明的类型与匹配过程无关。简单来说,@Resource 注解的作用和 @Autowired 一样,都是将 IoC 容器内部的对象注入到目标对象。
@Qualifier 注解通常要和 @Autowired 一起使用,完成依赖注入。@Qualifier 在当一个接口有多个实现的时候,为了指名具体调用哪个类的实现使用。一个接口有多个实现类,@Qualifier 指明 @Autowired 具体注入哪个实现类。
不同点:
- @Autowired 注解默认按照 byType 的方式进行匹配,如果发现多个,则依照 byName 的方式匹配,如果发现多个,则抛出异常。
- @Resource 注解在使用的时候,存在 name 和 type 属性,默认按照 byName 的方式进行匹配。如果只指定了 type 字段,那么将会返回唯一类型匹配的 Bean,如果发现多个,会抛出异常。如果指定了 name 属性,则按照 byName 的方式进行匹配。如果没有指定 name 属性,那么 byName 时使用的 Name 会从 get 和 set 中派生出来。
- 简而言之:@Autowired 先 byType 后 byName,默认使用 byType ;@Resource 先 byName 后 byType。
三十七、如何让自动注入在找到多个依赖Bean的时候不报错?
很简单的案例:假如在Service层中,一个接口拥有两个Impl实现类,那么Controller在调用这个Service服务的时候,使用的是多态的方式实现注入,那么将会出现错误:提示没有唯一的Bean定义异常。
解决方法:
- 在其中一个实现类上使用
@Primary
注解,将这个注解添加在其中一个类上,表示默认调用的主要的类。那么默认将调用该注解添加的类。 - 通过 @Autowired 和 @Qualifier(“interfaceImplName”) 联合使用,将 @@Qualifier("") 的值设定为要使用的实现类名称,类名首字母小写。
- 使用 @Resource(name = “interfaceImplName”) 指定注入哪一个接口的实现类。接口实现类的名字要注意将首字母更改成小写,根据 BeanNameGenerator 默认生成的规则,首字母要注意小写。
- 使用 @Service(“s1”) 和 @Resource(name = “s1”) ,在 Service 层的实现类上使用 @Service(“s1”) 注解传值将类取名,之后使用 @Resource 将指定名字的 Service 实现类注入即可。
三十八、如何让自动注入没有找到Bean的时候不报错?
默认在使用@Autowired注解的时候没有使用参数,找不到相关的Bean会报错:提示没有此Bean的异常,至少需要一个Bean才能实现自动注入。【BeanInitializingException】
这是因为在使用@Autowired注解的时候,默认的参数为required=true
,表示必须要实现注入。解决方法:
- 使用
@Autowired(required=false)
即可。
三十九、Spring通知有哪些?
通知指的是在某个特定的执行点上执行的动作。AOP中一个存在五个通知类型:
通知类型 | 接口 | 描述 |
Around 环绕通知 | org.aopalliance.intercept.MethodInterceptor | 拦截对目标方法调用 |
Before 前置通知 | org.springframework.aop.MethodBeforeAdvice | 在目标方法调用前调用 |
After 后置通知 | org.springframework.aop.AfterReturningAdvice | 在目标方法调用后调用,不必关心方法调用结果 |
After-returning 返回通知 | org.springframework.aop.AfterReturningAdvice | 在目标方法成功执行后调用 |
After-throwing 异常通知 | org.springframework.aop.ThrowsAdvice | 当目标方法抛出异常时调用 |
在Spring 5.2.7之前,通知的执行顺序是:
- 正常:前置—>方法—>后置—>返回
- 异常:前置—>方法—>后置—>异常
在Spring 5.2.7及之后的版本中,通知的执行顺序进行了调整:
- 正常:前置—>方法—>返回—>后置
- 异常:前置—>方法—>异常—>后置
四十、Spring AOP的完整实现流程是什么?
这里按照Java Config的方式讲解AOP的实现流程,大致分为三大步骤:
- 当使用AOP的时候,会调用@EnableAspectJAutoProxy注解,实际上该注解会通过@lmpor注册一个
BeanPostProcessor
处理AOP。 - ①解析切面:在创建Bean时调用
BeanPostProcessor
解析切面@Aspect
的时候,将切面中所有的通知解析为advisor
(该对象包含通知、切点、通知方法)排好序放入List并缓存。 - ②判断切点:在Bean初始化后调用
BeanPostProcessor
拿到之前缓存的advisor
,根据advisor中的pointcut判断当前Bean是否被切点表达式命中,如果匹配则为Bean创建动态代理(①JDK动态代理;②CGLib动态代理)。 - ③调用增强:通过之前创建的动态代理调用方法执行增强,通过调用链设计模式依次调用通知方法。
四十一、Spring多线程下事务能否保持一致性?
- Spring的事务信息是存在ThreadLocal中的Connection,所以一个线程永远只能有一个事务
- 所以Spring的事务是无法实现事务一致性,一个线程只对应一个Connection
- 可以通过编程式事务【编程控制事务提交回滚】,或者通过分布式事务的思路:二阶段提交方式
四十二、Spring事务传播行为的原理是什么?
- 在要开启事务的方法上加@Transactional注解
- 此时Spring就会使用AOP的思想,对你的这个方法在执行之前,先去开启事务,执行完毕之后根据方法是否报错,决定回滚或者提交事务。
Spring的事务信息是存在ThreadLocal中的, 所以一个线程永远只能有一个事务。
- 融入:当传播行为是融入外部事务则拿到ThreadLocal中的Connection,共享一个数据库连接共同提交、回滚;
- 创建新事务:当传播行为是创建新事务,会将嵌套新事务存入ThreadLocal、再将外部事务暂存起来;当嵌套事务提交、 回滚后,会将暂存的事务信息恢复到ThreadLocal中
四十三、Spring的AOP是在哪里创建的动态代理?
- 普通情况下,在Bean的生命周期的初始化之后,通过
BeanPostProcessr.postProcessAferlnitialization()
创建AOP - 有循环依赖的情况下,在Bean属性注入时存在的循环依赖的情况下,也会为循环依赖的Bean通过
MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition()
创建AOP
四十四、解释一下AOP中的常用名词?
四十五、什么是AOP,能做什么?
AOP,Aepect Oriented Programming,面向切面编程。将那些与业务无关,但是能对多个对象产生影响的公共行为和逻辑,抽象并封装成一个可重用的模块。这个模块就是“切面”(Aspect),可以减少代码重复,降低模块之间的耦合度,同时提高系统的可维护性。
一般用于权限认证、日志记录、事务处理。
四十六、什么情况下AOP会失效?怎么解决?
①排除开发者开发中的错误情况。
②目标方法的访问权限为private也会失效
③目标类没有配置为Bean是不能进行AOP的
④方法内部调用不会触发AOP,必须通过代理对象进行调用才能触发AOP
在这个图中可以看到,如果在目标方法mod中直接调用目标方法add,这种情况下就会导致add方法的AOP失效,需要通过动态代理来调用目标方法add才不会使得AOP失效。
解决方法:创建动态代理对象,最终使用动态代理对象调用目标方法
- 在本类中注入动态代理Bean对象,通过Bean来进行目标方法的调用【相当于多拿一份对象来调用】
- 设置暴露当前代理对象到本地线程【相当于扩大对象的影响范围】
- 在动态代理类上使用
@EnableAspectJAutoProxy(exposeProxy = true)
- 在需要调用的方法中使用
AopContext.currentProxy()
拿到当前正在使用的动态代理对象
四十七、Spring的声明式事务的实现过程?
- 使用:
- 类上使用
@EnableTransactionManagement
注解 - 在事务方法Bean上(类上面、接口上面、方法上面、接口方法上面)使用
@Transactional
注解
- 实现过程:
- Spring事务底层是基于数据库事务和AOP机制(同样通过BeanPostProcessor解析切面和创建动态代理)。
- 为使用了
@Transactional
注解的Bean创建一个代理。 - 如果是事务方法(类上面、接口上面、方法上面、接口方法上面)开始事务:动态代理
try{
// 创建一个数据库连接,并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的一步
// 然后执行目标方法,方法中会执行数据库操作sql
catch{
// 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
}
// 执行完当前方法后,如果没有出现异常就直接提交事务
- 实现原理:
- 解析切面:在Bean创建前的第一个Bean的后置处理器解析,解析advisor(pointcut、advise)
- 创建动态代理:在Bean的初始化之后调用Bean的后置处理器进行创建动态代理(有接口使用jdk,无接口使用cgLib),创建动态代理之前会根据advisor中的pointcut匹配@Transactional(方法上有无、类上有无、接口或父类有无),匹配到任意一个就创建动态代理
- 调用:上述的代码块中的内容
四十八、为什么@ComponentScan不设置basePackage也会扫描?
-
@ComponentScan
注解如果设置了basePackage属性,那么将会根据设置的属性值获取到扫描包路径 -
@ComponentScan
如果没有basePackage属性,那么将会在这个注解所在的类的包当作扫描包路径(即每个类的第一行声明的包路径)
四十九、AOP有几种实现方式?
- Spring 1.2 基于接口的配置:最早的Spring AOP是完全基于几个接口的。
- Spring 2.0 schema-based配置:Spring 2.0以后使用XML的方式来配置,使用命名空间
<aop></aop>
- Spring 2.0 @AspectJ配置:使用注解的方式来配置,这种方式感觉是最方便的,还有,这里虽然叫做@AspectJ,但是这个和AspectJ其实没啥关系。
- AspectJ方式,这种方式其实和Spring没有关系,采用AspectJ进行动态织入的方式实现AOP,需要用AspectJ单独编译。
五十、Spring的事务隔离级别?
并发事务中可能产生的问题:脏读、不可重复读、幻读
概念:通过设置隔离级别可以解决并发事务中可能产生的问题。
- 脏读
- 一个事务,读取了另一个事务未提交的数据,会导致读取的数据不一致
@Transactional(Isolation.READ_COMMITTED)
- 设置读已提交,规定其他事务只能读取该事务已经提交的数据
- 不可重复读
- 一个事务中,多次读取相同的字段,读取的数据不一致
@Transactional(Isolation.REPEATABLE_READ)
- 设置可重复读,即在一个事务进行中,禁止其他事务对该事务中的字段进行修改,可以保证该事务在进行中多次读取同一字段值都是相同的,解决了不可重复读的问题。(行锁)
- 幻读
- 一个事务中,多次读取全表中的数据,但是前后读取结果不一致,数据表的字段数量有变化(可能多行或者少行的情况)。区别于上面的不可重复读,前者是规定在字段上的级别,后者是在数据表的级别。
@Transactional(Isolation.SERIALIZABLE)
- 设置事务串行化,在这个事务执行期间,禁止其他事务对该表格进行更新删除增加操作,这样就可以解决幻读的问题。因为这种方式相互于在全表的级别上进行加锁操作,所以执行的效率比较低下。(表锁)
不可重复读,指的是在事务中多次读取同一字段值,所以只需要锁行
幻读,指的是在事务中多次读取全表中的数据,所以需要对整个表加锁
总结来说,三者的解决方案,由前至后执行效率越来越低;但是并发安全性上却越来越高。
- 当不设置隔离级别的时候,默认的数据隔离级别:
- MySQL:可重复读
- Oracle:读已提交
五十一、Spring的事务传播行为?
Spring的事务传播行为指的是,一个事务方法被另一个事务方法调用,这个事务方法该如何被执行。
五十二、Java Config如何启用AOP?如何强制使用CGLib?
Java Config方式启用AOP的步骤:
①在依赖中引入AspectJ
②在配置类上开启@EnableAspectJAutoProxy
③如果想要强制使用CGLib,可以在@EnableAspectJAutoProxy(proxyTargetClass = true)
④如果想要在线程中暴露代理对象,使用@EnableAspectJAutoProxy(exposeProxy = true)
将代理对象暴露在ThreadLocal中,之后使用AopContext.currentProxy()
获得代理ThreadLocal中的代理对象。
五十三、如果要将一个第三方的类配置为Bean有哪些方法?
- 第三方的类,要想注册成为一个Bean,一般首选采用
@Bean
的方式。在一个方法上使用该注解,方法内部返回一个新建的实例对象。 - 使用
@Import
注解,属性值选择要配置为Bean的类的class。例如:@Import(DataSource.class)
- 新建一个实现类,实现
ImportBeanDefinitionRegistrar
接口,之后使用beanDefinition
对象的getPropertyValues()
方法,往BeanDefinition
中添加新的属性信息,完成Bean的属性注入。当BeanDefinition被Import的后置处理器修改之后,那么将会修改Bean的生产方式,最终放映到实际上就是产生含有特定属性的Bean对象。 - 新建一个实现类,实现
BeanDefinitionRegistryPostProcessor
接口,然后实现其中的方法,添加或者修改BeanDefinition
中已经修改好的信息。
五十四、JDK动态代理和CGLib动态代理的区别是什么?
- JDK动态代理只支持接口的动态代理,不支持类的动态代理
- JDK动态代理在运行的时候会创建一个$Proxy*.class动态代理类
- 该代理类会实现目标类的接口,并且代理类会实现接口的所有方法来进行增强
- 调用时先通过代理类调用处理类进行增强,再通过反射的方式调用目标方法,实现AOP
- 如果代理类没有实现接口(可以不用实现接口),那么会使用CGLib创建动态代理
- CGLib的底层是通过ASM(一个开源的字节码生成库)在运行时动态的生成目标类的一个子类,会生成多个并且会重写父类所有的方法增强代码。
- 调用时先通过代理类进行增强,再直接调用父类对应的方法进行调用目标方法。从而实现AOP。
- CGLib是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLib做动态代理的。
- CGLib除了生成目标子类代理类,还有一个FastClass(路由类),可以但不是必须让本类方法调用进行增强,而不会像jdk代理那样本类方法调用增强会失效
性能上的对比,JDK动态代理的性能更好。无论是生成的速度还是调用的速度,现阶段经过JDK的多次迭代升级,都是jdk动态代理更快。
五十五、Spring的事务管理类型以及实现方式?
- 编程式事务管理:通过编程的方式进行事务管理,主要实现使用在JDBC中,取消事务的自动提交以及发生异常时直接回滚的操作。
- 声明式事务管理:采用注解的方式声明事务管理的类型,可以将业务代码和事务管理代码分开,降低代码的耦合度。
实现声明式事务的四种方式:
- 基于
TransactionInterceptor
接口的声明式事务:Spring声明式事务的基础,不建议使用 - 基于
TransactionProcyFactoryBean
接口的声明式事务:第一种方式的简化,简化了配置文件的书写 - 基于 和 命名空间创建的事务管理:使用xml文件来使用事务
- 基于
@Transactional
的全注解方式创建的注解:将声明式事务简化到了极致,是最简便的方式
五十六、事务的四大特性
事务的四大特性简称为ACID,主要为原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability。
- 原子性Atomicity,事务执行之后要么成功要么失败,不会产生一个中间状态,失败之后不会造成真正的影响。
- 一致性Consistency,事务必须使数据库从一个状态转变到另外一个状态,事务执行前后保持一致性。
- 隔离性Isolation,数据库为每个用户创建事务之后,事务之间彼此不会干扰,并发事务之间互相隔离。
- 持久性Durability,事务被提交了,就代表事务对于数据库产生的影响是持久性的,即便是数据库关闭也不会影响已经提交的事务。
五十七、Spring AOP和AspectJ AOP有什么区别?
Spring AOP在实现的时候需要使用到AspectJ所提供的切面以及切点解析匹配,前置后置增强等这些都是由AspectJ提供支持的。
AOP在实现的时候主要区别在于代理模式,分为静态代理和动态代理。
- Spring AOP使用的是动态代理,默认根据接口的有无分别使用jdk动态代理或者CGLib动态代理。
- AspectJ AOP主要使用的是静态代理,区别于Spring AOP的运行时增强,AspectJ AOP主要是一种编译增强,是通过修改运行的字节码文件来达到增强的效果,将增强的代码织入到.class字节码文件中。
根据字节码文件的运行时间点,这个增强加载的时机可以分为三种情况:①编译期织入;②编译后织入;③类加载时织入。
- 类加载时期织入比较困难,主要是要使用指定类加载器来完成。主要手段有两种:①使用自定义类加载器来执行;②在JVM启动的时候使用AspectJ提供的agent:
javaagent:xx/xx/aspectjweaver.jar
两者区别就是,AspectJ提供了AOP实现的完整解决方案,但是Spring AOP则并没有实现完整方案,切点解析以及切面匹配都是用的AspectJ中的方法提供的。所以无论如何在使用的时候都要导入AspectJ的依赖包。
两者在执行的时候,Spring AOP会在容器启动的时候生成代理实例,调用方法上也会增加调用栈的深度,加剧内存的使用,所以拖累运行性能,不如AspectJ AOP的性能。
五十八、将一个第三方的类配置成Bean有哪些方法?
一共有四种方式可以将一个第三方的类配置成 Bean 。因为是第三方的类,所以不能直接在类名上添加 @Component 来实现,这样会修改第三方代码,不符合预期。
一、使用 @Bean 方式
创建一个返回值类型为目标值类型的方法,最终返回需要创建的对象。这样的方法可以界定初始化流程,可以比较灵活地进行配置,还可以给 Bean 设定一个 Name ,符合很多场景下的需要。
二、使用 @Import(XXX.class) 方法
这种方式可以导入第三方的类,区别于 @Bean 方式,这种方式不能进行 Bean 的初始化过程的自定义,不是非常灵活。
三、使用 @Import(XXX.class) + 实现类 方法
这种方式区别于第二种单纯使用 @Import(XXX.class) 注解,这种方式还实现了对于 ImportBeanDefinitionRegistrar 的自定义实现,可以通过这个实现类修改 Bean 的创建过程,将 Bean 注册成一个符合自定义需求的 Bean ,解决了第二种方式的弊端。
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
// BeanDefinition 存储 Bean 的描述信息,决定 Bean 的生产方式
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(/*第三方类.class*/);
rootBeanDefinition.getPropertyValues().add("xxx", "xxx");
registry.registerBeanDefinition("beanName", rootBeanDefinition);
ImportBeanDefinitionRegistrar.super.registerBeanDefinitions(importingClassMetadata, registry);
}
}
四、实现 BeanDefinitionRegistryPostProcessor 接口,并将实现类注入
@Component
public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
BeanDefinition beanDefinition = new RootBeanDefinition(/*xxx.class/beanClassName*/);
beanDefinition.getPropertyValues().add("xxx", "xxx");
beanDefinitionRegistry.registerBeanDefinition("beanName", beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}
这种方式用的是 BeanDefinition 的后置增强方法,可以通过实现类将要注入的第三方类封装成一个 BeanDefinition ,之后通过注册器将其注册,最后交由 IoC 容器进行实例化。区别于第三种方式,这种方式生成 Bean 的顺序要落后于 Import + ImportBeanDefinitionRegistrar 的顺序了,且要通过 @Component 将实现类自身进行注入。
五十九、Spring MVC 以及 DispatcherServlet 的工作流程?
整个执行流程,可以参考开发中的步骤。首先,用户发起一个请求,请求会直接交给前端控制器来处理。如果用户发起的请求是 /user/login ,那么前端控制器会找到请求对应的 @RequestMapping 方法,也就是处理器映射器。之后前端控制器就得知了该调用哪个方法来响应这个请求,这里处理器映射器会返回一个执行链,前端控制器会根据执行链将请求发送给 HandlerAdapter 处理器适配器,由适配器将请求交给 Handler 处理器(Controller 方法)来响应请求,无论是返回 String 还是 Model 类型,最终都会由 Handler 将其封装成一个 ModelAndView 对象。这里的处理过程在 DispatcherServlet 的 doDispatch 方法中。
DispatcherServlet 会将 ModelAndView 对象交给 ViewResolver 视图解析器来进行处理,最后返回 View 对象,将返回的数据填充到视图中,最后返回给用户。
六十、Spring Boot 有哪些特性?
Spring Boot 是一个用来快速开发 Spring 的框架,设计目的就是用来简化 Spring 项目的初始搭建以及开发过程。
① Spring Boot 集成了很多内置的 starter ,可以做到开箱即用
② Spring Boot 简化开发,使用 JavaConfig 的方式可以做到无 xml 开发
③ Spring Boot 内置了 Web 容器,无需依赖外部 Web 服务器,省略了 Web.xml 文件
④ Spring Boot 管理了第三方依赖的版本,减少了依赖的版本冲突问题
⑤ Spring Boot 自带监控功能,可以监控程序的运行状况,提供了优雅关闭的功能
六十一、Spring Boot 外部 Tomcat 启动原理?
启动外部 Tomcat 需要编写一个类继承 SpringBootServletInitializer 类,重写其中的 Configure 方法,最后返回一个 SpringApplicationBuilder 类型的对象。实现过程利用了 SPI 机制。
public class TomcatStartApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(/*XXXApplication.class*/);
}
}
六十二、Spring Boot 内置 Tomcat 启动原理?
在 Spring Boot 中有相关的服务器启动配置类,为 ServletWebServerFactoryConfiguration 类,在配置类中有一个静态类表示内嵌的 Tomcat,标注了 @ConditionalOnClass 注解,其中标注了 Tomcat.class 属性值,表示 Tomcat 为内置支持的服务器。
除了 Tomcat 之外,Spring Boot 还内置了 Jetty 和 Undertow 。类名格式为 EmbeddedTomcat 、EmbeddedJetty 、EmbeddedUndertow 。Spring Boot 默认支持的是 Tomcat ,其他两个是需要自己手动打开下载的。
Spring Boot 在启动的时候会创建 Spring 容器,AnnotationConfigServletWebServerApplicationContext 。之后会调用容器的 refresh 方法,加载 IoC 容器。此时会解析自动配置类,实质就是在方法 invokeBeanFactoryPostProcessors(beanFactory) 中调用 Bean 工厂的后置增强方法。 refresh 方法的父类是 onRefresh 方法,会调用其中一个 createWebServer 方法,最终通过 getWebServer 启动 Tomcat 。
六十三、为什么要使用动态代理?
代理设计模式的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
动态代理可以在不修改被代理对象代码的基础上,通过扩展代理类,进行一些功能的附加与增强。
静态代理简单的实现方法就是通过继承被代理对象实现的,简单的实例如下:
① 编写一个接口,如下:
public interface Subject {
/**
* 原始方法
*/
void doSomething();
}
② 编写一个该接口的实现类,如下:
public class RealSubject implements Subject {
/**
* 实现类的默认方法
*/
@Override
public void doSomething() {
System.out.println("I'm reading.");
}
}
③ 编写一个代理类,对原有方法进行增强调用,如下:
public class Proxy extends RealSubject {
Subject demo = new RealSubject();
/**
* 代理类对于目标方法的执行
*/
@Override
public void doSomething() {
demo.doSomething();
after();
}
public void after() {
System.out.println("I'm listening to music.");
}
}
④ 测试代理类的执行,如下:
public class Test {
public static void main(String[] args) {
Subject subject = new Proxy();
subject.doSomething();
}
// I'm reading.
// I'm listening to music.
}
可以看到,通过利用代理类,可以在不修改原有方法的基础上,对现有方法进行增强调用。遵守了对扩展开放,对修改关闭的原则(开闭原则),算是代理类的一些基础实现。
但是在实际的业务方法中,往往需要进行功能的全局增强,比如记录日志。我们可以利用动态代理的方式,对目标方法进行增强,避免了修改所有原始代码,方便快捷且不易出错。比较常见的应用有 Spring 中的 AOP 功能。
动态代理常见的实现,分为 jdk 动态代理 和 CGLib 动态代理两种。
JDK 动态代理
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces); // proxyName 为类名,interfaces为顶层接口Class
//如果需要看,可以将字节码写入文件进行观察
File file = new File("D:/testProxy/Ddd.class");
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(bs);
fileOutputStream.flush();
fileOutputStream.close();
CGLib 动态代理
byte[] bs = DefaultGeneratorStrategy.INSTANCE.generate(enhancer);
FileOutputStream fileOutputStream = new FileOutputStream("D:/testProxy/Cc.class");
fileOutputStream.write(bs);
fileOutputStream.flush();
fileOutputStream.close();
更多的区别参见 五十四 题。
六十四、@SessionAttribute 与 @SessionAttributes 的作用?
如果要在多个请求之间共享数据,可以使用 @SessionAttributes 注解来完成。该注解使用在控制器类上,不能标注在方法上。使用 @SessionAttributes(“user”) 注解可以让同一个类中其他方法获取到 model 对象 key 为 user 的属性值,还会将 user 属性值放到 session 中。利用 @SessionAttributes 可以实现同一个类中的 model 对象值传递,同时还可以将指定属性添加到会话中,值分为 value 和 type 两种,两者是并集关系。
@Controller
@SessionAttributes("user")
public class ModelController {
@ModelAttribute("user")
public User initUser(){
User user = new User();
user.setName("default");
return user;
}
}
@SessionAttribute 属性是用于获取已经存储的 session 数据,并且作用于方法的层面,通常写在方法的参数列表的位置,用于将 session 中取出的值赋值给参数。
@RequestMapping("/session")
public String session(
@SessionAttribute("user") User user
){
// do something
return "index";
}
@SessionAttributes 是将 model 设置到 session 中去。
@SessionAttribute 是从 session 获取之前设置到 session 中的数据。
六十五、什么是跨域问题?跨域问题的解决办法有哪些?
跨越问题的产生归根于浏览器的同源策略的限制。同源策略是浏览器处于安全的基本限制, Web 相当于是构建在同源策略之上的。同源策略会阻止一个域的 JavaScript 脚本和另外一个域的内容进行交互,浏览器是针对于同源策略的一种实现。同源指的是两个页面具有相同的协议、域名、端口号。
非同源之间的限制有:
【1】无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
【2】无法接触非同源网页的 DOM
【3】无法向非同源地址发送 AJAX 请求
解决方法:
① 根据浏览器检测同源的措施来解决:
为两个页面设置相同的 domain 属性值。浏览器判断是否同源的标准就是判断 domain 属性值是否相等。
// 两个页面都设置
document.domain = 'test.com';
② CORS
1、普通跨域请求:只需服务器端设置Access-Control-Allow-Origin
2、带cookie跨域请求:前后端都需要进行设置
CORS 是跨域资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。
服务端要进行相关的设置,主要是针对 response 对象
/*
* 导入包:import javax.servlet.http.HttpServletResponse;
* 接口参数中定义:HttpServletResponse response
*/
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true");
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
③ @CrossOrigin 注解
该注解是 Spring MVC 框架中提供的一个注解,可以使用在控制器类上或者方法上,拥有两个属性:
- origins: 允许可访问的域列表
- maxAge:准备响应前的缓存持续的最大时间(以秒为单位)
@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
六十六、@Controller 和 @RestController 的区别?
@RestController 注解相当于 @ResponseBody + @Controller 合在一起的作用。
所以使用了 @RestController 的控制器类中的方法,不会返回页面或进行页面跳转,返回的只是 json 、mediaType 或者 xml 格式的数据。
如果使用的是 @Controller 注解标注控制器类,视图解析器会正常生效,可以返回页面。如果想要返回 json 等数据,需要添加 @ResponseBody 注解。
六十七、 Spring MVC 的拦截器和过滤器有什么区别?执行顺序有什么区别?
- 拦截器不依赖 Servlet 容器,过滤器依赖 Servlet 容器
- 拦截器只能对 action 请求( DispatcherServlet 映射的请求)起作用,而过滤器则可以对几乎所有的请求起作用
- 拦截器可以访问容器中的 Bean,相当于可以进行 DI 的操作,而过滤器不能访问,但是基于 Spring 注册的过滤器也可以访问容器中的 Bean
执行顺序上,首先启动 Tomcat 服务器,之后启动 filter 过滤器、Servlet ,再启动 interceptor 拦截器,最终启动 Controller 控制器。
六十八、 Spring MVC 的控制器是不是单例模式?如果是,有什么影响?
Spring MVC 在默认的情况下,控制器是单例的,且可能会出现线程安全问题。如果需要在控制器类中声明一些成员变量,一旦多个线程需要同时访问控制器中的成员变量,可能会造成线程安全的问题。此时有三种解决方法。
- 将控制器从有状态变成无状态:尽量将控制器中的变量声明在方法当中,将控制器从有状态变成无状态的,可以避免出现多个线程同时竞争成员变量的状况出现。
- 设置同步锁:在多个线程同时访问控制器的成员变量的时候,对成员变量加上同步锁,这样可以解决并发安全的问题。但是这样会将多个并发线程在访问的时候变成同步操作,拖累访问性能。【不推荐】
- 使用 ThreadLocal :使用 ThreadLocal 为每个线程创建一份单独的线程私有空间,可以避免线程之间的访问冲突,既可以提高访问效率也可以解决并发数据安全问题。需要注意内存泄漏的问题。
六十九、 Get 和 Post 的乱码问题如何解决?
① 首先确定文件保存的编码格式,尽量设置为 UTF-8 编码格式;
② 解决 Post 请求乱码:配置 CharacterEncodingFilter 过滤器,设置成 UTF-8 格式;
③ 解决 Get 请求乱码:主要是将请求的 URL 中拼接的参数设置为系统可以解析的编码格式。一种方式是修改 Tomcat 配置中文件添加编码和工程编码一致;另一种是对参数进行重新编码。
request.getParamter("xxx").getBytes("ISO8859-1","utf-8");
七十、Spring Boot 的启动原理是什么?
Spring Boot 的启动中,直接启动主项目 main 方法中的 SpringApplication.run() 方法。
① 运行 main 方法,会新建一个 SpringApplication 对象,读取 spring.factories 文件中 key 为 listener 和 ApplicationContextInitializer 的类,方便后续进行扩展和维护。
② 运行 run 方法,读取环境变量、配置信息
③ 创建 SpringApplication 上下文:ServletWebServerApplicationContext 对象
④ 预初始化上下文:将启动类作为配置类进行读取。读取启动类信息,将信息配置注册成一个 BeanDefinition 对象
⑤ 调用 refresh 加载 IoC 容器:在 invokeBeanFactoryPostProcessor 中加载所有的自动配置类(实质上是解析 @Import 注解)。在 onRefresh 中创建(内置)Servlet 容器
⑥ 整个过程中会调用监听器对外进行扩展
七十一、Spring 是如何管理 Mybaits 的 Mapper 接口的?
- 首先 MyBatis 的 Mapper 接口核心是 JDK 动态代理
- Spring 会排除接口,无法注册到 IoC 容器中
- MyBatis 实现了 BeanDefinitionRegistryPostProcessor 可以动态注册 BeanDefinition
- 需要自定义扫描器(继承 Spring 内部扫描器 ClassPathBeanDefinitionScanner )重写排除接口的方法(isCandidateComponent)
- 但是接口虽然注册成了 BeanDefinition 但是无法实例化 Bean 因为接口无法实例化
- 需要将 BeanDefinition 的 BeanClass 替换成 JDK 动态代理的实例
- Mybatis 通过 FactoryBean 的工厂方法设计模式可以自由控制 Bean 的实例化过程,可以在 getObject 方法中创建 JDK 动态代理
七十二、是否可以将 Bean 全部放入 Spring MVC 中进行管理?
可以。父容器的体现无非是为了获取到子容器无法获取的 Bean ,如果全部包含在子容器中,那么是可以不用父容器而直接使用子容器来进行管理的。
但是在实际的项目中并不推荐这种做法。如果项目中用到 AOP 或者事务,那么需要在配置 AOP 和事务的时候将配置放到 Spring MVC 的配置文件中,避免一部分内容在父容器一部分内容在子容器中,造成服务失效。
七十三、为什么 Spring Boot 的 jar 包可以直接运行?
我们在编写 Spring Boot 程序的时候,IDE 会在进行初始化之后,在 pom.xml 文件中添加一个插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
这个插件主要是用来部署 Spring Boot 的,加了这个插件之后,就可以直接运行 java -jar xxx
命令了。
在项目被打包成一个 jar 包之后,根据 jvm 规范需要提供一个 Main Class 来作为程序的启动类。部署了插件之后,打包完毕生成的 jar 文件中,有一个 META-INF 目录,其中提供了一个 MAINFEST.MF 文件(主程序清单文件),其中有一项配置信息记录了整个项目的 main 程序。
Main-Class: org.springframework.boot.loader.JarLauncher
整个过程依赖于一个启动依赖项。因为项目中添加了这个插件,其实就相当于添加了一个特定的依赖项:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
</dependency>
拥有了这个依赖之后,程序在打包之后,从 JarLauncher 开始启动。其中包含一个 launch 方法,在该方法中创建了一个类加载器,类加载器名为 LaunchedURLClassLoader ,可以根据指定的 URL 加载特定的类(主要是为了加载程序引入的依赖)。在获得类加载器之后,会根据主程序清单找到 Start-class 项,得到 Spring Boot 项目的启动类,项目开始运行。
- Spring Boot 提供了一个插件 spring-boot-maven-plugin 用于把程序打包成一 个可执行的 jar 包
- Spring Boot 应用打包之后,生成一个 Fat jar (包含了其他 jar 包的 jar )包含了应用依赖的 jar 包和 Spring Boot loader 相关的类
-
java -jar
会去找 jar 中的 manifest 文件,在那里面找到真正的启动类 - Fat jar 的启动 Main 函数是 JarLauncher,它负责创建一个 LaunchedURLClassLoader 来加载 boot-lib 下面的 jar ,并以一个新线程启动应用的 Main 函数
七十四、 Spring 中用到了哪些设计模式?
- 简单工厂—— BeanFactory
- 工厂方法—— FactoryBean
- 单例模式—— Bean 模式
- 适配器模式—— Spring MVC 中的 HandlerAdapter
- 装饰器模式—— BeanWrapper
- 代理模式—— AOP 底层
- 观察者模式—— Spring 中的事件监听
- 策略模式—— excludeFilters、includeFilters
- 模板方法模式—— Spring 几乎所有的外接扩展都采用这种模式
- 责任链模式—— AOP 中的方法调用
七十五、是否可以将 Spring MVC 中所有的 Bean 交给 Spring 容器进行管理?
不可以。这样会导致请求的时候发生 404 错误。如果都交给父容器进行管理子容器中的 Bean ,在 Spring MVC 在进行初始化 HandlerMethods 的时候,会无法根据 Controller 的 Handler 方法注册 HandlerMethod ,并没有去查找父容器的 Bean ,也就无法根据 URI 来获取到 HandlerMethod 来进行匹配。
七十六、 Spring Boot 的自动配置原理是什么?
自动配置这个过程依赖于项目的主程序。在项目的启动类上标注了一个 @SpringBootApplication 注解,该注解引入了一个 @SpringBootConfiguration 注解和一个 @EnableAutoConfiguration 注解。
实际上的自动配置主要依赖于 @EnableAutoConfiguration 负责。该注解引入了 @Import 注解,该注解包含了一个 AutoConfigurationImportSelector.class 属性。在该 @Import 注解被加载的时候,会导入这个自动配置导入选择器类。
Spring 容器在启动的时候,加载 IoC 容器的时候会解析 @Import 注解, @Import 注解实现了一个选择器组件 DeferredImportSelector ,它会使 SpringBoot 的自动配置类放到最后,方便我们进行扩展和覆盖。之后读取所有的 /META-INF 目录下的 spring.factories 文件(是一种伪 SPI 的扩展),过滤出所有 AutoConfigurationClass 的类,最后通过 @ConditionalOnXxx 注解排除无效的自动配置类。
七十七、 Spring Boot 的核心注解?
@SpringBootApplication
@SpringBootConfiguration
@EnableAutoConfiguration
@Conditional 类型注解,主要包括: @ConditionalOnBean 、@ConditionalOnClass 、@ConditionalOnExpression 、@ConditionalOnMissingBean 等
七十八、 Spring 事件监听核心机制是什么?
Spring 的事件监听机制主要用到了观察者模式
- 事件(ApplicationEvent)负责对应相应监听器事件源发生某事件是特定事件监听器被触发的原因。
- 监听器(ApplicationListener)对应于观察者模式中的观察者。监听器监听特定事件并在内部定义了事件发生后的响应逻辑。
- 事件发布器(ApplicationEventMulticaster)对应于观察者模式中的被观察者/主题,负责通知观察者对外提供发布事件和增删事件监听器的接口,维护事件和事件监听器之间的映射关系,并在事件发生时负责通知相关监听器。
七十九、Spring MVC 如何处理异步调用?
在处理异步请求的时候,都是以 JSON 格式的字符串作为中间对象来进行转换的。
① 引入 Jackson 依赖或者 FastJson 等依赖,用来处理 JSON 格式字符串的转换
② 在配置文件中配置 JSON 的消息转换器,Jackson 不需要配置 HttpMessageConverter
③ 在接受 Ajax 的方法中可以直接返回 Object 、List 等,但在方法前面要加上 @ResponseBody 注解
八十、常见的微服务组件有哪些?
注册中心的核心功能原理:(以 Nacos 为例)
- 服务注册:当服务启动的时候,会通过 Rest 请求的方式向 Nacos Server 注册自己的服务
- 服务心跳:Nacos Client 会维护一个定时心跳持续维护 Nacos Server ,默认 5s 一次。如果 15s 没有接收到心跳,会将服务健康状态设置为 false ,在服务拉取的时候会忽略掉 false 的服务;如果 30s 都没有接收到心跳,那么会将服务剔除。
- 服务发现:Nacos Client 会有一个定时任务,实时去 Nacos Server 拉取健康状态的服务
- 服务停止:Nacos Client 会主动通过 Rest 请求 Nacos Server 发送一个注销的请求