【面试】Spring框架面试题

一、谈谈你理解的 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配置文件形式:
  • @Component注解:该注解等价于 @Service@Controller@Repository注解。这种方式使用的时候需要配置扫描包,是通过反射的形式来利用类创建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中。这个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容器被创建的时候加载。

【面试】Spring框架面试题_第1张图片

十三、BeanFactory的作用?

  • BeanFactory是Spring中的非常核心的一个顶级接口,也属于一个Spring容器,管理着某个Bean的生命周期,只不过BeanFactory只能算是非常低级的容器,远没有ApplicationContext这样的高级容器这么多的功能
  • BeanFactory的作用就是传入一个标识产生一个Bean,利用的是getBean方法
  • BeanFactory实现了简单的工厂模式,拥有非常多的实现类,最强大的实现类是DefaultListenableBeanFactory。Spring的底层就是使用该实现工厂产生Bean对象的。

十四、自动注入有哪些需要注意的?

  • 一定要声明set方法
  • 覆盖:需要用配置来定义依赖,这些配置将始终覆盖自动注入
  • 简单的数据类型:不能注入简单的数据类型,比如基本数据类型、String、类(手动注入可以)

十五、什么是Bean的装配?

Bean装配就是将对象注入IoC容器,这个过程就是Bean的装配。

创建应用对象之间协作关系的行为通常称为装配(wiring),这也是依赖注入(DI)的本质

Spring一般通过两个角度来自动化装配Bean:

  1. 组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。(@Component)
  2. 自动装配(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 是如何解决循环依赖的? - 知乎 (zhihu.com)
spring的循环依赖_wojiao228925661的博客-CSDN博客_spring的循环依赖

Spring是通过三级缓存解决循环依赖的,简单来说就是三个Map。解决循环依赖的关键就是一定要有一个缓存保存早期对象,形成一个循环的出口。

  • singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
  • earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
  • singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

【面试】Spring框架面试题_第2张图片

A对应testService1,B对应于testService2。那么这个过程就是:

  1. A创建的过程发生需要B,于是A将自身存放在三级缓存中,去创建B
  2. B创建的时候会发现需要A,此时开始在缓存中寻找A,按照一二三的顺寻查找,在三级缓存中发现A之后,会把三级缓存中的A放到二级缓存,并删除三级缓存中的A。
  3. 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
    • 配置方法:
    • 包扫描:
    • 引入外部属性配置方式:
    • 指定其他配置文件:
    • 属性注入:
  • Config方式
    • 容器:AnnotationConfigApplicationContext(Config.class)
    • 配置类:Config (@Configuration)
    • 配置方法:@Bean @Lazy @Scope
    • 包扫描:@ComponentScan
    • 引入其他配置文件:@PropertySource(“xxx.properties”)
    • 指定其他配置文件:@Import
    • 属性注入:@Value

两者在实现的时候一般都使用多态的方式创建,利用共同的接口ApplicationContext以多态的方式创建出不同的容器,之后使用ClassPathXmlApplicationContextAnnotationConfigApplicationContext去调用不同的加载配置类的方法,解析配置信息,之后封装成BeanDefinition对象。

加载配置注解容器的时候,AnnotationConfigApplicationContext的过程:①读取配置类:使用AnnotatedBeanDefinitionReader类的this.reader.register(annotatedClasses);方法;②解析配置类:使用BeanDefinitionRegistryPostProcess类,调用ConfigurationClassParser配置类解析器,解析各种注解如@Bean @Component等,注册为BeanDefinition对象。

加载xml配置文件的时候,ClassPathXmlApplicationContext的过程:①加载xml配置文件:读取xml配置文件使用XmlBeanDefinitionReader类来对配置文件进行读取操作,使用AbstractXmlApplicationContext#loadBeanDefinitions()方法加载BeanDefinition的所需信息;②解析配置文件:使用LoadBeanDifinitionDefaultBeanDefinitionDocumentReader来解析 等配置标签,注册为BeanDefinition

二十五、Spring有哪几种配置方式?

①基于XML文件的配置方式:从Spring诞生就有的方式,使用applicationContext.xml文件和标签

②基于注解的配置方式:使用@Component注解标识该类是要注入到IoC容器的类,并且在applicationContext.xml文件中创建标注需要扫描的包路径。需要注入的时候,使用@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文件中指定;②用注解@PostConstructor实现初始化生命周期回调
    • 如果Bean实现了AOP,会在这一步创建动态代理
  • 销毁
    • Spring容器关闭的时候进行调用
    • 调用销毁生命周期回调(三种方式)
      • 实现Dispoable接口的destroy()方法
      • XML配置文件中配置
      • 使用注解@PreDestory创造销毁前置方法

你可能感兴趣的:(Java,面试/面试题,spring,面试,java)