面试题篇-04-Spring以及SpringBoot以及SpringCloud生态相关面试题

文章目录

1. 简介

2. 谈谈你对 Seata 的理解

在微服务架构下,由于数据库和应用服务的拆分,导致原本一个事务单元中的多个 DML 操作,变成了跨进程或者跨数据库的多个事务单元的多个 DML 操作,而传统的数据库事务无法解决这类的问题,所以就引出了分布式事务的概念。

分布式事务本质上要解决的就是跨网络节点的多个事务的数据一致性问题,业内常见的解决方法有两种

  • a.强一致性
    就是所有的事务参与者要么全部成功,要么全部失败,全局事务协调者需要知道每个事务参与者的执行状态,再根据状态来决定数据的提交或者回滚!
  • b.最终一致性
    也叫弱一致性,也就是多个网络节点的数据允许出现不一致的情况,但是在最终的某个时间点会达成数据一致。

基于CAP 定理我们可以知道,强一致性方案对于应用的性能和可用性会有影响,所以对于数据一致性要求不高的场景,就会采用最终一致性算法。

而Seata 就是其中一种,它是阿里开源的分布式事务解决方案,提供了高性能且简单易用的分布式事务服务。

Seata 中封装了四种分布式事务模式,分别是:

  • AT 模式
    是一种基于本地事务+二阶段协议来实现的最终数据一致性方案,也是 Seata 默认的解决方案
    在这里插入图片描述
  • TCC 模式
    TCC 事务是Try、Confirm、Cancel 三个词语的缩写,简单理解就是把一个完整的业务逻辑拆分成三个阶段,然后通过事务管理器在业务逻辑层面根据每个分支事务的执行情况分别调用该业务的 Confirm 或者Cacel 方法。
    在这里插入图片描述
  • Saga 模式
    Saga 模式是SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者。
    在这里插入图片描述
  • XA 模式
    XA 可以认为是一种强一致性的事务解决方法,它利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
    在这里插入图片描述

从这四种模型中不难看出,在不同的业务场景中,我们可以使用 Seata 的不同事务模型来解决不同业务场景中的分布式事务问题,因此我们可以认为 Seata 是一个一站式的分布式事务解决方案。

3. Dubbo 的服务请求失败怎么处理?

Dubbo 是一个RPC 框架,它为我们的应用提供了远程通信能力的封装,同时,Dubbo在RPC 通信的基础上,逐步在向一个生态在演进,它涵盖了服务注册、动态路由、容错、服务降级、负载均衡等能力,基本上在微服务架构下面临的问题,Dubbo 都可以解决。

而对于Dubbo 服务请求失败的场景,默认提供了重试的容错机制,也就是说,如果基于Dubbo 进行服务间通信出现异常,服务消费者会对服务提供者集群中其他的节点发起重试,确保这次请求成功,默认的额外重试次数是 2 次。

除此之外,Dubbo 还提供了更多的容错策略,我们可以根据不同的业务场景来进行选择。

  • 快速失败策略,服务消费者只发起一次请求,如果请求失败,就直接把错误抛出去。这种比较适合在非幂等性场景中使用
  • 失败安全策略,如果出现服务通信异常,直接把这个异常吞掉不做任何处理
  • 失败自动恢复策略,后台记录失败请求,然后通过定时任务来对这个失败的请求进行重发。
  • 并行调用多个服务策略,就是把这个消息广播给服务提供者集群,只要有任何一个节点返回,就表示请求执行成功。
  • 广播调用策略,逐个调用服务提供者集群,只要集群中任何一个节点出现异常,就表示本次请求失败

要注意的是,默认基于重试策略的容错机制中,需要注意幂等性的处理,否则在事务型的操作中,容易出现多次数据变更的问题。

4. 什么是 Dubbo?它有哪些核心功能?

Dubbo 是以高性能RPC 框架,它提供了分布式架构下的服务之间通信方案,使得开发者可以不需要关心网络通信的细节。通过该框架可以使得远程服务调用方式和本地服务调用方式一样简单。
Dubbo 是一款高性能、轻量级的开源RPC 框架。由 10 层模式构成,整个分层依赖由上至下。
通过这张图我们也可以将Dubbo 理解为三层模式:

在这里插入图片描述
第一层的Business 业务逻辑层由我们自己来提供接口和实现还有一些配置信息。
第二层的RPC 调用的核心层负责封装和实现整个 RPC 的调用过程、负载均衡、集群容错、代理等核心功能。
Remoting 则是对网络传输协议和数据转换的封装。

根据Dubbo 官方文档的介绍,Dubbo 提供了六大核心能力

  • 面向接口代理的高性能 RPC 调用。
  • 智能容错和负载均衡。
  • 服务自动注册和发现。
  • 高度可扩展能力。
  • 运行期流量调度。
  • 可视化的服务治理与运维。

5. Dubbo 负载均衡的几种策略

Dubbo 有五种负载策略:

  • 第一种是加权随机
    假设我们有一组服务器 servers = [A, B, C],他们对应的权重为 weights = [5, 3, 2],权重总和为 10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上就可以了。

  • 第二种是最小活跃数
    每个服务提供者对应一个活跃数 active,初始情况下,所有服务提供者活跃数均为 0。每收到一个请求,活跃数加 1,完成请求后则将活跃数减 1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求。

  • 第三种是一致性 hash
    通过 hash 算法,把 provider 的invoke 和随机节点生成hash,并将这个 hash 投射到 [0, 2^32 - 1] 的圆环上,查询的时候根据key 进行md5 然后进行hash,得到第一个节点的值大于等于当前 hash 的invoker。

  • 第四种是加权轮询
    比如服务器A、B、C 权重比为 5:2:1,那么在 8 次请求中,服务器 A 将收到其中的 5 次请求,服务器 B 会收到其中的 2 次请求,服务器 C 则收到其中的 1 次请求。

  • 第五种是最短响应时间权重随机
    计算目标服务的请求的响应时间,根据响应时间最短的服务,配置更高的权重进行随机访问。

6. RPC 和 HTTP 协议有什么区别?

  • RPC 全称(Remote Procedure Call),它是一种针对跨进程或者跨网络节点的应用之间的远程过程调用协议
    它的核心目标是,让开发人员在进行远程方法调用的时候,就像调用本地方法一样,不需要额外为了完成这个交互做过的编码。
  • 而Http 协议是为 Web 浏览器与 Web 服务器之间的通信而设计的远程通信协议,它定义了通信协议的报文规范,我们可以使用http 协议来实现跨网络节点的数据传输。
    在这里插入图片描述

基于这样的特点,在RPC 协议底层的数据传输,即可以直接使用 TCP 协议,也可以使用http 协议。
因此,Rpc 协议和 Http 协议完全不是同一个纬度的东西,这两者并没有什么可比性。

7. 什么是服务网格?

服务网格,也就是Service Mesh,它是专门用来处理服务通讯的基础设施层。它的主要功能是处理服务之间的通信,并且负责实现请求的可靠性传递。
Service Mesh,我们通常把他称为第三代微服务架构,既然是第三代,那么意味着他是在原来的微服务架构下做的升级。
为了更好的说明 Service Mesh,那我就不得不说一下微服务架构部分的东西。
首先,当我们把一个电商系统以微服务化架构进行拆分后,会的到这样的一个架构(如图),其中包括 Webserver、payment、inventory 等等。
在这里插入图片描述
(如图)这些微服务应用,会被部署到Docker 容器、或者Kubernetes 集群。由于每个服务的业务逻辑是独立的,比如 payment 会实现支付的业务逻辑、order 实现订单的处理、Webserver 实现客户端请求的响应等。
在这里插入图片描述
(如图)所以,服务之间必须要相互通信,才能实现功能的完整性。比如用户把一个商品加入购物车,请求会进入到 Webserver,然后转发到shopping cart 进行处理,并存到数据库。
在这里插入图片描述
而在这个过程中,每个服务之间必须要知道对方的通信地址,并且当有新的节点加入进来的时候,还需要对这些通信地址进行动态维护。所以,在第一代微服务架构中,每个微服务除了要实现业务逻辑以外,还需要解决上下游寻址、通讯、以及容错等问题。
(如图)于是,在第二代微服务架构下,引入了服务注册中心来实现服务之间的寻址,并且服务之间的容错机制、负载均衡也逐步形成了独立的服务框架,比如主流的 Spring Cloud、或者Spring Cloud Alibaba。
在这里插入图片描述
在第二代微服务架构中,负责业务开发的小伙伴不仅仅需要关注业务逻辑,还需要花大量精力去处理微服务中的一些基础性配置工作,虽然Spring Cloud 已经尽可能去完成了这些事情,但对于开发人员来说,学习 Spring Cloud,以及针对Spring Cloud 的配置和维护,仍然存在较大的挑战。另外呢,也增加了整个微服务的复杂性。
实际上,在我看来,“微服务中所有的这些服务注册、容错、重试、安全等工作,都是为了保证服务之间通信的可靠性”。
于是,就有了第三代微服务架构,Service Mesh。

(如图)原本模块化到微服务框架里的微服务基础能力,被进一步的从一个 SDK 中演进成了一个独立的代理进程-SideCar
SideCar 的主要职责就是负责各个微服务之间的通信,承载了原本第二代微服务架构中的服务发现、调用容错、服务治理等功能。使得微服务基础能力和业务逻辑迭代彻底解耦。
在这里插入图片描述

之所以我们称Service Mesh 为服务网格,是因为在大规模微服务架构中,每个服务的通信都是由SideCar 来代理的,各个服务之间的通信拓扑图,看起来就像一个网格形状(如图)。
在这里插入图片描述
Istio 是目前主流的Service Mesh 开源框架。

Service Mesh 架构其实就是云原生时代的微服务架构,对于大部分企业来说,仍然是处在第二代微服务架构下。

8. Dubbo 是如何动态感知服务下线的?

首先,Dubbo 默认采用Zookeeper 实现服务的注册与服务发现,简单来说啊,就是多个Dubbo 服务之间的通信地址,是使用 Zookeeper 来维护的。
(如图)在Zookeeper 上,会采用树形结构的方式来维护 Dubbo 服务提供端的协议地址,
Dubbo 服务消费端会从Zookeeper Server 上去查找目标服务的地址列表,从而完成服务的注册和消费的功能。

在这里插入图片描述
Zookeeper 会通过心跳检测机制,来判断 Dubbo 服务提供端的运行状态,来决定是否应该把这个服务从地址列表剔除。
在这里插入图片描述
当Dubbo 服务提供方出现故障导致 Zookeeper 剔除了这个服务的地址,
那么Dubbo 服务消费端需要感知到地址的变化,从而避免后续的请求发送到故障节点,导致请求失败。
也就是说Dubbo 要提供服务下线的动态感知能力。(如图)这个能力是通过Zookeeper 里面提供的Watch 机制来实现的,
简单来说呢,Dubbo 服务消费端会使用Zookeeper 里面的Watch 来针对Zookeeper Server 端的/providers节点注册监听,一旦这个节点下的子节点发生变化,Zookeeper Server 就会发送一个事件通知Dubbo Client 端.
在这里插入图片描述
Dubbo Client 端收到事件以后,就会把本地缓存的这个服务地址删除,这样后续就不会把请求发送到失败的节点上,完成服务下线感知。

9. 简单说一下你对序列化和反序列化的理解

首先,我认为,之所以需要序列化,核心目的是为了解决网络通信之间的对象传输问题

(如图)也就是说,如何把当前 JVM 进程里面的一个对象,跨网络传输到另外一个 JVM进程里面。
在这里插入图片描述

而序列化,就是把内存里面的对象转化为字节流,以便用来实现存储或者传输。
反序列化,就是根据从文件或者网络上获取到的对象的字节流,根据字节流里面保存的对象描述信息和状态。重新构建一个新的对象。

其次呢,序列化的前提是保证通信双方对于对象的可识别性,所以很多时候,我们会把对象先转化为通用的解析格式,
比如json、xml 等。然后再把他们转化为数据流进行网络传输,从而实现跨平台和跨语言的可识别性。
最后,我再补充一下序列化选择。市面上开源的序列化技术非常多,比如Json、Xml、Protobuf、Kyro、hessian 等等。那在实际应用里面,哪种序列化最合适,我认为有几个关键因素。

  • 序列化之后的数据大小,因为数据大小会影响传输性能
  • 序列化的性能,序列化耗时较长会影响业务的性能
  • 是否支持跨平台和跨语言
  • 是否支持跨平台和跨语言

10. Eureka server 数据同步原理

Eureka 是一个服务注册中心,在 Eureka 的设计里面,为了保证 Eureka 的高可用性,提供了集群的部署方式。
Eureka 的集群部署采用的是两两相互注册的方式来实现,也就是说每个 Eureka Server节点都需要发现集群中的其他节点并建立连接,然后通过心跳的方式来维持这个连接的状态。

Eureka Server 集群节点之间的数据同步方式非常简单粗暴,使用的是对等复制的方式来实现数据同步。也就是说,在Eureka Server 集群中,不存在所谓主从节点,任何一个节点都可以接收或者写入数据。一旦集群中的任意一个节点接收到了数据的变更,就直接同步到其他节点上。
在这里插入图片描述
这种无中心化节点的数据同步,需要考虑到一个数据同步死循环的问题,也就是需要区分Eureka Server 收到的数据
是属于客户端传递来的数据还是集群中其他节点发过来的同步数据。
Eureka 使用了一个时间戳的标记来实现类似于数据的版本号来解决这个问题。

另外,从 Eureka 的数据同步方案来看,Eureka 集群采用的是AP 模型,也就是只提供高可用保障,而不提供数据强一致性保障。
之所以采用AP,我认为注册中心它只是维护服务之间的通信地址,数据是否一致对于服务之间的通信影响并不大。
而注册中心对Eureka 的高可用性要求会比较高,不能出现因为 Eureka 的故障导致服务之间无法通信的问题。

11. 说说你对一致性 Hash 算法的理解

一致性hash,是一种比较特殊的hash 算法,它的核心思想是解决在分布式环境下,hash 表中可能存在的动态扩容和缩容的问题。

一般情况下,我们会使用hash 表的方式以key-value 的方式来存储数据,但是当数据量比较大的时候,我们就会把数据存储
到多个节点上,(如图)然后通过 hash 取模的方法来决定当前 key 存储到哪个节点上。
在这里插入图片描述
这种方式有一个非常明显的问题,就是当存储节点增加或者减少的时候,原本的映射关系就会发生变化。
也就是需要对所有数据按照新的节点数量重新映射一遍,这个涉及到大量的数据迁移和重新映射,迁移代价很大。

而一致性hash 就是用来优化这种动态变化场景的算法,它的具体工作原理也很简单。首先,一致性Hash 是通过一个Hash 环的数据结构来实现的,(如图),这个环的起点是 0,终点是 2^32-1。
也就是这个环的数据分布范围是[0,2^32-1]。
在这里插入图片描述
然后我们把存储节点的 ip 地址作为key 进行hash 之后,会在 Hash 环上确定一个位置。
在这里插入图片描述
接下来,(如图)就是把需要存储的目标 key 使用hash 算法计算后得到一个hash 值,同样也会落到hash 环的某个位置上。
在这里插入图片描述

然后这个目标key 会按照顺时针的方向找到离自己最近的一个节点进行数据存储。

假设现在需要新增一个节点(如图)node4,那数据的映射关系的影响范围只限于node3和node1,只有少部分的数据需要重新映射迁移就行了。
在这里插入图片描述
如果是已经存在的节点 node1 因为故障下线了(如图),只那只需要把原本分配在 node1 上的数据重新分配到 node2 上就行了。
同样对数据影响的范围非常小。
在这里插入图片描述
所以,在我看来,一致性 hash 算法的好处是扩展性很强,在增加或者减少服务器的时候,数据迁移范围比较小。

12. Nacos 配置更新的工作流程

首先,Nacos 是采用长轮训的方式向Nacos Server 端发起配置更新查询的功能。
所谓长轮训,(如图)就是客户端发起一次轮训请求到服务端,当服务端配置没有任何变更的时候,这个连接一直打开。
直到服务端有配置或者连接超时后返回。
在这里插入图片描述
Nacos Client 端需要获取服务端变更的配置,前提是要有一个比较,也就是拿客户端本地的配置信息和服务端的配置信息进行比较。
一旦发现和服务端的配置有差异,就表示服务端配置有更新,于是把更新的配置拉到本地。

在这个过程中,有可能因为客户端配置比较多,导致比较的时间较长,使得配置同步较慢的问题。于是Nacos 针对这个场景,做了两个方面的优化。

  • 减少网络通信的数据量,客户端把需要进行比较的配置进行分片,每一个分片大小是 3000,也就是说,每次最多拿 3000 个配置去Nacos Server 端进行比较。
  • 分阶段进行比较和更新
    • 第一阶段,客户端把这 3000 个配置的key 以及对应的value 值的md5 拼接成一个字符串,然后发送到Nacos Server 端
      进行判断,服务端会逐个比较这些配置中 md5 不同的key,把存在更新的 key 返回给客户端。
    • 第二阶段,客户端拿到这些变更的 key,循环逐个去调用服务单获取这些 key 的value值。

这两个优化,核心目的是减少网络通信数据包的大小,把一次大的数据包通信拆分成了多次小的数据包通信。
虽然会增加网络通信次数,但是对整体的性能有较大的提升。
最后,再采用长连接这种方式,既减少了 pull 轮询次数,又利用了长连接的优势,很好的实现了配置的动态更新同步功能。

13. 请你说一下你对服务降级的理解

服务降级是一种提升系统稳定性和可用性的策略。
简单来说,就是当服务器压力增加的情况下,根据实际业务的需求和流量的情况,不对外提供部分服务的功能。从而释放服务器的资源去保证核心业务的正常运行。

服务降级有两种方式,一种是主动降级,一种是基于特定情况的被动降级。

  • 主动降级:这种方式在大促的时候使用比较多,比如在电商平台中,核心服务是下单、支付。
    所以一般会把非核心服务比如评论服务关闭掉,这样就使得评论服务不会占用计算资源,从而保证核心服务的稳定运行
  • 被动降级:它有两种主要的触发场景
    • 熔断触发降级, 在一个请求链路中,为了避免某个服务节点出现故障导致请求堆积,造成资源消耗是的服务崩溃的问题,一般会采取熔断策略。当触发了熔断机制以后,如果后续再向故障节点发起请求的时候,这个请求不会发送到故障节点上,而是直接置为失败,这样就避免了请求堆积的问题。而直接置为失败之后需要给到用户一个反馈,而这个反馈就是降级策略,就相当于给用户一个处理结果。比如返回一个“系统繁忙”之类的信息。
    • 限流触发降级,因为系统资源是有限的,为了避免高并发流量把系统压垮导致不可用问题,所以我们会采取限流的策略去保护系统。通过限流去限制一部分用户的访问,然后保证整个系统的稳定运行
      同样,触发了限流之后,需要给到用户一个反馈,这个反馈同样也称为降级策略。
      比如可以反馈“当前访问人数较多,请稍候再试”,或者让这些用户排队,并显示当前排队的情况等。

因此,降级带来的结果是使得用户的体验下降,但是却保证了系统的稳定性和可用性。

14. 说说你对 CAP 的理解

CAP 模型,在一个分布式系统里面,不可能同时满足三个点

  • 一致性(Consistency),访问分布式系统中的每一个节点都能获得最新的数据。
  • 可用性(Availability),每次请求都能获得一个有效的访问,但不保证数据是最新的。
  • 分区容错性(Partition tolerance),分区相当于对通信耗时的要求,系统如果不能在时限范围内达成数据一致,就意味着发生了分区的情况。

在CAP 模型中只能满足 CP 或者AP,之所以不能满足CA,因为网络通信的不确定性可能会导致分区容错,
也就是分区容错性必然是存在的,因此我们只能在一致性和可用性之间做选择。

15. 谈谈你对 NoSQL 的理解

NoSQL 呢常见的解释有 Non-Relational SQL 或者 Not Only SQL,不过 Not Only SQL 被更多人接受,一般泛指非关系型数据库。它和关系型数据库不同的是,不保证关系数据的 ACID 特性。

随着互联网的发展,NoSQL 数据库的产生就是为了解决超大规模和高并发系统中多重数据种类带来的挑战,特别是大数据应用的难题。目前具有代表性的 NoSQL 数据库有 Redis、HBase、ES、MongoDB 等等。

NoSQL 非常适合以下几个场景:
1、数据模型比较简单;
2、需要灵活性更强的数据库;
3、对数据库性能要求较高;
4、不需要高度的数据一致性;
5、对于给定 Key,比较容易映射复杂值的环境。

通常来说,现在主流的 NoSQL 数据库可以分为四大类:K-V 键值数据库、列存储数据库、文档数据库和图形数据库。

  • 首先来看,K-V 键值数据库,主要应用于内容缓存、处理大量数据的高负载访问,也可以用于记录系统日志。比较有代表性的产品有 Redis、Memcached 等等。
  • 其次是,列存储数据库,主要应用于布式数据的储存与管理,分布式可扩展性强,比较有代表性的产品有 HBase、HadoopDB、BigTable 等等。
  • 然后就是,文档数据库,主要应用于管理半结构化数据或者面向文档的数据,比较有代表性的产品有 MongoDB,ES 等等,当然,ES 不仅仅只是数据库,它还是一个分布式搜索引擎,是基于 Lucene 来开发的,现在已经发展成为一个自有生态 Elastic Stack。
  • 最后就是,图形数据库,主要应用于复杂、互连接但又低结构化的图结构场合,可以用来构建数据关系图谱,Neo4J、InfoGrid、GraphDB 等等。

下面我把四种分类的 NoSQL 数据库整理成了一个表格,并总结了它们的优缺点
在这里插入图片描述
在这里插入图片描述

16. 放弃 FastDFS,拥抱MinIO 的 7 大理由

目前可用于文件存储的网络服务选择也有不少,好比阿里云 OSS、七牛云、腾讯云等等,可是收费都有点小贵。为了帮公司节约成本,以前一直是使用 FastDFS 做为文件服务器,准确的说是图片服务器。直到我发现了 MinIO,我决定放弃 FastDFS。

16.1 什么是 MinIO?

MinIO 是专门为海量数据存储、人工智能、大数据分析而设计的对象存储系统。

据官方介绍,单个对象最大可达 5TB。非常适合储海量图片、视频、日志文件、备份数据和容器/虚拟机镜像等。MinIO 主要采用 Golang 语言实现,整个系统都运行在操作系统的用户态空间,客户端与存储服务器之间采用 HTTP/HTTPs通信协议。

16.2 为什么选 MinIO

相比于其他方案来说,推荐选择 MinIO 的 7 大理由:

  • 安装部署非常简单
    小伙伴们应该都知道,部署 FastDFS 的时,需要掌握 Linux 基础、分布式原理、C 语言编译、Nginx 安装、 Ningx 防盗链插件这些前置知识。而 MinIO 只需要执行这几行命令便可轻松搞定,也大大降低了学习和维护成本。
  • 操作简单,自带 UI 管理界面
    MinIO 在服务端安装后就自带了 UI 界面,可谓开箱即用,而 FastDFS 默认是不带UI 界面的。
    在这里插入图片描述
  • 性能优秀,可以达到每秒 GB 级别的读写速度
    MinIO 官方号称是世界上速度最快的对象存储服务器。就算部署在普通的硬件上,读对象的速度能达到 183 GB/s,写对象的速度能达到 171 GB/s。
    MinIO 提供了与 K8s、ETCD、Docker 等容器化技术深度集成方案,可以说MinIO 就是为云环境而生的。
  • 提供多语言 SDK 的支持
    MinIO 几乎提供了全部主流开发语言的 SDK 以及文档,比如 Java、Python、Golang、JS、.NET 等等。
  • 兼容亚马逊 S3 API
    亚马逊云的 S3 API(接口协议) 是在全球范围内达到共识的对象存储的协议,是全世界认可的对象存储标准。而 MinIO 是第一个采用 S3 兼容协议的产品之一;
    兼容 S3 API 有什么好处呢?相当于目前为了节约服务器成本,选择用MinIO 自主开发对象存储系统,等企业壮大之后,不想再运维基础设施,可以直接将程序平移到云厂商,不需要重新开发。
  • 参考学习文档非常全面
    官方提供了非常多的参考文档,而且 是全面开源的,在很多开源社区也非常活跃。

17. Spring Boot 的约定优于配置,你的理解是什么?

  • 首先, 约定优于配置是一种软件设计的范式,它的核心思想是减少软件开发人员对于配置项的维护,从而让开发人员更加聚焦在业务逻辑上。
  • Spring Boot 就是约定优于配置这一理念下的产物,它类似于 Spring 框架下的一个脚手架,通过 Spring Boot,我们可以快速开发基于 Spring 生态下的应用程序。
  • 基于传统的Spring 框架开发web 应用,我们需要做很多和业务开发无关并且只需要做一次的配置,比如
    • 管理jar 包依赖
    • web.xml 维护
    • Dispatch-Servlet.xml 配置项维护
    • 应用部署到Web 容器
    • 第三方组件集成到Spring IOC 容器中的配置项维护

而在Spring Boot 中,我们不需要再去做这些繁琐的配置,Spring Boot 已经自动帮我们完成了,这就是约定由于配置思想的体现。

Spring Boot 约定由于配置的体现有很多,比如

  • Spring Boot Starter 启动依赖,它能帮我们管理所有jar 包版本
  • 如果当前应用依赖了 spring mvc 相关的jar,那么Spring Boot 会自动内置 Tomcat 容器来运行web 应用,我们不需要再去单独做应用部署。
  • Spring Boot 的自动装配机制的实现中,通过扫描约定路径下的 spring.factories 文件来识别配置类,实现Bean 的自动装配。
  • 默认加载的配置文件 application.properties 等等。

总的来说,约定优于配置是一个比较常见的软件设计思想,它的核心本质都是为了更高效以及更便捷的实现软件系统的开发和维护。

18. Spring Boot 中自动装配机制的原理

自动装配,简单来说就是自动把第三方组件的Bean 装载到Spring IOC 器里面,不需要开发人员再去写Bean 的装配配置。

在Spring Boot 应用里面,只需要在启动类加上@SpringBootApplication 注解就可以实现自动装配。

  • @SpringBootApplication 是一个复合注解,真正实现自动装配的注解是 @EnableAutoConfiguration

自动装配的实现主要依靠三个核心关键技术
在这里插入图片描述

  • 引入Starter 启动依赖组件的时候,这个组件里面必须要包含@Configuration 配置类,在这个配置类里面通过@Bean 注解声明需要装配到 IOC 容器的Bean 对象。
  • 这个配置类是放在第三方的 jar 包里面,然后通过SpringBoot 中的约定优于配置思想,把这个配置类的全路径放在 classpath:/META-INF/spring.factories 文件中。这样SpringBoot 就可以知道第三方jar 包里面的配置类的位置,这个步骤主要是用到了Spring 里面的SpringFactoriesLoader 来完成的。
  • SpringBoot 拿到所第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的 ImportSelector 接口,实现对这些配置类的动态加载。

在我看来,SpringBoot 是约定优于配置这一理念下的产物,所以在很多的地方,都会看到这类的思想。它的出现,让开发人员更加聚焦在了业务代码的编写上,而不需要去关心和业务无关的配置。

其实,自动装配的思想,在SpringFramework3.x 版本里面的@Enable 注解,就有了实现的雏形。@Enable 注解是模块驱动的意思,我们只需要增加某个@Enable 注解,就自动打开某个功能,而不需要针对这个功能去做 Bean 的配置,@Enable 底层也是帮我们去自动完成这个模块相关Bean 的注入。
以上,就是我对 Spring Boot 自动装配机制的理解。

19. 如何理解 Spring Boot 中的 Starter?

Starter 是Spring Boot 的四大核心功能特性之一,除此之外,Spring Boot 还有自动装配、Actuator 监控等特性。
Spring Boot 里面的这些特性,都是为了让开发者在开发基于 Spring 生态下的企业级应用时,只需要关心业务逻辑,
减少对配置和外部环境的依赖。其中,Starter 是启动依赖,它的主要作用有几个。
在这里插入图片描述

  • Starter 组件以功能为纬度,来维护对应的 jar 包的版本依赖,使得开发者可以不需要去关心这些版本冲突这种容易出错的细节。
  • Starter 组件会把对应功能的所有jar 包依赖全部导入进来,避免了开发者自己去引入依赖带来的麻烦。
  • Starter 内部集成了自动装配的机制,也就说在程序中依赖对应的starter 组件以后,这个组件自动会集成到 Spring 生态下,并且对于相关Bean 的管理,也是基于自动装配机制来完成。
  • 依赖Starter 组件后,这个组件对应的功能所需要维护的外部化配置,会自动集成到Spring Boot 里面,我们只需要在application.properties 文件里面进行维护就行了,比如Redis 这个 starter,只需要在application.properties文件里面添加redis 的连接信息就可以直接使用了。

在我看来,Starter 组件几乎完美的体现了 Spring Boot 里面约定优于配置的理念。另外,Spring Boot 官方提供了很多的 Starter 组件,比如 Redis、JPA、MongoDB等等。

但是官方并不一定维护了所有中间件的Starter,所以对于不存在的Starter,第三方组件一般会自己去维护一个。
(如图)官方的 starter 和第三方的starter 组件,最大的区别在于命名上。

  • 官方维护的starter 的以spring-boot-starter 开头的前缀。
  • 第三方维护的starter 是以spring-boot-starter 结尾的后缀这也是一种约定优于配置的体现。

在这里插入图片描述

20. SpringBoot 如何解决跨域问题?

跨域指的是浏览器在执行网页中的 JavaScript 代码时,由于浏览器同源策略的限制(如图)。
在这里插入图片描述

只能访问同源(协议域名端口号均相同)的资源,而不能访问其他源(协议、域名、端口号任意一个不同)的资源。

比如像这样一种情况就体现了跨域的问题。
在这里插入图片描述
而解决跨域问题的方法,就是在不破坏同源策略的情况下,能够安全地实现数据共享和交互。

常见的解决跨域问题的方法有两种,一种是 jsonp,另一种是CORS

  • 其中,CORS 是一种在服务器后端解决跨域的方案,它的工作原理很简单。如果一个网站需要访问另一个网站的资源,浏览器会先发送一个 OPTIONS 请求, 根据服务器返回的 Access-Control-Allow-Origin 头信息,决定是否允许跨域访问。所以,我们只需要在服务器端配置Access-Control-Allow-Origin属性,并配置允许哪些域名支持跨域请求即可。
    • 在Spring Boot 中,提供了两种配置Access-Control-Allow-Origin属性的方式来解决跨域问题
      • 通过@CrossOrigin(origins = " http://localhost:8080 ") 注解,指定允许哪些origins 允许跨域
      • 使用 WebMvcConfigurer 接口,重写addCorsMappings方法来配置允许跨域的请求源
@Configuration 
public class CorsConfig implements WebMvcConfigurer {

@Override publicvoidaddCorsMappings(CorsRegistryregistry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("*");
  }
}

21. Spring Bean 生命周期的执行流程

Spring 生命周期全过程大致分为五个阶段:

  • 创建前准备阶段
  • 创建实例阶段
  • 依赖注入阶段
  • 容器缓存阶段
  • 销毁实例阶段。

21.1 创建前准备阶段

  • 这个阶段主要的作用是,Bean 在开始加载之前,需要从上下文和相关配置中解析并查找Bean 有关的扩展实现,
    比如像init-method-容器在初始化bean 时调用的方法、destory-method,容器在销毁bean 时调用的方法。
  • 以及,BeanFactoryPostProcessor 这类的 bean 加载过程中的前置和后置处理。

这些类或者配置其实是 Spring 提供给开发者,用来实现 Bean 加载过程中的扩展机制,在很多和Spring 集成的中间件中比较常见,比如Dubbo。

21.2 创建实例阶段

这个阶段主要是通过反射来创建Bean 的实例对象,并且扫描和解析Bean 声明的一些属性。

21.3 依赖注入阶段

如果被实例化的 Bean 存在依赖其他Bean 对象的情况,则需要对这些依赖bean 进行对象注入。
比如常见的@Autowired、setter 注入等依赖注入的配置形式。

同时,在这个阶段会触发一些扩展的调用,比如常见的扩展类:BeanPostProcessors(用来实现bean 初始化前后的扩展回调)、
InitializingBean(这个类有一个 afterPropertiesSet(),这个在工作中也比较常见)、 BeanFactoryAware 等等。

21.4 容器缓存阶段

容器缓存阶段主要是把 bean 保存到容器以及 Spring 的缓存中,到了这个阶段,Bean就可以被开发者使用了。

这个阶段涉及到的操作,常见的有,init-method这个属性配置的方法, 会在这个阶段调用。
以及像BeanPostProcessors 方法中的后置处理器方法如: postProcessAfterInitialization,也会在这个阶段触发。

21.5 销毁实例阶段

当Spring 应用上下文关闭时,该上下文中的所有bean 都会被销毁。
如果存在Bean 实现了DisposableBean 接口,或者配置了destory-method属性,会在这个阶段被调用。

22. 简述你对 Spring MVC 的理解

在这里插入图片描述
SpringMVC 是一种基于Java 语言开发,实现了Web MVC 设计模式,请求驱动类型的轻量级Web 框架。
采用了MVC 架构模式的思想,通过把 Model,View,Controller 分离,将 Web 层进行职责解耦,从而把复杂的 Web 应用分成逻辑清晰的几个组件

  • 前端控制器 DispatcherServlet(不需要程序员开发)
    作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。

  • 处理器映射器HandlerMapping(不需要程序员开发)
    作用:根据请求的URL来查找Handler

  • 处理器适配器HandlerAdapter
    注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。

  • 处理器Handler(需要程序员开发)

  • 视图解析器 ViewResolver(不需要程序员开发)
    作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view)

  • 视图View(需要程序员开发jsp)
    View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等)

执行流程:

  1. 用户发送请求至前端控制器DispatcherServlet。

  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器。

  3. 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。

  4. DispatcherServlet调用HandlerAdapter处理器适配器。

  5. HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。

  6. Controller执行完成返回ModelAndView。

  7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。

  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器。

  9. ViewReslover解析后返回具体View。

  10. DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。

  11. DispatcherServlet响应用户。

简单理解:

  • 用户发起请求,请求先被Servlet 拦截转发给Spring MVC 框架
  • Spring MVC 里面的DispatcherSerlvet 核心控制器,会接收到请求并转发给 HandlerMapping
  • HandlerMapping 负责解析请求,根据请求信息和配置信息找到匹配的 Controller类,不过这里如果有配置拦截器,就会按照顺序执行拦截器里面的preHandle 方法
  • 找到匹配的Controller 以后,把请求参数传递给Controller 里面的方法
  • Controller 中的方法执行完以后,会返回一个 ModeAndView,这里面会包括视图名称和需要传递给视图的模型数据
  • 视图解析器根据名称找到视图,然后把数据模型填充到视图里面再渲染成 Html 内容返回给客户端

在 Spring MVC 中有 9 大重要的组件。下面详细说明一下这些组件的作用和初始化方法:

  • 1、MultipartResolver 文件处理器
    对应的初始化方法是 initMultipartResolver(context),用于处理上传请求。
  • 2、LocaleResolver 当前环境处理器
    其对应的初始化方法是 initLocaleResolver(context) SpringMVC 主要有两个地方用到了Locale:
    • 一是ViewResolver 视图解析的时候;
    • 二是用到国际化资源或者主题的时候。
  • 3、ThemeResolver 主题处理器
    其对应的初始化方法是 initThemeResolver(context),
    用于解析主题。 也就是解析样式、图片及它们所形成的显示效果的集合。
  • 4、HandlerMapping 处理器映射器
    其对应的初始化方法是 initHandlerMappings(context) ,在SpringMVC 中会有很多请求,每个请求都需要一个 Handler 处理。 HandlerMapping 的作用便是找到请求相应的处理器 Handler 和Interceptor。
  • 5、HandlerAdapter 处理器适配器
    其对应的初始化方法是 initHandlerAdapters(context)
    从名字上看,它就是一个适配器。HandlerAdapters 要做的事情就是如何让固定的 Servlet 处理方法调用灵活的 Handler 来进行处理
  • 6、HandlerExceptionResolver 异常处理器
    对应的初始化方法是 initHandlerExceptionResolvers(context)它的主要作用是处理其他组件产生的异常情况。
  • 7、RequestToViewNameTranslator 视图名称翻译器
    其对应的初始化方法是 initRequestToViewNameTranslator(context)它的作用是从请求中获取ViewName。
    有的Handler 处理完后并没有设置 View 也没有设置ViewName,这时就需要从 request 中获取,而RequestToViewNameTranslator 就是为request 提供获取 ViewName 的实现。
  • 8、ViewResolvers 页面渲染处理器
    其对应的初始化方法是 initViewResolvers(context)
    ViewResolvers 的主要作用是将String 类型的视图名和Locale 解析为View 类型的视图。
  • 9、FlashMapManager 参数传递管理器
    其对应的初始化方法是 initFlashMapManager(context)
    在实际应用中,为了避免重复提交,我们可以在处理完 post 请求后重定向到另外一个 get 请求,这个get 请求可以用来返回页面渲染需要的信息。FlashMap 就是用于这种请求重定向场景中的参数传递。

在Spring MVC 的九大组件中,涉及到请求处理响应的核心组件分别是:

  • 1.HandlerMapping、
  • 2.HandlerAdapter、
  • 3.ViewResolver

1、HandlerMapping 回到调用HandlerAdapter
2、HandlerAdapter 会返回ModelAndView
3、ModelAndView 根据用户传入参数得到 ViewResolvers
4、ViewResolvers 会将用户传入的参数封装为View,交给引擎进行渲染。

注意:有大家最熟悉的两个类:ModelAndView 和View 类并不属于Spring MVC 九大组件之列。

23. Spring 是如何解决循环依赖问题的?

循环依赖有三种形态:

  • 第一种互相依赖:A 依赖 B,B 又依赖 A,它们之间形成了循环依赖。
    在这里插入图片描述
  • 第二种三者间依赖:A 依赖 B,B 依赖 C,C 又依赖 A,形成了循环依赖。
    在这里插入图片描述
    • 第三种是自我依赖:A 依赖A 形成了循环依赖。
      在这里插入图片描述
      而Spring 中设计了三级缓存来解决循环依赖问题
  • 当我们去调用getBean()方法的时候,Spring 会先从一级缓存中去找到目标Bean,如果发现一级缓存中没有便会去二级缓存中去找
  • 而如果一、二级缓存中都没有找到,意味着该目标 Bean 还没有实例化。于是,Spring 容器会实例化目标Bean(PS:刚初始化的 Bean 称为早期Bean) 。
  • 然后,将目标Bean 放入到二级缓存中,同时,加上标记是否存在循环依赖。如果不存在循环依赖便会将目标 Bean 存入到二级缓存,否则,便会标记该Bean 存在循环依赖,然后将等待下一次轮询赋值,也就是解析@Autowired 注解。等@Autowired 注解赋值完成后(PS:完成赋值的 Bean 称为成熟Bean) ,会将目标 Bean 存入到一级缓存。

这里我可以做个总结,我们来看这张图Spring 一级缓存中存放所有的成熟 Bean,二级缓存中存放所有的早期 Bean,先取一级缓存,再去二级缓存。
在这里插入图片描述

23.1 前面有提到三级缓存,三级缓存的作用是什么?

三级缓存是用来存储代理Bean,当调用getBean()方法时,发现目标Bean 需要通过代理工厂来创建,此时会将创建好的实例保存到三级缓存,最终也会将赋值好的Bean同步到一级缓存中。

24.Spring 中哪些情况下,不能解决循环依赖问题?

  • 多例Bean 通过setter 注入的情况,不能解决循环依赖问题
  • 构造器注入的Bean 的情况,不能解决循环依赖问题
  • 单例的代理Bean 通过Setter 注入的情况,不能解决循环依赖问题
  • 设置了@DependsOn 的Bean 的情况,不能解决循环依赖问题

24. Spring 中 Bean 的作用域有哪些?

理论上来说,常规的生命周期只有两种:

  • singleton, 也就是单例,意味着在整个Spring 容器中只会存在一个 Bean 实例。
    Spring 中的Bean 默认都是单例的。它的作用域范围是 ApplicationContext 容器

  • prototype,翻译成原型,意味着每次从IOC 容器去获取指定 Bean 的时候,都会返回一个新的实例对象。
    也就是说在每次请求获取 Bean 的时都会重新创建实例,因此每次获取到的实例对象都是不同的。它的作用域范围是调用 getBean 方法直至获取对象。

但是在基于Spring 框架下的Web 应用里面,增加了一个会话纬度来控制 Bean 的生命周期,主要有三个选择

  • request, 针对每一次http 请求,都会创建一个新的Bean
    用来定义一个作用范围仅在 request 中的Bean,也就是说在每次 HTTP请求时会创建一个实例,该实例仅在当前 Request 中有效。它的作用域范围是每次发起HTTP 请求直至拿到响应结果。
  • session,以sesssion 会话为纬度,同一个session 共享同一个Bean 实例,不同的session 产生不同的Bean 实例
    用来定义一个作用范围仅在 session 中的Bean,也就是说在每次HTTP请求时会创建—个实例,该实例仅在当前 HTTP Session 中有效。它的作用域范围是浏览器首次访问至浏览器关闭。
  • globalSession,针对全局session 纬度,共享同一个 Bean 实例
    用来定义一个作用范围仅在中的 Bean。这种方式仅用于
    应用环境,也就是说该实例仅存在于 WebApplicationContext 环境中。它的作用域范围是整个WebApplicationContext 容器。
    在这里插入图片描述
    在这里插入图片描述

25. Spring 中有哪些方式可以把Bean 注入到 IOC 容器?

把Bean 注入到IOC 容器里面的方式有 7 种方式:

  • 使用xml 的方式来声明Bean 的定义,Spring 容器在启动的时候会加载并解析这个xml,把bean 装载到IOC 容器中。
  • 使用@CompontScan 注解来扫描声明了@Controller、@Service、 @Repository、@Component 注解的类。
  • 使用@Configuration 注解声明配置类,并使用@Bean 注解实现Bean 的定义,这种方式其实是 xml 配置方式的一种演变,是Spring 迈入到无配置化时代的里程碑。
  • 使用@Import 注解,导入配置类或者普通的Bean
  • 使用FactoryBean 工厂bean,动态构建一个 Bean 实例,Spring Cloud OpenFeign 里面的动态代理实例就是使用FactoryBean 来实现的。
  • 实现ImportBeanDefinitionRegistrar 接口,可以动态注入Bean 实例。这个在 Spring Boot 里面的启动注解有用到。
  • 实现ImportSelector 接口,动态批量注入配置类或者Bean 对象,这个在Spring Boot 里面的自动装配机制里面有用到。

26. BeanFactory 和FactoryBean的区别

  • BeanFacotry是Spring中比较原始的Factory。

    • 如XMLBeanFactory就是一种典型的BeanFactory。
      原始的BeanFactory无法支持Spring的许多插件,如AOP功能、Web应用等。

    • ApplicationContext接口,它由BeanFactory接口派生而来,ApplicationContext包含BeanFactory的所有功能

    • BeanFactory,以Factory结尾,表示它是一个工厂类(接口), 它负责生产和管理bean的一个工厂。

    • 在Spring中,BeanFactory是IOC容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。

    • BeanFactory只是个接口,并不是IOC容器的具体实现,但是Spring容器给出了很多种实现,如 DefaultListableBeanFactoryXmlBeanFactoryApplicationContext等,其中XmlBeanFactory就是常用的一个,该实现将以XML方式描述组成应用的对象及对象间的依赖关系。XmlBeanFactory类将持有此XML配置元数据,并用它来构建一个完全可配置的系统或应用。

    • ApplicationContext以一种更向面向框架的方式工作以及对上下文进行分层和实现继承,ApplicationContext包还提供了以下的功能:
      MessageSource, 提供国际化的消息访问
      资源访问,如URL和文件
      事件传播
      载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层;

  • FactoryBean是个Bean,也是接口,在Spring中所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。
    但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似 ;

BeanFactory 的理解了有两个:

  • BeanFactory 是所有Spring Bean 容器的顶级接口,它为 Spring 的容器定义了一套规范,并提供像getBean 这样的方法从容器中获取指定的Bean 实例。
  • BeanFactory 在产生Bean 的同时,还提供了解决Bean 之间的依赖注入的能力,也就是所谓的DI。

FactoryBean 是一个工厂Bean,它是一个接口,主要的功能是动态生成某一个类型的 Bean 的实例,也就是说,我们可以自定义一个 Bean 并且加载到 IOC 容器里面。它里面有一个重要的方法叫 getObject(),这个方法里面就是用来实现动态构建 Bean的过程。
Spring Cloud 里面的OpenFeign 组件,客户端的代理类,就是使用了 FactoryBean来实现的。

27. @Resource 和 @Autowired 的区别

@Resource 和@Autowired 这两个注解的作用都是在Spring 生态里面去实现Bean的依赖注入。

  • @Autowired 是Spring 里面提供的一个注解,默认是根据类型来实现 Bean 的依赖注入。
    • @Autowired 注解里面有一个required属性默认值是true,表示强制要求 bean 实例的注入,在应用启动的时候,如果IOC 容器里面不存在对应类型的 Bean,就会报错; 当然,如果不希望自动注入,可以把这个属性设置成false。
    • 其次呢, 如果在 Spring IOC 容器里面存在多个相同类型的 Bean 实例。由于 @Autowired 注解是根据类型来注入 Bean 实例的; 针对这个问题,我们可以使用 @Primary 或者@Qualifier 这两个注解来解决。
    • @Primary 表示主要的bean,当存在多个相同类型的Bean 的时候,优先使用声明了 @Primary 的Bean。
    • @Qualifier 的作用类似于条件筛选,它可以根据 Bean 的名字找到需要装配的目标Bean。
  • @Resource 是JDK 提供的注解,只是 Spring 在实现上提供了这个注解的功能支持。它的使用方式和@Autowired 完全相同;
    • 最大的差异于@Resource 可以支持 ByName 和ByType 两种注入方式。
    • 如果使用name,Spring 就根据bean 的名字进行依赖注入,如果使用type,Spring就根据类型实现依赖注入。
    • 如果两个属性都没配置,就先根据定义的属性名字去匹配,如果没匹配成功,再根据类型匹配。两个都没匹配到,就报错。
  • @Autowired 注释,是通过AutowiredAnnotationBeanPostProcessor后置处理器处理的 当容器扫描到@Autowied
  • @Resource注释是CommonAnnotationBeanPostProcessor后置处理器处理的

最后,我再总结一下:

  • @Autowired 是根据type 来匹配,@Resource 可以根据name 和type 来匹配,默认是name 匹配。
  • @Autowired 是Spring 定义的注解,@Resource 是JSR 250 规范里面定义的注解,而Spring 对JSR 250 规范提供了支持。
  • @Autowired 如果需要支持name 匹配,就需要配合@Primary 或者@Qualifier来实现。
  • @Autowired 注释,是通过AutowiredAnnotationBeanPostProcessor后置处理器处理的 当容器扫描到@Autowied
  • @Resource注释是CommonAnnotationBeanPostProcessor后置处理器处理的

28. Spring 中,有两个 id 相同的 bean,会报错吗,如果会报错,在哪个阶段报错

首先,在同一个 XML 配置文件里面,不能存在 id 相同的两个bean,否则 spring 容器启动的时候会报错(如图)。
在这里插入图片描述
因为id 这个属性表示一个 Bean 的唯一标志符号,所以 Spring 在启动的时候会去验证id 的唯一性,一旦发现重复就会报错,这个错误发生Spring 对XML 文件进行解析转化为 BeanDefinition 的阶段。

但是在两个不同的Spring 配置文件里面,可以存在 id 相同的两个bean。 IOC 容器在加载Bean 的时候,默认会多个相同id的bean 进行覆盖。

在Spring3.x 版本以后,这个问题发生了变化, 我们知道Spring3.x 里面提供@Configuration 注解去声明一个配置类,然后使用 @Bean 注解实现Bean 的声明,这种方式完全取代了 XMl。在这种情况下,如果我们在同一个配置类里面声明多个相同名字的 bean,在 Spring IOC 容器中只会注册第一个声明的 Bean 的实例。后续重复名字的 Bean 就不会再注册了。

29. 谈谈你对 Spring IOC 和 DI 的理解?

首先,Spring IOC,全称控制反转(Inversion of Control)。在传统的Java 程序开发中,我们只能通过 new 关键字来创建对象,这种导致程序中对象的依赖关系比较复杂,耦合度较高。而IOC 的主要作用是实现了对象的管理,也就是我们把设计好的对象交给了 IOC 容器控制,然后在需要用到目标对象的时候,直接从容器中去获取。有了IOC 容器来管理 Bean 以后,相当于把对象的创建和查找依赖对象的控制权交给了容器,这种设计理念使得对象与对象之间是一种松耦合状态,极大提升了程序的灵活性以及功能的复用性。

然后,DI 表示依赖注入,也就是对于 IOC 容器中管理的Bean,如果 Bean 之间存在依赖关系,那么IOC 容器需要自动实现依赖对象的实例注入,通常有三种方法来描述 Bean 之间的依赖关系。

  • 接口注入
  • setter 注入
  • 构造器注入

30. @Conditional 注解有什么用?

@Conditional 注解的作用是为Bean 的装载提供了一个条件判断。只有满足条件的情况下,Spring 才会把当前 Bean 装载到IOC 容器中。

这个条件的实现逻辑,我们可以实现 Condition 接口并重写matches 方法自己去实现。所以@Conditional 注解增加了Bean 装载的灵活性。
在Spring Boot 里面,对@Conditional 注解做了更进一步的扩展,比如增加了 @ConditionalOnClass、@ConditionalOnBean
等注解,使得我们在使用的过程中不再需要去写条件的逻辑。

31. 为什么有些公司禁止使用@Transactional 声明式事务?

我认为有几个方面的考虑:

  • 在方法上增加@Transaction 声明式事务,如果一个方法中的存在较多耗时操作,就容易引发长事物问题,而长事物会带来锁的竞争影响性能,同时也会导致数据库连接池被耗尽,影响程序的正常执行。
  • 如果方法存在嵌套调用,而被嵌套调用的方法也声明了@Transaction 事物,就会出现事物的嵌套调用行为,容易引起事物的混乱造成程序运行结果出现异常
  • @Transaction 声明式事务是将事物控制逻辑放在注解中,如果项目的复杂度增加,事务的控制可能会变得更加复杂,导致代码可读性和维护性下降。

所以,为了避免这类问题,有些公司会推荐使用编程式事务,这样可以更加灵活地控制事务的范围,减少事务的锁定时间,提高系统的性能。

32. 过滤器和拦截器有什么区别?

  • 运行顺序不同(如图):过滤器是在 Servlet 容器接收到请求之后,但在 Servlet被调用之前运行的;而拦截器则是在 Servlet 被调用之后,但在响应被发送到客户端之前运行的
    在这里插入图片描述
  • 配置方式不同:过滤器是在 web.xml 中进行配置;而拦截器的配置则是在Spring的配置文件中进行配置,或者使用注解进行配置。
  • Filter 依赖于Servlet 容器,而Interceptor 不依赖于Servlet 容器;
  • Filter 在过滤是只能对 request 和response 进行操作,而interceptor 可以对 request、response、handler、modelAndView、exception 进行操作;

33. Spring 容器如何加载Bean?

Spring 解析这些声明好的配置内容,将这些配置内容都转化为 BeanDefinition 对象, BeanDefinition 中几乎保存了配置文件中声明的所有内容,再将 BeanDefinition 存到一个叫做beanDefinitionMap 中。以 beanName 作为Key,以 BeanDefinition 对象作为Value。之后 Spring 容器,根据beanName 找到对应的BeanDefinition,再去选择具体的创建策略。而Spring 具体的创建策略如图所示
在这里插入图片描述

34. Spring Bean 的定义包含哪些内容?

关于Spring Bean 的配置内容非常多,我主要列举九个关键的配置属性,比如:class、 scope、lazy-init、depends-on、name、constructor-arg、properties、init-method、 destroy-method 等。

这些属性都是要在Spring 配置文件中声明的内容。在 Spring 容器启动后,这些配置内容都会映射到一个叫做BeanDefinition 的对象中。
然后,所有的 BeanDefinition 对象都会保存到一个叫做beanDefinitionMap 的容器中,这个容器是 Map 类型,以Bean的唯一标识作为 key,以 BeanDefinition 对象实例作为值。这样 Spring 容器创建Bean时,就不需要再次读取和解析配置文件,只需要根据Bean 的唯一标识,去 beanDefinitionMap 中取到对应的BeanDefinition 对象即可。
那么,接下来我们看一下BeanDefinition 是如何定义的。

我们可以对照源码来看,BeanDefinition 的基础实现类AbstractBeanDefinition 类,这个类下面的所有属性都能够和声明配置文件中的内容一一对应上,来看代码:

public AbstractBeanDefinition implements BeanDefinition {
...

@Nullable
private volatile Object beanClass;

@Nullable
private String scope = SCOPE_DEFAULT;
private boolean lazyInit = false; 

@Nullable
private String[] dependsOn;

@Nullable
private String factoryBeanName;

@Nullable
private ConstructorArgumentValues constructorArgumentValues;

@Nullable
private MutablePropertyValues propertyValues;

@Nullable
private String initMethodName;

@Nullable
private String destroyMethodName;

...

我们可以看到,BeanDefinition 中定义的属性和声明式的配置内容从命名上看比较类似

  • beanClass 对应的配置是 class,这个属性为必填项,用于指向一个具体存在的 Java类,Spring 容器创建的 Bean 就是这个Java 类的实例。
  • lazyInit 对应的配置是lazy-init,用于指定 Bean 实例是否延时加载,我们能清楚地看到默认值是false。也就是说容器启动时就会创建Bean 对应的实例,如果设置为 true,则只有在首次获取Bean 的实例时才创建。
  • dependsOn 对应的配置是depends-on,用于定义Bean 实例化的依赖关系。在 Spring 容器对Bean 的实例初始化之前,有可能存在其他依赖,这需要需要保证其所以依赖的Bean 需要提前实例化,depends-on 可以用来定义Bean 的依赖顺序。在 BeanDefinition 中属性定义的数据类型是字符串数组,也就是说可以同时定义多个依赖对象。
  • factoryBeanName 对应的配置就是 name,这个属性用于定义 Bean 的唯一标识,且不能以大写字母开头。在 XML 配置中,使用 id 或name 属性来指定。如果没有设值, Spring 默认使用类名首字母小写作为唯一标识。
  • constructorArgumentValues 对应的配置是constructor-arg,它其实也是一个数组。如果Java 类中定义了有参构造方法,则可以使用此属性给有参构造方法注入参数值。如果没有默认的无参构造方法,那么,这个属性必填。

Spring Bean 声明式配置和BeanDefinition 属性定义对照表
在这里插入图片描述

35. Spring 如何解析配置文件?

Spring 容器启动之后,会调用BeanDefinitionReader 工具类的loadBeanDefinitions()方法,启动对配置文件的加载和解析。 BeanDefinitionReader 的主要作用是读取 Spring 配置文件中的内容,将其转换为BeanDefinition 对象。而 BeanDefinitionReader 又有非常多的实现类,每种类型的配置具体解析的过程又不一样,比如

  • XmlBeanDefinitionReader , 用于读取 XML 文件并解析为BeanDefinition 对象。
  • PropertiesBeanDefinitionReader,用于读取属性文件,将Resource,Property 等解析为BeanDefinition 对象。
  • GroovyBeanDefinitionReader,用于读取 Groovy 语言定义的 Bean,将它们解析为 BeanDefinition 对象。

36. Spring里面使用到了哪些设计模式?

Spring中用到的设计模式及应用场景如下:

36.1 单例模式

Spring中创建的Bean对象默认是单例的;

36.2 工厂模式

beanFactory,ApplicationContext创建对象使用都是工厂模式;

36.3 代理模式

Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术。

36.4 策略模式

例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略。
如加载资源文件时,使用了不同的方法,如:classPathResource ,urlResource, FileSystemResource 等,在加载处理时,实现机制不同,将if else 抽象成 策略模式;

36.5 观察者模式

Spring中会发布一系列的实践,刷新,完成事件,回调机制,需要通过回调实现,如 Spring中的ApplicationEvent
,ApplcicationListen,ApplicationEventPublisten等

36.6 模板模式

BeanFactory实现中的空方法,只要继承当前类,就可以实现拓展了;

36.7 适配器模式

SPring Mvc 中的一些 adapter, ThrowadiceAdapter, AfterReturnAdapter 等都是适配器

36.8 装饰者模式

源码中自带的Wrapper 或者 Decorator 都是

36.9 责任链模式

责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推

36.10 委派模式

委派模式又叫委托模式,是一种面向对象的设计模式,允许对象组合实现与继承相同的代码重用,它的作用就是负责任务的调用和分配。是一种特殊的静态代理,可以理解为全权代理,但是代理模式注重过程,而委派模式注重结果,委派模式属于行为型模式

37. Spring里面几种注入方式的区别,为啥推荐使用构造器注入

Spring为什么不推荐你使用@Autowired ?
当使用IDEA写代码的时候,@Autowired注解会报黄,我们把鼠标悬停在上面,可以看到这个如下图所示的警告信息:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
当我们把光标放在@Autowired 按住alt+回车键,idea就会帮我们修改成这样:
在这里插入图片描述
所以为啥Spring 会推荐使用构造器来注入呢,下面探讨一下Spring的三种注入方式: 属性注入,构造器注入, set注入

37.1 属性注入

这种注入方式就是在bean的变量上使用注解进行依赖注入。
本质上是通过反射的方式直接注入到field。

这是我平常开发中看的最多也是最熟悉的一种方式。用法也很简单,就直接使用@Autowired来完成依赖注入
在这里插入图片描述
这种注入方式通过Java的反射机制实现,所以private的成员也可以被注入具体的对象。

依赖属性注入有几个很明显的缺点

  • 不能像构造器那样注入不可变的对象
  • 依赖对外部不可见,外界可以看到构造器和setter,但无法看到私有字段,自然无法了解所需依赖
  • 会导致组件与IoC容器紧耦合
  • 依赖过多时是不太明显,容易忽略是否违背了单一职责

Java变量的初始化顺序为:

  1. 静态变量或静态语句块
  2. 实例变量或初始化语句块
  3. 构造方法
  4. @Autowired

Field注入虽然有很多缺点,但它的好处也不可忽略:那就是太方便了。使用构造器或者setter注入需要写更多业务无关的代码,十分麻烦,而字段注入大幅简化了它们。

@Resouce和@Autowired他们的基本功能都是通过注解实现依赖注入,只不过@Autowired是Spring定义的,而@Resource是Java自带的注解。

  • @Resource有2个属性name和type。它默认按照byName方式进行装配;如果没有匹配,按照byType进行装配。
  • @Autowired只根据type进行注入,不会去匹配name。如果涉及到type无法辨别注入对象时,那需要依赖@Qualifier注解一起来修饰。

37.2 构造器注入

在这里插入图片描述
这种注入方式很直接,通过对象构建的时候建立关系,所以这种方式对对象创建的顺序会有要求,当然Spring会为你搞定这样的先后顺序,除非你出现循环依赖,然后就会抛出异常。

37.3 Set方法注入

在这里插入图片描述
set方法注入也会用到@Autowired注解,但使用方式与Field注入有所不同,Field注入是用在成员变量上,而set方法注入的时候,是用在成员变量的Setter函数上。就是通过调用成员变量的set方法来注入想要使用的依赖对象。

可以看到@Autowired注解没有出现黄色警告。

37.4 三者区别

注入方式可靠性可维护性灵活性循环关系的检测性能影响
Field注入不可靠灵活不检测启动快
构造方法注入可靠不灵活检测启动慢
Set方法注入不可靠灵活不检测启动快

依赖构造方法注入在很多方面都是优于其他两种方式的,但是为了是代码更加简洁,大多数程序员依旧会选择使用Field注入。

Spring 推荐使用构造器注入主要原因有这些

  1. 避免对象在不完整状态下使用:通过构造器注入,对象在实例化时就拥有了所有必须的依赖,这样可以避免对象在不完整的状态下被使用。而 setter 注入则可能导致对象实例化后长时间处于不完整状态。
  2. 确保依赖不为 null:通过构造器注入,如果某个依赖不存在,会直接导致对象无法实例化,从而快速失败。而 setter 注入则可能会导致对象处于某些依赖为 null 的不一致状态。
  3. 线程安全:构造器只会在对象实例化时调用一次, 对象中所有属性都会有确定的值。而 setter 可能会在实例化后任意时间被多次调用,这可能会引起线程安全问题
  4. 表达清晰的语义:构造器的参数通常包含对象的必要依赖,这表达了清晰的语义,即无法在没有这些依赖的情况下构造该对象。而 setter 的语义则不那么清晰。
  5. 有利于测试:通过构造器注入,在测试时只需要验证一次构造方法的参数,就可以验证对象所有的必要依赖是否存在。而 setter 注入则需要逐个验证所有的 setter 方法。

构造器注入在对象完整性、一致性、线程安全性、语义清晰性以及测试方面都有一定的优势。构造器注入也有一定的缺点,比如构造器参数过多时会影响可读性,以及难以处理循环依赖的情况。所以,在具体使用时还是需要根据场景选择更加合适的注入方式。

如果使用构造器注入,如果存在循环依赖,在spring项目启动的时候,就会抛出:BeanCurrentlyInCreationException:Requested bean is currently in creation: Is there an unresolvable circular reference?从而提醒你避免循环依赖,如果是field注入的话,启动的时候不会报错,在使用那个bean的时候才会报错。

38. Feign、Ribbon、Hystrix超时时间 优先级对比

在微服务整个系统中,需要设置相应的服务调用超时时间来保护服务,常见的设置超时时间的几个方面:

  • 服务间的调用(feign)
  • 服务间的熔断(hystrix)
  • 服务间的负载均衡(ribbon)

38.1 feign

超时时间默认为1s

feign:
  client:
        config:
         # default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间,超时时间默认为1s
         default:
              #不设置connectTimeout会导致readTimeout设置不生效
              #服务之间建立连接所用的时间
             connectTimeout: 10000
              #建立连接后从服务端读取到数据用的时间
             readTimeout: 10000

38.2 ribbon

如果不配置的话,超时时间默认为1s,要注意这是个坑,一般要指定的配置和hystrix配合

#OpenFeign默认支持ribbon
ribbon:
  #对所有操作请求都进行重试,默认false
  OkToRetryOnAllOperations: false
  #服务之间建立连接所用的时间
  ConnectTimeout: 10000
  #建立连接后从服务端读取到数据用的时间
  ReadTimeout: 10000
  #对当前实例的重试次数,默认0
  MaxAutoRetries: 0  
  #对切换实例的重试次数,默认1   
  MaxAutoRetriesNextServer: 1 

ribbon和Feign默认超时时间都是1s,从RibbonClientConfiguration也能看到指明了ribbon的默认连接超时时间和默认读取超时时间都是1s

38.3 feign和ribbon的优先级关系

  • Feign 和 Ribbon 的超时时间只会有一个生效
  • 如果没有配置feign的超时时间,只是配置了ribbon的超时时间,则只有ribbon的配置超时时间会生效,feign默认的1s超时无效
  • 如果feign和ribbon的超时时间都配置了,则以feign配置的为准

38.4 hystrix

hystrix:
  command:
  	# default全局有效,service id指定应用有效
    default:
      execution:
        timeout:
          enabled: true
        isolation:
          thread:
          	#熔断超时时间,默认1000ms
            timeoutInMilliseconds: 200000
    # 针对单个服务及方法的设置方法
    AService#save(TestDto):
      execution:
        timeout:
          enabled: true
        isolation:
          thread:
          	#熔断超时时间,默认1000ms
            timeoutInMilliseconds: 300000 
public abstract class HystrixCommandProperties {
	private static final Integer default_executionTimeoutInMilliseconds = 1000;
}

可以看到HystrixCommandProperties类中,超时时间默认为1000ms

38.5 hystrix和ribbon的超时时间关系

  • 如果hystrix.command.default.execution.timeout.enabled为true,则hystrix的超时时间配置和ribbon的超时时间配置是同时生效的,一个就是ribbon的ReadTimeout,一个就是熔断器hystrix的timeoutInMilliseconds, 此时谁的值小谁生效
  • 如果hystrix.command.default.execution.timeout.enabled为false,则熔断器不进行超时熔断,而是根据ribbon的ReadTimeout抛出的异常而熔断,也就是取决于ribbon
  • ribbon的ConnectTimeout,配置的是请求服务的超时时间,除非服务找不到,或者网络原因,这个时间才会生效
    由于ribbon的重试机制,通常熔断的超时时间需要配置的比ReadTimeout长,ReadTimeout比ConnectTimeout长,否则还未重试,就熔断了
  • hystrix超时时间的配置:
    • 先计算Ribbon重试次数(包含首次) = 1 + ribbon.MaxAutoRetries + ribbon.MaxAutoRetriesNextServer + (ribbon.MaxAutoRetries * ribbon.MaxAutoRetriesNextServer)
    • hystrix超时时间 = Ribbon的重试次数(包含首次) * (ribbon.ReadTimeout + ribbon.ConnectTimeout)
    • 如果hystrix配置的时间比ribbon计算重试次数超时时间小,会出现警告级别日志The Hystrix timeout of 60000 ms for the command “foo” is set lower than the combination of the Ribbon read and connect timeout, 200000ms.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alan0517

感谢您的鼓励与支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值