0%

Spring Cloud 服务注册与发现源码笔记 (Nacos/Consul/Eureka)

Eureka

关键类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 服务注册
1.EurekaClientAutoConfiguration
注册了众多的 bean
一部分用于和 Eureka Server 交互
一部分和 Commons 项目对接
注册了(EurekaClient/EurekaAutoServiceRegistration/ApplicationInfoManager/EurekaRegistration)

2.EurekaClient
与 Eureka Server 端交互
负责向 Eureka Server 端注册/注销服务实例
在构造方法和 shutdown 方法中根据配置处理自动注册和自动注销.

3.InstanceInfoReplicator
负责定义一个注册或更新服务实例的任务
负责管理任务执行器

4.RestTemplateEurekaHttpClient
负责根据服务实例信息构造注册/注销的 Http 请求
使用 RestTemplate 发送请求

5.EurekaAutoServiceRegistration
负责管理服务实例的自动注册/注销, 与容器生命周期挂钩

6.ApplicationInfoManager
负责管理服务实例状态变更事件(即管理监听者并在适当时机触发他们)

7.ApplicationInfoManager.StatusChangeListener
状态改变事件监听者类

8.EurekaHealthCheckHandler
服务实例状态健康检查类, 注册/注销服务实例前会调用此类进行检查其状态


# 服务发现
1.BlockingLoadBalancer (Commons 项目上)
负责调用实际的负载均衡器去选择一个服务实例
负责调度负载均衡整个过程, 触发相应的生命周期.

2.RoundRobinLoadBalancer (Commons 项目上)
实际的负载均衡策略算法类
负责连接 ServiceInstanceListSupplier 从其中获取服务实例列表

3.ServiceInstanceListSupplier
定义了获取服务实例实例列表的接口(任意方式)

4.DiscoveryClient
定义了从服务端获取服务实例列表的接口(更明确了)

4.DiscoveryClientServiceInstanceListSupplier
ServiceInstanceListSupplier 的实现类
连接 ReactiveDiscoveryClient 类, 将服务端获取道德服务实例列表返回出去

5.EurekaDiscoveryClient
实现了 DiscoveryClient 接口
负责调用 EurekaClient 从 Eureka Server 端获取服务实例列表.

6.EurekaClient
与 Eureka Server 端交互
负责向 Eureka Server 端获取服务实例列表
在构造方法和 shutdown 方法中根据配置处理自动注册和自动注销.

7.EurekaServiceInstance
服务实例信息对象
实现 ServiceInstance, 与 Commons 对接

# 总结
实现了 Commons 的 DiscoveryClient 接口(即 EurekaDiscoveryClient), 于是服务发现实现了;
实现了 Commons 的 ServiceRegisty 接口(即 EurekaServiceRegistry), 于是服务注册也实现了.
再总结: Commons 大发好.

服务注册流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# EurekaClient 构造方法触发(前提 shouldRegisterWithEureka 为 true)
1.EurekaClientAutoConfiguration 让容器里注册了一个 EurekaClient
2.EurekaClient 这个类在构造方法中的 initScheduledTasks() 生成了一个 InstanceInfoReplicator 对象
3.然后调用其 instanceInfoReplicator.start(), 逻辑是添加一个定时任务(仅执行一次), 定时任务执行 run()
4.run() 里面会执行 discoveryClient.register() 也就是注册实例信息到 Eureka 上去.
5.register() 里面会调用 RestTemplateEurekaHttpClient#register()
6.这个方法是构造一个 HTTP 请求, 地址为 serviceUrl + "apps/" + info.getAppName(), Method 为 POST, 即 Eureka server 的ip/apps/服务实例名称(如spring.application.name), 当然请求 body 还会带上实例信息 info 对象.

# 根据容器生命周期触发
1.EurekaClientAutoConfiguration 让容器里注册了一个 EurekaAutoServiceRegistration.
2.这个类即是 SmartLifecycle(与容器生命周期绑定), 也是 SmartApplicationListener(监听容器加载/关闭事件)
3.因此其对应的 start()/stop() 和 onApplicationEvent() 都实现了对应的逻辑.
4.如 start() 的逻辑为调用 EurekaServiceRegistry#register()
5.register() 先修改本地服务实例的装填, 再通过 com.netflix.discovery.DiscoveryClient#registerHealthCheck() 来往任务管理器中提交一个任务, 后台执行, 任务 InstanceInfoReplicator#onDemandUpdate()
6.此任务就是最终也会调用前面提到的 (4) 中的 run(). 然后就注册上去了.

# PS
(6) 中的任务, 与状态监听是一致的

总结: 容器注册 EurekaClient, 调用构造方法完成大量初始化工作后, 另起一个线程调用 restTemplate 发送 HTTP 请求将当前服务实例信息发送给 Eureka server. 完成服务注册工作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
graph TB

A(EurekaClientAutoConfiguration)
A1(EurekaClient)
A2(InstanceInfoReplicator)
A3(EurekaClient#register)
A4(RestTemplateEurekaHttpClient)
A5(RestTemplate)
A6(Eureka Server)
A7(EurekaHttpResponse)

A--让容器里注册一个-->A1
A1--在构造方法中生成一个-->A2
A2--在 run 方法中调用-->A3
A3--又把服务实例信息交给-->A4
A4--根据服务实例信息构造HTTP请求-->A5
A5--将服务实例信息发送给-->A6
A6--返回一个-->A7

image-20210129171345329

服务注销流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 容器关闭(配置了容器 shouldUnregisterOnShutdown=true)
1.在 DiscoveryClient#shutdown() 中根据 shouldUnregisterOnShutdown 判断是否需要注销
2.然后在 unregister() 中调用 RestTemplateEurekaHttpClient#cancel()
3.cancel() 中使用 restTemplate 构造 Method 为 DELETE, URL 为 serviceUrl + "apps/" + appName + '/' + id 的请求并发送给 Eureka server 告知其注销 appName 下对应的服务实例(根据 id).

# 接收到容器关闭事件
1.EurekaClientAutoConfiguration 让容器里注册了一个 EurekaAutoServiceRegistration.
2.这个类即是 SmartLifecycle(与容器生命周期绑定), 也是 SmartApplicationListener(监听容器加载/关闭事件)
3.因此其对应的 start()/stop() 和 onApplicationEvent() 都实现了对应的逻辑.
4.如 stop() 的逻辑为调用 EurekaServiceRegistry#deregister()
5.deregister() 的作用是执行 ApplicationInfoManager#setInstanceStatus() 将状态改为 DOWN
6.因为 ApplicationInfoManager 这个类专门管理状态变化事件, 因此还会将事件发布出去.
7.而在 initScheduledTasks 中就添加了这样的一个监听者, 起作用为调用 InstanceInfoReplicator#onDemandUpdate()
8.接着会调用 InstanceInfoReplicator#run(), 第一行代码 refreshInstanceInfo() 会确保状态为最新的(那也还是 DOWN)
9.但是其最终并不是调用 cancel 注销, 而 register 注册, 不过其中的状态是 DOWN, 因此会有 server 那边判断; 所以容器关闭事件并不会触发 cancel, 但效果应是一样的.

总结: 要么是 shutdown() 中 unregister 调用了 cancel(), 发出了 Method 为 DELETE 的请求来注销; 要么就是 EurekaClientAutoConfiguration 中与容器生命周期做关联, 全程使用 register 接口来更新状态.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
graph TD

A(容器关闭#close)
A1(DiscoveryClient#shutdown)
A2(DiscoveryClient#unregister)
A3(RestTemplateEurekaHttpClient#cancel)
A4(RestTemplate)
A5(Eureka Server)

A--触发-->A1
A1--调用-->A2
A2--调用-->A3
A3--构造HTTP请求交给-->A4
A4--发送注销申请给-->A5

image-20210129171237603

服务发现流程

1
2
3
4
5
1.Spring Cloud Commons 中注册了一个 ServiceInstanceListSupplier, 具体为(DiscoveryClientServiceInstanceListSupplier)
2.这个类的作用是借助 ReactiveDiscoveryClient 的 getInstances(String serviceId) 方法向 LoadBalancer 提供从具体的 server(如Eureka) 获取服务实例对象列表, 这样只要实现 ReactiveDiscoveryClient 并放入容器就可以和 Spring cloud LoadBalancer 对接了.
3.在 EurekaDiscoveryClientConfiguration 中让容器注册了一个 DiscoveryClient(具体为 EurekaDiscoveryClient).
4.因为 Spring Cloud Commons 做了大量的预备对接工作, 所以对接其实就结束了.
5.那再简单说下 EurekaDiscoveryClient 的实现, 即注入一个 EurekaClient eurekaClient, 然后调用 DiscoveryClient#getInstancesByVipAddress() 就获取到了 ServiceInstance 列表.

总结: Commons 中准备好了对接的方式: 实现 DiscoveryClient 接口, 接着我们的确实现了 DiscoveryClient 接口, 即 EurekaDiscoveryClient, 而这这个类则会调用 EurekaClient 的 getInstancesByVipAddress 从 Eureka Server 端获取注册了的服务实例信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
graph TD

A1(BlockingLoadBalancerClient#位于Commons项目)
A2(RoundRobinLoadBalancer#位于Commons项目)
A4(LoadBalancerClientConfiguration#位于Commons项目)
A5(DiscoveryClientServiceInstanceListSupplier)
A6(EurekaDiscoveryClient#getInstances)
A7(EurekaClient#getInstancesByVipAddress)
A9(Eureka Server)
A8(ServiceInstance 集合)

A1--choose 方法调用-->A2
A2--choose 方法调用-->A5
A4--注入一个-->A5
A5--调用-->A6
A6--调用-->A7
A7--从-->A9
A9--获取-->A8
A10(EurekaDiscoveryClientConfiguration)--注入一个-->A6

image-20210129233854498

Consul

关键类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 服务注册
1.ConsulAutoServiceRegistrationAutoConfiguration
负责添加自动注册/注销相关的 bean
注册了 ConsulAutoServiceRegistration/ConsulAutoServiceRegistrationListener/ConsulAutoRegistration

2.ConsulServiceRegistryAutoConfiguration
负责注册服务注册相关的 bean
注册了 ConsulServiceRegistry

3.ConsulDiscoveryClientConfiguration
负责注册服务发现相关的 bean
注册了 ConsulDiscoveryClient

4.ConsulServiceRegistry
负责与 ConsulClient 对接, 再提供注册/注销功能

5.ConsulClient
与 Consul Server 端交互
负责向 Consul Server 端注册/注销服务实例

6.AgentConsulClient
负责根据服务实例信息构造注册/注销的 Http 请求

7.ConsulAutoServiceRegistration/ConsulAutoServiceRegistrationListener
负责管理服务实例的自动注册/注销, 与容器生命周期挂钩



# 服务发现
1.BlockingLoadBalancer (Commons 项目上)
负责调用实际的负载均衡器去选择一个服务实例
负责调度负载均衡整个过程, 触发相应的生命周期.

2.RoundRobinLoadBalancer (Commons 项目上)
实际的负载均衡策略算法类
负责连接 ServiceInstanceListSupplier 从其中获取服务实例列表

3.ServiceInstanceListSupplier
定义了获取服务实例实例列表的接口(任意方式)

4.DiscoveryClient
定义了从服务端获取服务实例列表的接口(更明确了)

4.DiscoveryClientServiceInstanceListSupplier
ServiceInstanceListSupplier 的实现类
连接 ReactiveDiscoveryClient 类, 将服务端获取道德服务实例列表返回出去

5.ConsulDiscoveryClient
实现了 DiscoveryClient 接口
负责调用 ConsulClient 从 Consul Server 端获取服务实例列表.

6.ConsulClient
与 Consul Server 端交互
负责向 Consul Server 端获取服务实例列表

7.ConsulServiceInstance
服务实例信息对象
实现 ServiceInstance, 与 Commons 对接

# 总结
实现了 Commons 的 DiscoveryClient 接口(即 ConsulDiscoveryClient), 于是服务发现实现了;
实现了 Commons 的 ServiceRegisty 接口(即 ConsulServiceRegistry), 于是服务注册也实现了.
再总结: Commons 大发好.

服务注册流程

1
2
3
4
1.ConsulAutoServiceRegistration 调用 org.springframework.cloud.consul.serviceregistry.ConsulServiceRegistry#register() 完成注册
2.接着 register() 调用 ConsulClient#agentServiceRegister()
3.然后会调用 AgentConsulClient#agentServiceRegister()
4.生成并发送请求, 请求地址为 /v1/agent/service/register

总结: 其实和 Eureka 差不多, 只是我跳过了一点点细节. 基本是就是 与 ServiceRegistry 交互了, 非常的配合 Commons 项目, 就像一个人写的一样…

服务发现流程

1
2
3
1.首先是 DiscoveryClientServiceInstanceListSupplier 会调用 ConsulDiscoveryClient#getInstances()
2.接着 getInstances() 会调用 ConsulClient#getHealthServices()
3.然后就是发送 HTTP 请求了, 请求地址为 /v1/health/service/, 返回的对象封装处理下得到 ServiceInstance 的实现类 ConsulServiceInstance.

总结: 这次真的是和 Eureka 类似, 从 Commons 到 ConsulDiscoveryClient, 流程是一样的. 而其实 ConsulDiscoveryClient 的逻辑也和 EurekaDiscoverClient 类似… 只能说其实服务注册发现这个框架, 我们关注的功能其实并不是难点. 难点是 server 端的管理.

Nacos

关键类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 服务注册
1.NacosServiceRegistryAutoConfiguration
负责添加自动注册/注销相关的 bean
注册了 NacosAutoServiceRegistration/NacosServiceRegistry/NacosRegistration

2.NacosDiscoveryClientConfiguration
负责注册服务发现相关的 bean
注册了 NacosDiscoveryClient

3.NacosServiceRegistry
负责与 NamingService 对接, 再提供注册/注销功能

4.NamingService
与 Nacos Server 端交互
负责向 Nacos Server 端注册/注销服务实例
调用 NamingProxy

5.NamingProxy
负责根据服务实例信息构造注册/注销的 Http 请求

6.NacosAutoServiceRegistration
负责管理服务实例的自动注册/注销, 与容器生命周期挂钩

7.NacosRegistration
本地实例对象, 相比服务实例数据更多


# 服务发现
1.BlockingLoadBalancer (Commons 项目上)
负责调用实际的负载均衡器去选择一个服务实例
负责调度负载均衡整个过程, 触发相应的生命周期.

2.RoundRobinLoadBalancer (Commons 项目上)
实际的负载均衡策略算法类
负责连接 ServiceInstanceListSupplier 从其中获取服务实例列表

3.ServiceInstanceListSupplier
定义了获取服务实例实例列表的接口(任意方式)

4.DiscoveryClient
定义了从服务端获取服务实例列表的接口(更明确了)

4.DiscoveryClientServiceInstanceListSupplier
ServiceInstanceListSupplier 的实现类
连接 ReactiveDiscoveryClient 类, 将服务端获取道德服务实例列表返回出去

5.NacosDiscoveryClient
实现了 DiscoveryClient 接口
负责调用 NamingService 从 Nacos Server 端获取服务实例列表.

6.NamingService
与 Nacos Server 端交互
负责向 Nacos Server 端获取服务实例列表

7.NacosServiceInstance
服务实例信息对象
实现 ServiceInstance, 与 Commons 对接

# 总结
实现了 Commons 的 DiscoveryClient 接口(即 NacosDiscoveryClient), 于是服务发现实现了;
实现了 Commons 的 ServiceRegisty 接口(即 ConsulServiceRegistry), 于是服务注册也实现了.
再总结: Commons 大发好.

服务注册流程

1
2
3
4
1.NacosAutoServiceRegistration 调用 NacosServiceRegistry#register 完成注册
2.接着 register() 调用 NacosNamingService#registerInstance()
3.然后会调用 NamingProxy#registerService()
4.生成并发送请求, 请求地址为 /nacos/v1/ns/instance

总结: 这和 Consul 差不多; 还是继承 AbstractAutoServiceRegistration 来与 ServiceRegistry 交互了, 也是非常的配合 Commons 项目啊!!

服务发现流程

1
2
3
1.首先是 DiscoveryClientServiceInstanceListSupplier 会调用 NacosDiscoveryClient#getInstances()
2.接着 getInstances() 会调用 NacosServiceDiscovery#getInstances()
3.然后就是发送 HTTP 请求了, 请求地址为 /nacos/v1/ns/instance/list, 返回的对象封装处理下得到 ServiceInstance 的实现类 NacosServiceInstance.

总结: 依然是和 Eureka/Consul 类似, 从 Commons 到 NacosDiscoveryClient, 流程是一样的. 而其实 NacosDiscoveryClient 的逻辑也和 ConsulDiscoveryClient/EurekaDiscoverClient 类似, 最终总是发起 HTTP 查询 server 端, 所以 server 端的代码才比较有趣啊.

Nacos Config

Nacos Config Client 加载 nacos server 配置原理

关键类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1.NacosConfigBootstrapConfiguration
注册了一个 NacosPropertySourceLocator

2.NacosPropertySourceLocator
用于从 nacos server 上加载 dataId 对应的配置文件到 environment 的 PropertySource 集合中.

3.ConfigService
用于与 nacos server 通信, 获取配置文件, 乃至订阅更新

4.NacosPropertySourceBuilder
借助 ConfigService 与 NacosDataParserHandler 从 nacos 获取 NacosPropertySource

5.NacosDataParserHandler
用于序列化(转码)服务端的配置文件数据为一个 PropertySource(即 NacosPropertySource)


# 自动刷新
1.NacosConfigAutoConfiguration
2.NacosContextRefresher

总结: ConfigService 与 NacosDataParserHandler 是两个干事的, 其他都是渣渣!!!

从 server 获取配置流程

1
2
3
4
5
6
7
8
1.在 NacosConfigBootstrapConfiguration 中注册了一个 PropertySourceLocator(即 NacosPropertySourceLocator), 可用于为 environment 添加 PropertySource
2.然后实现 locate 方法, 即 NacosPropertySourceLocator#locate()
3.在 locate() 的最后面, 调用了 loadApplicationConfiguration()
4.这个方法通过多次调用 loadNacosDataIfPresent() 加载 dataId 和不同文件后缀以及profile 组和得到不同的 dataId.
5.loadNacosDataIfPresent() 调用了 loadNacosPropertySource()
6.其最终调用了 NacosPropertySourceBuilder#loadNacosData()
7.loadNacosData() 调用 ConfigService#getConfig()
8.getConfig() 会使用 ClientWorker#getServerConfig() 发起 HTTP 请求从 nacos server 获取配置文件. 其请求地址是 /v1/cs/configs

还是挺简单的, 发个 HTTP 获取配置文件, 然后转化成一个 PropertySource, 再在 NacosPropertySourceLocator 的 locate 中扔进 environment 中去. Bingo!!!

从 server 实时更新原理

1
2
3
4
5
6
7
8
9
10
11
12
1.在 NacosConfigAutoConfiguration 注册了一个 NacosContextRefresher
2.其实现了 ApplicationListener<ApplicationReadyEvent>, 也就是会在容器准备事件触发后调用 NacosContextRefresher#registerNacosListenersForApplications()
3.在进行两个配置判断后(即 isRefreshEnabled() 和 PropertySource 的 isRefreshable), 调用 NacosContextRefresher#registerNacosListener()
4.这个方法通过 NacosConfigService#addListener() 注册一个 AbstractSharedListener 监听者到 cacheData.
5.而 ClientWorker#checkConfigInfo() 中添加的 LongPollingRunnable 任务, 会在 run() 中执行 getServerConfig() 获取服务配置文件, 然后与当前缓存对比 md5, 若更新则通过 cacheData 取出刚刚添加的 listener, 触发事件(即 receiveConfigInfo() )
6.触发后又会再发布一个 RefreshEvent 事件
7.接着流转到 RefreshEventListener.onApplicationEvent()
8.然后又会调用 ContextRefresher.refresh(), 这个方法中的 refreshEnvironment() 会调用 addConfigFilesToEnvironment(), 这方法先复制一个 StandardEnvironment, 将其放入到 SpringApplication 中, 再调用 SpringApplication 的 run() 走一遍, 会触发 NacosPropertySourceLocator.locate(), 于是复制的 environment 里面有新数据了, 再将其拷贝替换到当前的 environment. 完成 environment 的刷新.
9.接着发布一个 EnvironmentChangeEvent 事件, 用来刷新 @ConfigurationProperties 注解的 Bean. (这么喜欢事件?不愧是写出 RocketMQ 的阿里啊!!!)
10.接着在 ConfigurationPropertiesRebinder 中接收到这个事件, 触发 onApplicationEvent()
11.然后会执行 ConfigurationPropertiesRebinder#rebind()
12.其逻辑是将 postProcessBeforeInitialization 中存起来的 ConfigurationPropertiesBean 类型的 map 遍历, 进行重新绑定(即 destroyBean 后再 initializeBean, 则会从新配置重新赋值).

总结: 有的地方(NacosContextRefresher)监听容器初始化添加配置更新的监听者, 自己不干活只发布配置刷新事件;

有的地方(ClientWorker)添加轮询任务获取服务端配置文件与本地 cacheData 对比 md5 来判断是否触发配置更新的监听者;

然后有的地方(RefreshEventListener)监听配置刷新事件然后用 SpringApplication.run() 通过触发 NacosPropertySourceLocator#locate() 为一个复制出来的 environment 对象添加从服务端获取最新配置, 再拷贝到当前的 environment 对象, 完成 environment 配置的刷新, 之后发布 environment 刷新事件;

有的地方(ConfigurationPropertiesRebinder)监听 environment 刷新事件, 然后为 @ConfigurationProperties 注解生成的 bean 重新绑定配置.

再总结: 挺好的, 奥运火炬手啊这是, 不停传递!!!!!

关键类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
1.NacosConfigAutoConfiguration
注册一个监听容器初始化事件的 bean (NacosContextRefresher)

2.NacosContextRefresher
再初始化事件触发后添加一个 AbstractSharedListener 监听者到 cacheData

3.AbstractSharedListener
当服务端配置与本地不同后触发
触发后发布 RefreshEvent 事件

4.CacheData
管理配置更新事件监听者
对比服务端配置与本地配置的 md5
md5 不同则触发 AbstractSharedListener

5.RefreshEvent
通过容器发布
当服务端配置发送更新触发
触发后调用 ContextRefresher#refresh()

6.LongPollingRunnable
轮询任务, 获取服务端最新配置, 与 cacheData 对比

7.ClientWorker
管理轮询任务(LongPollingRunnable)
负责与 nacos 服务端通信

8.ContextRefresher
监听 RefreshEvent 事件
负责获取服务端最新配置到 environment 对象中
发布 EnvironmentChangeEvent 事件

9.EnvironmentChangeEvent
通过容器发布
当 environment 对象更新后触发
触发后调用 ConfigurationPropertiesRebinder#rebind()

10.ConfigurationPropertiesRebinder
监听 EnvironmentChangeEvent 事件
负责重新绑定用了 @ConfigurationProperties 注解的 Bean 的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
graph TB

A1(NacosConfigAutoConfiguration)
A2(NacosContextRefresher)
A3(ApplicationReadyEvent)
A4(NacosContextRefresher#registerNacosListener)
A5(NacosConfigService#addListener)

A7(ClientWorker)
A8(LongPollingRunnable#轮询任务)
A9(NacosConfigService#getServerConfig)
B1(AbstractSharedListener#配置更新事件的监听者)
B2(cacheData)
B3(RefreshEvent)
B4(RefreshEventListener)
B5(ContextRefresher#refresh)
B6(ContextRefresher#refreshEnvironment)
B7(ContextRefresher#addConfigFilesToEnvironment)
B8(SpringApplication#run)
B9(StandardEnvironment)
C1(NacosPropertySourceLocator#locate)
C2(当前容器的 environment 对象)
C3(EnvironmentChangeEvent)
C4(ConfigurationPropertiesRebinder#onApplicationEvent)
C5(ConfigurationPropertiesRebinder#rebind)
C6(用了 ConfigurationProperties 注解 Bean)

A1--注册了一个-->A2
A2--监听了-->A3
A3--事件触发后调用-->A4
A4--通过-->A5
A5--注册了一个-->B1


A7--构造方法中创建了一个-->A8
A8--run 方法中调用-->A9
A9--获取最新配置与-->B2

B2--比较 md5, 不同则触发-->B1
B1--事件触发后发布-->B3
B4--监听了-->B3
B3--事件触发后调用-->B5
B5--调用-->B6
B6--调用-->B7
B7--利用-->B8
B7--复制当前容器 environment 对象到-->B9
B8--触发-->C1
C1--获取了最新配置到-->B9
B9--复制最新配置回到-->C2

C2--更新完配置后发布-->C3
G1(ConfigurationPropertiesRebinder)--监听了-->C3
C3--事件触发后调用-->C4
C4--调用-->C5
C5--获取新配置绑定到-->C6

Spring Cloud Openfeign 源码笔记

关键类分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 1.FeignAutoConfiguration
配置了一个管理 feign 子容器的工厂(FeignContext).
配置一个 Targeter, 直接中专 fegin 的 target 方法(DefaultTargeter, 这里扩展可以实现降级哦)
配置了一个 feign client (ApacheHttpClient), 用于执行 HTTP 请求
还配备了 ok http client 方式的 feign client, 但默认不启用

# 2.FeignClientsRegistrar
被 @EnableFeignClients 引入
扫描带 @FeignClient 注解的接口, 生成代理对象(FeignClientFactoryBean)注册到容器中

# 3.FeignClientFactoryBean
继承自 FactoryBean, Spring 的东西, getBean() 时调用跳转到 getObject()
getObject() 会调用通过 feign 对象生成代理对象

# 4.FeignInvocationHandler
JDK 动态代理生成对象的的方法拦截器
通过调用 SynchronousMethodHandler 的 invoke() 实现发送请求的功能

# 5.SynchronousMethodHandler
invoke() 会调用 FeignBlockingLoadBalancerClient 的 execute() 通过负载均衡获取 url 再调用 ApacheHttpClient 的 execute() 发送带实际 url 的 HTTP 请求.

# 6.ParseHandlersByName
具有核心方法 apply, 解析 @FeignClient 接口的所有方法的注解和参数信息, 转化为 RequestTemplate, 可用于构造 HTTP 请求对象. 转化后的信息存于 SynchronousMethodHandler 字段中.

# 7.FeignBlockingLoadBalancerClient
execute() 会调用 LoadBalancerClient 的 choose() 根据 serviceId(即 HTTP 的 host) 获取 url.
还负责调用 ApacheHttpClient 的 execute() 真正的发送 HTTP 请求.

# 8.LoadBalancerClient
commons 下 loadbalancer 项目的老伙计了... 干啥的来着? 忘了

# 9.ApacheHttpClient
负责发送 HTTP 请求.

openfeign 原理(@EnableFeignClients 生效步骤)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.先解析 @EnableFeignClients 导入 FeignClientsRegistrar.class
2.FeignClientsRegistrar 将扫描带 @FeignClient 注解的接口, 注册到容器中
3.注册进容器的是一个 FeignClientFactoryBean
4.FeignClientFactoryBean 其本质是一个 FactoryBean, 会在被 getBean() 时调用 getObject()
5.getObject() 会从容器中取 Feign.Builder builder 对象再根据配置文件进行配置
6.接着会走 Targeter 对象(默认为 DefaultTargeter)的 target(), 其默认为 builder.target(target); 其中 builder 为 Feign.Builder 对象, target 为 type(class), name(serviceId), url(http://name/path)
7.target() 会先调用 build()生成一个 feign 对象, 再调用 feign.newInstance(target) 生成一个代理对象
8.先看 build() 方法, 初始化了重要属性 targetToHandlersByName, 值为 new ParseHandlersByName(), 这个类的 apply() 会在过一会用到. apply() 逻辑是遍历挨个解析方法上的注解(如@RequestMapping, @RequestParam, @PathVariable等等)和参数, 转化后包装成 HTTP 请求相关的实体类, 存到 SynchronousMethodHandler 的字段中, 再返回 SynchronousMethodHandler 对象存到 map 里.
9.再看 feign.newInstance(), 先调用刚说的重要属性 targetToHandlersByName.apply(), 获得一个 map, 里面键大致为类名+方法名+形参组成, 值是 SynchronousMethodHandler 对象(见 SynchronousMethodHandler.Factory.create()), 然后遍历代理类的所有方法, 将方法所对应的 SynchronousMethodHandler 从 map 中取出再存到另一个 map, 这个 map 的键则为 method; 接着使用 JDK 动态代理生成一个代理对象, 其用于拦截方法的类 InvocationHandler 具体为 FeignInvocationHandler, 这个类里面的 invoke 逻辑很简单, 先排除 Object 通用的方法的影响, 对于正常方法会调用 SynchronousMethodHandler 的 invoke()... 闹了半天就是个中转呗, 有类的 Handler 到了方法的 MethodHandler.
10.SynchronousMethodHandler 的 invoke() 的核心代码是 this.client.execute(), 这里会走到 feign.Client 的具体实现类. 可能是 ApacheHttpClient(当 url 有地址时), 那就直接执行请求了, 也可能是 FeignBlockingLoadBalancerClient, 那么会调用 loadBalancerClient 的 choose 方法获取 url, 再执行 feignClient.execute(), 这次这个 feignClient 则肯定是 ApacheHttpClient 了, 于是最终就这样发送了请求.

#PS:
有人问我(无中生友)发送的 HTTP 请求怎么构建?
其实我上面说的很清楚了, 关键就在于 apply(), 好吧, 我说的具体一点... 关键在于 SpringMvcContract#parseAndValidateMetadata()
这个的入口在 apply() 的第一行, this.contract.parseAndValidateMetadata() 一直往里跳转, 就会进入 SpringMvcContract... 具体代码挺多的... 真的没必要细读... 因为其本身只相当于一个工具类, 而读源码应该放眼大局, 否则每段代码都认真读, 那太难了!!!

总结: 扫描 @FeignClient, 生成代理对象, 扔进容器. 拦截代理对象的方法, 调用方法对应的 SynchronousMethodHandler, 此类调用之前解析好的 RequestTemplate 生成请求对象, 再通过 Client 执行请求, 若配置了负载均衡, 则会调用 LoadBalancerClient 将 serviceId 先解析成具体的地址再转交给 ApacheHttpClient 执行请求.

代理对象的生成步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
graph TB

A1(FeignClientFactoryBean)
A2(Feign.Builder)
A3(Feign)
A4(FeignInvocationHandler)
A5(SynchronousMethodHandler)
A6(FeignBlockingLoadBalancerClient)
A7(ApacheHttpClient)
A8(LoadBalancerClient)
A9(ServiceInstance)
A10(Response)
A11(Decoder 链)

A1--getObject 触发-->A2
A2--build 得到-->A3
A3--Proxy.newProxyInstance 用到-->A4
A4--invoke 里调用-->A5
A5--invoke 里调用-->A6
A6--execute 中先调用 -->A8
A6--execute 中后调用-->A7
A8--获取-->A9
A7--得到-->A10
A10--交给-->A11
A11--编码得到-->A12(json)

iShot2021-01-28 20.45.21

Spring Cloud Commons 之 loadbalancer 源码笔记

Spring Cloud Commons 是什么样的? 有什么作用? 如何与 Spring Cloud 和 Cloud Alibaba 整合?
让我们带着这些问题去研究源码吧!

loadbalancer 原理分析

1
2
3
4
5
# 先来认识一下 Spring Cloud Commons 吧
是定义了诸多接口(如ServiceRegistry/DiscoveryClient/LoadBalancerClient)和注解(如!EnableDiscoveryClient/@LoadBalanced)为主, 少量代码实现(如 RandomLoadBalancer).
以及对 Spring 容器(Context) 的扩展(如 NamedContextFactory, bootstrap 配置文件的加载, 容器重启, 容器跟随配置文件刷新等等)
当然还有一些打包好的 starter.
我们要研究的 loadbalancer 就是其中一个子项目.

loadbalancer 关键类解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 0.惊! 一堆类仅为添加一个拦截器
LoadBalancerRequestFactory: 一个工厂, 包装一个为请求对象 HttpRequest 加料的回调 LoadBalancerRequest

LoadBalancerClient: 用于根据 serviceId 选取一个 ServiceInstance, 执行从 LoadBalancerRequestFactory 获得的那个回调

LoadBalancerInterceptor: restTemplate 的拦截器, 拦截后调用 LoadBalancerClient 修改 HttpRequest 对象(主要是 url), 且传入调用 LoadBalancerRequestFactory 生成的回调给 LoadBalancerClient

RestTemplateCustomizer: 为 restTemplate 加上一个拦截器(也可以干点别的, 默认就这一个用处)

SmartInitializingSingleton: 调用 RestTemplateCustomizer 为容器中所有加了 @LoadBalanced 的 RestTemplate 加上一个拦截器

# 1.获取对象的工厂, 以 Spring 容器作为载体管理对象.
NamedContextFactory
继承 DisposableBean, 用于类销毁是执行点东西(指创建的好多个子容器)
继承 ApplicationContextAware, 用于将子容器和当前容器关联起来(所以 Spring 树形扩展这个设计真不错)
泛型 C extends NamedContextFactory.Specification, 无它, 就是个 POJO, 存个 name 和对应的配置 class, 用于初始化容器的(会被注册进去, 然后解析里面的注解啥的...)
此类作用就是管理一大堆(取决于你微服务拆分的程度)子容器, 获取其他代码需要的类型对象


ReactiveLoadBalancer.Factory
定义了获取 ReactiveLoadBalancer 的接口以及与其相关的扩展


LoadBalancerClientFactory
继承 NamedContextFactory, 构造参数指定了几个属性值
实现了 ReactiveLoadBalancer.Factory 的接口, 即提供获取 ReactiveLoadBalancer 的方法.
泛型具体为 LoadBalancerClientSpecification, 还是个POJO


# 2.包含算法逻辑的负载均衡策略的类
Response
server 的封装类, 一般持有一个 ServiceInstance 对象, 如 DefaultResponse

Publisher
响应式编程的东西, 可获取 Response<T> 对象, 一般为 Response<ServiceInstance>

ReactiveLoadBalancer.Factory
定义了获取 ReactiveLoadBalancer 的接口以及与其相关的扩展

ReactiveLoadBalancer
定义了 choose 方法, 即如何选取一个 ServiceInstance, 如轮播, 随机...

ReactorLoadBalancer
定义了 choose 方法的另一形式, 仅返回值不同, 为 Mono<Response<T>> 是 Publisher<Response<T>> 的子类, 返回值为抽象类.

ReactorServiceInstanceLoadBalancer
继承 ReactorLoadBalancer
仅仅作为一个标记类, 无新接口

类大致调用图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
graph TB

A(SmartInitializingSingleton)
A1(RestTemplateCustomizer)
A2(LoadBalancerInterceptor)
A3(restTemplate)
A4(LoadBalancerClient)
A5(LoadBalancerRequestFactory)
A6(ReactorServiceInstanceLoadBalancer)
A8(LoadBalancerClientFactory)

A--调用-->A1
A1--添加一个-->A2
A2--到-->A3
A2--调用-->A4
A2--调用-->A5
A5--生成一个回调给-->A4
A4--调用-->A8
A8--获取-->A6
A6--获取-->A7(ServiceInstance)

a copy

瞅瞅有哪些负载均衡策略吧(看完发现这才是最简单的… 外面那些结构反而不容易理清)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 1.RandomLoadBalancer: 就是随机数呗, 0-size, 简单!!
// 在 RandomLoadBalancer#getInstanceResponse() 中
// 觉得这个方法可以做出 protected, 这样有些实现只需要重写这个方法就行了
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + serviceId);
}
return new EmptyResponse();
}
int index = ThreadLocalRandom.current().nextInt(instances.size());

ServiceInstance instance = instances.get(index);

return new DefaultResponse(instance);
}

// 2.RoundRobinLoadBalancer
// 在 RoundRobinLoadBalancer#getInstanceResponse() 中
// 用一个 position 保存位置, 这个主意高啊, 即保证了数据的正确性, 还.... 编不下去了!!
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + serviceId);
}
return new EmptyResponse();
}
// TODO: enforce order?
int pos = Math.abs(this.position.incrementAndGet());

ServiceInstance instance = instances.get(pos % instances.size());

return new DefaultResponse(instance);
}

这代码简单的, 特别不想分析…. 但其实最开始就是冲着这个来的… 总得看看吧, 咳咳!!

loadbalancer 原理分析

  1. 先拦截 RestTemplate 对象的请求, 使其调用 LoadBalancerClient 的接口获取真实IP
1
2
3
4
5
6
7
# org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration 中
1.@Bean 加入一个 LoadBalancerRequestFactory, 并且带有用户自定义的 transformers(作用: 对选取真实 url 后的请求对象进行干预)
2.@Bean 加入一个 LoadBalancerClient, 其作用是, 根据 serviceId 获取/选取真实 url, 以及执行请求
3.@Bean 加入一个 LoadBalancerInterceptor, 即核心拦截器. 逻辑是: 获取 host, 调用 LoadBalancerRequestFactory 生成请求, 用 LoadBalancerClient 执行.
4.@Bean 加入一个 RestTemplateCustomizer, 其作用是: 为给定的 RestTemplate 添加一个 LoadBalancerInterceptor.
5.@Bean 加入一个 SmartInitializingSingleton, 作用是单例都加载后触发回调, 回调代码为:
遍历所有的 RestTemplateCustomizer 和 restTemplates, 用 RestTemplateCustomizer 对 RestTemplate 做设置. 包括(4)刚刚加入的那个.

总结: 为用户自定义(如配置类中写了个@Bean + return new RestTemplate() 这种形式)的 RestTemplate 添加一个拦截器, 在请求执行前进行拦截, 然后将请求数据的 host 作为 serviceId, 接着使用某个具体的 LoadBalancerClient 实现类调用其方法获取真实的 url. 若对应存在多个 url, 由其算法策略决定如何选择.

  1. 再看 LoadBalancerClient 的默认实现类(在 BlockingLoadBalancerClientAutoConfiguration 中配置的), 其逻辑是, 通过工厂获取 ReactorServiceInstanceLoadBalancer 对象并调用其接口执行负载均衡算法.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient
// 先进入这个方法, 然后会调用第二个方法.
// 这两个方法其实就是从工厂获取对象执行 choose 后再让其完成请求的执行, 大部分代码都是 LoadBalancerLifecycle 的触发.
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
// 遍历 LoadBalancerLifecycle 触发 onStart 钩子
// 调用 choose 方法选取一个 IP:PORT 得到包装类 ServiceInstance
// 遍历 LoadBalancerLifecycle 触发 onComplete 钩子
// 执行请求
String hint = getHint(serviceId);
LoadBalancerRequestAdapter<T, DefaultRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(request,
new DefaultRequestContext(request, hint));
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
DefaultRequestContext.class, Object.class, ServiceInstance.class);
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));

// 从 serviceId 对应的容器中获取一个负载均衡算法实现类对象, 即 ReactorServiceInstanceLoadBalancer.
// 调用其 choose 方法. 从响应中获取 ServiceInstance 并返回.
ServiceInstance serviceInstance = choose(serviceId, lbRequest);
if (serviceInstance == null) {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, new EmptyResponse())));
throw new IllegalStateException("No instances available for " + serviceId);
}
// 可以执行了
return execute(serviceId, serviceInstance, lbRequest);
}

@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request)
throws IOException {
// 遍历 LoadBalancerLifecycle 触发 onStartRequest 钩子
// 调用 request.apply 方法执行请求(即进入之前 LoadBalancerRequestFactory 中的代码)
// 遍历 LoadBalancerLifecycle 触发 onComplete 钩子

DefaultResponse defaultResponse = new DefaultResponse(serviceInstance);
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(
loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
DefaultRequestContext.class, Object.class, ServiceInstance.class);
Request lbRequest = request instanceof Request ? (Request) request : new DefaultRequest<>();
supportedLifecycleProcessors
.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, new DefaultResponse(serviceInstance)));
try {
// 请求调用前先使用 transformers 对原始请求对象进行一些改变处理后再执行请求
T response = request.apply(serviceInstance);
Object clientResponse = getClientResponse(response);
supportedLifecycleProcessors
.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
lbRequest, defaultResponse, clientResponse)));
return response;
}
catch (IOException iOException) {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
new CompletionContext<>(CompletionContext.Status.FAILED, iOException, lbRequest, defaultResponse)));
throw iOException;
}
catch (Exception exception) {
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
new CompletionContext<>(CompletionContext.Status.FAILED, exception, lbRequest, defaultResponse)));
ReflectionUtils.rethrowRuntimeException(exception);
}
return null;
}
  1. 所以再看看工厂是怎么获取和存放对象的, 关键类: LoadBalancerClientFactory, 其继承自 NamedContextFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// 先看其如何获取对象的 LoadBalancerClientFactory#getInstance()
@Override
public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
// 从 serviceId 对应的容器中获取一个负载均衡算法实现类对象, 即 ReactorServiceInstanceLoadBalancer.
return getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class);
}
// getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class):
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
try {
return context.getBean(type);
}
catch (NoSuchBeanDefinitionException e) {
// ignore
}
return null;
}
// getContext(name):
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
// 结论: 容器里有点东西, 但不多... 主要是于父容器打通... 所以又啥都有了.
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}

// createContext(name):
protected AnnotationConfigApplicationContext createContext(String name) {
// 0.结合实现类 LoadBalancerClientFactory 做出如下注释
// 1.将 LoadBalancerAutoConfiguration 扫描到 configurations 注册到 name 对应的容器中.
// 这里的 name 其实就是 serviceId, 也就是说, 若我们想给某个容器加入一些东西, 则实现 LoadBalancerClientSpecification 时, name 需要与 serviceId 对应起来(相同)
// 2.当我上面那句没说啊... 原来 name 为 default. 开头是可以加入任意 serviceId 对应的容器的.........................(qiao)
// 3.为容器加入一个占位符解析器, 和一个 defaultConfigType(=LoadBalancerClientConfiguration.class, 作用配置一些 bean)
// LoadBalancerClientConfiguration 会加入一个 RoundRobinLoadBalancer, 看来就是默认的负载均衡类了.
// 4.默认为加了一个名为 loadbalancer 的 PropertySource, 里面有一个 loadbalancer.client.name=serviceId 的配置....
// 5.设定父容器, 父容器通过 ApplicationContextAware 获得, 这样刚才那么辛苦的注册方式, 就仅适合于特性, 而非通用了.
// 6.设置名称(啥意义呢?), 然后调用容器的 refresh() 完成容器加载


AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
context.register(configuration);
}
}
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);
// 默认为加了一个名为 loadbalancer 的 PropertySource, 里面有一个 loadbalancer.client.name=serviceId 的配置....
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName,
Collections.<String, Object>singletonMap(this.propertyName, name)));
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
// jdk11 issue
// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
context.setClassLoader(this.parent.getClassLoader());
}
context.setDisplayName(generateDisplayName(name));
context.refresh();
return context;
}

// 根据这句 context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);
// 而 defaultConfigType 在 LoadBalancerClientFactory 定义为 LoadBalancerClientConfiguration.class, 其配置了一个 bean, 代码如下
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
// 这里会与服务发现结合起来, 即 loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class)
// 此方法从容器中获取能提供 ServiceInstanceListSupplier.class 类型的 BeanProvider, 其实就是能获取这种类型的 bean 呗, 然后用这个类来获取 url 列表... 具体实现要看 Nacos / Consul 了.

// 所以默认获取的负载均衡策略就是它了: RoundRobinLoadBalancer

总结: 用了一个 LoadBalancerAutoConfiguration, 为 RestTemplate 加一个拦截器使得执行请求前先修改一下请求对象(主要修改url呗), 修改的步骤是 LoadBalancerClient.execute(), 里面则会使用 choose 获取微服务真实url, choose 是 ReactorLoadBalancer 的接口, 代表负载均衡策略. 啊对了, 既然是负载均衡算法, 那就是负责选取, 不负责获取才对… 于是我发现 loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class) 才是得到的对象, 才是获取 url 列表的代码(肯定和consul或nacos有关了)!!

暗示下集出 Nacos!!

Cloud Alibaba 和 Spring Cloud 整合 Spring Cloud Commons 步骤(指服务注册与发现)

1
2
3
1.希望别太简单
2.就不写这里了(因为还没写啊!)
3.下集见

Spring Cloud 整合 spring-cloud-loadbalancer

1
2
3
4
5
6
1.pom 添加依赖即可
2.pom 添加依赖即可
3.pom 添加依赖即可

# 总结
我也没想到, 没多写一个类, 直接就能用... 原理上面分析了 😭😭😭😭😭

Cloud Alibaba 整合 spring-cloud-loadbalancer

1
2
3
4
5
6
1.pom 添加依赖即可
2.pom 添加依赖即可
3.pom 添加依赖即可

# 总结
我也没想到, 没多写一个类, 直接就能用... 原理上面分析了 😭😭😭😭😭

总结: 你没卡, 你电脑没问题, 我就是写(zhan)了(tie)两遍!!!

Spring Cloud Commons 的核心类及其作用

1
2
3
4
5
1.LoadBalancerClient: 实现它就实现了负载均衡策略
但其实实现 ReactorServiceInstanceLoadBalancer 更简单
2.DiscoveryClient: 实现它就实现了服务发现
3.ServiceRegistry: 实现它就实现了服务注册
4.ServiceInstance: 代表一个服务, 前面加个 Micro 就是微服务了 :D

PS: 就这!

JDK 源码笔记

ArrayList

核心就是 newCapacity 方法,这个方法用于确定扩容后的数组大小,正常是原来的 1.5 倍(老二进制运算了),若扩容后仍不够大,则仅保证能放下新加入的数据即可(当使用 ``addAll方法时可能触发);若扩容后溢出,则仅保证能放下新加入的数据即可;若扩容后逼近溢出,则返回MAX_ARRAY_SIZEInteger.MAX_VALUE;另外两次扩容后过大也会检查minCapacity` 是否溢出,防止数据错误。

HashMap

核心是根据 hash 取数组下标 index,并且 1.8 的扩容后 rehash 利用二进制高低位优化了计算下标的效率;扩容为原来的两倍,即左移 1 位。

为什么 HashMap 的初始容量以及扩容后的容量均为 2 的指数幂

因为计算机做运算时, 取模运算速度远远慢于位运算, 而若容量始终为 2 的指数幂, 则根据 hash 获取数组下标时只需要 使用 (数组长度-1) & hash 值 即可确定数组下标, 与取模得到的下标一样可靠.

而扩容后后, 因为需要进行 rehash 运算来确定 数据的新下标, 多次进行取数组下标则更能体现位运算的优势.

为什么 HashMap 的加载因子是 0.75 (3/4)

1
2
3
4
使用排除法:
1.若加载因子为 1. 则每次 HashMap 满了才进行扩容, 必将有更高的几率触发 hash 碰撞导致数组下标一致需要转成链表或红黑树, 导致读取和更新速度降低.
2.若加载因子为 0.5. 则每次 HashMap 都有一半容量剩余, 空间大大浪费, 对内存开销太大. 容易引发 OOM 事故.
3.0.5-1 之间那么多可能, 选哪个都行, 但作为 HashMap 的默认值, 选中间的 0.75, 走中庸之路, 也是解释的通的.

为什么 HashMap 1.8 扩容无需 rehash

1
2
3
4
5
6
7
1. 因为1.8的获取 hash 值的算法优化了. 无需一个 hashSeed 进行辅助运算 (主因)
2. 由于 hash 值不变, 原链表中的所有节点只有 2 种可能:
一是 hash 值高于原数组长度, 则属于高位, 这些高位的节点, 新的下标一定是 (当前下标 + 旧数组长度).
另一种是 hash 值低于原数组长度, 属于低位, 这些节点的下标无需重新计算, 必然与当前下标一致
(不信自己那几个示例数据用画出完整二进制计算一下)(神奇的位运算)
3. 重新计算下标时, 根据第 2 点可知, 其下标大小一定不高于(当前下标+旧数组长度), 即下一次循环的下标必然比上一次循环的下标要高, 所以 1.8 源码 resize 进行高低位分组然后转移数据时, 无需担心下一次循环会将刚刚放到新数组的值覆盖(下标相同则会覆盖)
4. 1.8 的 resize 优化了算法, 保持了原有的链表顺序(不知道有啥用)

总得来说, 1.8 优化了 hash 算法, 使 hashcode 的高 16 位与 低 16 位进行异或运算, 降低了碰撞率

而 resize 算法也优化链表节点的迁移, 避免了 1.7 的链环产生

最大的区别就是, 1.7 没有将二进制的神奇发挥到极致, 依然像普通 java 程序一般逻辑. 而 1.8 则充分利用了二进制的优点(也充分的让人头晕), 提高了 HashMap 的效率.

为什么 HashMap 从链表达到 8 个时转成红黑树, 达到 6 个时转回链表?

1
2
1.根据 Poisson distribution 定律, 凑齐8个节点碰撞到同一个下标, 组成长度为 8 的链表概率极低, 约为 0.00000006, 而超过 8 个的几率则更低, 大约为千万分之一. 所以将阈值设置为 8, 因为这种概率极低. 因此可以减少链表转红黑树的, 提高增删改效率.
2.若达到 7 个时转回链表, 则可能会导致HashMap 不停的在链表和红黑树之间转换, 所以阈值设置为 6, 可起到缓冲效果.

JUC

ReentrantLock

加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// AQS 的 acquire: AbstractQueuedSynchronizer#acquire()
public final void acquire(int arg) {
if (
!tryAcquire(arg)
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
) {
selfInterrupt();
}
}
// 1.tryAcquire:判断 state 状态,无线程持有则 CAS 加持 state,并返回 true,加锁结束。
// 2.addWaiter:已被持有则先加入队列
// 3.acquireQueued:两件事,第一,循环尝试持有锁,失败则休眠;第二,持有成功则更新队列头为自己(队列向前移动)
// 加入队列后,不能立刻睡,防止没人唤醒自己(比如还没加入等待队列,上个持有锁的线程就结束了然后触发解锁代码,而解锁代码的唤醒依赖于等待队列)
// 确定得不到锁(需要等待)则进入睡眠,醒来后再尝试 tryAcquire 持有锁,失败则睡眠(循环尝试)
// 醒来尝试持有锁成功后,维护队列(即队列头部设为自己,宏观上队列往前移动了)

总结:先判断后更新 state 状态(即是否有线程持有此锁),被持有则加入队列并睡眠;醒来后再重复判断 state 过程,若持有成功则更新队列(主要目的是令我的下一个节点被唤醒后可以尝试持有锁,因为一醒来就是判断 node.prev==head)。

释放锁

1
2
3
4
5
6
7
8
9
10
11
12
13
// AQS 的 release:AbstractQueuedSynchronizer#release()
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 1.tryRelease:减持 state 数量,减到 0 代表线程彻底释放了这个锁
// 2.unparkSuccessor:唤醒下一个节点的线程(若下一个节点为空,则从队列尾往前查找队列中状态不是 CANCELLED 的且最靠前的节点来唤醒)
// 3.唤醒后处于 acquireQueued 的循环中。

总结:先更新后判断 state 状态,若为 0,则代表可唤醒下一个节点,找到下一个节点,唤醒即可!

从上得到可重入锁原理,同一线程第二次加锁,会令 state+1,

而释放锁时,会先令 state-1,若 state 减到 0,才唤醒下一个节点的线程。

AQS

1
2
3
4
5
6
7
8
9
10
AQS
acquire:通过 tryAcquire 判断是否需要加入队列以及休眠
release:通过 tryRelease 判断是否需要唤醒下一个节点的线程
tryAcquire:正向维护 state,有自己的逻辑决定返回值
tryRelease:反向维护 state,一般与 tryAcquire 互逆

acquireShared:通过 tryAcquireShared 判断是否需要加入队列以及休眠(此时节点标记为 SHARED)
releaseShared:通过 tryReleaseShared 判断是否需要唤醒下一个节点,若下一个节点是 SHARED 节点,则还会继续唤醒下一个节点,若都是 SHARED 节点,则都会被唤醒。
tryAcquireShared:正向维护 state,返回值小于 0 代表需要进入队列
tryReleaseShared:反向维护 state,一般与 tryAcquireShared 互逆

总结:一个具体的 AQS 维护一个队列以及 state 数据,总是通过 tryAcquire、tryRelease 维护 state,并在 acquire 中完成队列的添加与移除。

AQS 维护的队列总是在尾部增加节点,并且移除节点时,总是移除 head 节点。

通过实行具体的 tryAcquire、tryRelease 方法,可控制 线程是否等待,何时唤醒线程。

再总结: AQS 的主要作用就是将线程加入到队列、移除队列、控制休眠和唤醒。

ReentrantReadWriteLock

  • 读锁加锁:只要 state 中没有写锁,又没有超过最大数量(受 32 位二进制长度限制),则能加锁成功(非公平锁不进队列等待,公平锁只可能会排在队列中写锁节点后面);
  • 读锁解锁:先减少 state 中的读锁数量,减到 0 则可触发 releaseShared 来唤醒队列中可能存在的写锁节点;
  • 写锁加锁:若没有写锁也没有读锁,令 state+1 写锁,记录重入次数;
  • 写锁解锁:先减少 state (可重入锁),若写锁数量为 0,则可唤醒队列中的下一个节点,若下一个节点为读锁加入的节点,则会唤醒下下个是读锁的节点,重复,则唤醒大量读锁节点。

总结:重写 AQS 的核心方法,使其得以控制读锁、写锁能否加锁成功,利用 AQS 的 SHARED 节点在 releaseShared 时的传播性,使得写锁结束后,可以唤醒一连串的读锁。

CountDownLatch

1
2
3
// new CountDownLatch(n):设置 state 的初始值为 n,使得需要调用 n 次 countDown 才会唤醒调用 await 的线程
// countDown:state - 1,若 state 减到 0,则唤醒所有队列中的节点
// await:加入到 AQS 队列休眠,节点标记为 SHARED

总结:利用 releaseShared 会传播唤醒队列上相连的 SHARED 节点特性和 state 的维护,使 await 的线程进入休眠队列,直到 countDown 调用次数足够多才会唤醒这些线程。

Semaphore

1
2
3
4
5
// new Semaphore(n):设置 state 的初始值为 n,当想唤醒调用 acquire(x) 的线程,则需要调用 release(x-n)
// acquire(x):加入到 AQS 队列休眠(前提 state < x)
// release(x):state + x,总是尝试唤醒队列

理解方式:acquire 多少个数,则代表需要等待多少个 release,若有初始值,则需要的 release 再减去初始值;满足则唤醒,为满足则进入队列休眠

总结:利用 AQS 可控制线程休眠和唤醒的特性,重写 tryAcquireShared、tryReleaseShared 方法实现逻辑;另外,虽然 acquire、release 的语义与正常 AQS 保持一致,但其对 state 的操作(加减)却是反着来的。

CyclicBarrier

含义: 凑足一定个数线程, 然后批量唤醒.
await(): 利用 ReenrantLock 的 lock 和 condition 的 await 进入休眠
当凑足后,用condition 的 singleAll 唤醒所有 await 的线程.

ConcurrentHashMap

不同之处有二

一是 put 一个新的 key 时,会使用 CAS 确保这次 put 没有线程竞争;

二是 put 覆盖一个 key 时,直接使用 synchronized 同步块进行同步。

ThreadPoolExecutor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// java.util.concurrent.ThreadPoolExecutor#execute
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
  1. 若当前线程数少于核心线程数,则添加一个线程来执行任务,直到线程数超过核心数;
  2. 超过之后,会放入到 workQueue 队列中,并重新检查线程池状态及是否有线程可执行队列中的任务
  3. 若放入失败(即队列已满),则尝试添加一个线程来执行这个任务,此时若线程达到最大线程数,则会失败并调用 reject 方法触发用户设置的拒绝策略。

总结:线程池先创建满核心线程数,超出放入队列,队列也放不下则会新建线程来救急,如果救急的线程创建过多,最终总线程数超过最大线程数,则触发拒绝策略,线程池不再接收任务,除非队列空出位置或线程数量降下来。

Spring MVC 源码笔记

关键类分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
WebMvcConfigurationSupport
默认注册了很多东西,如 HandlerMapping 几个实现, HandlerAdaptor 几个实现

HandlerMapping
添加容器内所有带有 RequestMaping 的类的公开方法到 mappings 中存起来
(AbstractHandlerMethodMapping#afterPropertiesSet中)
根据 request 的 uri 查找对应的 HandlerMethod, 步骤概述:
把RequestMapping注解内的 path 作为 key 保持到一个 map1
其他信息封装成 mapping 作为 key 也保持到另一个 map2
根据 uri去 map1 获取 mapping, 再根据 mapping 获取 HandlerMethod
封装成 Match 对象, 与其他匹配对象做比较后, 返回 HandlerMethod

HandlerAdapter
初始化参数解析,返回值解析等
(RequestMappingHandlerAdapter#afterPropertiesSet)
根据 handler 确定对应的 HandlerAdapter, 然后 HandlerAdapter 负责执行这个 handler
如 RequestMappingHandlerAdapter 则负责执行 HandlerMethod
简单说就是封装 HandlerMethod, 根据参数值设置参数, 然后调用方法, 再处理返回值封装成 ModelAndView
另外,这里如果使用了@ResponseBody,会进入 RequestResponseBodyMethodProcessor
然后使用 messageConverters(json)写入到响应流
最后 mv 也直接返回null, 不需要render了.

ViewResolver
负责将 ModelAndView 解析成HTML, 如JSP, FreeMarker

HandlerExecutionChian
管理拦截器和封装Handler, 负责拦截器的实际调用逻辑实现

DispatcherServlet
调度整个HTTP请求响应流程, 调用各个子组件负责执行处理方法, 解析视图, 处理异常等.

SSM项目 Spring 容器和 Spring MVC 容器的创建过程及关系

Spring 容器的创建

1
2
3
4
5
6
7
8
org.springframework.web.context.ContextLoader#initWebApplicationContext
1.先判断此方法是否重复运行, 若重复则报异常.
2.记录日志, 记录开始时间用于计算启动消耗时间
3.将容器对象存储在本地而非 servletContext, 防止 ServletContext 关闭后无法访问
4.根据配置的类名, 实例化一个容器并赋值给 this.context
5.标准代码, 但其实就是 setParent(null), 前提是不扩展子类咯. 也就是说这个就是顶级容器了
6.根据 web.xml 的配置对容器做相应的配置, 初始化, 将 ServletContext 存入到 environment 对象中; 执行 InitializerClass, 调用容器 refresh 完成容器加载
7.打印容器初始化完成日志和记录耗时

总结: 监听 ServletContext 创建完毕的事件, 然后创建一个 web 容器, 配置一些东西(id, parent, environment)并初始化(refresh)

MVC 子容器的创建

1
2
1.首先是 DispatchServlet 本身是一个 Servlet, 因此他有生命周期 init(), 其父类 init() 会将 ServletConfig 的配置(web.xml 中的 initParams)与 servlet 对象绑定, 这样 DispatchServlet 对象的字段就有值了.
2.然后在 DispatchServlet 的父类 FrameworkServlet#initServletBean 中, 会创建一个容器并使其加载.(详细见 FrameworkServlet#initWebApplicationContext() )

总结: 就是利用 Servlet 的生命周期只会执行一次 init 的特性, 查找父容器, 创建子容器.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
org.springframework.web.servlet.HttpServletBean#init
// 1.把 servletConfig 的 initParameters 都加入 PropertySource, 根据 requiredProperties 判断所需配置项是否齐全
// 2.将配置项绑定到 Servlet 对象(this) 上(子类的字段也算).
// 3.调用子类初始化方法

org.springframework.web.servlet.FrameworkServlet#initServletBean
// 0.父类的 init 执行完, 此时 contextConfigLocation 已经有值了
// 1.记录日志和开始时间
// 2.创建子容器, 与父容器绑定, 做一些配置, 调用 refresh() 完成加载.
// 3.打印日志, 打印耗时


org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext
// 1.从 ServletContext 中获取 Spring 容器.(那么谁放进去的呢? 没错, 就是 ContextLoader,监听了 ServletContext 事件)
// 2.若容器在构造方法处被注入(可能是注解开发 Spring MVC 的方式会触发), 先忽视 -- 来自 web.xml 形式启动分析
// 3.根据绑定得到的配置的 contextClass 创建一个子容器, 进行一些配置和初始化, 调用 refresh() 完成容器加载
// 4.防止子容器不支持 refresh, 或子容器不是刚刚创建的, 因此手动触发 onRefresh(), 这个方法会加载一些默认的 bean(用处很大的那种)
// 5.将容器设置到 ServletContext 中(根据配置, 默认允许)
// 6.返回创建的子容器对象


org.springframework.web.servlet.FrameworkServlet#configureAndRefreshWebApplicationContext
// 1.根据绑定得到的配置设置容器 id, 若无则生成默认的
// 2.设置 ServletContext/ServletConfig 对象
// 3.为子容器添加一个监听只监听子容器刷新事件的监听器, 用于容器加载完毕后调用 FrameworkServlet.onApplicationEvent()
// 4.将 ServletConfig/ServletContext 加入到 environment 中.
// 5.执行 web.xml 中配置的 Initializers, 为子容器做的初始化
// 6.调用子容器的 refresh(), 完成容器的加载


org.springframework.web.servlet.DispatcherServlet#initStrategies
// 加载文件上传处理 bean
// 加载国际化处理 bean
// 加载主题切换处理 bean
// 加载 HandlerMapping bean
// 加载 HandlerAdapter bean
// 加载异常处理 bean
// 加载 HttpServletRequest 转视图名称处理策略 bean
// 加载视图解析器
// 加载 FlashMap 管理 bean

// 每一个加载的逻辑都是类似的, 先从容器中根据 Xxx.class getBean
// 若无, 则调用 getDefaultStrategy() 从 DispatcherServlet.properties 配置文件中读取

DispatchServlet 的创建

1
2
3
1.Tomcat 会创建配置在 web.xml 的 Servlet
2.接着会触发 init 方法, init 调用了创建子容器的方法后, 还添加了容器加载完毕事件监听来回调 DispatcherServlet#onRefresh
3.DispatcherServlet#onRefresh 会 initStrategies() 加载很多策略接口 bean.

总结: 和子容器的创建息息相关.

Dispatch 过程

1
2
3
4
5
6
7
1.覆盖 HttpServlet 的 service 方法, 调用 FrameworkServlet#processRequest()
2.此方法中进行一些上下文的准备工作, 以及处理日志, 异常(非 controller 异常), 然后调用 DispatcherServlet#doService()
3.doService() 中对 request 做一些准备工作, 然后调用 DispatcherServlet#doDispatch()
4.doDispatch() 先用 handlerMappings 查找合适的 handler(并加入拦截器链), 再通过 handlerAdapters 得到 handler 的适配器, 在合适的地方触发拦截器; 然后调用适配器的 handle() 得到 ModelAndView
5.得到 ModelAndView 后, 先判断是否捕获到了异常, 是则调用 handlerExceptionResolvers 的 resolveException() 处理异常
6.接着, 调用 viewResolvers 的 resolveViewName, 将 viewName 解析成一个 View 对象
7.调用 View 对象的 render(), 将视图通过 response 响应到前端.

总结: handlerMappings 找 handler 并包装拦截器链, handlerAdapters 找可执行的 HandlerAdapter, viewResolvers 解析视图, 渲染视图.

超长源码注释

1
2
3
4
5
6
7
8
9
10
11
12
13
// 入口 HttpServlet.service
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
processRequest(request, response);
}
else {
super.service(request, response);
}
}

接着进入到 processRequest, 我只写注释了啊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
org.springframework.web.servlet.FrameworkServlet#processRequest
// 1.记录启动时间
// 2.准备国际化处理上下文
// 3.准备属性管理上下文
// 4.获取并缓存一个 asyncManager(异步)
// 5.初始化两个 ContextHolder
// 6.处理文件上传, 根据不同策略查找可执行的 controller 方法, 查到后加入拦截器, 再通过适配器来执行 controller 方法, 然后把得到的 ModelAndView 解析成视图对象, 并渲染到前端.
// 7.重置两个 ContextHolder
// 8.打印日志, 发布事件


org.springframework.web.servlet.DispatcherServlet#doService
// 1.打印日志, 记录请求信息
// 2.保存一份请求上下文的快照, 这样嵌套的请求在回归时可以恢复数据
// 3.将容器中的一些 bean 配置到请求上下文中
// 4.处理文件上传, 根据不同策略查找可执行的 controller 方法, 查到后加入拦截器, 再通过适配器来执行 controller 方法, 然后把得到的 ModelAndView 解析成视图对象, 并渲染到前端.
// 5.若需要, 将快照恢复到请求上下文


org.springframework.web.servlet.DispatcherServlet#doDispatch
// 1.准备一些变量
// 2.判断并处理文件上传请求, 若是, processedRequest 则变为 MultipartHttpServletRequest 类型的对象.
// 3.遍历之前加载的 handlerMappings, 调用 getHandler 接口获取执行链对象并返回. 找不到则返回 null
// 4.根据 handler 获取合适的适配器
// 5.遍历执行拦截器的 preHandle(), 若遇到 renturn false, 则结束 doDispatch
// 6.执行实际的 controller 的方法得到 ModelAndView 对象.
// 7.处理默认的 viewName
// 8.遍历执行拦截器的 postHandle()
// 9.若有异常则处理异常: 遍历之前加载的异常处理器策略类, 调用 resolveException()
//10.若无异常则根据需要根据 ModelAndView的对象的视图名配合之前加载的视图解析器获取 View 对象, 再调用 render() 渲染视图.
//11.最后执行拦截器的 triggerAfterCompletion


org.springframework.web.servlet.DispatcherServlet#getHandler
// 遍历之前加载的 handlerMappings, 调用其 getHandler 接口获取 handler 和拦截器封装成 chain 对象并返回. 找不到则返回 null


org.springframework.web.servlet.DispatcherServlet#getHandlerAdapter
// 遍历之前加载的 handlerAdapters, 调用 supports 判断是否支持 handler, 支持则返回 adapter


org.springframework.web.servlet.HandlerExecutionChain#applyPreHandle
// 从头到尾遍历拦截器, 执行 preHandle(), 若拦截器返回 false, 则立刻 triggerAfterCompletion, 然后 return false


org.springframework.web.servlet.DispatcherServlet#processDispatchResult
// 若有异常则处理异常: 遍历之前加载的异常处理器策略类, 调用 resolveException()
// 若无异常则根据需要根据视图名和视图解析器渲染视图
// 最后执行拦截器的 triggerAfterCompletion


org.springframework.web.servlet.DispatcherServlet#processHandlerException
// 遍历之前加载的异常处理器策略类, 调用 resolveException()


org.springframework.web.servlet.DispatcherServlet#render
// 先遍历视图解析器, 根据视图名获取实际视图对象
// 最后调用实际视图对象的 render (渲染方法)


org.springframework.web.servlet.DispatcherServlet#resolveViewName
// 遍历视图解析器, 根据视图名获取实际视图对象

某些实现原理

1
2
3
4
5
1.拦截器原理
2.默认的 HandlerMapping/HandlerAdapter 在何时加入?
3.参数是如何与 HTTP 请求 body 绑定的(序列化, 格式化, 绑定)?
4.参数校验是如何进行的?
5.有关参数与返回值的一些拦截与干预(@RequestBody, @ResponseBody).

拦截器原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
何时加入?
从 WebMvcConfigurationSupport 的子类中调用 addInterceptors
添加一些拦截器和拦截器的路径配置 InterceptorRegistry 和 MappedInterceptor
实现拦截器路径匹配, 在 new HandlerExecutionChian 时判断

何时执行?
DispatcherServlet 负责在正确的时机调用 HandlerExecutionChian 来调用 preHanlde 等方法.
拿到 HandlerExecutionChian 后调用 preHanlde
HandlerAdapter 执行完 handler 后, 调用 postHandle
解析视图并渲染到response之后, 调用 afterCompletion
如果中途出现异常, 或 preHandle 提前结束, 则也调用 afterCompletion

总结
DispatcherServlet 去调用 HandlerExecutionChian 去调用 拦截器具体方法.
复杂点是添加一个拦截器到被加入到 HandlerExecutionChian 比较复杂一点, 以及带路径匹配的拦截器实现略复杂一些.

默认的 HandlerMapping/HandlerAdapter 在何时加入?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
org.springframework.web.servlet.DispatcherServlet#initStrategies
protected void initStrategies(ApplicationContext context) {
// 加载文件上传处理 bean
// 加载国际化处理 bean
// 加载主题切换处理 bean
// 加载 HandlerMapping bean
// 加载 HandlerAdapter bean
// 加载异常处理 bean
// 加载 HttpServletRequest 转视图名称处理策略 bean
// 加载视图解析器
// 加载 FlashMap 管理 bean

// 每一个 init 的逻辑都是类似的, 先从容器中根据 Xxx.class getBean
// 若无, 则调用 getDefaultStrategy() 从 DispatcherServlet.properties 配置文件中读取
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}

参数是如何与 HTTP 请求 body 绑定的(序列化, 格式化, 绑定)?

1
2
3
4
5
1.在适配器执行 handler 的时候, 即 RequestMappingHandlerAdapter#invokeHandlerMethod
2.此方法会进行一些其他处理, 然后准备执行方法前解析参数, 即在 InvocableHandlerMethod#getMethodArgumentValues() 中
3.这个方法中会遍历每一个参数, 再遍历配置的所有 resolvers, 通过 supportsParameter 接口判断是否支持参数解析, 是则调用 resolveArgument 接口获得实参
4.这其中, 最重要的是 resolvers, 其一般在 RequestMappingHandlerAdapter#getDefaultArgumentResolvers() 中添加默认和用户自定义的 resoloves.
5.如 RequestResponseBodyMethodProcessor#resolveArgument() 用于处理带 @RequestBody 注解的参数.

总结: 适配器执行具体方法前, 先用反射获取这个方法的 参数(形参)集合, 挨个遍历从 resolvers 找支持解析的类来解析, 得到的返回值作为实参先存起来, 最后调用具体方法时就可以带上实参们执行就实现了将 HTTP 的数据绑定到 controller 的方法参数上的功能.

参考 RequestResponseBodyMethodProcessor#resolveArgument()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 调用 readWithMessageConverters 读取 body 的数据, 再序列化 json 成相应的 Java Bean
// 使用 binder 检查 arg 的值是否与 @Valid 的那些注解相关的规则相符, 若有错误, 则抛异常.

parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);

if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}

return adaptArgumentIfNecessary(arg, parameter);
}

参数校验是如何进行的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 参考上面的代码
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
// 这段代码 validateIfApplicable() 就进行了参数校验, 代码如下:
// 逻辑是: 遍历参数所有的注解, 包含 validatedAnn 或以 Valid 开头的注解都进行校验
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
binder.validate(validationHints);
break;
}
}
}

// 然后是 validate 具体的对象.
// 逻辑是: 遍历所有的 validators, 挨个调用 validate 校验
public void validate(Object... validationHints) {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
// Call each validator with the same binding result
for (Validator validator : getValidators()) {
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
validator.validate(target, bindingResult);
}
}
}
// 再往下就没啦... Spring 没有具体的实现, 所以要导入 Hibernate 的啥啥啥包, 这是有原因的.

有关参数与返回值的一些拦截与干预(@RequestBody, @ResponseBody).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 参考 RequestMappingHandlerAdapter#afterPropertiesSet()
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();

if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}

由上可知, argumentResolvers/returnValueHandlers 都是此时初始化的, 再点进去看代码发现, 除了默认的, 还有用户自定义的 customArgumentResolvers/customReturnValueHandlers;

1
2
3
4
// 寻找字段的引用发现
WebMvcConfigurationSupport#addReturnValueHandlers
WebMvcConfigurationSupport#addArgumentResolvers
// 这两个方法可以继承后加入加入自己的 ArgumentResolvers/ReturnValueHandlers, 也就是项目里面继承 WebMvcConfiguration 的那个类, 重写这两个方法, 加入自己的类即可实现对参数/返回值的干预; 常用于统一加密/解密, 记录日志等.

除此之外, 两边的默认值都有 RequestResponseBodyMethodProcessor, 这就是用于处理@RequestBody/@ResponseBody 的了, 往里面深入的看, 能看到其使用 messageConverters 读取 body 数据, 然后也会在对应的地方触发 advice 的方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 参考 AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}

参数的N种绑定方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 参考 RequestMappingHandlerAdapter#getDefaultArgumentResolvers
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());

// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}

// Catch-all
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));

return resolvers;
}

看几个常见的

1
2
3
4
5
6
7
8
1.RequestParamMethodArgumentResolver: 负责解析带 @RequestParam 注解的普通参数
2.RequestParamMapMethodArgumentResolver: 负责解析带 @RequestParam 的 Map 参数....
3.PathVariableMethodArgumentResolver/PathVariableMapMethodArgumentResolver: 同上解析带 @PathVariable 注解的参数
4.RequestResponseBodyMethodProcessor: 负责解析带 @RequestBody 的参数
5.RequestHeaderMethodArgumentResolver: 负责解析带 @RequestHeader 的参数 (表示没用过)
6.ServletRequestMethodArgumentResolver: 负责解析 HttpServletRequest 等类型的参数(即 req, 用的贼多)
7.ServletResponseMethodArgumentResolver: 负责解析 HttpServletResponse 等类型的参数(即 resp, 下载接口没少用)
8.RequestParamMethodArgumentResolver: 啥都解析.... 即不带任何注解的普通类型.

Spring Boot 源码分析

run 流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.StopWatch 提供的计算耗时的功能, 创建一个后立即开始计时.
2.创建一个引导容器, 并在此时(容器未使用前)把 spring.factories 找到 Bootstrapper 接口的类对应的方法触发, 来给引导容器里注册一些东西(如果有需要)
3.从 spring.factories 找 SpringApplicationRunListener 的类, 实例化后存到 SpringApplicationRunListeners 中.
4.触发所有存入的 SpringApplicationRunListener 的 starting 事件.
5.将 args 内容中的参数们(类似 --spring.port=9999)解析成键值对存到 applicationArguments 对象中.
6.创建了一个 environment 对象, 添加了好几个功能各异的 PropertySource, 触发所有存入的 SpringApplicationRunListener 的 environmentPrepared 事件
7.根据 this.bannerMode 判断是否打印 Banner, 以及打印在哪里, 根据 this.banner 判断打印什么样的 Banner (佛祖保佑.png)
8.根据 web 类型创建不同的 ApplicationContext, 这里的创建, 仅实例化而已, 没有从构造方法调用 loadBeanDefinitions 和 refresh 的逻辑
9.将 applicationStartup (步骤记录器)赋值给 context.
10.对容器做些配置, 然后发布 contextPrepared 事件, 接着关闭引导容器; 然后使用 BeanDefinitionLoader 扫描解析 getAllSources (如Class) 并将得到的 BeanDefinition 注册到容器, 最后发布 contextLoaded 事件
11.注册一个钩子, 当 JVM 关闭时, 相应的关闭 context, 然后调用容器的 refresh 方法(然后进入到 Spring 源码分析那段, 自行脑补)
12.StopWatch 计时器停止计时, 接着打印计时数据
13.发布 started 事件
14.从容器中取出 ApplicationRunner/CommandLineRunner 两类 bean, 并调用它们的 run 方法.
15.catch 到异常则发布 failed 事件
16.发布 running 事件

从整体结构下来, 我们发现, 其主要是创建了一个引导容器(似乎也就是事件监听用到了, 其他地方完全无瓜), 然后扫描得到一些 SpringApplicationRunListener 存起来, 之后在合适的地方发布事件, 然后创建以及配置 environment 对象, 创建以及配置 ApplicationContext 对象, 解析 primarySources 来加载 BeanDefinition 到容器, 然后调用容器的 refresh 方法进入 Spring 的加载流程, 最后处理事件和调用 ApplicationRunner/CommandLineRunner 的 bean. 其中 加载 BeanDefinition 和执行 refresh 方法其实就是与 Spring 一样的逻辑.

SpringApplication 与 ApplicationContext 的关系与联系

1
2
3
4
5
1.SpringApplication 有独特的 environment 对象, 这是因为 application.xml 配置以及 Spring Cloud Config 这些都需要在容器创建前加载配置文件.
2.ApplicationContext 是 new 的时候就会立刻触发 加载 BeanDefinition 和 refresh(), 而 SpringApplication 则是先 new 一个, 配置好后, 才分别调用方法去 加载 BeanDefinition 和 refresh().


总得来讲, SpringApplication 相比 ApplicationContext 多出的就是 SpringApplicationRunListener 的事件管理, environment 对象加载与配置, 以及 run 方法的生命周期.

这些 SpringApplicationRunListener 事件都被谁监听了, 有什么作用?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 在 spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories
# 有一个 SpringApplicationRunListener, 其作用是:
org.springframework.boot.context.event.EventPublishingRunListener
1.构造方法: 初始化一个事件广播器, 并将 SpringApplication 的 listeners 注册进去. (listeners 是在构造方法中从 spring.factories 加载 ApplicationListener 得到的数据)
2.转发 starting,environmentPrepared,contextPrepared,contextLoaded 事件给 ApplicationListener
3.在 contextLoaded 事件时, 遍历所有的 ApplicationListener 对象, 若其实现了 ApplicationContextAware 接口, 则将 context 注入.
4.started,running,failed 事件均直接用 context.publishEvent 发布事件, 与 listeners 无关 (而且 listeners 这些东西如果仅配置在 spring.factories 而没有被扫描到容器内, 那么就是真的无关了)

# 总结: 配置在 spring.factories 的 ApplicationListener 并不会触发所有 run 生命周期的事件. 因此有时实现 SpringApplicationRunListener 还是很有必要的) 当然, 若你写的 ApplicationListener 即配置在 spring.factories 中也会被扫描到容器内, 则无此忧虑.

# 接着看 ApplicationListener 的作用
1.ClearCachesApplicationListener: 在 ContextRefreshedEvent(容器加载完后) 清空缓存
2.ParentContextCloserApplicationListener: 给子容器加一个监听, 使得父容器关闭后, 子容器也跟着关闭(还会递归子容器的容器吧.png)
3.FileEncodingApplicationListener: 对比配置的编码格式, 不符合则报异常(若配置了)
4.DelegatingApplicationListener: 新建一个事件广播器, 将配置文件中 context.listener.classes 的 class 实例化并作为监听者注册到广播器, 然后转发所有事件.
5.EnvironmentPostProcessorApplicationListener
接受 ApplicationEnvironmentPreparedEvent 事件, 然后把从 spring.factories 获得的 EnvironmentPostProcessor 的 classNames 实例化, 然后遍历执行 postProcessEnvironment(), 其中 ConfigFileApplicationListener 用于加载 application.xxx(yml,xml,properties) 文件的配置到 environment 中的 PropertySource 里. (另外一提, Spring Cloud Config 应该也是这里实现的)

# 总结: 原来配置文件是这么被加载进去的, 没有在主流程中写, 而是监听事件, 再调用 EnvironmentPostProcessor, 层层封装, 扩展性好强(读起来也好蓝).

超长源码注释

1
2
3
4
5
// 我们从源码的 spring-boot/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat 下, 找到 SampleTomcatApplication.java 类, 直接看它的 main 方法.

public static void main(String[] args) {
SpringApplication.run(SampleTomcatApplication.class, args);
}

接着, 我们点进 run 方法, 瞧瞧里面干了啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// 看起来不多嘛......(看前如是写道, 看完已是凌晨四点)
public ConfigurableApplicationContext run(String... args) {
// 1.StopWatch 提供的计算耗时的功能, 创建一个后立即开始计时.
// 2.创建一个引导容器, 并在此时(容器未使用前)把 spring.factories 找到 Bootstrapper 接口的类对应的方法触发, 来给引导容器里注册一些东西(如果有需要)
// 3.从 spring.factories 找 SpringApplicationRunListener 的类, 实例化后存到 SpringApplicationRunListeners 中.
// 4.触发所有存入的 SpringApplicationRunListener 的 starting 事件.
// 5.将 args 内容中的参数们(类似 --spring.port=9999)解析成键值对存到 applicationArguments 对象中.
// 6.创建了一个 environment 对象, 添加了好几个功能各异的 PropertySource, 触发所有存入的 SpringApplicationRunListener 的 environmentPrepared 事件
// 7.根据 this.bannerMode 判断是否打印 Banner, 以及打印在哪里, 根据 this.banner 判断打印什么样的 Banner (佛祖保佑.png)
// 8.根据 web 类型创建不同的 ApplicationContext, 这里的创建, 仅实例化而已, 没有从构造方法调用 loadBeanDefinitions 和 refresh 的逻辑
// 9.将 applicationStartup (步骤记录器)赋值给 context.
//10.对容器做些配置, 然后发布 contextPrepared 事件, 接着关闭引导容器; 然后根据 primarySource 使用 BeanDefinitionLoader 加载 BeanDefinition 到容器, 最后发布 contextLoaded 事件
//11.注册一个钩子, 当 JVM 关闭时, 相应的关闭 context, 然后调用容器的 refresh 方法(然后进入到 Spring 源码分析那段, 自行脑补)
//12.计时器停止计时, 接着打印计时数据
//13.发布 started 事件
//14.从容器中取出 ApplicationRunner/CommandLineRunner 两类 bean, 并调用它们的 run 方法.
//15.catch 到异常则发布 failed 事件
//16.发布 running 事件

StopWatch stopWatch = new StopWatch();
stopWatch.start();

// 创建一个引导容器, 并在此时(容器未使用前)从 spring.factories 扫描一些实现了 Bootstrapper 接口的类, 来给引导容器里注册一些东西(如果有需要)
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;

// 往系统变量里设置一个变量 headless, 看上去和 AWT 有关, 暂且忽略之
configureHeadlessProperty();

// 扫描 spring.factories 中配置的实现了 SpringApplicationRunListener 的类们
// 实例化后放到 SpringApplicationRunListeners (其就是个容器管理类) 存起来.
SpringApplicationRunListeners listeners = getRunListeners(args);

// 触发所有存入的 SpringApplicationRunListener 的 starting 事件.
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 将 args 内容中的参数(类似 --spring.port=9999)解析成键值对存到 applicationArguments 对象中.
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

// 创建了一个 environment 对象, 添加了好几个功能各异的 PropertySource,
// 触发所有存入的 SpringApplicationRunListener 的 environmentPrepared 事件
// 将 environment 中 spring.main 开头的配置数据, 一一对应绑定到 SpringApplication(即this)的字段上去
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);

// 将 environment 的 spring.beaninfo.ignore 配置复制到 System.Property 去(若不存在)
configureIgnoreBeanInfo(environment);

// 根据 this.bannerMode 判断是否打印 Banner, 以及打印在哪里, 根据 this.banner 判断打印什么样的 Banner (佛祖保佑.png)
Banner printedBanner = printBanner(environment);

// 根据 web 类型创建不同的 ApplicationContext, 但其实差别不大, 就时多了 web 容器的特征(如启动 tomcat)
// 另外与 Spring 源码分析时不同, 这里的创建, 仅实例化而已, 没有从构造方法调用 loadBeanDefinitions 和 refresh 的逻辑
context = createApplicationContext();

// 将 applicationStartup (步骤记录器)赋值给 context.
context.setApplicationStartup(this.applicationStartup);

// 对 context 做一些配置, 执行 initializers 的 initialize()
// 发布 contextPrepared 事件
// 关闭引导容器, 即发布 BootstrapContextClosedEvent 事件给之前加的 closeListener (监听者)
// 使用 BeanDefinitionLoader 根据 sources 和 run 方法参数 primarySource 加载 BeanDefinition 到容器中.
// 发布 contextLoaded 事件
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

// 注册一个钩子, 当 JVM 关闭时, 相应的关闭 context, 然后调用容器的 refresh 方法(然后进入到 Spring 源码分析那段, 自行脑补)
refreshContext(context);

// 留给子类扩展吧
afterRefresh(context, applicationArguments);

// 计时器停止计时, 接着打印计时数据
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}

// 发布 started 事件
listeners.started(context);

// 从容器中取出 ApplicationRunner/CommandLineRunner 两类 bean, 并调用它们的 run 方法.
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 发布 failed 事件
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}

try {
// 发布 running 事件
listeners.running(context);
}
catch (Throwable ex) {
// 发布 failed 事件
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}

温馨提示: 先在想看的地方打断点, 在打开调试模式, 可以清楚的看到对应的变量变化情况, 以此理解代码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// 来看看所有过程涉及方法的注释(大多数)
org.springframework.boot.SpringApplication#createBootstrapContext
// 1.创建一个引导容器(这个容器作用和 BeanFactory 类似, 但更简单得多, 仅有获取/注册 bean 对象等的功能)
// 触发存于 this.bootstrappers 的对象的 intitialize 方法来对引导容器进行初始化(即可以在引导容器未使用前往里面注册 bean 对象)
// 然后 this.bootstrappers 的数据有一部分是从 spring.factories 找 Bootstrapper 的类实例化后得到的, 然后也可以代码手动添加


org.springframework.boot.SpringApplication#getRunListeners
// 扫描 spring.factories 中配置的实现了 SpringApplicationRunListener 的类, 并调用形如 types 的参数的构造函数来实例化得到对象集合
// 再将这些对象集合放到 SpringApplicationRunListeners(其就是个容器管理类)中.
// 并绑定 applicationStartup (步骤记录器)


org.springframework.boot.SpringApplicationRunListeners#doWithListeners(java.lang.String, java.util.function.Consumer<org.springframework.boot.SpringApplicationRunListener>, java.util.function.Consumer<org.springframework.core.metrics.StartupStep>)
// 遍历持有的所有 SpringApplicationRunListener, 触发对应事件
// 在触发监听事件前后加入 StartupStep 监听所耗时间
// 步骤监听器的 accept 处理


org.springframework.boot.SpringApplication#prepareEnvironment
// 1.根据 web 类型创建一个 environment 对象
// 2.配置 environment
// 通过 ConversionService 类添加一些类型转换支持(如文件大小1024k=1M等)
// 将用户设置的 defaultProperties 添加到 environment 的 sources 中, 将 args 解析成一个 PropertySource 后加入到 environment 中
// 3.为 environment 添加一个名为 configurationProperties 的源, 其作用是将每一个 PropertySource 适配成 ConfigurationPropertySource.
// 4.触发所有存入的 SpringApplicationRunListener 的 environmentPrepared 事件.
// 5.将名为 defaultProperties 的 Property 源移最后(降低优先级), 另此 defaultProperties 不是 this.defaultProperties
// 6.将用户通过代码设置的要附加的 profile 设置到 activeProfiles 中去 (若存在且 environment 中不存在)
// 7.将 environment 中 spring.main 开头的配置数据, 一一对应绑定到 SpringApplication(即this)的字段上去
// 8.若不开启自定义 environment, 则将 environment 转换成 StandardEnvironment(默认行为)
// 9.由于上一步可能做了转换, 所以需要重新 attach 一次.


org.springframework.boot.SpringApplication#getOrCreateEnvironment
// 1.若存在一个 environment, 则直接返回
// 2.若不存在, 则根据之前推断得出的 webApplicationType 来创建对应的 Environment 对象.
// 3.这几个不同的类型区别也不大, 就 StandardServletEnvironment 多了3个 propertySources(其中一个是 JDNI, 比较重要)


org.springframework.boot.SpringApplication#configureEnvironment
// 1.通过 ConversionService 类添加一些类型转换支持(如文件大小1024k=1M等)
// 2.将用户设置的 defaultProperties 添加到 environment 的 sources 中, 将 args 解析成一个 PropertySource 后加入到 environment 中.
// 3. configureProfiles 是一个空方法, 看来是留给我们实现子类时扩展的.


org.springframework.boot.context.properties.source.ConfigurationPropertySources#attach
// 为 environment 添加一个名为 configurationProperties 的源, 其作用是将每一个 PropertySource 适配成 ConfigurationPropertySource.


org.springframework.boot.SpringApplication#configureAdditionalProfiles
// 将用户通过代码设置的要附加的 profile 设置到 activeProfiles 中去 (若存在且 environment 中不存在)


org.springframework.boot.SpringApplication#bindToSpringApplication
// 将 environment 中的配置数据, 绑定到 SpringApplication(即this)的一些字段上去
// 绑定规则是 spring.main 开头的配置数据与 SpringApplication 一一对应, 如若存在 spring.main.banner-mode=OFF, 则 this.bannerMode=OFF




org.springframework.boot.SpringApplication#printBanner
// 根据 this.bannerMode 判断是否打印 Banner, 以及打印在哪里
// this.banner 为文件路径, 若为空, 则打印 Spring Boot 默认准备的文本(这不换个佛祖保佑?)


org.springframework.boot.SpringApplication#createApplicationContext

// 根据 web 类型创建不同的 ApplicationContext, 但其实差别不大, 就几个细节不同罢了
// 以 AnnotationConfigServletWebServerApplicationContext 为例, 与 AnnotationConfigApplicationContext 的区别大致为
// 多加了一个 BeanPostProcessor 用于给 ServletContextAware/ServletConfigAware 接口注入 servletContext/servletConfig 对象
// onRefresh() 时, 调用 createWebServer() 启动 tomcat/jetty/undertow


org.springframework.boot.SpringApplication#prepareContext
// 1.设置 environment 对象
// 2.将 beanNameGenerator 注册到容器中 (若存在), 配置容器的 resourceLoader 和 conversionService (从 SpringApplication 获取)
// 3.将 initializers 根据 @Order 配置排序后, 遍历执行其 initialize 方法.
// 4.发布 contextPrepared 事件
// 5.关闭销毁引导容器(毕竟真正的容器已经准备好了, 这玩意就没用了)
// 6.为容器添加一些特殊的 bean, 对 beanFactory 做点小设置; 然后添加一个懒加载功能的 BeanFactoryPostProcessor, 作用是将所有的 BeanDefinition 的 lazyInit 设置为 true
// 7.创建一个 BeanDefinitionLoader, 解析 sources 得到 BeanDefinition 再注册到容器中. 和 Spring 的 new 容器时的 load 过程类似.
// 8.发布 contextLoaded 事件


org.springframework.boot.SpringApplication#postProcessApplicationContext
// 1.将 beanNameGenerator 注册到容器中 (若存在)
// 2.将 SpringApplication 的 resourceLoader 赋值给容器 (若存在)
// 3.为容器设置 ConversionService(类型转换工具)对象 (若配置了允许)


org.springframework.boot.SpringApplication#load
// 创建一个 BeanDefinitionLoader, 解析 sources 得到 BeanDefinition 再注册到容器中. 和 Spring 的 new 容器时的 load 过程类似.


org.springframework.boot.SpringApplication#refreshContext
// 1.注册一个钩子, 当 JVM 关闭时, 相应的关闭 context
// 2.调用容器的 refresh 方法


org.springframework.boot.SpringApplication#callRunners
// 从容器中取出 ApplicationRunner/CommandLineRunner 两类 bean, 并调用它们的 run 方法.
// 这里有两个不同点
// 1. XxxRunner 和 ApplicationListener 有何不同?
// 答案是 XxxRunner 的 run 方法可以直接取到程序启动的 args 参数, 而监听器要取则还需借助 environment
// 2.ApplicationRunner 和 CommandLineRunner 有何不同?
// 答案是 run 方法接受的参数形式不同, 一个是 字符串数组(原始的), 一个是解析好的 key:value 方便直接取用.

规矩我懂得, GitHub在这 注意分支哦

各种实现的原理

1
2
3
4
5
1.application.properties 是如何被加载到 Environment 中的?
2.@ConfigurationProperties 如何实现自动注入`application.properties/application.yml`中配置的值?
3.一些只需要改改依赖jar就可以切换(如tomcat->undertow)是怎么做到的? (@ConditionalXxx 的实现原理)
4.JdbcTemplateAutoConfiguration如何确保能够获取到 DataSource? (@AutoConfigureAfter 的实现原理)
5.为什么 Spring Boot 启动 main 方法就能访问 Tomcat? (SpringApplication.run() 启动 tomcat 实现原理)

application.properties 是如何被加载到 Environment 中的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1) run方法中创建了 Environment 对象, 当初始化好一些东西后会触发 environmentPrepared 事件
2) 通过 SpringApplicationRunListener 的 EventPublishingRunListener 转发事件给 ApplicationListener 这类监听者, 即 EnvironmentPostProcessorApplicationListener.
3) 这个类接收事件后, 遍历从 spring.factories 获得的 EnvironmentPostProcessor 对象, 执行其 postProcessEnvironment()
4) 其中 ConfigFileApplicationListener#postProcessEnvironment() 实现了配置文件的加载
5) 具体为 postProcessEnvironment 下的 addPropertySources()
6) 此方法将会扫描指定的路径下指定的某些文件
7) 然后使用 spring.factories 下的 PropertySourceLoader 一一尝试解析
8) 文件存在且解析正确则加入到 environment 的 sources 集合中.
某些路径: getSearchLocations() ,默认: classpath:/,classpath:/config/ ...
某些文件: getSearchNames() ,默认: application

# PS:
PropertySourceLoader 有 PropertiesPropertySourceLoader/YamlPropertySourceLoader
一个尝试后缀有 xml/properties, 另一个是 yml/yaml, 因为是遍历后 load, 所以所有可能性有:
classpath:/application.xml; classpath:/application.properties
classpath:/application.yml; classpath:/application.yaml
......

@ConfigurationProperties 如何实现自动注入application.properties/application.yml中配置的值?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1) 首先加了 @EnableConfigurationProperties 也会解析里面的 @Import 
2) @Import 则引入了 EnableConfigurationPropertiesRegistrar.class
3) 这是一个 ImportBeanDefinitionRegistrar 的实现类, 因此调用指定方法 registerBeanDefinitions
4) 指定方法 registerBeanDefinitions() 会将 @EnableConfigurationProperties 注解的值对应的类注册到容器中, 如 @EnableConfigurationProperties(RabbitProperties.class) 则会加载 RabbitProperties.class
5) 指定方法还注册了一些工具 bean 和一个重要的 BeanPostProcessor 在 registerInfrastructureBeans()中
6) 即 ConfigurationPropertiesBindingPostProcessor, 当我们要使用配置文件 bean(如 RabbitProperties)时,会实例化 bean 并触发 postProcessorBeforeInitialization()
7) 在 postProcessorBeforeInitialization() 中, 通过 org.springframework.boot.context.properties.ConfigurationPropertiesBinder#bind() 来完成实际的绑定
8) 其本质就是 Binder 的 bind 方法, 设定配置文件前缀即可将配置文件中的配置对应的绑定到 bean(如 rabbitProperties 对象) 中.

# PS
这里的 Binder 和 SpringApplication 里绑定 spring.main 开头配置文件那个类是同一个.

# 总结
就是先将 XxxProperties 类注册到容器中, 这样就可以通过 BeanPostProcessor 再实例化后将配置文件与属性绑定.

一些只需要改改依赖jar就可以切换(如tomcat–>undertow)是怎么做到的? (@ConditionalXxx 的实现原理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1) 在类上加上注解 @Conditional 或带有 @Conditional 的其他注解(即注解的子类亦会扫描)
2) 在所有的扫描类和注解的地方,如解析@Configuration, AnotatedBeanDefinitionReader 等 reader
会使用 ConditionEvaluator 的 shouldSkip() 判断是否可以加载, 时机点如下
AnnotatedBeanDefinitionReader#doRegisterBean() 的第二行代码
ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod() 第四行
ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass()
ConfigurationClassParser#doProcessConfigurationClass() 处理 ComponentScan 那段
3) 然后再 shouldSkip 中判断, 判断逻辑大致如下:
先遍历所有注解取得所有的 @Conditional 的 value, 这个 value 是具体的 Condition 实现, 如 OnClassCondition(实现了 Condition 的 matches 接口, 很多抽象类用以增强代码扩展性), 然后其加入到 conditions 中, 排序后遍历调用 matches(), 一个不匹配则返回 true, 代表应该跳过.

# TIPS:
ConditionOutcome 封装了是否匹配和匹配日志信息[为啥成功/为啥失败]
SpringBootCondition 提供了通用的根据 ConditionOutcome 判断是否匹配并记录日志信息的抽象类.
子类只需实现 getMatchOutcome(): 根据 metadata[注解信息] 返回 ConditionOutcome 对象.
因此, 如果我们要实现自己的 Condition, 可以继承它.

JdbcTemplateAutoConfiguration 如何确保能够获取到 DataSource? (@AutoConfigureAfter 的实现原理)

1
2
3
4
5
6
7
8
9
1) 首先所有的 XxxAutoConfiguration 都是借助 AutoConfigurationImportSelector 加载的, 其继承了 DeferredImportSelector.  
2) 这种 DeferredImportSelector 会延迟加载, 原理是 parse 后再加载, 而非 parse 执行过程中就加载.
3) 延迟加载机制 会先调用 process 方法, 将要加载的 class 保存起来, 然后再调用 selectImports 返回.
4) 此时 AutoConfigurationImportSelector.AutoConfigurationGroup 的 selectImports() 会调用 sortAutoConfigurations(), 也就是调用了 AutoConfigurationSorter.getInPriorityOrder()
5) getInPriorityOrder() 调用了 sortByAnnotation() 这个方法根据2个注解 @AutoConfigureBefore/@AutoConfigureAfter 排序.
6) 最后返回的就是有序的了. 另外, 这两个注解只能作用在 spring.factories 中 EnableAutoConfiguration 的类上才有效.

# 总结:
利用 DeferredImportSelector 的延迟加载, 将所有的 AutoConfiguration 所引入的 class 先存起来不加载, 然后又加入排序的逻辑, 使得真正加载时会根据排序结果依次加载.

为什么 Spring Boot 启动 main 方法就能访问 Tomcat? (SpringApplication.run() 启动 tomcat 实现原理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1) main 方法会调用 SpringApplication 的 run 方法, 里面会创建 applicationContext, 如是 webApplicationType = SERVLET, 则实现类为 AnnotationConfigServletWebServerApplicationContext
另外一提, webApplicationType 是根据 classpath 下是否有哪些类来推断的.
2) 这个类继承了 ServletWebServerApplicationContext
3) ServletWebServerApplicationContext 实现了 onRefresh()
4) onRefresh() 调用了 createWebServer()
5) createWebServer() 使用 ServletWebServerFactory.getWebServer() 获取 webServer 对象
6) ServletWebServerFactory 的具体实现类由 ServletWebServerFactoryAutoConfiguration 引入
详细见 ServletWebServerFactoryConfiguration.EmbeddedTomcat.class
7) 引入后调用 getWebServer(), 大概为 new Tomcat(), 绑定配置文件到 Tomcat 的一些属性上, 然后启动它.
8) 至此, run() 启动了 tomcat/jetty/undertow.

# TIPS:
Spring 使用工厂模式获取不同的 webServer, 而不同的 webServer 实现类其实是用 XxxAutoConfiguration 来自动注入的(还可以配合 @Conditional 决定何时加载).
这样如果新增一种 webServer, 只需要在写一个 XxxAutoConfiguration 注册一个 webServer 实现类的 bean 即可, 非常灵活(非常 nice).

常见的 XxxAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1.连接池
DataSourceAutoConfiguration 会 Import DataSourceConfiguration.Hikari.class
2.Mybatis
MybatisAutoConfiguration: 注册并配置了 SqlSessionFactory/SqlSessionTemplate
3.Spring MVC
DispatcherServletAutoConfiguration: 配置前端控制器和文件上传处理器
ServletWebServerFactoryAutoConfiguration: 注册 WebServerFactory 的实现类 bean
4.RocketMQ
RabbitAutoConfiguration: 注册并配置 RabbitTemplate
5.Redis
RedisAutoConfiguration: 注册并配置 RedisTemplate/StringRedisTemplate
6.邮件发送
MailSenderAutoConfiguration: 注册并配置 JavaMailSenderImpl
7.MyBatis-Plus
MybatisPlusAutoConfiguration: 注册 SqlSessionFactory, 并配置了其 plugins, 使 Mybatis-Plus 生效
8.定时任务
TaskSchedulingAutoConfiguration: 注册 ThreadPoolTaskScheduler
9.AOP
AopAutoConfiguration: 通过静态内部类加载了 @EnableAspectJAutoProxy

我所见到的设计模式

1
2
3
4
5
6
7
8
9
10
11
12
13
1.适配器模式: SpringConfigurationPropertySources
要将 Iterable<PropertySource<?>>
适配成 Iterable<ConfigurationPropertySource>
也即是 PropertySource --> ConfigurationPropertySource
#其实主要工作是: ConfigurationPropertySource#getConfigurationProperty() 里面调用了 PropertySource#getProperty(), 以此完成适配... 很适配器模式, 存一个未适配的对象, 在适配的方法中调用存储的对象的要适配的方法.

2.简单工厂模式: org.springframework.boot.ApplicationContextFactory#create
根据 webApplicationType 创建不同的 ConfigurableApplicationContext

3.观察者模式:
监听者: SpringApplicationRunListener
观察对象: SpringApplication 的生命周期(starting/environmentPrepared/contextPrepared/contextLoaded/started/running/failed)
管理者: SpringApplicationRunListeners, 负责遍历监听者广播对应事件

为了面向对象(首先你得有个对象)

method

定义一个method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Person struct {
name string
age int
}

func (p *Person) growup() { //想想去掉 * 会怎样?
p.age += 1
}

func (p Person) getName() string {
return p.name
}

func (p *Person) setName(name string) {
p.name = name
}

func main(){
p := Person{"wq",10}
println(p, p.name, p.age)
p.growup()
println(p.age)
p.setName("gudqs")
println(p.name)
}

使用 func ( 方法属于者 方法属于者类型) 方法名 (方法参数列表) (返回参数列表) {..}定义一个方法
传递 指针类型 使结构体值改变
所有自定义类型, 和一些内置类型均可作为方法属于者,即可拥有方法

方法继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Person struct {
name string
age int
}

type Student struct {
Person
no int
phone string
}

func (p Person) sayhi() {
println(p.name,p.age)
}

func main(){
s := Student{Person{"wq",20},1,"110"}
s.sayhi()
}

利用匿名字段可继承改字段类型的方法 同样的可以直接调用

方法重写

在之前代码 后添加一个 Student 的sayhi方法 , 实现重写

1
2
3
func (s Student) sayhi() {
println(s.name,s.no,s.phone,s.age)
}

查看执行结果, 如果学过java ,你一定已经露出了纯洁的笑容     : )

TIP

在方法定义时 , 方法属于者类型 为一个指针时, 方法种操作 指针不需要 加 *也可, go会自动加
不能为 int , []int 等类型 添加方法, 但通过自定义类型 ,如 type Int int , 则又可以添加方法 ,因此自定义类型具有高扩展性啊

interface (接口)

定义接口

1
2
3
4
5
type UserDao interface {
add()
update()
remove()
}

使用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main

import f "fmt"

type UserDao interface {
add(u User)
update(u User)
remove(u User)
findAll() []User
}

type User struct {
name string
id int
}

type UserDaoMysqlImpl struct {

}

type UserDaoOracleImpl struct {

}

func (dao UserDaoMysqlImpl) add(u User) {
f.Println("add user[mysql] :",u)
}

func (dao UserDaoMysqlImpl) update(u User) {
f.Println("update user[mysql] :",u)
}

func (dao UserDaoMysqlImpl) remove(u User) {
f.Println("remove user[mysql] :",u.id)
}

func (dao UserDaoOracleImpl) add(u User) {
f.Println("add user[oralce] :",u)
}

func (dao UserDaoOracleImpl) update(u User) {
f.Println("update user[oracle] :",u)
}

func (dao UserDaoOracleImpl) remove(u User) {
f.Println("remove user[oracle] :",u.id)
}

func (dao UserDaoMysqlImpl) findAll() []User {
users := make([]User,3)
users[0], users[1], users[2] = User{"wq",1}, User{"aa",2}, User{"gudqs",3}
return users;
}

func (dao UserDaoOracleImpl) findAll() []User {
users := make([]User,3)
users[0] = User{"gg",007}
return users
}

func main(){
var dao UserDao
u := User{"qq",88}
dao =UserDaoOracleImpl{}
dao.add(u)
dao.update(u)
dao.remove(u)
for i,u := range dao.findAll() {
f.Println(i,u)
}
dao =UserDaoMysqlImpl{}
dao.add(u)
dao.update(u)
dao.remove(u)
for j,u2 := range dao.findAll() {
f.Println("mysql:",j,u2)
}
}

一个东东 具有 某个接口的所有方法, 那么这个东东 可以赋值给这个接口类型的变量
这个特性说白了就是类似多态

接口类型 作为函数(方法) 参数

在上面代码基础上添加 如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type UserServiceImpl struct {}

func (service UserServiceImpl) add(u User, dao UserDao) {
print("service do :")
dao.add(u)
}

func main(){
service := UserServiceImpl{}
u := User{"wq",20}
dao := UserDaoMysqlImpl{}
service.add(u,dao)
dao =UserDaoOracleImpl{}
service.add(u,dao)
}

添加类型UserServiceImpl和对应的add方法, 使用UserDao作为参数, 然后修改main()

方法参数为接口类型, 好处是 可以接受更多的类型 , 只要那种类型有 这个接口的所有方法

空interface

1
2
3
4
5
6
7
8
9
10
type a interface {}

func main(){
var i int = 99
var str string = "wq"
a = i
println(a)
a = str
println(str)
}

这样 a类型 就可以接受任何类型了, 就像 java 的Object类型一样 !

嵌套 interface

1
2
3
4
5
6
7
8
9
type a interface {
swap()
len()
}

type b interface {
a
value()
}

然后b就有了 3个方法 , 对的, 想匿名字段继承一样

Comma-ok断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type a interface {}

type b struct {
name string
}

func main(){
list := make([]a,3)
a[0] = 1
a[1] = "wq"
a[2] = b{"wq"}
for _,element := range list {
ok,val := element.(b)
if ok {
println(" b is a a")
}
ok,val = element.(int)
if ok {
println("int is a a")
}
}
//solution 2
for _,ele := range list {
switch value :=ele.(type) {
case int :
println("ele is an int",)
case b :
println("ele is a b")
default :
println("ele is default type")
}
}
}

通过 变量.(类型) 获得返回值 ok 判断 该变量存的是否是 该类型
变量.(type) 仅在switch中 可以使用

为了扩展的扩展

流程控制

if

1
2
3
4
5
if condition {
// do something
} else if condition {

}

if 后接条件语句(表达式) , 无括号

1
2
3
4
5
6
7
if 9>8 {
//do some...
} else if 8>8 {

} else{

}

for

1
2
3
for expr1; expr2 ;expr3 {
// some code
}

expr1 为初试化变量语句, 仅执行一次
expr2 为循环条件语句, 每次循环前都会判断其值
expr3 是改变循环变量的地方, 每次循环后执行
其中expr1,expr2,expr3均可省略

1
2
3
for ; ; {
// some code
}

另一种是仅留下 循环条件语句 如:

1
2
3
for ; 9>8 ; {
// some code
}

此时 2个分号 也可省略, 即:

1
2
3
for 9>8 {
//some code
}

啊, 和java的while 多像

1
2
3
4
me := map[string]string{"name":"wq","age":"19","long":"200"}
for k,v := range me {
fmt.Println(k,v)
}

嗯, 是的foreach(for in) 就是这个feel
尤其提示, 如果不使用 k , 请用_ 替代, 避免编译错误

switch

1
2
3
4
5
6
7
8
switch exp {
case 0:
//do some ...
case 1,2,3:
//do some..
default:
//do some..
}

对于exp不需要括号
每个case后, 都默认带有break, 不会往下执行
如需执行后面的case,可使用fallthrough 关键字
可以用 , 号分隔代表多个case
default就不说了, 类似与if的else

最后写个冒泡排序法复习巩固下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

func main(){
slice := []int{3,4,2,9,1,0,88,44,20}
maopao(slice)
fmt.Println(slice)
}

func swap(slice []int, index1 int,index2 int) {
temp := slice[index1]
slice[index1]=slice[index2]
slice[index2]=temp
}

func maopao(slice []int) {
for i := 0; i< len(slice)-1;i++ {
for j:=0; j< len(slice)-1-i; j++ {
if slice[j]>slice[j+1] {
swap(slice,j,j+1)
}
}
}
}

99 乘法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main(){
num, num2 := 1, 1
for num<=9 {
num2 = 1
for num2<=num {
fmt.Printf("%d * %d = %d\t",num2,num,num*num2)
num2++
}
fmt.Println()
num++
}
}

使用了for的完整版 , if 判断
slice指针传递 可变性

函数

func语法

1
2
3
4
func funcName (params...) (returns...) {
//some code
return ...
}

如,无参无返回值

1
2
3
func test(){
//some code
}

单参数,单返回值

1
2
3
4
func test(id int) int {
//some code
return 0
}

多参数, 多返回值

1
2
3
4
5
func test (id int, skills []string) (int , map[string]int) {
//some code
numbers := map[string]int{"one":1,"two":2,"three":3}
return 0 , numbers
}

大概就是这些, 写个阶乘玩玩

1
2
3
4
5
6
7
8
9
func jiechen(n int) int {
if n==1 {
return 1
}
return n * jiechen(n-1)
}
func main(){
sum := jiechen(4) //24
}

defer延迟

1
2
3
4
5
6
7
func test(){
fmt.Println("func code run")
for i:=0; i<8; i++ {
defer fmt.Println("defer",i)
}
fmt.Println("func code last")
}

后进先出的队列机制, 逆序输出了 i
延迟执行, 在return之前

import

1
2
3
4
5
import (
. "fmt"
_ "fmt"
f "fmt"
)

用 . 代表无需前缀, 直接调用fmt的方法
用_ 代表不使用fmt的函数, 仅加载
其他名称则为别名 , 如 f.Println()

a func is a type or a value ??

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import "fmt"

func main(){
slice := []int{1,2,3,4,5,6,7,8,9}
fmt.Println("slice:",slice)
odd := filter(slice,isOdd)
even:= filter(slice,isEven)
fmt.Println(odd,even)
}

type funcType func(int) bool

func isOdd(i int) bool {
if i%2 ==0 {
return false
}
return true;
}

func isEven(i int) bool {
if i%2 ==0 {
return true
}
return false
}

func filter(slice []int,f funcType) []int {
var result []int
for _, value := range slice {
if f(value){
result = append(result, value)
}
}
return result
}

将func作为一个类型, 置于参数中, 由于go也是强类型, 所以需要添加自定义类型
将func作为实参(值)传递给另一个函数, 然后被调用

struct

语法定义

1
2
3
4
type person struct {
name string
age int
}

使用

1
2
3
4
5
6
var p person
p.name="wq"
p.age = 79

p1 := person{"wq",89}
p2 := person{name:"wq",age:99}

匿名字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type humen struct {
name string
age int
long int
}

type Skills []string

type person struct {
humen
int
Skills
name string
phone string
}

func main(){
p := person{humen{"wq",39,99},666,[]int{"c","java","c#"},"pwq","110"}
fmt.Println(p,p.name,p.age,p.long,p.phone,p.Skills,p.int)
//可直接访问匿名字段的 字段, 但可能会覆盖匿名字段的 字段
}

通过2个struct 实现了类似 继承
通过不给字段名,仅给字段类型定义一个匿名字段
可直接访问匿名字段, 但可能覆盖匿名字段中的字段
匿名字段 还可以为 自定义类型, 基础类型 , slice, map, array等

为了基础的基础

package , import

1
2
3
4
5
6
7
package main

import "fmt"

func main(){
fmt.Println("Hello World !")
}

包 , 与python类似, 与java不同. 用于模块化. 通过声明包, 和导入包可以实现程序的相互调用. 如 导入 fmt , 使用fmt的函数Println()
main.main()是 程序的运行入口, 无参数, 无返回值

变量声明

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"

func main(){
var complete string = "I 'm complete !"
var autoType = "I'm auto "
var simple := "I'm simple "
complete, autoType, simple = "complete again", "auto again", "simple again"
fmt.Println(complete,autoType,simple)
}

如上所示, 获得一个变量有3种方式

完整的声明和赋值 (也可只声明, 后赋值)
声明时不写类型 , 必须同时赋值自动推导类型
省略var , 使用 := 声明+赋值, 仅用于声明
###常量

1
2
3
4
5
6
7
const SEX_MALE int = 0
const SEX_FEMALE = 1
const (
MALE = iota //0
FEMAILE //1
WHAT //2
)

使用const关键字可声明常量, 使用()可一次声明多个 使用iota可便捷声, 规则为:
遇到const重置为0
同一行值相同
上一行为iota, 则本行不赋值时为 上一行iota值 + 1, 以此类推

数据类型小计

整形

int : int8 , int16, int32, int 64
uint : uint8, uint16, uint32, uint64
rune, int, byte

其中 int 与 uint 的区别是 有无符号 , rune 是 int32的别称, byte是uint8的别称, int虽然是32位但与int32不可互用

Boolean

默认为false, 与java类似, 数0,空等不代表 false

1
2
3
4
var ok bool 
fmt.Println(ok) //false
var gameover = true
isover := false

字符串

与java类似, 双引号包围

1
var str = "I'm a string!"

当多行时 使用 反引号

1
2
3
4
var multistr = `This is 
a
multiline
`

使用 ` 时, 不转义\n,\t等

使用string类型时, 注意string不可变, 类似java . 但可强转为byte[] 类型后操作

1
2
3
4
var str="hello"
str[0] = 'c' //wrong!!!
var c = (byte[])str
c[0]='c'

error

Go内置有一个error类型,专门用来处理错误信息,Go的package里面还专门有一个包errors来处理错误:

1
2
3
4
err := errors.New("I am error message!")
if err != nil {
fmt.Print(err)
}

array

在go中, array是值类型, 而slice(切片),map 才是引用类型

1
2
3
4
5
6
arr := [3]int{1,2,3}
array:=[...]int{3,2,1,0}
arr2 := [10]int{1,2,3} //后7个元素则为零值
var arr3 [10]int
arr3[0]=9
arr3[1]=10

关于数组的赋值,取值与c和java类似
对于数组的初始化,分完整,部分(不写长度, 不写所有元素)
注意array是值类型, 互相传递赋值时, 不会相互影响

slice

1
2
3
4
5
6
array := [10]int{1,2,3,4,5,6,7,8,9,10}
aslice := array[:]
bslice := aslice[2:]
bslice[3] = 99
array[0] = -99
fmt.Println(aslice,bslice)

切片 , 可通过数组[n:m] 获取
切片是引用类型, 改变bslice , 同时aslice也会改变
改变arr不会改变aslice , 因为array是值类型

map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main(){
var numbers map[string]int =map[string]int{"all":9999}
others := make(map[string]int)
others["name"]=007
numbers["one"] = 1
numbers["ten"] = 10
numbers["three"] = 3
delete(numbers,"one")
val , ok := numbers["not ex"]
fmt.Println(numbers,numbers["one"],others,len(numbers),val,ok)
}

map类似与python种的字典, 操作与slice类似
元素赋值, 取值使用[]
map初始化使用 make 或者 map[string]int{}
使用delete删除key
使用多返回值检测是否存在key
使用len查看元素个数

Fomo3D

Fomo3D 合约源码分析

准备工作

环境准备 (用于调试合约)

  • git, nodejs, Chrome
  • ganache-cli, remix-ide

代码 及 IDE

安装好 Git 后, 下载源码 git clone https://github.com/reedhong/fomo3d_clone.git
安装好 nodejs 后, 使用 npm 安装2个东西(建议使用国内镜像源:cnpm)
npm install ganache-cli -g & npm install remix-ide -g

至于IDE 上的选择, 只要 IDE 支持 sol 语法, 如 idea 就有 solidity 插件, 亦或者 vscode 也很棒, 而且中文支持比较好, 还对于大文件 js 及 json 打开速度比较快, 编辑也比较流畅( idea 可能是插件太多, 各种语法解析比较卡)

源码结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+-- interface	 
| +-- DiviesInterface.sol
| +-- F3DexternalSettingsInterface.sol
| +-- HourglassInterface.sol
| +-- JIincForwarderInterface.sol
| +-- JIincInterfaceForForwarder.sol
| +-- PlayerBookInterface.sol
| +-- PlayerBookReceiverInterface.sol
| +-- TeamJustInterface.sol
| +-- otherFoMo3D.sol
+-- library
| +-- F3DKeysCalcLong.sol
| +-- F3Ddatasets.sol
| +-- MSFun.sol
| +-- NameFilter.sol
| +-- SafeMath.sol
| +-- UintCompressor.sol
+-- Divies.sol
+-- F3Devents.sol
+-- F3DexternalSettings.sol
+-- FoMo3Dlong.sol
+-- Hourglass.sol
+-- JIincForwarder.sol
+-- PlayerBook.sol
+-- TeamJust.sol
+-- modularLong.sol

以上就是 reed 大佬整理的源码结构, 看到这么多文件, 心里感觉好慌, 别怕, 其实大多数文件都是摆设, 没有太多逻辑代码, 我们主要需要看的, 也就是那么几个合约, 既然如此, 我们先排除一些用处不大, 非游戏关键核心的合约

各大收款合约

  • JIincForwarder.sol (JIincForwarderInterface 类型变量的实际引用), 用于向项目基金会转账
  • otherFoMo3D.sol (游戏 activate 前必须设置的 otherFomo 变量的实际引用), 向不知道哪个地址转账
  • Divies.sol (DeviesInterface 类型变量的实际引用), 用于 p3d 分红

JIincForwarder.sol

这个合约就是向 基金会地址 转发 ether, 单独写一个中转的好处就是灵活, 这个合约可以做到基金会地址安全转移, 也就是说中途可以改变基金会的转账地址, 而这个过程需要新旧2个合约共同完成(旧.startMigration(新地址)–> 新.finishMigration(), 中途 旧方可以 旧.cancelMigration(), 而完成地址转移后, 新地址完全替代旧地址 )
其中比较转账逻辑就是调用下面的这个接口对应的实际合约 的 deposit 方法

1
2
3
4
interface JIincInterfaceForForwarder {
function deposit(address _addr) external payable returns (bool);
function migrationReceiver_setup() external returns (bool);
}

至于现在这个基金会的地址到底是啥, 可以通过 status() 方法查看哦

otherFoMo3D.sol

这个合约很有意思, 或者说它的背后很有意思, 大家都想知道 其他的 fomo 到底是啥, 据说不是 soon 版
至于逻辑上, 这个 potSwap 的调用时机是在玩家买 key 的时候, 而它的作用, 我认为是游戏间的奖池交换
比如说, fomolong 共有100个 ether 买入, 那么就会有1%流向 otherFomo 的奖池, 同理, otherFomo 里应该也会有这个逻辑的存在, 这么做有啥用就交给大家自己思考了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

interface otherFoMo3D {
function potSwap() external payable;
}

fomo3Dlong 代码: (fomo3Dlong本身也可以是一个 otherFomo, 甚至在 真正的otherFomo 里它的那个 otherFomo 就是 fomo3Dlong 也不一定)

function potSwap()
external
payable
{
// setup local rID
uint256 _rID = rID_ + 1;

round_[_rID].pot = round_[_rID].pot.add(msg.value); // 奖池金额增加
emit F3Devents.onPotSwapDeposit(_rID, msg.value);
}

Divies.sol

这部分是给 P3D 分红的, 代码很简单, 就一个转账的调用, 调用时机上, 首先是买 key 的钱被瓜分时, 有它的一份, 其次当一轮 (Round) 结束后, 又会根据赢的队伍来分配奖池, 抽出一部分给到 P3D

1
2
3
interface DiviesInterface {
function deposit() external payable;
}

当然这其中如何给 P3D 分红我还没搞太懂, 大致流程貌似是: 买 key分红 –> 调用 Divies 的 deposit 方法, Divies 合约中此方法无具体实现(空方法, 啥也不干, 就收钱) –> 预计什么时候会有 P3D 的玩家来调用这个合约的 distribute 方法, 而 这个方法的作用似乎是将 分红转来的钱拿去投入 P3D, 然后卖出, 根据传入的百分比决定是否继续投入或重复投入和售出多少次, 最后把钱提现回来(可能就没多少了), 而钱通过10% 的分红机制全给了 P3D 的用户??? 这一块一直不太懂, 而且这个方法的 调用时机不明, 调用时还增加了 时间限制和拥挤队列的限制. 总的将这里面就是存在给 P3D 分红的钱, 但这钱啥时候 给 P3D, 我还是没猜出来.

3大合约

光是转账合约就感觉有些看不懂了, 真是头疼啊, 只好把不懂的放下, 留待日后琢磨. 还是先分析游戏核心代码吧

  • TeamJust.sol
  • PlayerBook.sol
  • FoMo3Dlong.sol

TeamJust.sol

首先看 TeamJust.sol , 这个是用来做权限控制的, 里面 除了与 muitiSig( 这个以后说 )相关的几个方法, 也就是管理 admin 和 dev 了, 如 addAdmin removeAdmin, 而 isDev isAdmin 则是拿来给其他合约调用(比如 playerBook 的 onlyDevs)

1
2
3
4
5
6
7
function setup(address _addr)
onlyDevs()
public
{
require( address(Jekyll_Island_Inc) == address(0) );
Jekyll_Island_Inc = JIincForwarderInterface(_addr);
}

经过我的观察发现, 这个 teamJust 合约应该是比较后加的, 比如 fomo3Dlong 合约的激活就没有使用, 而2个合约不同的对于 Jekyll_Island_Inc 的赋值也让我推测这可能是较新的写法. 我也觉得这种通过调用合约赋值的方式比较好, 所以在我整的 项目 fomo3d_truffle 中, 我把 activate 函数的用户限制 也改成了 用 teamJust 来做, 而 其中的 playerBook 和 teamJust 实际合约地址也是通过 类似上面 setup 的方式 赋值, 这么做还有个好处就是可以通过 truffle 一键把这些合约部署且赋值, 而不是弄一个改源码重新编译这种测试起来比较麻烦的方式

PlayerBook.sol

这个合约主要是管理 玩家信息, 而玩家信息则分为 name, id, addr, id 是根据地址是否存在自增生成的, 而 name 则是通过 花钱注册可用于推广获取提成的! 合约内大多方法都像个数据库一样均为 crud 操作, 夹带的逻辑无非就是一些验证, 其他的都比较少, 里面比较有意思的点就是 addGame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function addGame(address _gameAddress, string _gameNameStr)
onlyDevs()
public
{
require(gameIDs_[_gameAddress] == 0, "derp, that games already been registered");

if (multiSigDev("addGame") == true)
{deleteProposal("addGame");
gID_++;
bytes32 _name = _gameNameStr.nameFilter();
gameIDs_[_gameAddress] = gID_;
gameNames_[_gameAddress] = _name;
games_[gID_] = PlayerBookReceiverInterface(_gameAddress);

games_[gID_].receivePlayerInfo(1, plyr_[1].addr, plyr_[1].name, 0);
games_[gID_].receivePlayerInfo(2, plyr_[2].addr, plyr_[2].name, 0);
games_[gID_].receivePlayerInfo(3, plyr_[3].addr, plyr_[3].name, 0);
games_[gID_].receivePlayerInfo(4, plyr_[4].addr, plyr_[4].name, 0);
}
}

这里是把 fomo3Dlong 的地址和名称传入, 然后就会通过接口向 fomo3Dlong 传入几个预设的玩家信息(来自 playerbook的构造方法), 而调用过这个方法后, registerNameXxxxFromDapp 这样的方法才能不被 isRegisteredGame 拦截. 所以部署时, 这一步是必做的.

其他的几个点: 可设置的注册费用, 且费用被转到基金会; 购买 key 邀请分红总是和访问的链接的推广码有关, 只有在无推广码时, 才从历史中获取 laff, 而 laff 每访问一个推广码(并买了 key)都在改变

FoMo3Dlong.sol

主要合约啊, 先看下 所有的 state 变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
string constant public name = "FoMo3D Long Official";
string constant public symbol = "F3D";
uint256 private rndExtra_ = extSettings.getLongExtra(); // length of the very first ICO
uint256 private rndGap_ = extSettings.getLongGap(); // length of ICO phase, set to 1 year for EOS.
uint256 constant private rndInit_ = 1 hours; // round timer starts at this
uint256 constant private rndInc_ = 30 seconds; // every full key purchased adds this much to the timer
uint256 constant private rndMax_ = 24 hours; // max length a round timer can be
uint256 public airDropPot_; // person who gets the airdrop wins part of this pot
uint256 public airDropTracker_ = 0; // incremented each time a "qualified" tx occurs. used to determine winning air drop
uint256 public rID_;

mapping (address => uint256) public pIDxAddr_; // (addr => pID) returns player id by address
mapping (bytes32 => uint256) public pIDxName_; // (name => pID) returns player id by name
mapping (uint256 => F3Ddatasets.Player) public plyr_; // (pID => data) player data
mapping (uint256 => mapping (uint256 => F3Ddatasets.PlayerRounds)) public plyrRnds_; // (pID => rID => data) player round data by player id & round id
mapping (uint256 => mapping (bytes32 => bool)) public plyrNames_; // (pID => name => bool) list of names a player owns. (used so you can change your display name amongst any name you own)

mapping (uint256 => F3Ddatasets.Round) public round_; // (rID => data) round data
mapping (uint256 => mapping(uint256 => uint256)) public rndTmEth_; // (rID => tID => data) eth in per team, by round id and team id

mapping (uint256 => F3Ddatasets.TeamFee) public fees_; // (team => fees) fee distribution by team
mapping (uint256 => F3Ddatasets.PotSplit) public potSplit_; // (team => fees) pot split distribution by team

大部分都可以通过 变量名 猜出个大概, 实在不行可以搜索大致看一下哪里用了, 结合的先看一下, 其他都是各种数据, 没啥复杂的, 这里就主要看下 fees_ 和 potSplit_

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Team allocation percentages
// (F3D, P3D) + (Pot , Referrals, Community)
// Referrals / Community rewards are mathematically designed to come from the winner's share of the pot.
fees_[0] = F3Ddatasets.TeamFee(30,6); //50% to pot, 10% to aff, 2% to com, 1% to pot swap, 1% to air drop pot
fees_[1] = F3Ddatasets.TeamFee(43,0); //43% to pot, 10% to aff, 2% to com, 1% to pot swap, 1% to air drop pot
fees_[2] = F3Ddatasets.TeamFee(56,10); //20% to pot, 10% to aff, 2% to com, 1% to pot swap, 1% to air drop pot
fees_[3] = F3Ddatasets.TeamFee(43,8); //35% to pot, 10% to aff, 2% to com, 1% to pot swap, 1% to air drop pot

// how to split up the final pot based on which team was picked
// (F3D, P3D)
potSplit_[0] = F3Ddatasets.PotSplit(15,10); //48% to winner, 25% to next round, 2% to com
potSplit_[1] = F3Ddatasets.PotSplit(25,0); //48% to winner, 25% to next round, 2% to com
potSplit_[2] = F3Ddatasets.PotSplit(20,20); //48% to winner, 10% to next round, 2% to com
potSplit_[3] = F3Ddatasets.PotSplit(30,10); //48% to winner, 10% to next round, 2% to com

fees_ 就是用来决定 玩家 买 key 后, 买 key 的 ether 怎么分配, 其中 2% 基金会(com) + 1% (otherFomo) + 1% 空投池 + fees_[].p3d % P3D + fees_[].gen % 收益, 10% 给 推荐人(无则给P3D)
总结就是 14% 固定 + 86% 可设定, 86% 分3块( gen+p3d+pot ),所以2队是56% gen + 10% p3d + 20% pot, 其他队伍类似
potSplit_ 类似, 固定的 48%(win)+2%(com) + 50% 可设定, 分3块(gen+p3d+nextround), 如2队的 20 gen + 20 p3d + 10 next

然后讲讲所有的方法, 简单的归类下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
修饰器
isActivated() //拦截游戏未激活
isHuman() //听说拦截非人类?
isWithinLimits(eth) //拦截太穷的人和 v 神 ???

ether 买 //从不同地址进的, 第一个参数是推荐人标识, 第二个是选的 team
buyXid(id, team)
buyXaddr(addr, team)
buyXname(name, team)

valuts 买 //从不同地址进的, 第一个参数是推荐人标识, 第二个是选的 team, 第三个是根据 key 数量计算出来的 eth
reLoadXid(id, team, eth)
reLoadXaddr(addr, team, eth)
reLoadXname(name, team, eth)


buyCore // 这里就是判断了一下本轮是否结束了, 然后直接调用的 core,当然结束会走 endRound
reLoadCore // 同上, 结束的判断, 还有就是减去 gen 的金额, 再调用 core
core // 限制前100eth, 更新 end 时间, 超过0.1eth 判断空投, 更新玩家及轮次等数据, 调用2个分红方法
distributeExternal // 给固定的13% (10% aff,2% com,1% otherFomo) 及 P3D 打钱
distributeInternal // 给空投1% 和 gen 和 pot 打钱

提现跑路
withdraw()

结束一轮
endRound() // pot 分成5分, win 拿48%, 2%给 com, 还有 gen, p3d, nextRound 则根据配置来分配, 其中 p3d 和下一轮逻辑比较简单, 而 gen 我还没太懂, 因为涉及到 mask 的我都没看明白( 没时间细看, 全是数学, 要慢慢推理分析 )

注册 name //注册一个 name 用于推广获取提成, 第一个参数是 name 标识, 第二个是推荐人的标识, 第三个是是否同步到其他游戏
registerNameXID(name, id, all)
registerNameXaddr(name, addr, all)
registerNameXname(name, name, all)

玩家信息相关 , 前2个一般是给外部调用的
receivePlayerInfo //将传入玩家信息储存
receivePlayerNameList //储存玩家的所有name
determinePID //确定玩家信息, 若无则生成一个 pid

玩家分红, keys相关
calcUnMaskedEarnings // 实现看不懂, 不过方法作用是用来计算能提现的收益
calcKeysReceived(rid, eth) // 根据轮次返回 用eth能买多少 keys
iWantXKeys // 根据 key 数量返回需要多少 eth
managePlayer // 第 x 轮时将上一轮的收益移至此轮, 仅轮次开始后第一次购买执行
updateGenVault // 计算及更新收益
updateMasks // 更新被锁定的收益
withdrawEarnings // 计算可提现的收益

这么多方法, 我也只能列个大致作用和我看的懂的逻辑, 具体的细节等我参透再出文章

最后总结游戏大致逻辑 : 玩家买 key–> buyXxx(relaodXxx) 方法–> xxxCore –> core –> distributeExternal & distributeInternal –> 游戏结束 –> 玩家 buy 触发 endRound –> 分了钱 pot 的钱, 部分转入下一轮 –> 激活新一轮 –> 接上最开始 进入循环 !!! 当然中途可以提现自己没锁住的收益, 以及注册 name 拉人啥的.

几个有意思的类库

MSFun.sol

首先说下, 这个库是用来做多重签名的, 啥意思呢? 就是一个方法, 必须好几个(多)人同意执行, 最后才会执行. 用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
*                                ┌────────────────────┐
* │ Setup Instructions │
* └────────────────────┘
* (Step 1) import the library into your contract
*
* import "./MSFun.sol";
*
* (Step 2) set up the signature data for msFun
*
* MSFun.Data private msData;
* ┌────────────────────┐
* │ Usage Instructions │
* └────────────────────┘
* at the beginning of a function
*
* function functionName()
* {
* if (MSFun.multiSig(msData, required signatures, "functionName") == true)
* {
* MSFun.deleteProposal(msData, "functionName");
*
* // put function body here
* }
* }

大致就是先导包, 然后定义一个 MSFun.data 作为区分合约的标识, 然后再方法中使用 if 包围, if 第一句就是将之前的签名清空

MSFun.multiSig( data标识, 需要签名的数量, 方法名称 )

最后说下此类库在 fomo 中的样子: 首先 data 照旧, 而需要签名的数量来自 teamJust.sol, 它的定义是构造是初始为1, 以后每 add 一个 admin 或 dev 就把对应的 requiredSignatures 加一, remove 同理, 减一. 所以在部署时不改代码的话, 只要满足对应的身份限制, 加了这个MSFun.muitiSig 的方法默认是一个人调用就能执行

SafeMath.sol

这个没啥好说的, 操作金额必备, 听说狼人杀就是少了这个被攻击的(整形溢出), 也许可以不懂怎么攻击, 但一定要懂怎么防范, so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @dev Multiplies two numbers, throws on overflow.
*/
function mul(uint256 a, uint256 b)
internal
pure
returns (uint256 c)
{
if (a == 0) {
return 0;
}
c = a * b;
require(c / a == b, "SafeMath mul failed");
return c;
}

如你所见, 简单的判断即可确保不会由于溢出导致数据错乱

F3DKeysCalcLong.sol

我只能猜到作用, 至于完全理解… 没上过大学的我瑟瑟发抖

1
2
3
4
keysRec(curEth, newEth)			// 第一个参数就是using 后的调用方, 第二个参数是 准备花的 eth, 如我花0.01 eth , 用 round_[rId].eth.keysRec(0.01 eth); 得出的就是当前轮次时0.01eth 能买多少个 key, 注意返回的 keys 很大, 1个 实际上是 1e18 吧, 
ethRec(curKeys, sellKeys) // 同上, 输入想买的 keys 数量, 返回当前轮次 keys 基数下购买 keys 需要花的 eth
keys(eth) // 根据 eth 计算可得多少 keys
eth(keys) // 根据 keys 计算需要多少 eth

bundle.js 中, iWantKeys 逻辑

1
2
count = BN(parseInt(count) * 1e18)
let priceQuotation = await JUST.Bridges.Browser.contracts.Quick.read('iWantXKeys', count)

keys 和 eth 应该是对应的, 而 eth 的变化规律如果画图的话应该是 指数级上升? 可以画成函数看看

Fomo3D 源码部署指南

代码在我的 gayhub 上 : fomo3d_truffle , 以下是 README

部署合约:

一共部署了3个合约, 其中3个收款地址被砍掉(改成了部署者的地址), p3d 也砍掉了
3个合约 我偷懒没有把合约地址写死, 用的是后续的 set 方法, 所以如果 truffle migrate 最后那段报错了, 可能没有 set 成功, 需要用其他方式调用( truffle console, 或者 remix 等)
合约部署完, 如无报错, 直接复制走 FoMo3Dlong 的地址就行了

1
2
3
4
5
6
npm install ganache-cli -g
ganache-cli -l 471238800 -g 1 # 开启 testrpc 同时设定 gasLimit 和 gasPrice
truffle compile
truffle migrate --reset # 执行后, 复制 FoMo3Dlong: 后跟的地址
#直接输出最终合约地址, 将不会打印编译过程
truffle migrate --reset | grep 'FoMo3Dlong: 0x' | awk '{ print $2 }'

推荐做法
truffle migrate –reset > migrate.log
cat migrate.log # 查看有无错误, 如合约均部署成功,但最后报错, 可能有几个赋值方法没有执行(我部署到 kovan 时就发生了这事,可以参考 migrations/2_deploy_fomo3d.js逻辑手动执行)
cat migrate.log | grep ‘FoMo3Dlong: 0x’ | awk ‘{ print $2 }’

部署前端:

前端没有太多需要改的地方, 若使用英文版, 可参考下面命令行修改地址方式
若选择 bundle-cn.js 这个中文版, 则自己找到要修改的地方, 手动修改也行的
顺便说下 cn 里面还是 kovan测试网络的配置, 如需使用可把 bundle.js 里面的本地配置拷贝下

1
2
3
4
cd src/js
sed -i "" 's/{{address}}/0x00/g' bundle.js # 非 mac 去除 -i 后的 ""
cd ../../
npm install & npm run start

游戏激活

刚想到一个不一定靠谱的简单方式, 把 migrations 下那个 js 里面加一个 activate 的方法调用

1
2
npm install remix-ide -g  # 安装个本地的 remix-ide
remix-ide #注意此时处于项目根目录

OK, 浏览器访问 remix-ide, 点击左上角第6个图标( Connect to localhost ), 弹框继续 connect

左边多出 localhost, 点击 contracts 下的 FoMo3Dlong.sol 文件, ctrl + s , 触发编译 , 下一步

点击右边的上边的 Run, 选择 web3 provider, 如端口不变, 一路 next, ok, 往下看, 有个选择 合约的 select, 选中 FoMo3Dlong, 然后在 输入框中输入 migrate 得到的合约地址, 然后点击 At Address

最后点下 合约的 activate 方法