在微服务架构下,由于数据库和应用服务的拆分,导致原本一个事务单元中的多个 DML 操作,变成了跨进程或者跨数据库的多个事务单元的多个 DML 操作,而传统的数据库事务无法解决这类的问题,所以就引出了分布式事务的概念。
分布式事务本质上要解决的就是跨网络节点的多个事务的数据一致性问题
,业内常见的解决方法有两种
基于CAP 定理我们可以知道,强一致性方案对于应用的性能和可用性会有影响,所以对于数据一致性要求不高的场景,就会采用最终一致性算法。
而Seata 就是其中一种,它是阿里开源的分布式事务解决方案,提供了高性能且简单易用的分布式事务服务。
Seata 中封装了四种分布式事务模式,分别是:
AT 模式
TCC 模式
Saga 模式
XA 模式
从这四种模型中不难看出,在不同的业务场景中,我们可以使用 Seata 的不同事务模型来解决不同业务场景中的分布式事务问题,因此我们可以认为 Seata 是一个一站式的分布式事务解决方案。
Dubbo 是一个RPC 框架,它为我们的应用提供了远程通信能力的封装,同时,Dubbo在RPC 通信的基础上,逐步在向一个生态在演进,它涵盖了服务注册、动态路由、容错、服务降级、负载均衡等能力,基本上在微服务架构下面临的问题,Dubbo 都可以解决。
而对于Dubbo 服务请求失败的场景,默认提供了重试的容错机制,也就是说,如果基于Dubbo 进行服务间通信出现异常,服务消费者会对服务提供者集群中其他的节点发起重试,确保这次请求成功,默认的额外重试次数是 2 次。
除此之外,Dubbo 还提供了更多的容错策略,我们可以根据不同的业务场景来进行选择。
要注意的是,默认基于重试策略的容错机制中,需要注意幂等性的处理,否则在事务型的操作中,容易出现多次数据变更的问题。
Dubbo 是以高性能RPC 框架,它提供了分布式架构下的服务之间通信方案,使得开发者可以不需要关心网络通信的细节。通过该框架可以使得远程服务调用方式和本地服务调用方式一样简单。
Dubbo 是一款高性能、轻量级的开源RPC 框架。由 10 层模式构成,整个分层依赖由上至下。
通过这张图我们也可以将Dubbo 理解为三层模式:
第一层的Business 业务逻辑层由我们自己来提供接口和实现还有一些配置信息。
第二层的RPC 调用的核心层负责封装和实现整个 RPC 的调用过程、负载均衡、集群容错、代理等核心功能。
Remoting 则是对网络传输协议和数据转换的封装。
根据Dubbo 官方文档的介绍,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 次请求。
第五种是最短响应时间权重随机
计算目标服务的请求的响应时间,根据响应时间最短的服务,配置更高的权重进行随机访问。
RPC 全称(Remote Procedure Call),它是一种针对跨进程或者跨网络节点的应用之间的远程过程调用协议
。而Http 协议是为 Web 浏览器与 Web 服务器之间的通信而设计的远程通信协议
,它定义了通信协议的报文规范,我们可以使用http 协议来实现跨网络节点的数据传输。基于这样的特点,在RPC 协议底层的数据传输,即可以直接使用 TCP 协议,也可以使用http 协议。
因此,Rpc 协议和 Http 协议完全不是同一个纬度的东西,这两者并没有什么可比性。
服务网格,也就是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 架构其实就是云原生时代的微服务架构
,对于大部分企业来说,仍然是处在第二代微服务架构下。
首先,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 端收到事件以后,就会把本地缓存的这个服务地址删除,这样后续就不会把请求发送到失败的节点上,完成服务下线感知。
首先,我认为,之所以需要序列化,核心目的是为了解决网络通信之间的对象传输问题
。
(如图)也就是说,如何把当前 JVM 进程里面的一个对象,跨网络传输到另外一个 JVM进程里面。
而序列化,就是把内存里面的对象转化为字节流,以便用来实现存储或者传输。
反序列化,就是根据从文件或者网络上获取到的对象的字节流,根据字节流里面保存的对象描述信息和状态。重新构建一个新的对象。
其次呢,序列化的前提是保证通信双方对于对象的可识别性,所以很多时候,我们会把对象先转化为通用的解析格式,
比如json、xml 等。然后再把他们转化为数据流进行网络传输,从而实现跨平台和跨语言的可识别性。
最后,我再补充一下序列化选择。市面上开源的序列化技术非常多,比如Json、Xml、Protobuf、Kyro、hessian 等等。那在实际应用里面,哪种序列化最合适,我认为有几个关键因素。
Eureka 是一个服务注册中心,在 Eureka 的设计里面,为了保证 Eureka 的高可用性,提供了集群的部署方式。
Eureka 的集群部署采用的是两两相互注册的方式来实现,也就是说每个 Eureka Server节点都需要发现集群中的其他节点并建立连接,然后通过心跳的方式来维持这个连接的状态。
Eureka Server 集群节点之间的数据同步方式非常简单粗暴,使用的是对等复制的方式来实现数据同步。也就是说,在Eureka Server 集群中,不存在所谓主从节点,任何一个节点都可以接收或者写入数据。一旦集群中的任意一个节点接收到了数据的变更,就直接同步到其他节点上。
这种无中心化节点的数据同步,需要考虑到一个数据同步死循环的问题,也就是需要区分Eureka Server 收到的数据
是属于客户端传递来的数据还是集群中其他节点发过来的同步数据。
Eureka 使用了一个时间戳的标记来实现类似于数据的版本号来解决这个问题。
另外,从 Eureka 的数据同步方案来看,Eureka 集群采用的是AP 模型,也就是只提供高可用保障,而不提供数据强一致性保障。
之所以采用AP,我认为注册中心它只是维护服务之间的通信地址,数据是否一致对于服务之间的通信影响并不大。
而注册中心对Eureka 的高可用性要求会比较高,不能出现因为 Eureka 的故障导致服务之间无法通信的问题。
一致性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 算法的好处是扩展性很强,在增加或者减少服务器的时候,数据迁移范围比较小。
首先,Nacos 是采用长轮训的方式向Nacos Server 端发起配置更新查询的功能。
所谓长轮训,(如图)就是客户端发起一次轮训请求到服务端,当服务端配置没有任何变更的时候,这个连接一直打开。
直到服务端有配置或者连接超时后返回。
Nacos Client 端需要获取服务端变更的配置,前提是要有一个比较,也就是拿客户端本地的配置信息和服务端的配置信息进行比较。
一旦发现和服务端的配置有差异,就表示服务端配置有更新,于是把更新的配置拉到本地。
在这个过程中,有可能因为客户端配置比较多,导致比较的时间较长,使得配置同步较慢的问题。于是Nacos 针对这个场景,做了两个方面的优化。
这两个优化,核心目的是减少网络通信数据包的大小,把一次大的数据包通信拆分成了多次小的数据包通信。
虽然会增加网络通信次数,但是对整体的性能有较大的提升。
最后,再采用长连接这种方式,既减少了 pull 轮询次数,又利用了长连接的优势,很好的实现了配置的动态更新同步功能。
服务降级是一种提升系统稳定性和可用性的策略。
简单来说,就是当服务器压力增加的情况下,根据实际业务的需求和流量的情况,不对外提供部分服务的功能。从而释放服务器的资源去保证核心业务的正常运行。
服务降级有两种方式,一种是主动降级,一种是基于特定情况的被动降级。
主动降级
:这种方式在大促的时候使用比较多,比如在电商平台中,核心服务是下单、支付。被动降级
:它有两种主要的触发场景
熔断触发降级
, 在一个请求链路中,为了避免某个服务节点出现故障导致请求堆积,造成资源消耗是的服务崩溃的问题,一般会采取熔断策略。当触发了熔断机制以后,如果后续再向故障节点发起请求的时候,这个请求不会发送到故障节点上,而是直接置为失败,这样就避免了请求堆积的问题。而直接置为失败之后需要给到用户一个反馈,而这个反馈就是降级策略,就相当于给用户一个处理结果。比如返回一个“系统繁忙”之类的信息。限流触发降级
,因为系统资源是有限的,为了避免高并发流量把系统压垮导致不可用问题,所以我们会采取限流的策略去保护系统。通过限流去限制一部分用户的访问,然后保证整个系统的稳定运行因此,降级带来的结果是使得用户的体验下降,但是却保证了系统的稳定性和可用性。
CAP 模型,在一个分布式系统里面,不可能同时满足三个点
一致性(Consistency)
,访问分布式系统中的每一个节点都能获得最新的数据。可用性(Availability)
,每次请求都能获得一个有效的访问,但不保证数据是最新的。分区容错性(Partition tolerance)
,分区相当于对通信耗时的要求,系统如果不能在时限范围内达成数据一致,就意味着发生了分区的情况。在CAP 模型中只能满足 CP 或者AP,之所以不能满足CA,因为网络通信的不确定性可能会导致分区容错,
也就是分区容错性必然是存在的,因此我们只能在一致性和可用性之间做选择。
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 键值数据库、列存储数据库、文档数据库和图形数据库。
下面我把四种分类的 NoSQL 数据库整理成了一个表格,并总结了它们的优缺点
目前可用于文件存储的网络服务选择也有不少,好比阿里云 OSS、七牛云、腾讯云等等,可是收费都有点小贵。为了帮公司节约成本,以前一直是使用 FastDFS 做为文件服务器,准确的说是图片服务器。直到我发现了 MinIO,我决定放弃 FastDFS。
MinIO 是专门为海量数据存储、人工智能、大数据分析而设计的对象存储系统。
据官方介绍,单个对象最大可达 5TB。非常适合储海量图片、视频、日志文件、备份数据和容器/虚拟机镜像等。MinIO 主要采用 Golang 语言实现,整个系统都运行在操作系统的用户态空间,客户端与存储服务器之间采用 HTTP/HTTPs通信协议。
相比于其他方案来说,推荐选择 MinIO 的 7 大理由:
而在Spring Boot 中,我们不需要再去做这些繁琐的配置,Spring Boot 已经自动帮我们完成了,这就是约定由于配置思想的体现。
Spring Boot 约定由于配置的体现有很多,比如
总的来说,约定优于配置是一个比较常见的软件设计思想,它的核心本质都是为了更高效以及更便捷的实现软件系统的开发和维护。
自动装配,简单来说就是自动把第三方组件的Bean 装载到Spring IOC 器里面,不需要开发人员再去写Bean 的装配配置。
在Spring Boot 应用里面,只需要在启动类加上@SpringBootApplication 注解就可以实现自动装配。
@SpringBootApplication 是一个复合注解
,真正实现自动装配的注解是 @EnableAutoConfiguration
。classpath:/META-INF/spring.factories 文件中
。这样SpringBoot 就可以知道第三方jar 包里面的配置类的位置,这个步骤主要是用到了Spring 里面的SpringFactoriesLoader
来完成的。 ImportSelector
接口,实现对这些配置类的动态加载。在我看来,SpringBoot 是约定优于配置这一理念下的产物,所以在很多的地方,都会看到这类的思想。它的出现,让开发人员更加聚焦在了业务代码的编写上,而不需要去关心和业务无关的配置。
其实,自动装配的思想,在SpringFramework3.x 版本里面的@Enable 注解,就有了实现的雏形。@Enable 注解是模块驱动的意思,我们只需要增加某个@Enable 注解,就自动打开某个功能,而不需要针对这个功能去做 Bean 的配置,@Enable 底层也是帮我们去自动完成这个模块相关Bean 的注入。
以上,就是我对 Spring Boot 自动装配机制的理解。
Starter 是Spring Boot 的四大核心功能特性之一,除此之外,Spring Boot 还有自动装配、Actuator 监控等特性。
Spring Boot 里面的这些特性,都是为了让开发者在开发基于 Spring 生态下的企业级应用时,只需要关心业务逻辑,
减少对配置和外部环境的依赖。其中,Starter 是启动依赖,它的主要作用有几个。
在我看来,Starter 组件几乎完美的体现了 Spring Boot 里面约定优于配置的理念。另外,Spring Boot 官方提供了很多的 Starter 组件,比如 Redis、JPA、MongoDB等等。
但是官方并不一定维护了所有中间件的Starter,所以对于不存在的Starter,第三方组件一般会自己去维护一个。
(如图)官方的 starter 和第三方的starter 组件,最大的区别在于命名上。
跨域指的是浏览器在执行网页中的 JavaScript
代码时,由于浏览器同源策略
的限制(如图)。
只能访问同源(协议
、域名
、端口号
均相同)的资源,而不能访问其他源(协议、域名、端口号任意一个不同)的资源。
比如像这样一种情况就体现了跨域的问题。
而解决跨域问题的方法,就是在不破坏同源策略的情况下,能够安全地实现数据共享和交互。
常见的解决跨域问题的方法有两种,一种是 jsonp
,另一种是CORS
。
OPTIONS
请求, 根据服务器返回的 Access-Control-Allow-Origin
头信息,决定是否允许跨域访问。所以,我们只需要在服务器端配置Access-Control-Allow-Origin
属性,并配置允许哪些域名支持跨域请求即可。
@CrossOrigin(origins = " http://localhost:8080 ")
注解,指定允许哪些origins 允许跨域addCorsMappings
方法来配置允许跨域的请求源@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override publicvoidaddCorsMappings(CorsRegistryregistry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("*");
}
}
Spring 生命周期全过程大致分为五个阶段:
init-method
-容器在初始化bean 时调用的方法、destory-method
,容器在销毁bean 时调用的方法。这些类或者配置其实是 Spring 提供给开发者,用来实现 Bean 加载过程中的扩展机制,在很多和Spring 集成的中间件中比较常见,比如Dubbo。
这个阶段主要是通过反射来创建Bean 的实例对象,并且扫描和解析Bean 声明的一些属性。
如果被实例化的 Bean 存在依赖其他Bean 对象的情况,则需要对这些依赖bean 进行对象注入。
比如常见的@Autowired
、setter 注入等依赖注入的配置形式。
同时,在这个阶段会触发一些扩展的调用,比如常见的扩展类:BeanPostProcessors(用来实现bean 初始化前后的扩展回调)、
InitializingBean(这个类有一个 afterPropertiesSet(),这个在工作中也比较常见)、 BeanFactoryAware 等等。
容器缓存阶段主要是把 bean 保存到容器以及 Spring 的缓存中,到了这个阶段,Bean就可以被开发者使用了。
这个阶段涉及到的操作,常见的有,init-method
这个属性配置的方法, 会在这个阶段调用。
以及像BeanPostProcessors 方法中的后置处理器方法如: postProcessAfterInitialization,也会在这个阶段触发。
当Spring 应用上下文关闭时,该上下文中的所有bean 都会被销毁。
如果存在Bean 实现了DisposableBean 接口,或者配置了destory-method
属性,会在这个阶段被调用。
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等等)
执行流程:
用户发送请求至前端控制器DispatcherServlet。
DispatcherServlet收到请求调用HandlerMapping处理器映射器。
处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
DispatcherServlet调用HandlerAdapter处理器适配器。
HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
Controller执行完成返回ModelAndView。
HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
ViewReslover解析后返回具体View。
DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
DispatcherServlet响应用户。
简单理解:
在 Spring MVC 中有 9 大重要的组件。下面详细说明一下这些组件的作用和初始化方法:
在Spring MVC 的九大组件中,涉及到请求处理响应的核心组件分别是:
1、HandlerMapping 回到调用HandlerAdapter
2、HandlerAdapter 会返回ModelAndView
3、ModelAndView 根据用户传入参数得到 ViewResolvers
4、ViewResolvers 会将用户传入的参数封装为View,交给引擎进行渲染。
注意:有大家最熟悉的两个类:ModelAndView 和View 类并不属于Spring MVC 九大组件之列。
循环依赖有三种形态:
这里我可以做个总结,我们来看这张图Spring 一级缓存中存放所有的成熟 Bean,二级缓存中存放所有的早期 Bean,先取一级缓存,再去二级缓存。
三级缓存是用来存储代理Bean,当调用getBean()方法时,发现目标Bean 需要通过代理工厂来创建,此时会将创建好的实例保存到三级缓存,最终也会将赋值好的Bean同步到一级缓存中。
理论上来说,常规的生命周期只有两种:
singleton, 也就是单例,意味着在整个Spring 容器中只会存在一个 Bean 实例。
Spring 中的Bean 默认都是单例的。它的作用域范围是 ApplicationContext 容器
prototype,翻译成原型,意味着每次从IOC 容器去获取指定 Bean 的时候,都会返回一个新的实例对象。
也就是说在每次请求获取 Bean 的时都会重新创建实例,因此每次获取到的实例对象都是不同的。它的作用域范围是调用 getBean 方法直至获取对象。
但是在基于Spring 框架下的Web 应用里面,增加了一个会话纬度来控制 Bean 的生命周期,主要有三个选择
把Bean 注入到IOC 容器里面的方式有 7 种方式:
BeanFacotry
是Spring中比较原始的Factory。
如XMLBeanFactory就是一种典型的BeanFactory。
原始的BeanFactory无法支持Spring的许多插件,如AOP功能、Web应用等。
ApplicationContext接口,它由BeanFactory接口派生而来,ApplicationContext包含BeanFactory的所有功能
BeanFactory,以Factory结尾,表示它是一个工厂类(接口), 它负责生产和管理bean的一个工厂。
在Spring中,BeanFactory是IOC容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。
BeanFactory只是个接口,并不是IOC容器的具体实现
,但是Spring容器给出了很多种实现,如 DefaultListableBeanFactory
、XmlBeanFactory
、ApplicationContext
等,其中XmlBeanFactory就是常用的一个,该实现将以XML方式描述组成应用的对象及对象间的依赖关系。XmlBeanFactory类将持有此XML配置元数据,并用它来构建一个完全可配置的系统或应用。
ApplicationContext以一种更向面向框架的方式工作以及对上下文进行分层和实现继承,ApplicationContext包还提供了以下的功能:
• MessageSource, 提供国际化的消息访问
• 资源访问,如URL和文件
• 事件传播
• 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层
;
FactoryBean是个Bean
,也是接口,在Spring中所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。
但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean
,它的实现与设计模式中的工厂模式和修饰器模式类似 ;
BeanFactory 的理解了有两个:
FactoryBean 是一个工厂Bean,它是一个接口,主要的功能是动态生成某一个类型的 Bean 的实例,也就是说,我们可以自定义一个 Bean 并且加载到 IOC 容器里面。它里面有一个重要的方法叫 getObject(),这个方法里面就是用来实现动态构建 Bean的过程。
Spring Cloud 里面的OpenFeign 组件,客户端的代理类,就是使用了 FactoryBean来实现的。
@Resource 和@Autowired 这两个注解的作用都是在Spring 生态里面去实现Bean的依赖注入。
required
属性默认值是true,表示强制要求 bean 实例的注入,在应用启动的时候,如果IOC 容器里面不存在对应类型的 Bean,就会报错; 当然,如果不希望自动注入,可以把这个属性设置成false。AutowiredAnnotationBeanPostProcessor
后置处理器处理的 当容器扫描到@AutowiedCommonAnnotationBeanPostProcessor
后置处理器处理的最后,我再总结一下:
AutowiredAnnotationBeanPostProcessor
后置处理器处理的 当容器扫描到@AutowiedCommonAnnotationBeanPostProcessor
后置处理器处理的首先,在同一个 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 就不会再注册了。
首先,Spring IOC,全称控制反转(Inversion of Control)。在传统的Java 程序开发中,我们只能通过 new 关键字来创建对象,这种导致程序中对象的依赖关系比较复杂,耦合度较高。而IOC 的主要作用是实现了对象的管理,也就是我们把设计好的对象交给了 IOC 容器控制,然后在需要用到目标对象的时候,直接从容器中去获取。有了IOC 容器来管理 Bean 以后,相当于把对象的创建和查找依赖对象的控制权交给了容器,这种设计理念使得对象与对象之间是一种松耦合状态,极大提升了程序的灵活性以及功能的复用性。
然后,DI 表示依赖注入,也就是对于 IOC 容器中管理的Bean,如果 Bean 之间存在依赖关系,那么IOC 容器需要自动实现依赖对象的实例注入,通常有三种方法来描述 Bean 之间的依赖关系。
@Conditional 注解的作用是为Bean 的装载提供了一个条件判断。只有满足条件的情况下,Spring 才会把当前 Bean 装载到IOC 容器中。
这个条件的实现逻辑,我们可以实现 Condition 接口并重写matches 方法自己去实现。所以@Conditional 注解增加了Bean 装载的灵活性。
在Spring Boot 里面,对@Conditional 注解做了更进一步的扩展,比如增加了 @ConditionalOnClass、@ConditionalOnBean
等注解,使得我们在使用的过程中不再需要去写条件的逻辑。
我认为有几个方面的考虑:
所以,为了避免这类问题,有些公司会推荐使用编程式事务,这样可以更加灵活地控制事务的范围,减少事务的锁定时间,提高系统的性能。
过滤器是在 Servlet 容器接收到请求之后,但在 Servlet被调用之前运行的
;而拦截器则是在 Servlet 被调用之后,但在响应被发送到客户端之前运行的
。Spring 解析这些声明好的配置内容,将这些配置内容都转化为 BeanDefinition 对象, BeanDefinition 中几乎保存了配置文件中声明的所有内容,再将 BeanDefinition 存到一个叫做beanDefinitionMap 中。以 beanName 作为Key,以 BeanDefinition 对象作为Value。之后 Spring 容器,根据beanName 找到对应的BeanDefinition,再去选择具体的创建策略。而Spring 具体的创建策略如图所示
关于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 中定义的属性和声明式的配置内容从命名上看比较类似
Spring Bean 声明式配置和BeanDefinition 属性定义对照表
Spring 容器启动之后,会调用BeanDefinitionReader 工具类的loadBeanDefinitions()方法,启动对配置文件的加载和解析。 BeanDefinitionReader 的主要作用是读取 Spring 配置文件中的内容,将其转换为BeanDefinition 对象。而 BeanDefinitionReader 又有非常多的实现类,每种类型的配置具体解析的过程又不一样,比如
Spring中用到的设计模式及应用场景如下:
Spring中创建的Bean对象默认是单例的;
beanFactory,ApplicationContext创建对象使用都是工厂模式;
Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术。
例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略。
如加载资源文件时,使用了不同的方法,如:classPathResource ,urlResource, FileSystemResource 等,在加载处理时,实现机制不同,将if else 抽象成 策略模式;
Spring中会发布一系列的实践,刷新,完成事件,回调机制,需要通过回调实现,如 Spring中的ApplicationEvent
,ApplcicationListen,ApplicationEventPublisten等
BeanFactory实现中的空方法,只要继承当前类,就可以实现拓展了;
SPring Mvc 中的一些 adapter, ThrowadiceAdapter, AfterReturnAdapter 等都是适配器
源码中自带的Wrapper 或者 Decorator 都是
责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推
委派模式又叫委托模式,是一种面向对象的设计模式,允许对象组合实现与继承相同的代码重用,它的作用就是负责任务的调用和分配。是一种特殊的静态代理,可以理解为全权代理,但是代理模式注重过程,而委派模式注重结果,委派模式属于行为型模式