一、介绍一下Spring的 IOC
所谓的 IOC ,就是控制反转的意思。何为控制反转?
在传统的程序设计中,应用程序代码通常控制着对象的创建和管理。例如,一个对象需要依赖于其他对象,那么它会直接new出来对象。这样的设计通常被称为 “流程控制” 。
而在IOC中,控制关系发生了反转。控制权被转移到Spring容器中,容器负责创建和管理对象,并在需要的时候将它们注入到应用程序当中。
所以,原来这个对象的控制权在我们的代码中,我们自己new的对象,在Spring中,应用程序不再控制对象的创建,而是被动地接受由容器注入的对象。
没有Spring的话,我们要使用的对象,需要我们自己创建,而有了Spring的IOC之后,对象由IOC容器创建并管理,我们只需要在想要使用的时候从容器中获取就行了。
优点:
① 使用者不用关心引用bean的实现细节。
② 不用创建多个相同的bean导致浪费。
③ Bean的修改使用无需感知。
二、介绍一下Spring的AOP
AOP(Aspect-Oriented-Programming),即面向切面编程,就是把公共的逻辑抽出来,让开发者可以更加专注于业务逻辑开发。
个IOC一样,AOP也是一种思想。AOP的思想是OOP的补充。OOP是面向类和对象的,但是AOP是面向不同切面的。一个切面可以横跨多个类和对象去操作,极大的丰富了开发者的使用方式,提高了开发效率。
Spring AOP有如下概念
通知类型:
AOP是怎么实现的?
Spring AOP是通过代理模式实现的,具体有两种方式,一种是基于Java原生的动态代理,一种是基于cglib的动态代理。
Spring AOP默认使用标准的JDK动态代理进行AOP代理。这使得任何接口可以被代理。但是JDK动态代理有一个缺点,就是它不能代理没有接口的类。
Spring AOP就使用cglib代理没有接口的类。
三、Spring Bean的生命周期是怎么样的?
一个Spring Bean从出生到销毁的全过程就是它的整个生命周期,会经历以下几个阶段:
整个生命周期可以大致分为3个大的阶段,分别是:创建、使用、销毁。还可以进一步分为5个小的阶段:实例化、初始化、注册Destruction回调、Bean的正常使用以及Bean的销毁。
以下是更加详细的过程:
1、实例化bean:Spring容器首先创建Bean的实例。
2、设置属性值:Spring容器注入必要的属性到Bean中。
3、检查Aware:如果Bean实现了BeanNameAware、BeanClassLoaderAware等这些Aware接口,Spring容器会调用它们。
4、调用BeanPostProcessor前置处理方法:在Bean初始化之前,允许自定义的BeanPostProcessor对Bean实例进行处理,如修改Bean的状态。
5、调用InitializingBean的afterPropertiesSet方法:提供一个机会,在所有的Bean属性设置完成后进行初始化操作。如果Bean实现了InitializingBean接口,afterPropertiesSet方法会被调用。
6、调用自定义init-method方法:提供一种配置方式,在XML配置中指定Bean的初始化方法。
7、调用BeanPostProcessor的后置处理方法:在Bean初始化之后,再次允许BeanPostProcessor对Bean处理。
8、注册Destruction回调:如果Bean实现了DidposableBean接口或者在Bean定义中指定了自定义的销毁方法,Spring容器会为这些Bean注册一个销毁回调,确保在容器关闭时能够正确地清理资源。
9、Bean准备就绪:此时,Bean已经完成初始化,可以处理应用程序地请求了。
10、调用DisposanleBean地destroy方法:当容器关闭时,如果Bean实现了DisposableBean接口,destroy方法会被调用。
11、调用自定义的destory-method:如果Bean在配置文件中定义了销毁方法,那么该方法会被调用。
实例化和初始化的区别?
实例化:
实例化是创建对象的过程。在Spring中,这通常指的是通过调用类的构造器来创建Bean的实例。这是对象生命周期开始的阶段。
初始化:
初始化时在Bean实例创建后,进行一些设置或准备工作的过程。在Spring中,包括设置Bean的属性,调用各种前置&后置处理器。
四、Spring的事务传播机制有哪些?
Spring的事务传播机制用于控制多个事务方法相互调用时事务的行为。
在复杂的业务场景中,多个事务方法之间的调用可能会导致事务的不一致,如出现数据丢失、重复提交等问题,使用事务传播机制可以避免这些问题的发生,保证事务的一致性和完整性。
Spring的事务规定了7中事务传播级别,默认的传播机制是 REQUIRED(required)
① REQUIRED(required):如果不存在事务则开启一个事务,如果存在事务则加入之前的事务,总是只有一个事务在执行。
② REQUIRED_NEW(required new):每次执行新开一个事务。
③ SUPPORTS(supports):有事务则加入事务,没有事务则普通执行。
④ NOT_SUPPORTED(not supported):有事务则暂停该事务,没有则普通执行。
⑤ MANDATORY(mandatory):强制有事务,没有则报异常。
⑥ NEVER(never):有事务则报异常。
⑦ NESTED(nested):如果之前有事务,则创建嵌套事务,嵌套事务回滚不影响父事务,反之父事务影响嵌套事务。
五、Autowired和Resource的关系?
相同点:
对于下面的代码来说,如果是Spring容器的话,两个注解的功能基本是等价的,它们都可以将bean注入到对应的field中。
不同点:
1、byName和byType匹配顺序不同
Autowired在获取bean的时候,先是以byType的方式,再是以byName的方式。而Resource在获取bean的时候,和Autowired正好相反,先是byName的方式,然后再是byType的方式。
2、作用域不同
Autowired可以作用在构造器,字段,setter方法上
Resource只可以使用在field,setter方法上
3、支持方不同
Autowired是Spring提供的自动注入注解,只有Spring容器会支持,如果做容器迁移,是需要修改代码的。
Resource是JDK官方提供的自动注入注解。它等于说是一个标准或者约定,所有的IOC容器都会支持这个注解。假如系统容器从Spring迁移到其他IOC容器中,是不需要修改代码的。
六、BeanFactory和FactoryBean的关系?
从代码上看,这两个都是接口,然后都是在 org.springframework.beans.factory包下面的。
BeanFactory
BeanFactory比较常用,名字也比较容易理解,就是Bean工厂,它是整个Spring IOC容器的一部分,负责管理Bean的创建和生命周期。所以,BeanFactory是Spring IOC容器的一个接口,用来获取Bean以及管理Bean的依赖注入和生命周期。
FactoryBean
FactoryBean是一个接口,用于定义一个工厂Bean,它可以产生某种类型的对象。当在Spring配置文件中定义一个Bean时,如果这个Bean实现了FactoryBean接口,那么Spring容器不直接返回这个Bean的实例,而是返回FactoryBean # getObject() 方法所返回的对象。
FactoryBean通常用于创建很复杂的对象,比如需要通过某种特定的创建过程才能得到的对象。例如,创建JNDI资源的连接或与代理对象的创建。就比如Dubbo中的ReferenceBean。
七、Spring中如何开启事务?
事务管理是系统开发中是不可缺少的一部分,Spring提供了很好的事务管理机制,主要分为编程式事务和声明式事务。
编程式事务
编程式事务方式需要开发者在代码中手动的管理事务的开启、提交、回滚等操作
当然,也可以使用Spring中提供的TransactionTemplate来实现编程式事务。
声明式事务
声明式事务管理方法允许开发者使用配置的帮助下来管理事务,而不需要依赖底层API进行硬编码。开发者可以只使用注解或者基于配置的XML来管理事务。
声明式事务的优点
1、声明式事务帮助开发者节省了很多代码,它会自动帮我们进行事务的管理,把程序员从事务管理中解放出来。
2、声明式事务使用了AOP实现的,本质是在目标方法执行前后进行拦截。在目标方法执行前加入或者创建一个事务,在执行方法之后,根据实际情况选择提交或者回滚事务。
3、使用这种方式,对代码没有入侵性,方法内只需要写业务逻辑就可以了。
声明式事务用不对容易失效
① @Transaction 应用在非 public 修饰的方法上
② @Transaction 注解属性 propagation设置错误
③ @Transaction 注解属性 rollbackFor 设置错误
④ 同一个类型方法调用,导致@Transaction 失效
⑤ 异常被catch捕获,导致 @Transaction 失效
⑥ 数据库引擎不支持事务。
八、Spring中用到了哪些设计模式?
1、工厂模式
Spring IOC 就是一个非常好的工厂模式。Spring IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件 / 注解即可,完全不用考虑对象是如何被创建出来的。IOC容器负责创建对象,将对象连接在一起,配置这些对象,并从创建中处理这些对象的整个生命周期,直到它们完全销毁。
2、代理模式
Spring AOP就是代理模式的典型代表。
3、适配器模式
适配器模式简而言之就是上游为了适应下游,而要做一些适配,承担适配工作的模块,就叫做适配器。在Spring MVC 中,HandlerAdapter就是典型的适配器模式。
4、单例模式
单例模式是Spring一个非常核心的功能,Spring中的bean默认都单例的,这样可以尽可能最大程度保证对象的复用和线程安全。
Spring Bean也不止是单例的,还有其他作用域,如下:
① prototype:每次获取都会创建一个新的bean实例。也就是说,连续getBean() 两次,得到的是不同的 Bean实例。
② request(仅web应用可用):每一次HTTP请求都会产生一个新的Bean(请求bean),该bean仅在当前HTTP request 内有效
③ session(仅web应用可用):每一次来自新的session 的HTTP 请求都会产生一个新的bean(会话bean),该bean仅在当前 HTTP session 有效
④ global-session(仅web应用可用):每个web应用在启动时创建的一个bean(应用Bean),该bean仅在当前应用启动时间内有效。
⑤ websocket(仅web应用可用):每一次WebSocket 会话产生一个新的bean。
九、什么是Spring的循环依赖问题?
在Spring框架中,循环依赖是指两个或多个bean之间相互依赖,形成了一个循环引用的情况。如果不加以处理,这种情况会导致应用程序启动失败。
在Spring中解决循环依赖的方式就是引入了三级缓存。
三级缓存:
在Spring的BeanFactory体系中,BeanFactory是Spring IOC容器的基础接口,其DefaultSingletonBeanRegistry类实现了BeanFactory接口,并且维护了三级缓存
singletonObjects是一级缓存,存储的是完整创建好的单例bean对象。在创建一个单例bean时,会先从
singletonObjects中尝试获取该bean的实例,如果能获取到,则直接返回该实例,否则继续创建该bean。
earlySingletonObjects是二级缓存,存储的是尚未完全创建好的单例bean对象。在创建单例bean时,如果发现该bean存储循环依赖,则会先创建该bean的 “半成品” 对象,并将“半成品” 对象存储到 earlySingletonObjects 中。当循环依赖的bean创建完成后,Spring会将完整的bean实例对象存储到singletonObjects 中,并将earlySingletonObjects 中存储的代理对象替换为完整的bean实例对象。这样可以保证单例bean的创建过程不会出现循环依赖问题。
singletonFactories是三级缓存,存储的是单例bean的创建工厂。当一个单例bean被创建时,Spring会先将该bean的创建工厂存储到singletonFactories 中,然后再执行创建工厂的 getObject() 方法,生成该bean的实例对象。在该bean被其他bean引用时,Spring会从singletonFactories 中获取该bean的创建工厂,创建出该bean的实例对象,并将该bean的实例对象存储到singletonObjects 中。
但是,Spring解决循环依赖是有一定限制的:
① 要求互相依赖的bean必须要是单例的bean
② 依赖注入的方式不能是构造函数注入的方式
十、什么是MVC?
MVC 是指Model-View-Controller,是一种软件设计模式,它将应用程序分为三个部分:模型,视图,控制器。这个模式的目的是将应用程序的视图和控制器分开,以及将应用程序的数据和模型与视图和处理分开。
具体来说,MVC模式的思想如下:
模型(Model):表示应用程序的核心业务逻辑和数据。模型通常包含一些数据结构和逻辑规则,用于处理数据的输入、输出、更新和存储等操作。模型不关心数据的显示或用户的交互方式,它只关注数据本身以及对数据的操作。
视图(View):表示应用程序的用户界面,用于显示模型中的数据。视图通常包含一些控件和元素,用于展示数据,并且可以与用户进行交互。视图并不关心数据的处理方式或存储方式,它只关心如何呈现数据以及如何与用户交互。
控制器(Controller):表示应用程序的处理逻辑,用于控制视图和模型之间的交互。控制器通常包含一些事件处理和动作触发等操作,用于响应用户的输入或视图的变化,并对模型进行操作。控制器通过将用户的输入转化为对模型的操作,从而实现了视图和模型之间的解耦。
MVC模式的核心思想是将应用程序的表示和处理分离开来,从而使得应用程序更加灵活、易于维护和扩展。这种模式可以提高代码的可读性和可维护性,同时也可以促进代码的复用和分工,使得多人协作开发变得更加容易。
十一、Spring MVC的执行流程?
1、客户端发送请求:客户端(浏览器)向服务器发送HTTP请求。
2、前端控制器接受请求: DispatcherServlet是Spring MVC的核心组件,它会接收所有的请求,并将请求转发给相应的处理器。
3、处理器映射器:前端控制器接收请求之后传递给处理器映射器,它会根据请求的URL来确定哪个控制器来处理该请求。
4、处理器适配器:处理器适配器负责调用选定的处理器,并把请求传递给它。
5、控制器处理请求:选定的控制器处理请求,并进行具体的业务逻辑处理,比如查询数据库,调用业务方法等。
6、处理器返回模型和视图:处理器处理完请求后,会返回包含一个模型数据和视图名称的对象给处理器是配置。
7、视图解析器:视图解析器根据视图名称解析出具体的视图对象。
8、渲染视图:前端控制器将模型数据传递给视图对象,由视图对象生成HTML或其他格式的响应。
9、视图返回响应:生成的响应返回给客户端。
10、在处理器处理请求前后可以通过拦截器进行一些额外的处理,例如权限检查,日志记录等。
11、渲染最终视图:如果有必要,前端控制器可以将生成的视图再次传递给其他视图进行进一步的处理和渲染,直到最终响应返回给客户端。
客户端发送请求到前端控制器,前端控制器根据请求的url确定处理器映射器该选择哪个控制器来处理请求,处理器适配器用选定的控制器处理请求,控制器进行具体业务逻辑处理后返回模型和视图信息,视图解析器解析视图名称生成具体视图对象,前端控制器将模型数据传递给视图对象,生成响应返回给客户端,期间可以通过拦截器进行额外处理,最终渲染最终视图返回给客户端。十二、Spring Boot是如何实现自动装配的?
Spring Boot会根据类路径中的jar包、类,为jar包里的类自动配置,这样可以极大的减少配置的数量。简单点说就是它会根据定义在classpath下的类,自动的生成一些bean,并加载到Spring的Context中。
SpringBoot通过Spring的条件配置决定哪些bean可以被配置,将这些条件定义成具体的Configuration,然后将这些Configuration配置到spring.factories文件中。
在容器启动的时候,由于使用了 EnableAutoConfiguration 注解,该注解的import的 AutoConfigurationImportSelector 会去扫描classpath下的所有spring.factories文件,然后进行自动化配置。
AutoConfigurationImportSelector 的 getCandidateConfigurations() 方法读取classpath下面的 META-INF/ spring.factories文件。
十三、SpringBoot是如何实现main方法启动web项目的?
在SpringBoot中,通过SpringApplication类的静态方法 run 来启动web项目,当我们在main方法中调用run 方法时,Spring Boot使用一个内嵌的Tomcat服务器,并将其配置问处理web请求。
当应用程序启动时,Spring Boot会自动扫描应用程序中所有的Spring组件,并使用默认的配置来启动内嵌的Tomcat服务器。在默认情况下,Spring Boot会自动配置大部分的web开发所需的配置,包括请求处理、视图解析、静态资源处理等。
这样,在应用程序启动后,我们就可以通过web浏览器访问应用程序了,默认情况下可以通过访问 http://localhost:8080来访问应用程序的首页。
十四、SpringBoot的启动流程是怎么样的?
先从SpringBoot启动的入口开始
重点看SpringApplication.run(Application.class,args); 方法。它的实现如下:
主要看new SpringApplication(sources).run(args) 这部分代码了,从两方面介绍SpringBoot的启动过程。一个是new SpringApplication的初始化过程,一个是SpringApplication.run 的过程
new SpringApplication()
1、添加源:将提供的源(通常是配置类)添加到应用的列表中。
2、设置web环境:判断应用释放应该运行在web环境中,这会影响后续的web相关配置
3、加载初始化器:从spring.factories文件中加载所有列出的 ApplicationContextInitializer 实现,并将它们设置到SpringApplication实例中,以便在应用上下文的初始化阶段执行它们。
4、设置监听器:加载和设置 ApplicationListener 实例,以便应用能够响应不同的事件。
5、确定主应用类:这个主应用程序类通常是包含 public static void main(String[] args) 方法的类,是启动整个SpringBoot应用的入口点。
这里面的第3步,加载初始化器这一步是SpringBoot的自动配置的核心,因为在这一步会从spring.factories文件中加载并实例化指定类型的类。
以下就是new SpringApplication的主要流程,主要依赖于 initialize 方法初始化 Spring Boot 应用的关键组件和配置。
SpringApplication.run
run方法主要做了以下:
1、启动&停止计时器:在代码中,用stopWatch来进行计时。所以在最开始要启动计时,在最后要停止计时。这个计时就是最终用来统计启动过程的时长的。最终在应用启动详细输出的实时打印出来的,如:
2、获取和启动监听器:这一步从spring.factories 中解析初始化所有的 SpringApplicationRunListener 实例,并通知它们应用的启动过程已经开始。
3、装配环境参数:这一步主要是用来做参数绑定的,prepareEnvironment 方法会加载应用的外部配置。这包括 application.properties或者application.yml文件中的属性,环境变量,系统属性等。所以,我们自定义的那些参数就是在这一步被绑定的。
4、打印Banner:这一步作用很简单,就是在控制台打印启动横幅Banner,如下图:
5、创建应用上下文:到这一步就真开始启动了,第一步就是要先创建一个Spring的上下文出来,只有有了这个上下文才能进行Bean的加载、配置等工作。
6、准备上下文:这一步非常关键,很多核心操作都是在这一步完成的:
7、刷新上下文:这一步,是Spring启动的核心步骤了,这一步骤包括了实例化所有的bean、设置它们之间的依赖关系以及执行其他的初始化任务。这一步中,最主要就是创建BeanFactory,然后再通过BeanFactory来实例化bean。
最后,SpringBoot的启动过程如下:
十五、Spring的 AOP 在什么场景下会失效?
Spring的 AOP 是通过动态代理来实现的,所以,要想让 AOP 生效,前提必须是动态代理生效,并且可以调用到代理对象的方法。
1、私有方法调用
因为在对象内部直接调用其他方法,就会使用原始对象直接调用了,不会调用到代理对象,所以代理会失效。
2、静态方法调用
因为静态方法是属于类的,并不是对象的,所以无法被AOP
3、final方法调用
由于AOP是通过创建代理对象来实现的,而无法对final方法进行子类化覆盖,所以无法拦截这些方法。
4、类内部自调用
5、内部类方法调用
十六、Spring中的Bean是线程安全的吗?
Spring的Bean是否线程安全,取决于它的作用域。Spring的Bean有很多种作用域,其中用的比较多的就是 Singleton(单例) 和 Prototype。
默认情况下,Spring Bean是单例的(Singleton)。这意味着在整个Spring 容器中只存在一个Bean实例。如果将Bean的作用域设置为原型(Prototype),那么每次从容器获取Bean都会创建一个新的实例。
对于Prototype 这种作用域的Bean,它的Bean实例不会被多个线程共享,所以不存在线程安全问题。
但是对于Singleton的Bean,就可能存在线程安全的问题了,但是也不绝对,要看这个Bean中是否有共享变量。
如以下代码:
默认情况下,Spring Bean是单例的,count字段是一个共享变量,那么如果多个线程同时调用increment方法,可能会导致计数器的值不正确,那么这段代码就不是线程安全的了。这种Bean通常叫做有状态的Bean,有状态的Bean是非线程安全的,我们需要自己来考虑它的线程安全问题。
那如果一个Singleton的Bean是无状态的,既没有成员变量,或者成员变量只读不写,那么它就是个线程安全的。
总结:Prototype的Bean是线程安全的,无状态的Singleton的Bean是线程安全的。有状态Singleton的Bean是非线程安全的。
有状态的Bean如何解决线程安全问题?
① 修改作用域为Prototype
② 在并发修改共享变量的地方加锁
③ 使用并发工具类,如原子类,线程安全的集合等。
十七、Spring的事务在多线程下生效吗?为什么?
如果使用@Transaction这种声明式事务的话,在多线程环境下是无法生效的。主要是因为@Transaction 的事务管理使用的是ThreadLocal 机制来存储事务上下文,而 ThreadLocal 变量是线程隔离的,即每个线程都有自己的上下文副本。因此,在多线程环境下,Spring的声明式事务会 失效 ,即新线程中的操作不会被包含在原有的事务中。
不过,如果需要管理跨线程的事务,可以使用编程式事务,即自己用 TransactionTemplate 或 PlatformTransactionManager 来控制事务的提交。