[Interview]Java 面试宝典系列之 Spring

Posted Spring-_-Bear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Interview]Java 面试宝典系列之 Spring相关的知识,希望对你有一定的参考价值。

文章目录

1. 请你说说 Spring 的核心是什么

IoC 和 AOP 是 Spring 框架的核心。Spring 框架包含众多模块,如 Core、Testing、Data Access、Web Servlet 等,其中 Core 是整个 Spring 框架的核心模块。Core 模块提供了 IoC 容器、AOP 功能、数据绑定、类型转换等一系列的基础功能,而这些功能以及其他模块的功能都是建立在 IoC 和 AOP 之上的。

IoC(Inversion of Control)是控制反转的意思,这是一种面向对象编程的设计思想。在不采用这种思想的情况下,我们需要自己维护对象与对象之间的依赖关系,很容易造成对象之间的耦合度过高,在一个大型的项目中这十分的不利于代码的维护。IoC 则可以解决这种问题,它可以帮我们维护对象与对象之间的依赖关系,降低对象之间的耦合度。

说到 IoC 就不得不说 DI(Dependency Injection)依赖注入,它是 IoC 的具体实现方式,就是说 IoC 是通过 DI 来实现的。由于 IoC 这个词汇比较抽象而 DI 更直观,所以很多时候我们就用 DI 来代替它,在很多时候我们简单地将 IoC 和 DI 划等号,这是一种习惯。而实现依赖注入的关键是 IoC 容器,它的本质就是一个工厂。

AOP(Aspect Oriented Programing)是面向切面编程,这种思想是对 OOP 的补充,它可以在 OOP 的基础上进一步提高编程的效率。简单来说,它可以统一解决一批组件的共性需求(如权限检查、记录日志、事务管理等)。在 AOP 思想下,我们可以将解决共性需求的代码独立出来,然后通过配置的方式,声明这些代码在什么地方、什么时机调用。当满足调用条件时,AOP 会将该业务代码织入到我们指定的位置,从而统一解决了问题,又不需要修改这一批组件的代码。

2. 说一说你对 Spring 容器的了解

Spring 主要提供了两种类型的容器:BeanFactoryApplicationContext

  • BeanFactory:是基础类型的 IoC 容器,提供完整的 IoC 服务支持。如果没有特殊指定,默认采用延迟初始化策略。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。对于资源有限,并且功能要求不是很严格的场景,BeanFactory 是比较合适的 IoC 容器选择。
  • ApplicationContext:它是在 BeanFactory 的基础上构建的,是相对比较高级的容器实现,除了拥有 BeanFactory 的所有支持,ApplicationContext 还提供了其他高级特性,比如事件发布、国际化信息支持等。ApplicationContext 所管理的对象在该类型容器启动之后,默认全部初始化并绑定完成。所以,相对于 BeanFactory 来说,ApplicationContext 要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之 BeanFactory 也会长一些。在那些系统资源充足,并且要求更多功能的场景中,ApplicationContext 类型的容器是比较合适的选择。

3. 说一说你对 BeanFactory 的了解

BeanFactory 是一个类工厂,与传统类工厂不同的是,BeanFactory 是类的通用工厂,它可以创建并管理各种类的对象。这些可被创建和管理的对象本身没有什么特别之处,仅是一个 POJO,Spring 称这些被创建和管理的 Java 对象为 Bean。并且,Spring 中所说的 Bean 比 JavaBean 更为宽泛一些,所有可以被 Spring 容器实例化并管理的 Java 类都可以成为 Bean。

BeanFactory 是 Spring 容器的顶层接口,Spring 为 BeanFactory 提供了多种实现,最常用的是 XmlBeanFactory。但它在 Spring 3.2 中已被废弃,建议使用 XmlBeanDefinitionReader、DefaultListableBeanFactory 替代。BeanFactory 最主要的方法就是 getBean(String beanName),该方法从容器中返回特定名称的 Bean。

4. 说一说你对 Spring IOC 的理解

IoC(Inversion of Control)是控制反转的意思,这是一种面向对象编程的设计思想。在不采用这种思想的情况下,我们需要自己维护对象与对象之间的依赖关系,很容易造成对象之间的耦合度过高,在一个大型的项目中这十分的不利于代码的维护。IoC 则可以解决这种问题,它可以帮我们维护对象与对象之间的依赖关系,降低对象之间的耦合度。

说到 IoC 就不得不说 DI(Dependency Injection)依赖注入,它是 IoC 的具体实现方式,就是说 IoC 是通过 DI 来实现的。由于 IoC 这个词汇比较抽象而 DI 更直观,所以很多时候我们就用 DI 来代替它,在很多时候我们简单地将 IoC 和 DI 划等号,这是一种习惯。而实现依赖注入的关键是 IoC 容器,它的本质就是一个工厂。

在具体的实现中,主要有三种注入方式:

  1. 构造方法注入:就是被注入对象可以在它的构造方法中声明依赖对象的参数列表,让外部知道它需要哪些依赖对象。然后,IoC Service Provider 会检查被注入的对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。构造方法注入方式比较直观,对象被构造完成后,即进入就绪状态,可以马上使用。
  2. setter 方法注入:通过 setter 方法,可以更改相应的对象属性。所以,当前对象只要为其依赖对象所对应的属性添加 setter 方法,就可以通过 setter 方法将相应的依赖对象设置到被注入对象中。setter 方法注入虽不像构造方法注入那样,让对象构造完成后即可使用,但相对来说更宽松一些, 可以在对象构造完成后再注入。
  3. 接口注入:相对于前两种注入方式来说,接口注入没有那么简单明了。被注入对象如果想要 IoC Service Provider 为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。IoC Service Provider 最终通过这些接口来了解应该为被注入对象注入什么依赖对象。相对于前两种依赖注入方式,接口注入比较死板和烦琐。

5. Spring 是如何管理 Bean 的?

Spring 通过 IoC 容器来管理 Bean,我们可以通过 XML 或者注解配置,来指导 IoC 容器对 Bean 的管理。因为注解配置比 XML 配置方便很多,所以现在大多时候会使用注解配置的方式。

以下是 Bean 管理常用的一些注解:

  1. @ComponentScan:声明扫描策略,通过它的声明,容器就知道要扫描哪些包下带有声明的类,也可以知道哪些特定的类是被排除在外的。
  2. @Component、@Repository、@Service、@Controller 用于声明 Bean
  3. @Autowired、@Qualifier:用于注入 Bean,即告诉容器应该为当前属性注入哪个 Bean。@Autowired 是按照 Bean 的类型进行匹配的,如果这个属性的类型具有多个 Bean,就可以通过 @Qualifier 指定 Bean 的名称,以消除歧义。
  4. @Scope:用于声明 Bean 的作用域,默认情况下 Bean 是单例的,即在整个容器中这个类型只有一个实例。可以通过 @Scope 注解指定 prototype 值将其声明为多例的,也可以将 Bean 声明为 session 级作用域、request 级作用域等等,但最常用的还是默认的单例模式。
  5. @PostConstruct、@PreDestroy:用于声明 Bean 的生命周期。其中,被 @PostConstruct 修饰的方法将在 Bean 实例化后被调用, @PreDestroy 修饰的方法将在容器销毁前被调用。

6. 介绍 Bean 的作用域

默认情况下,Bean 在 Spring 容器中是单例的,我们可以通过 @Scope 注解修改其作用域。

类型说明
singleton在 Spring 容器中仅存在一个实例,即 Bean 以单例的形式存在
prototype每次调用 getBean() 时,都会执行 new 操作,返回一个新的实例
request每次 HTTP 请求都会创建一个新的 Bean
session同一个 HTTP Session 共享一个 Bean,不同的 HTTP Session 使用不同的 Bean
globalSession同一个全局的 Session 共享一个 Bean,一般用于 Portlet 环境

7. 说一说 Bean 的生命周期

Spring 容器管理 Bean,涉及对 Bean 的创建、初始化、调用、销毁等一系列的流程,这个流程就是 Bean 的生命周期。整个流程参考下图:

整个过程是由 Spring 容器自动管理的,以下两个环节可以人为进行干预:

  1. 自定义初始化方法,并在该方法前增加 @PostConstruct 注解,届时 Spring 容器将在调用 setBeanFactory 方法之后调用该方法
  2. 自定义销毁方法,并在该方法前增加 @PreDestroy 注解,届时 Spring 容器将在自身销毁前,调用这个方法

8. Spring 是怎么解决循环依赖的?

首先,需要明确的是 Spring 对循环依赖的处理有三种情况:

  1. 构造器的循环依赖:这种依赖 Spring 是处理不了的,直接抛出 BeanCurrentlylnCreationException 异常
  2. 单例模式下的 setter 循环依赖:通过 “三级缓存” 处理循环依赖
  3. 非单例循环依赖:无法处理

Spring 单例对象的初始化大略分为三步:

  1. createBeanInstance:实例化,就是调用对象的构造方法创建对象
  2. populateBean:填充属性,这一步主要是多 Bean 的依赖属性进行填充
  3. initializeBean:调用 Spring xml 中的 init 方法

从上面讲述的单例 bean 初始化步骤我们可以知道,循环依赖主要发生在第一步、第二步。也就是构造器循环依赖和 field 循环依赖。 Spring 为了解决单例的循环依赖问题,使用了三级缓存:

/** Cache of singleton objects: bean name –> bean instance */ 
private final Map singletonObjects = new ConcurrentHashMap(256); 
/** Cache of singleton factories: bean name –> ObjectFactory */ 
private final Map> singletonFactories = new HashMap>(16); 
/** Cache of early singleton objects: bean name –> bean instance */ 
private final Map earlySingletonObjects = new HashMap(16);

这三级缓存的作用分别是:

  • singletonFactories : 进入实例化阶段的单例对象工厂的 cache (三级缓存)
  • earlySingletonObjects :完成实例化但是尚未初始化的,提前曝光的单例对象的 cache (二级缓存)
  • singletonObjects:完成初始化的单例对象的 cache(一级缓存)

这样做有什么好处呢?让我们来分析一下,“A 的某个 field 或者 setter 依赖了 B 的实例对象,同时 B 的某个 field 或者 setter 依赖了 A 的实例对象” 这种循环依赖的情况:

  1. A 首先完成了初始化的第一步,并且将自己提前曝光到 singletonFactorie s中。此时进行初始化的第二步,发现自己依赖对象 B,此时就尝试去 get(B),发现 B 还没有被 create,所以走 create 流程
  2. B 在初始化第一步的时候发现自己依赖了对象 A,于是尝试 get(A),尝试一级缓存 singletonObjects(肯定没有,因为 A 还没初始化完全),尝试二级缓存 earlySingletonObjects(也没有)
  3. 尝试三级缓存 singletonFactories,由于 A 通过 ObjectFactory 将自己提前曝光了,所以 B 能够通过 ObjectFactory.getObject 拿到 A 对象(虽然 A 还没有初始化完全,但是总比没有好呀),B 拿到 A 对象后顺利完成了初始化阶段 1、2、3,完全初始化之后将自己放入到一级缓存 singletonObjects 中
  4. 此时返回 A 中,A 此时能拿到 B 的对象顺利完成自己的初始化阶段 2、3,最终 A 也完成了初始化,进去了一级缓存 singletonObjects 中,而且更加幸运的是,由于 B 拿到了 A 的对象引用,所以 B 现在 hold 住的 A 对象完成了初始化

9. @Autowired 和 @Resource 注解有什么区别?

  1. @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解
  2. @Autowired 是只能按类型注入,@Resource 默认按名称注入,也支持按类型注入
  3. @Autowired 按类型装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许 null 值,可以设置它 required 属性为 false,如果我们想使用按名称装配,可以结合 @Qualifier 注解一起使用
  4. @Resource 有两个中重要的属性:name 和 type。name 属性指定 byName,如果没有指定 name 属性,当注解标注在字段上,即默认取字段的名称作为 bean 名称寻找依赖对象,当注解标注在属性的 setter 方法上,即默认取属性名作为 bean 名称寻找依赖对象。需要注意的是,@Resource 如果没有指定 name 属性,并且按照默认的名称仍然找不到依赖对象时, @Resource注解会回退到按类型装配。但一旦指定了 name 属性,就只能按名称装配了。

10. Spring 中默认提供的单例是线程安全的吗?

不是,Spring 容器本身并没有提供 Bean 的线程安全策略。

  • 如果单例的 Bean 是一个无状态的 Bean,即线程中的操作不会对 Bean 的成员执行查询以外的操作,那么这个单例的 Bean 是线程安全的。比如 Controller、Service、DAO 这样的组件,通常都是单例且线程安全的。

  • 如果单例的 Bean 是一个有状态的 Bean,则可以采用 ThreadLocal 对状态数据做线程隔离,来保证线程安全。

11. 说一说你对 Spring AOP 的理解

AOP(Aspect Oriented Programming)是面向切面编程,它是一种编程思想,是对面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。

AOP 术语:

  1. 连接点(join point):对应的是具体被拦截的对象,因为 Spring 只能支持方法,所以被拦截的对象往往就是指特定的方法,AOP 将通过动态代理技术把它织入对应的流程中。
  2. 切点(point cut):有时候,我们的切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点,切点就是提供这样一个功能的概念。
  3. 通知(advice):就是按照约定的流程下的方法,分为前置通知、后置通知、环绕通知、事后返回通知和异常通知,它会根据约定织入流程中。
  4. 目标对象(target):即被代理对象。
  5. 引入(introduction):是指引入新的类和其方法,增强现有 Bean 的功能。
  6. 织入(weaving):它是一个通过动态代理技术,为原有服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。
  7. 切面(aspect):是一个可以定义切点、各类通知和引入的内容,SpringAOP 将通过它的信息来增强 Bean 的功能或者将对应的方法织入流程。

Spring AOP:AOP 可以有多种实现方式,而 Spring AOP 支持如下两种实现方式:

  1. JDK 动态代理:这是 Java 提供的动态代理技术,可以在运行时创建接口的代理实例。Spring AOP 默认采用这种方式,在接口的代理实例中织入代码。
  2. CGLib 动态代理:采用底层的字节码技术,在运行时创建子类代理的实例。当目标对象不存在接口时,Spring AOP 就会采用这种方式,在子类实例中织入代码。

12. 请你说说 AOP 的应用场景

Spring AOP 为 IoC 的使用提供了更多的便利

  • 一方面,应用可以直接使用 AOP 的功能,设计应用的横切关注点,把跨越应用程序多个模块的功能抽象出来,并通过简单的 AOP 的使用,灵活地编织到模块中,比如可以通过 AOP 实现应用程序中的日志功能
  • 另一方面,在 Spring 内部,一些支持模块也是通过 Spring AOP 来实现的,比如事务处理。从这两个角度就已经可以看到 Spring AOP 的核心地位了

13. Spring AOP 不能对哪些类进行增强?

  1. Spring AOP 只能对 IoC 容器中的 Bean 进行增强,对于不受容器管理的对象不能增强
  2. 由于 CGLib 采用动态创建子类的方式生成代理对象,所以不能对 final 修饰的类进行代理

14. JDK 动态代理和 CGLIB 有什么区别?

  1. JDK 动态代理:这是 Java 提供的动态代理技术,可以在运行时创建接口的代理实例。Spring AOP 默认采用这种方式,在接口的代理实例中织入代码
  2. CGLib 动态代理:采用底层的字节码技术,在运行时创建子类代理的实例。当目标对象不存在接口时,Spring AOP 就会采用这种方式,在子类实例中织入代码

15. 既然有没有接口都可以用 CGLIB,为什么 Spring 还要使用 JDK 动态代理?

在性能方面,CGLib 创建的代理对象比 JDK 动态代理创建的代理对象高很多。但是,CGLib 在创建代理对象时所花费的时间比 JDK 动态代理多很多。

所以,对于单例的对象因为无需频繁创建代理对象,采用 CGLib 动态代理比较合适。

反之,对于多例的对象因为需要频繁的创建代理对象,则 JDK 动态代理更合适。

16. Spring 如何管理事务?

Spring 为事务管理提供了一致的编程模板,在高层次上建立了统一的事务抽象。也就是说,不管是选择 MyBatis、Hibernate、JPA 还是Spring JDBC,Spring 都可以让用户以统一的编程模型进行事务管理。

Spring 支持两种事务编程模型:

  1. 编程式事务:Spring 提供了 TransactionTemplate 模板,利用该模板我们可以通过编程的方式实现事务管理,而无需关注资源获取、复用、释放、事务同步及异常处理等操作。相对于声明式事务来说,这种方式相对麻烦一些,但是好在更为灵活,我们可以将事务管理的范围控制的更为精确。
  2. 声明式事务:Spring 事务管理的亮点在于声明式事务管理,它允许我们通过声明的方式,在 IoC 配置中指定事务的边界和事务属性,Spring 会自动在指定的事务边界上应用事务属性。相对于编程式事务来说,这种方式十分的方便,只需要在需要做事务管理的方法上,增加 @Transactional 注解,以声明事务特征即可。

17. Spring 的事务传播方式有哪些?

当我们调用一个业务方法时,它的内部可能会调用其他的业务方法,以完成一个完整的业务操作。这种业务方法嵌套调用的时候,如果这两个方法都是要保证事务的,那么就要通过 Spring 的事务传播机制控制当前事务如何传播到被嵌套调用的业务方法中。

Spring 在 TransactionDefinition 接口中规定了 7 种类型的事务传播行为,它们规定了事务方法和事务方法发生嵌套调用时如何进行传播,如下表:

事务传播类型说明
PROPAGATION_REQUIRED如果当前没有事务,则新建一个事务;如果已存在一个事务,则加入到这个事务中。这是最常见的选择
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,则以非事务方式执行
PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,则抛出异常
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,则把当前事务挂起
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,则把当前事务挂起
PROPAGATION_NEVER以非事务方式执行操作,如果当前存在事务,则抛出异常
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作

18. Spring 的事务如何配置,常用注解有哪些?

事务的打开、回滚和提交是由事务管理器来完成的,我们使用不同的数据库访问框架,就要使用与之对应的事务管理器。在 Spring Boot中,当你添加了数据库访问框架的起步依赖时,它就会进行自动配置,即自动实例化正确的事务管理器。

对于声明式事务,是使用 @Transactional 进行标注的,这个注解可以标注在类或者方法上。

  • 当它标注在类上时,代表这个类所有公共(public)非静态的方法都将启用事务功能
  • 当它标注在方法上时,代表这个方法将启用事务功能

另外,在 @Transactional 注解上,我们可以使用 isolation 属性声明事务的隔离级别,使用 propagation 属性声明事务的传播机制

19. 说一说你对声明式事务的理解

Spring 事务管理的亮点在于声明式事务管理,它允许我们通过声明的方式,在 IoC 配置中指定事务的边界和事务属性,Spring 会自动在指定的事务边界上应用事务属性。相对于编程式事务来说,这种方式十分的方便,只需要在需要做事务管理的方法上,增加 @Transactional 注解,以声明事务特征即可。

以上是关于[Interview]Java 面试宝典系列之 Spring的主要内容,如果未能解决你的问题,请参考以下文章

[Interview]Java 面试宝典系列之 Spring Boot

[Interview]Java 面试宝典系列之 Java 多线程

[Interview]Java 面试宝典系列之 MyBatis

[Interview]Java 面试宝典系列之 Java 集合类

[Interview]Java 面试宝典系列之 JavaWeb

[Interview]Java 面试宝典系列之 Spring MVC