谈一下你对Spring的理解:
Spring 最主要的核心就是 IOC 容器,这个容器根据我们的配置文件创建了各种 Bean 且编织它们之间的依赖关系,管理这些 Bean 的生命周期。
所以最核心的代码模块是:
- spring-core:核心类库,包括资源访问等一些工具类,别的模块都会依赖此模块
- spring-beans:对 Bean 的支持,包括控制反转和依赖注入,核心工厂 BeanFacotry 就在这个模块。
- spring-context:Spring 的上下文,支持数据验证、国际化、事件传播等支持,ApplicationContext 就在这里,可以理解为运行时的 Spring 容器。
基于上面的这些核心模块,Spring 也提供了很多重要的支持:
- spring-aop:AOP 的支持
- spring-jdbc:jdbc 的支持
- spring-orm: orm 的支持
- spring-webmvc: mvc 的支持
- 等等
什么是 IOC?
IOC,即 Inversion of Control,控制反转。
首先要明确 IOC 是一种思想,而不是一个具体的技术,其次 IOC 这个思想也不是 Spring 创造的。
然后我们要理解到底控制的是什么,其实就是控制对象的创建,IOC 容器根据配置文件来创建对象,在对象的生命周期内,在不同时期根据不同配置进行对象的创建和改造。
那什么被反转了?其实就是关于创建对象且注入依赖对象的这个动作,本来这个动作是由我们程序员在代码里面指定的,例如对象 A 依赖对象 B,在创建对象 A 代码里,我们需要写好如何创建对象 B,这样才能构造出一个完整的 A。
而反转之后,这个动作就由 IOC 容器触发,IOC 容器在创建对象 A 的时候,发现依赖对象 B ,根据配置文件,它会创建 B,并将对象 B 注入 A 中。
这里要注意,注入的不一定非得是一个对象,也可以注入配置文件里面的一个值给对象 A 等等。
至此,你应该明确了,控制和反转。
IOC 有什么好处?
对象的创建都由 IOC 容器来控制之后,对象之间就不会有很明确的依赖关系,使得非常容易设计出松耦合的程序。
例如,对象 A 需要依赖一个实现 B,但是对象都由 IOC 控制之后,我们不需要明确地在对象 A 的代码里写死依赖的实现 B,只需要写明依赖一个接口,这样我们的代码就能顺序的编写下去。
然后,我们可以在配置文件里定义 A 依赖的具体的实现 B,根据配置文件,在创建 A 的时候,IOC 容器就知晓 A 依赖的 B,这时候注入这个依赖即可。
如果之后你有新的实现需要替换,那 A 的代码不需要任何改动,你只需要将配置文件 A 依赖 B 改成 B1,这样重启之后,IOC 容器会为 A 注入 B1。
这样就使得类 A 和类 B 解耦了, very nice!
并且也因为创建对象由 IOC 全权把控,那么我们就能很方便的让 IOC 基于扩展点来“加工”对象,例如我们要代理一个对象,IOC 在对象创建完毕,直接判断下这个对象是否需要代理,如果要代理,则直接包装返回代理对象。
这等于我们只要告诉 IOC 我们要什么,IOC 就能基于我们提供的配置文件,创建符合我们需求的对象。
正是这个控制反转的思想,解放了我们的双手。
什么是 DI
DI,Dependency Injection,依赖注入。
普遍的答案是 DI 是 IOC 的一种实现。
其实它跟 IOC 的概念一致,只是从不同角度来描述罢了。
Martin Fowler 提出了 DI 的概念,它觉得用 DI 来形容更加具体。
大致理解为容器在运行的时候,可以找到被依赖的对象,然后将其注入,通过这样的方式,使得各对象之间的关系可由运行期决定,而不用在编码时候明确。
什么是 Bean
可以认为,由 Spring 创建的、用于依赖注入的对象,就是 Bean。
例如调用 getBean 能返回的玩意,就是 Bean。
BeanFactory 什么用
BeanFactory 其实就是 IOC 的底层容器。
我们都说 Spring 是 IOC 容器,说的再直白点,其实就是 Bean 的工厂,它帮我们生产 Bean,如果我们需要 Bean 就从工厂拿到 bean,所以再来理解下 BeanFactory 这个名字,就知晓它就是 Spring 的核心。
例如我们调用 getBean ,这就是 BeanFactory 定义的方法,通过它得到 Bean。
不过一般我们所述的 BeanFactory 指的是它实现类,例如 DefaultListableBeanFactory。
BeanFactory 本身只是一个接口。
FactoryBean 又是啥
从命名角度来看,我们可以得知它就是一个 Bean,而不是一个工厂。
那为什么名字如此奇怪,它其实是一个接口,并且有以下几个方法
如果一个对象实现了这接口,那它就成为一种特殊的 Bean,注册到 IOC 容器之后,如果调用 getBean 获取得到的其实是 FactoryBean#getObject() 方法返回的结果。
为什么要这样做?
假设你依赖一个第三方的类 A,而我们又不能修改第三方的类,并且这个对象创建比较复杂,那么你就可以创建一个 bean 来封装它:
public class AFactoryBean implements FactoryBean<A> {
public A getObject() throws Exception {
A a = new A();
a.setXXX
....
...
return A
}
....省略一些实现
}Copy to clipboardErrorCopied
这样,我们 getBean("A") 会得到 AFactoryBean#getObject 的结果,如果单纯只想要 AFactoryBean, 那么加个 “&” 即可,即 getBean("&A")
ObjectFactory 又是啥
从命名角度来看,它是一个工厂。
好吧,摊牌了,没啥特殊含义,就是一个工厂。
Spring 的循环依赖(后面会细说)里就用到它了,三级缓存的 map 里面存储的就是 ObjectFactory,用于延迟代理对象的创建。
其实就是封装对象的创建过程,像三级缓存里的 ObjectFactory 逻辑就是判断这个 Bean 是否有代理,如果有则返回代理对象,没有则返回原来传入的对象。
ApplicationContext 呢
ApplicationContext 对我们来说应该很熟悉,我们基本上都是基于 ApplicationContext 操作的。
它也继承了 BeanFactory 的实现类,也属于一个 BeanFactory ,但是它内部还包装了一个 BeanFactory 的实现,这属于组合。
简单理解就是我 ApplicationContext 虽然是个 BeanFactory ,但是关于 Bean 的操作我还是委托内部组合的 BeanFactory 。
即,关于 Spring Beans 的一些操作都是委托内部的 BeanFactory 来操作的。
所以它有 BeanFactory 的所有功能。看到这肯定有同学发出疑问,那这玩意又啥用?
别急嘛,因为它还扩展了很多其他功能:
- AOP
- 国际化
- 资源管理
- 事件
- 注解
- 等等
因此,我们开发直接用的肯定是 ApplicationContext 而不是 BeanFactory。
Bean 一共有几种作用域
从官网,我们很容易可以得知,最新版本一共有六种作用域:
- singleton:默认是单例,含义不用解释了吧,一个 IOC 容器内部仅此一个
- prototype:原型,多实例
- request:每个请求都会新建一个属于自己的 Bean 实例,这种作用域仅存在 Spring Web 应用中
- session:一个 http session 中有一个 bean 的实例,这种作用域仅存在 Spring Web 应用中
- application:整个 ServletContext 生命周期里,只有一个 bean,这种作用域仅存在 Spring Web 应用中
- websocket:一个 WebSocket 生命周期内一个 bean 实例,这种作用域仅存在 Spring Web 应用中
别背网上那些多年前的答案了,以上才是最新的~
Spring 一共有几种注入方式
- 构造器注入,Spring 倡导构造函数注入,因为构造器注入返回给客户端使用的时候一定是完整的。
- setter 注入,可选的注入方式,好处是在有变更的情况下,可以重新注入。
- 字段注入,就是平日我们用 @Autowired 标记字段
- 方法注入,就是平日我们用 @Autowired 标记方法
- 接口回调注入,就是实现 Spring 定义的一些内建接口,例如 BeanFactoryAware,会进行 BeanFactory 的注入
其实官网上关于注入就写了构造器和setter :
像字段注入其实官方是不推荐的使用的,因为依赖注解,然后没有控制注入顺序且无法注入静态字段。
emmmm...实际上@Autowired用的还是很多的。。。。。。
说下 AOP
AOP,Aspect Oriented Programming,面向切面编程。
将一些通用的逻辑集中实现,然后通过 AOP 进行逻辑的切入,减少了零散的碎片化代码,提高了系统的可维护性。
具体是含义可以理解为:通过代理的方式,在调用想要的对象方法时候,进行拦截处理,执行切入的逻辑,然后再调用真正的方法实现。
例如,你实现了一个 A 对象,里面有 addUser 方法,此时你需要记录该方法的调用次数。
那么你就可以搞个代理对象,这个代理对象也提供了 addUser 方法,最终你调用的是代理对象的 addUser ,在这个代理对象内部填充记录调用次数的逻辑,最终的效果就类似下面代码:
class A代理 {
A a;// 被代理的 A
void addUser(User user) {
count();// 计数
a.addUser(user);
}
}
最终使用的是:
A代理.addUser(user);Copy to clipboardErrorCopied
这就叫做面向切面编程,当然具体的代理的代码不是像上面这样写死的,而是动态切入。
实现上代理大体上可以分为:动态代理和静态代理。
- 动态代理,即在运行时将切面的逻辑进去,按照上面的逻辑就是你实现 A 类,然后定义要代理的切入点和切面的实现,程序会自动在运行时生成类似上面的代理类。
- 静态代理,在编译时或者类加载时进行切面的织入,典型的 AspectJ 就是静态代理。
Spring AOP默认用的是什么动态代理,两者的区别
Spring AOP 的动态代理实现分别是:JDK 动态代理与 CGLIB。
默认的实现是 JDK 动态代理。
ok,这个问题没毛病(对实际应用来说其实不太准确),然后面试官接着问那你平时有调试过吗,确定你得到的代理对象是 JDK 动态代理实现的?
然后你信誓旦旦的说,对,我们都实现接口的,所以是 JDK 动态代理。
然而你简历上写着项目使用的框架是 SpringBoot,我问你 SpringBoot 是什么版本,你说2.x。
然后我就可以推断,你没看过,你大概率仅仅只是网上看了相关的面试题。
要注意上面说的默认实现是 Spring Framework (最新版我没去验证),而 SpringBoot 2.x 版本已经默认改成了 CGLIB。
而我们现在公司大部分使用的都是 SpringBoot 2.x 版本,所以你要说默认 JDK 动态代理也没错,但是不符合你平日使用的情况,对吧?
如果你调试过,或者看过调用栈,你肯定能发现默认用的是 CGLIB(当然你要是没用 SpringBoot 当我没说哈):
市面上大部分面试题答案写的就是 JDK 动态代理,是没错,Spring 官网都这样写的,但是咱们现在不都是用 SpringBoot 了嘛,所以其实不符合我们当下使用的情况。
所以面试时候不要只说 Spring AOP 默认用的是 JDK 动态代理,把 SpringBoot 也提一嘴,这不就是让面试官刮目一看嘛(不过指不定面试官也不知道~)
说下 Spring Bean 的生命周期
在说具体的生命周期前,我们需要先知晓之所以 Bean 容易被添加一些属性,或者能在运行时被改造就是因为在生成 Bean 的时候,Spring对外暴露出很多扩展点。
基于这些点我们可以设置一些逻辑,Spring 会在 Bean 创建的某些阶段根据这些扩展点,基于此进行 Bean 的改造。
有了上面的认识,我们再来看 Spring Bean 的生命周期,我用一幅图先总结一下:
大致了解生命周期之后,我们再来看详细的操作,可以看到有好多扩展点可以搞事情:
注意细节,这幅图的颜色和上面那副有对应关系的。
我再用文字描述一下:
- 实例化Bean
- 根据属性,注入需要的 Bean
- 如果 Bean 实现了 BeanNameAware 等 aware 接口,则执行 aware 注入
- 如果有 BeanPostProcessor,则执行
BeanPostProcessor#postProcessBeforeInitialization方法 - 如果 Bean 是 InitializingBean,则执行 afterPropertiesSet 方法
- 如果有 initMethod ,则执行
- 如果有 BeanPostProcessor,执行
BeanPostProcessor#postProcessAfterInitialization方法 - 使用 Bean
- 如果 Bean 是 DisposableBean,则执行 destroy 方法
- 如果有 destroy 方法,则执行
说下对 Spring MVC 的理解?
Spring MVC 是基于 Servlet API 构建的,可以说核心就是 DispatcherServlet,即一个前端控制器。
还有几个重要的组件:处理器映射、控制器、视图解析器等。
由这几个组件让我们与 Servlet 解耦,不需要写一个个 Servlet ,基于 Spring 的管理就可以很好的实现 web 应用,简单,方便。
然后关于 MVC 的解释,我就不提了,什么 Model,View,Controller 啥的。
Spring MVC 具体的工作原理?
当一个请求过来的时候,由 DispatcherServlet 接待,它会根据处理器映射(HandlerMapping)找到对应的 HandlerExecutionChain(这里面包含了很多定义的 HandlerInterceptor,拦截器)。
然后通过 HandlerAdapter 适配器的适配(适配器模式了解一下)后,执行 handler,即通过 controller 的调用,返回 ModelAndView。
然后 DispatcherServlet 解析得到 ViewName,将其传给 ViewResoler 视图解析器,解析后获得 View 视图。
然后 DispatcherServlet 将 model 数据填充到 view ,得到最终的 Responose 返回给用户。
我们常用的视图有 jsp、freemaker、velocity 等。
SpringMVC 父子容器是什么知道吗
官网上有幅图可以了解下:
可以看到,services 和 repositories 是属于父容器的,而 Controllers 等是属于子容器的。
那为什么会有父子之分?
其实 Spring 容器在启动的时候,不会有 SpringMVC 这个概念,只会扫描文件然后创建一个 context ,此时就是父容器。
然后发现是 web 服务需要生成 DispatcherServlet ,此时就会调用 DispatcherServlet#init,这个方法里面最会生成一个新的 context,并把之前的 context 置为自己的 Parent。
这样就有了父子之分,这样指责就更加清晰,子容器就负责 web 部分,父容器则是通用的一些 bean。
也正是有了父子之分,如果有些人没把 controller 扫包的配置写在 spring-servlet.xml ,而写到了 service.xml 里,那就会把 controller 添加到父容器里,这样子容器里面就找不到了,请求就 404 了。
当然,如果你把 services 和 repositories 添加到子容器是没影响的,不过没必要,分层还是比较好的方式。
对了,子容器可以用父容器的 Bean,父容器不能用子容器的 Bean。
你了解的 Spring 都用到哪些设计模式
工厂模式,从名字就看出来了 BeanFacotry。
模板方法,什么 JdbcTemplate、RestTemplate 。
代理模式,AOP 整的都是代理。
单例,这都不需要说了。
责任链模式,比如拦截器
观察者模式,Spring里的监听器
适配器模式...SpringMVC 提到的 handlerApdaper
太多啦...
Spring 事务有几个隔离级别
从源码定义我们可以看到,一共有 5 种隔离级别,而 DEFAULT 就是使用数据库定义的隔离级别。
其他几种分别是:读未提交、读已提交、可重复读、序列化。
具体几个隔离级别的概念我就不介绍了,应该都很清楚。
Spring 有哪几种事务传播行为?
从源码来看,一共有 7 种事务传播行为:
- PROPAGATION_REQUIRED(默认) 如果当前存在事务,则用当前事务,如果没有事务则新起一个事务
- PROPAGATION_SUPPORTS 支持当前事务,如果不存在,则以非事务方式执行
- PROPAGATION_MANDATORY 支持当前事务,如果不存在,则抛出异常
- PROPAGATION_REQUIRES_NEW 创建一个新事务,如果存在当前事务,则挂起当前事务
- PROPAGATION_NOT_SUPPORTED 不支持当前事务,始终以非事务方式执行
- PROPAGATION_NEVER 不支持当前事务,如果当前存在事务,则抛出异常
- PROPAGATION_NESTED 如果当前事务存在,则在嵌套事务中执行,内层事务依赖外层事务,如果外层失败,则会回滚内层,内层失败不影响外层。