开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

算法转 Java 后端,2021秋招斩获腾讯、京东、百度等大

发表于 2021-05-18

看到公众号上逐渐的放出了 2022 的秋招信息,意识到又一年的秋招大戏要开始了,也不由的想起了去年这会的自己。就想写这篇文章记录一下曾经的秋招之路,也希望能帮助到看到这篇文章的各位。

推荐👍:Github标星100k的Java面试指南

个人简介

不用太多介绍,也不用太多指教,三秒钟之内就能让大家尖叫…嗯…好了,不闹了。开始!

我是 2021 年毕业的研究生。本科是在一所双非大学读的,专业是软件工程,本科时学过一些 Java 开发。因为学历一般,又对科研存在幻想,我选择了考研,拼了老命总算是考上了一所中游 985。

读研期间,我主要做的是自然语言处理方向。但是!因为我们组做自然语言处理是在我读研刚入学时起步的,甚至当时整个实验室只有一块显卡,所以,研一基本上没做什么东西。研一暑假的时候,我才在师兄的帮助下入了门,搞了一个研究性的项目和一个工程类的算法项目。

迷茫期

当时是 2020 年的 2 月底,也就是研二的下学期,因为疫情被困在家,当时的想法还是做算法,于是就把算法的项目准备了一下,又准备了准备基础知识,就开始投实习试水。但是因为没有比较硬的论文,也没有比较硬的项目,所以投了几家大厂都没理我。

虽然,美团给了我面试机会,但是,我被面试官吊打了。到最后,只过了两家还行的“中厂”。但是,因为导师严禁出去实习,所以也没实习成。

那段时间特别迷茫,天天在网上问一些算法大佬,焦虑的不行!😥

我当时想:以我自己的条件准备到 6 月,几乎没可能在秋招面试上岸大厂的算法岗。

算法转 Java 后端

整个 3 月都在纠结一个事情,继续硬刚算法还是转做 Java 后端。算法确实工资比较高,但是自己其实打心里更喜欢做一些工程应用(因为就算让我做算法,我也喜欢做工程应用类型的算法)。

并且,当时自己的条件进大厂算法已经很难了,肯定去不了好的算法团队,拼一拼开发或许还能去比较好的团队。

但是,因为 Java 主要是在本科时学的,已经很久没有做过,很多都忘了,甚至 Java 语法都很生疏了。并且,像 JVM,Redis,RabbitMQ 相关的,当时也没学过,一直在想就几个月的时间真的能不能把这些东西学好。还好在四月初的一天中午,躺在床上睡不着,做出了一个重要的决定: 转 Java 后端。

推荐👍:2021 最新Java实战项目!太凎了!

在咨询了几个学长学姐,以及几个朋友以后,订好了学习计划。当时的计划是这样的。

  1. 首先复习 Java 的基本语法以及一些集合用法,先达到能比较熟练的用 Java 写代码。因为从 2 月多就开始刷 LeetCode 了,当时已经用 Python 刷了 30 多道,就又用 Java 把这些做过的题写了一遍,到 9 月底我的秋招结束时一共刷了 130 道左右,刷了两遍。
  2. 把自己本科时用 Java 做的电商系统又拿出来温习了一下改成了秒杀系统,这当作我的第一个项目。另一个项目是看着网课仿照 Spring 源码做了一个简化版的 Spring 框架(只实现了几个最基本的功能)。
  3. 开始学习 JVM,MySQL,Redis 的原理,以及 Java 集合的一些实现方法等。
  4. 这是在当时 4-6 月所做的准备。到六月的时候,项目梳理的不太清楚,其实有些地方是看网上代码写的,只是能用了,具体怎么做自己也说不清,那些面试基础知识也记得不太劳。但是因为时间也差不多了,就开始往外投简历。

推荐👍:计算机优质书籍搜罗+学习路线推荐!

崩溃期

2020 年的 6 月底到 8 月中下旬,可以说是最艰难的一段时间。6 月中下旬开始陆续向一些公司投简历,大部分简历投了都石沉大海,或者笔试做了没了消息。

好不容易在七月初收到了招银网络的笔试和面试机会。招银网络的面试算是问的很简单了,但是因为当时准备的不好,一面后我被无情的放入了招银网络的人才库中。

我很难受,第一场面试就打了败仗,这让我有点措手不及!

接下来将近有 10 多天的时间,我没有接到任何公司 hr 的电话。然后又在随后的一个月中,百度一面挂、一点咨询三面挂、快手一面挂、字节二面挂、网易笔试没过…以及各种公司投完简历没信,或笔试没过。这段时间一天可能会有两场笔试外加三场面试,到了晚上累的说话都不想多说。但是迎面而来的是一封封进入人才库的感谢信。

守得云开见月明

还好的是我在那段时间并没有放弃学习。我在学长学姐的指导下,开始梳理我的项目。

我在秒杀项目上分别从 MySQL 部分以及 Redis 部分做了一些自己的优化(自己设计的架构,也算是有了一些创新点)。并且,将简化版的 Spring 框架做了一些梳理,较为深刻的理解了 Spring 那几个基本功能的设计方法以及意图。

这两个项目在面试后期确实起到了很大的作用。并且对 MySQL 的一些底层设计做了一些了解,比如 MySQL 怎样实现回滚。并且开始在网上看一些还不错的面试题总结,就比如 JavaGuide~(我真看了,不是打广告)。并且在一场场的面试中,将这些知识点记得很牢。

在八月下旬时,我觉得自己的实力应该有很大的提升了,就开始换了个部门重新面试百度,一面的面试官是个比较较真的人,我刚介绍了项目几句,那个面试官直接打断我说,你等会,咱们一点一点的来,于是他一句我一句的一点点的把秒杀项目梳理了一遍。在他严肃的追问了很多问题后,突然语气很满意的说,我觉得你做的挺不错的,在那一瞬间我感觉我的任督二脉都打通了,感觉已经不怕任何面试官再问这个项目了,后续的半个小时,他问了我一些基础知识并且聊了一些职业规划,就满意的结束面试了(但是事后那个部门貌似招够人了,在二面时面试官也很满意,可是依然没通过面试,但是当时不知道)。

在百度一面结束后就收到了度小满打来的约面电话,约了第二天的面试。度小满的面试经历算是我整个面试的转折点,重要性和情节跌宕起伏性堪比李云龙打平安县城。一面的面试官因为他们那边开会迟到了几分钟,他坐下跟我表达了歉意,我也表示理解,说了声您辛苦了。然后面试官就开始了面试,但是面试官在面试过程中不知道什么原因全程很愤怒的感觉,并且时常打断我,语气不善的提出一些刁钻的问题。

我在忍了几次以后也不惯他这毛病,于是就同样开始语气不善的回答他的问题,并且对于一些我觉得不太好的问题,我就直接的回问:“您觉得您问我这些问题有什么意义吗?”。

在将近一个小时的面试中,我觉得我们就要吵起来了。在面试的结尾我准备关视频时,面试官突然来了一句,你等下下一个面试官。当时我的表情可能有点难以形容…

在等了几分钟以后,二面的面试官来了,刚坐下他就说:“一面的面试官对你评价不错,咱们开始吧!”。

和二面面试官的面试在很好的氛围中结束,面试官问我的问题基本都是我擅长的,面试官边面试边问我怎么学的这么好。于是紧接着就要约三面主管面,但是因为晚上有事,就约到了第二天。好不容易到主管面了,自然要好好表现,面试开始以后我表现的很有礼貌,但是那位主管不知道怎么回事,可能也是心情不好?面试过程中总是怼我,在比忍一面面试官多忍了几次以后,我又开始了反怼,在一个小时的面试中,又几次差不多吵起来的感觉…在面试结束问我有什么问题吗?我直接语气不善的说了句我没什么问题,就准备关电脑,这时,神奇的一幕又来了,主管突然和我说:“我觉得你学习能力挺不错的,我们打算给你 offer,但是你在收到 offer 以后,一定要确定在我们这稳定做一段时间,你再接受 offer”。

???嗯?你们是怎么回事…接下来的故事就很神奇,在主管面完的当天晚上 9 点左右,我手机震动了一下就收到了我人生中的第一份录用意向书,当时激动的差点哭了出来。

推荐👍:计算机优质书籍搜罗+学习路线推荐!

收获

后来的故事就很美好。在九月,之前投的简历都收到了回信,陆续通过了京东的三轮技术面试、腾讯的三轮技术面试、华为的三轮面试、美团的三轮面试。记得在 9 月的最后一天,上午是美团的最后一轮 hr 面,在我们导师的会议室,摆好电脑,和 hr 愉快的聊完,结束了最后一场面试。那天也是女朋友的生日,面完后要进城去给女朋友过生日,边出校门边回忆这几个月走过来的路,感觉所有的努力都没有白费。这段经历也是我人生中一段珍贵的回忆。

总结

接下来是个人的一些见解,供大家参考。

  1. 我认为算法和开发这两个方向,没有好坏之分,大家应该结合自己的兴趣去选择,并且最终尽量去一个在这个方向上比较优秀的团队。
  2. 在面试前期都会很艰难的,但是不要放弃学习,等面到 9 月,10 月,甚至 11 月时你会感觉面的很顺畅的。(并且个人感觉,面试到 10 月后,因为还在招的公司都是没招够人的,面试要求会有一定降低)。
  3. 要注意好好梳理自己的项目,在讲解自己的项目时逻辑清楚,这样很加分。
  4. 虽然做 offer 收割机也没必要,但是不要拿到一个 offer 就结束秋招了,这样你后期谈薪的时候很被动。

我是 Guide哥,拥抱开源,喜欢烹饪。Github 接近 10w 点赞的开源项目 JavaGuide 的作者。未来几年,希望持续完善 JavaGuide,争取能够帮助更多学习 Java 的小伙伴!共勉!凎!点击查看我的2020年工作汇报!

原创不易,欢迎点赞分享。咱们下期再会!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

盘点 SpringIOC Bean 创建主流程

发表于 2021-05-17

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

Spring IOC 体系是一个很值得深入和研究的结构 , 只有自己真正的读一遍 , 才能有更好的理解.

这篇文章主要说明一下 CreateBean 整个环节中的大流程转换 , 便于查找问题的原因 :

此篇文章的目的 :

  • 梳理Bean 的创建流程 , 便于后续查找问题点
  • 梳理过程中的参数情况 , 减少Debug的需求
  • 梳理整体家族体系

Bean 创建的几个触发场景 :

  • BeanFactory 的 #getBean(…) 方法来请求某个实例对象的时候
  • 使用 ApplicationContext 容器时 , 会在启动时立即注册部分 Bean

二 . 流程梳理

先来看一个很常见的图 , 来源于 @ topjava.cn/article/139…

beanSelf.jpg

同样的 , 这篇的流程整理也是按照此流程 , 先看一下整个流程大纲

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
java复制代码> 实例化过程
1 实例化Bean 对象 : Spring 容器根据实例化策略对 Bean 进行实例化
?- 常用策略模式 , 通常包括反射和 CGLIB 动态字节码
- SimpleInstantiationStrategy : 反射
- CglibSubclassingInstantiationStrategy : 通过 CGLIB 的动态字节码 (默认)
- createBeanInstance(...) 方法实现 ,返回 BeanWrapper
- BeanWrapper : 低级 Bean 基础结构的核心接口
?- 低级 Bean : 无任何属性
- BeanWrapperImpl 对 Bean 进行“包裹” ,用于注入Bean 属性
|- InstantiationStrategy -> SimpleInstantiationStrategy
|- SimpleInstantiationStrategy -> CglibSubclassingInstantiationStrategy
2 注入对象属性
|- 实例化完成后,如果该 bean 设置了一些属性的话,则利用 set 方法设置一些属性
3 检测 , 激活 Aware
| -感知 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
|- 如果该 Bean 实现了 BeanNameAware 接口
|- 则调用 #setBeanName(String beanName) 方法
4 BeanPostProcessor 前置处理
|- 如果该 bean 实现了 BeanClassLoaderAware 接口
|- 则调用 setBeanClassLoader(ClassLoader classLoader) 方法。
|- 如果该 bean 实现了 BeanFactoryAware接口
|- 则调用 setBeanFactory(BeanFactory beanFactory) 方法
|- 如果该容器注册了 BeanPostProcessor
|- 则会调用#postProcessBeforeInitialization
|- 完成 bean 前置处理
5 检查 InitializingBean 和 init-method
|- 如果该 bean 实现了 InitializingBean 接口
|-则调用#afterPropertiesSet() 方法
|- 如果该 bean 配置了 init-method 方法,则调用其指定的方法。
6 BeanPostProcessor 后置处理
|- 初始化完成后,如果该容器注册了 BeanPostProcessor
|- 则会调用 #postProcessAfterInitialization,完成 bean 的后置处理。
7 注册必要的Destruction 回调
8 使用Bean
|- 对象完成初始化,开始方法调用
9 检查 DisposableBean 和 destory-method
|- 在容器进行关闭之前,如果该 bean 实现了 DisposableBean 接口
|- 则调用 #destroy() 方法
|- 在容器进行关闭之前,如果该 bean 配置了 destroy-method 
|- 则调用其指定的方法。

2.1 实例化创建

引言 : 谁调用的 ?

doGetBean 会有2种调用途径 :

一种是 ApplicationContext 加载的时候 , 会初始化当前容器需要的 Bean :

C- SpringApplication # run

C- SpringApplication # prepareContext

C- SpringApplication # applyInitializers : 调用 ApplicationContextInitializer 集合 , 分别执行对应的 initialize

C- ApplicationContextInitializer # initialize

C- ConditionEvaluationReport # get

C- AbstractBeanFactory # getBean

第二种是容器必要 Bean 加载完成后 ,refresh 时处理所有的 Bean

  • C- SpringApplication # run
  • C- SpringApplication # refreshContext
  • C- AbstractApplicationContext # refresh()
    • 此处refresh 中有多处会调用 getBean
    • C- AbstractApplicationContext # invokeBeanFactoryPostProcessors
    • C- AbstractApplicationContext # registerBeanPostProcessors
    • C- AbstractApplicationContext # onRefresh();
    • C- AbstractApplicationContext # finishBeanFactoryInitialization
  • C- AbstractBeanFactory # getBean

PS : 另外还有一种就是因为依赖关系被递归调用的

2.1.1 doGetBean 入口

1
2
3
java复制代码C171- AbstractBeanFactory 
M171_01- getBean(String name, Class<T> requiredType)
?- 直接调用 doGetBean , 这里会根据类型不同调用不同的 getBean

doGetBean 可以分为 5 个阶段

  • 阶段一 : 生成 beanName 后尝试从单例缓存中获取
  • 阶段二 : 单例缓存中获取失败后 ,尝试 ParentBeanFactory 中获取
  • 阶段三 : 依赖检查 , 保证初始化当前bean所依赖的bean
  • 阶段四 : 三种不同的类型获得 Bean 实例 (Singleton / prototype / other)
  • 阶段五 : 此时 Bean 已经准备完成了 , 此处检查所需的类型是否与实际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
java复制代码// 核心方法一 : 
M171_02- doGetBean( String name, Class<T> requiredType,Object[] args, boolean typeCheckOnly)

// 阶段一 : 生成 beanName 后尝试从单例缓存中获取
1- transformedBeanName 生成 beanName -> PS:M171_02_01
2- getSingleton(beanName) : 单例方式获取一个 Bean , 循环依赖就是这个环节处理 -> -> PS:M171_02_02
IF- sharedInstance != null : 如果此时已经生成 , 且 args 为空不需要继续加载
- getObjectForBeanInstance(sharedInstance, name, beanName, null)

// 阶段二 : 单例缓存中获取失败后 ,尝试 ParentBeanFactory 中获取
ELSE-
3- isPrototypeCurrentlyInCreation(beanName) : 如果是原型模式且存在循环依赖则抛出异常
4- getParentBeanFactory() : 检查这个工厂中是否存在bean定义
?- 如果工厂中已经存在了 , 会有四种情况会直接 return -> PS:M171_02_03
4.1- AbstractBeanFactory : parentBeanFactory.doGetBean
4.2- args != null : parentBeanFactory.getBean(nameToLookup, args)
4.3- requiredType != null: parentBeanFactory.getBean(nameToLookup, requiredType)
4.4- 都不符合 : parentBeanFactory.getBean(nameToLookup)
- 如果为类型检查而获取实例,而不是实际使用 , 则将指定的bean标记为已经创建 -> PS:M171_02_04
- RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName) + checkMergedBeanDefinition
?- RootBeanDefinition的获取和检查 LV171_001

// 阶段三 : 依赖检查 , 保证初始化当前bean所依赖的bean
- 对于属性 LV171_001:mbd , 通过 getDependsOn 获取所有依赖
FOR- 循环所有的依赖 , 分别调用 registerDependentBean + getBean 进行递归操作

// 阶段四 : 三种不同的类型获得 Bean 实例
5- 判断 Bean 的类型不同创建 Bean -> PS:M171_02_05
5.1- isSingleton : getSingleton + createBean + getObjectForBeanInstance
5.2- isPrototype : beforePrototypeCreation + createBean + afterPrototypeCreation + getObjectForBeanInstance
5.3- 其他 : 主要是通过 Scope 控制域 + Prototype 流程

// 阶段五 : 此时 Bean 已经准备完成了 , 此处检查所需的类型是否与实际bean实例的类型匹配
IF- 如果实例不匹配 , 则需要转换, 转换后直接返回
- return getTypeConverter().convertIfNecessary(bean, requiredType)
// 如果上面没有返回 , 则直接发返回原本的Bean


// 其他方法
M171_10- getSingleton(String beanName, ObjectFactory<?> singletonFactory) : 获取单例 Bean
M171_20- getObject() : 这里实际是调用 FactoryBean
?- 这里会通过一个 方法回调的语法糖 , 调用 createBean , 整个就连起来了 -> M173_02


// 核心方法二 : 实例化 Bean
// 首先要知道 , 前面传过来的是什么 : TODO
M171_ - getObjectForBeanInstance(Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd)


// PS : doGetBean 在附录中展示

2.1.2 doGetBean 补充节点

PS:M171_02_01 获取 beanName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码--> canonicalName(BeanFactoryUtils.transformedBeanName(name))
---->
C181- SimpleAliasRegistry
F181_01- Map<String, String> aliasMap
M- canonicalName(BeanFactoryUtils.transformedBeanName(name))
- 主要是从 F181_01 中获取 alias 别名
?- PS : 这里的代码有点意思 , 看样子是为了解决别名链的问题 , 即别名对应的还有别名 , 直到取不出来

public String canonicalName(String name) {
String canonicalName = name;
String resolvedName;
do {

resolvedName = this.aliasMap.get(canonicalName);
if (resolvedName != null) {
canonicalName = resolvedName;
}
// 循环获取别名对应的是否存在别名 , 直到获取不到
}while (resolvedName != null);
return canonicalName;
}

PS:M171_02_03 为什么四种情况会直接返回 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码4.1- AbstractBeanFactory :  parentBeanFactory.doGetBean
4.2- args != null : parentBeanFactory.getBean(nameToLookup, args)
?- 使用显式参数委托给父类
4.3- requiredType != null: parentBeanFactory.getBean(nameToLookup, requiredType)
?- 委托给标准的getBean方法
4.4- 都不符合 : parentBeanFactory.getBean(nameToLookup)
?- Pro2

// 这里直接返回是因为存在父 BeanFactory , 且存在 BeanDefinition , 这就意味着ParentBeanFactory 能处理 , 基于 Pro 1 的原因 , 选择直接返回


// Pro 1 : 为什么从父工厂里面获取 -> BeanFactory parentBeanFactory = getParentBeanFactory();
这是一个递归操作 , 也是仿照双亲委派的方式来处理 , 即先有父对象来加载对应的对象

同样的 , 当进入 doGet 的时候 , 默认通过父方法去加载 , 如果父方法处理完成了 , 即加载出 Bean 了 , 就直接返回
好处呢 , 想了一下 , 可以保证每个实现能专注于其本身需要处理的 Bean , 而不需要关注原本就会加载的 Bean
回顾一下双亲委派的目的 : 避免重复加载 + 避免核心类篡改

// Pro 2 : 四种方式去返回的区别
Object getBean(String name) throws BeansException;
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

PS:M171_02_04 对于只检查的那样处理有什么目的 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码    
这里如果是只检查而无需创建 , 会在 markBeanAsCreated 方法中做2件事
clearMergedBeanDefinition(beanName);
this.alreadyCreated.add(beanName);

这样的目的是为了即标注方法已经检查成功 , 而避免走没必要的反复流程 ,允许bean工厂为重复创建指定bean而优化其缓存
--> 实际逻辑

// Step 1 : alreadyCreated 是什么 ?
Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256));
?- alreadyCreated 包含 至少已经创建过一次的bean的名称的 Set 集合

// Step 2 : 如何利用这个对象 ?
1 : AbstractAutowireCapableBeanFactory # getTypeForFactoryBean
?- 这里会通过这个对象校验引用的工厂bean还不存在,那么在这里退出——不会仅仅为了获取FactoryBean的对象类型而强制创建另一个bean

2 : clearMetadata 时 , 需要判断是否为空

PS:M171_02_05 三种不同方式的本质区别 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码相同点 :  最终调用 getObjectForBeanInstance    
5.1- isSingleton : getSingleton + createBean + getObjectForBeanInstance
- 首先通过 getSingleton 方法, 保证加载的唯一性
- 回调 createBean 创建 Bean
- 通过 getObjectForBeanInstance 实例化真正的 Bean

5.2- isPrototype : beforePrototypeCreation + createBean + afterPrototypeCreation + getObjectForBeanInstance
- 仔细看就能发现 , 没有相关的 scope 对象控制 , 直接进行创建

5.3- 其他 : 主要是通过 Scope 控制域 + Prototype 流程
- 它是 singleton 的结合体 ,
- 首先会准备一个 Scope 对象用于控制 Scope 域内的 Bean 情况
- 再调用和 Prototype 一致的流程进行创建

PS:M171_02_06 sharedInstance 和 bean 的区别

这里可以看到 , 首先通过 createBean 创建了一个 Object 对象 sharedInstance , 又通过 getObjectForBeanInstance 生成了一个 Bean ,他们有什么本质的区别 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JAVA复制代码if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

// 这就涉及到 Bean 的工厂了 , 一般情况下 , createBean 创建的 Bean 就是一个 完整的 Bean
// 但是 ,如果它是一个FactoryBean,需要使用它来创建一个bean实例,除非调用者实际上想要一个对工厂的引用。

sharedInstance 中的 Bean 是已经走完 Bean 创建流程的 Bean了
image.png

2.1.3 AbstractBeanFactory # createBean

1
2
3
4
java复制代码C171- AbstractBeanFactory 
M171_03- createBean(String beanName, RootBeanDefinition mbd, Object[] args) 
P- mbd : BeanDefinition 对象 , 已经合并了父类属性
p- args : 用于构造函数或者工厂方法创建 Bean 实例对象的参数

2.1.4 AbstractAutowireCapableBeanFactory # createBean 主流程

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
java复制代码C173- AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
M173_02- createBean(String beanName, RootBeanDefinition mbd,Object[] args)
?- M170_01 方法的默认实现
LV173_02_01- RootBeanDefinition mbdToUse : 内部属性 , 用于标识根 BeanDefinition
1- Class<?> resolvedClass = resolveBeanClass(mbd, beanName) -> PS:M173_02_01
?- 解析获得指定 BeanDefinition 的 class 属性 -> M173_04
IF- 解析的 Class 不为 null , 且存在 BeanClass 和 BeanClassName
- new RootBeanDefinition(mbd)
- mbdToUse.setBeanClass(resolvedClass)
2- mbdToUse.prepareMethodOverrides() : 处理 Overrides 属性 -> PS:M173_02_02
- GetMethodOverrides().getOverrides()
- prepareMethodOverride(MethodOverride mo)
3- Object bean = resolveBeforeInstantiation(beanName, mbdToUse) --- 实例化的前置处理
- 如果 bean 不为 null , 则直接返回 (直接返回是因为上一步生成了一个代理类 ,AOP )
4- Object beanInstance = doCreateBean(beanName, mbdToUse, args); --- 创建对象
M173_03- createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
M173_04- resolveBeanClass : 将bean类名解析为class引用



// PS:M173_02_01 RootBeanDefinition
// 确保bean类在此时被实际解析,如果动态解析的class不能存储在共享合并bean定义中,则克隆bean定义
RootBeanDefinition mbdToUse = mbd;
Class<?> resolvedClass = resolveBeanClass(mbd, beanName);
if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {
mbdToUse = new RootBeanDefinition(mbd);
mbdToUse.setBeanClass(resolvedClass);
}

// PS:M173_02_02 prepareMethodOverrides 处理 Overrides
public void prepareMethodOverrides() throws BeanDefinitionValidationException {
if (hasMethodOverrides()) {
getMethodOverrides().getOverrides().forEach(this::prepareMethodOverride);
}
}

// >>>>>>> 对应 prepareMethodOverride 方法
C- AbstractBeanDefinition
M- prepareMethodOverride(MethodOverride mo)
1- ClassUtils.getMethodCountForName(getBeanClass(), mo.getMethodName())
?- 获取当前Method Name 对应的方法数量
2- 如果没有对应方法 ,抛出 BeanDefinitionValidationException
3- 如果 count = 1 , 将 Overloaded 标记为未重载 , 在后续调用的时候,可以直接找到方法而不需要进行方法参数的校验


// M173_02 源码简介
M- createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
- beanInstance = this.resolveBeforeInstantiation(beanName, mbdToUse);
- beanInstance = this.doCreateBean(beanName, mbdToUse, args); -> M173_05

doCreateBean 主流程

doCreate 分为几大步骤 :

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
java复制代码 C173- AbstractAutowireCapableBeanFactory extends AbstractBeanFactory   
M173_05- doCreateBean(beanName, mbdToUse, args) : 创建 Bean 对象
?- 非代理对象的 常规Bean 创建 , 主要节点有以下几个
- 准备 BeanWrapper : BeanWrapper 是对 Bean 的包装
- 如果单例模型,则从未完成的 FactoryBean 缓存中删除
- createBeanInstance(beanName, mbd, args) : 进入Bean 创建流程 , 创建一个 Bean 的封装 BeanWrapper
?- 这里的获取如果是单例 , 则直接获取 , 并且移除缓存中的对象
?- 否则调用 createBeanInstance 获取
- instanceWrapper.getWrappedInstance();
?- 包装的实例对象
- instanceWrapper.getWrappedClass();
?- 包装的实例对象的类型
IF- applyMergedBeanDefinitionPostProcessors
?- 如果有后置处理 , 则在此处进行后置处理 , synchronized 上锁
IF- addSingletonFactory
?- 如果是单例 , 且允许+ 存在循环依赖 , 则在此处进行单例模式的处理
- populateBean : 属性注入操作 -> M173_30
- initializeBean : 初始化 Bean
?- 此处会进行 Init-Method 的处理 -> PS:M173_05_03
IF- getSingleton
?- 循环依赖情况 , 则会递归初始依赖 bean , 此处返回的是一个用于循环处理的空对象
?- 2种情况 , 返回早期对象或者 getDependentBeans 递归所有的 -> PS:M173_05_01
- registerDisposableBeanIfNecessary
M173_07- prepareMethodOverrides
- 获取所有的 Overrides , 并且依次调用 prepareMethodOverride -> M173_08

M173_08- prepareMethodOverride
- getMethodOverrides().getOverrides().forEach(this::prepareMethodOverride)
?- Foreach 所有的 Overrides , 调用 prepareMethodOverride
M173_10- autowireByName : 根据属性名称,完成自动依赖注入
M173_11- autowireByType : 根据属性类型,完成自动依赖注入
- resolveDependency -> resolveDependency
M173_12- resolveDependency : 完成了所有注入属性的获取
- doResolveDependency
M173_13- doResolveDependency
M173_15- applyPropertyValues : 应用到已经实例化的 bean 中
M173_50- initializeBean



// doCreateBean 核心代码流程
M- doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) : createBean 主流程
- mbd.isSingleton() : 判断后获取 BeanWrapper (ConfigurationClassPostProcessor)
- instanceWrapper = (BeanWrapper)this.factoryBeanInstanceCache.remove(beanName);
- instanceWrapper = this.createBeanInstance(beanName, mbd, args);
- this.applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
- this.populateBean(beanName, mbd, instanceWrapper) : 主要是属性填充
- Step 1 : bw == null&&mbd.hasPropertyValues() : BeanWrapper 空值判断
- Step 2 : this.getBeanPostProcessors().iterator() : 迭代BeanPostProcessors
- ibp.postProcessAfterInstantiation : 此处是 After
- Step 3 : 获取 PropertyValue , 并且通过 autowireByName 或者 autowireByType 注入
- Step 4 : hasInstantiationAwareBeanPostProcessors
- this.getBeanPostProcessors().iterator();
- ibp.postProcessPropertyValues((PropertyValues)pvs, filteredPds, bw.getWrappedInstance(), beanName);
- Step 5 :
- Step 6 : this.checkDependencies(beanName, mbd, filteredPds, (PropertyValues)pvs) : 依赖检查
- Step 7 : this.applyPropertyValues(beanName, mbd, bw, (PropertyValues)pvs) :
- this.initializeBean(beanName, exposedObject, mbd);
- this.registerDisposableBeanIfNecessary(beanName, bean, mbd);


// 附录 : M173_05 doCreateBean 源码比较长 ,放在结尾

PS:M173_05_01 : 递归循环细说

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
java复制代码
在 doCreateBean 中 , 会对循环依赖进行处理

// 循环依赖情况 , 则会递归初始依赖 bean , 此处返回的是一个用于循环处理的空对象
// 2种情况 , 返回早期对象或者 getDependentBeans 递归所有的
if (earlySingletonExposure) {
// 参考循环依赖那一章 , 这里可能会返回一个未完善的前置对象引用
// 只有存在循环依赖 , 这里才不会为空
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
// 判断存在循环依赖
}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
// 返回依赖于指定bean的所有bean的名称(如果有的话)
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
// 如果有被用于类型检查之外的其他目的时 ,则不可以删除 -> PS:M173_05_03
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
// 无需删除 , 则添加
actualDependentBeans.add(dependentBean);
}
}
// 因为上文添加 , 这里就形成依赖
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(".....");
}
}
}
}

// PS:M173_05_03 : 什么是其他的目的 ?
removeSingletonIfCreatedForTypeCheckOnly 核心是校验 alreadyCreated
1 . 如果 alreadyCreated 已经存在了 ,则说明对应的对象已经创建完成了
2 . 如果对应的对象已经创建完了了 , 其中依赖的当前对象不是正在创建的对象
3 . 但是其中的属性又不是当前的对象 , 说明循环依赖不成立 , 依赖已经串了

@ https://www.cnblogs.com/qinzj/p/11485018.html

M173_05 doCreateBean 源码

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
java复制代码protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {

// Step 1 : 准备 BeanWrapper : BeanWrapper 是对 Bean 的包装
BeanWrapper instanceWrapper = null;

if (mbd.isSingleton()) {
// 如果单例模型,则从未完成的 FactoryBean 缓存中删除
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
// 进入Bean 创建流程 , 创建一个 Bean 的封装 BeanWrapper
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
// 获取包装的实例对象
final Object bean = instanceWrapper.getWrappedInstance();
// 包装的实例对象的类型
Class<?> beanType = instanceWrapper.getWrappedClass();
if (beanType != NullBean.class) {
mbd.resolvedTargetType = beanType;
}

// Step 2 : 如果有后置处理 , 则在此处进行后置处理 , synchronized 上锁
synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException("....");
}
mbd.postProcessed = true;
}
}

// Step 3 :如果是单例 , 且允许+ 存在循环依赖 , 则在此处进行单例模式的处理
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

// Step 4 : 此处会进行 Init-Method 的处理 -> PS:M173_05_03
Object exposedObject = bean;
try {
// Step 5 :属性注入操作 -> M173_30
populateBean(beanName, mbd, instanceWrapper);
// Step 6 : 初始化 Bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException("....");
}
}


// 循环依赖情况 , 则会递归初始依赖 bean , 此处返回的是一个用于循环处理的空对象
// 2种情况 , 返回早期对象或者 getDependentBeans 递归所有的 -> PS:M173_05_01
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(".....");
}
}
}
}

// Step 7 : 注册 Bean
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException("...");
}

return exposedObject;
}

总结

其中有几个锚点 :

Step 1 : 入口

入口中主要是会统筹管理 , 如果缓存中有 , 则直接使用 , 如果没有 , 则区别 Scope 分别使用不同的方式获取一个 Bean

1
2
java复制代码- C171- AbstractBeanFactory 
- M171_02- doGetBean

Step 2 : 创建主流程

创建主流程就是创建一个完整的 Bean , 走一个 Bean 创建的完整周期 , 包括 process , 属性注入 , init 初始化等等 , 这些我们在后面的文章中再详细说说

1
2
3
4
5
6
7
java复制代码// Step 1 : 
C173- AbstractAutowireCapableBeanFactory
M173_02- createBea

// Step 2 :
C173- AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
M173_05- doCreateBean

附录

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
java复制代码protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

// 阶段一 : 生成 beanName 后尝试从单例缓存中获取
final String beanName = transformedBeanName(name);
Object bean;

// 单例方式获取一个 Bean , 循环依赖就是这个环节处理 -> -> PS:M171_02_02
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// 直接从实例化中获取 Bean
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}

else {
// 阶段二 : 单例缓存中获取失败后
if (isPrototypeCurrentlyInCreation(beanName)) {
// 如果是原型模式且存在循环依赖则抛出异常
throw new BeanCurrentlyInCreationException(beanName);
}

// 检查这个工厂中是否存在bean定义
BeanFactory parentBeanFactory = getParentBeanFactory();

// 如果工厂中已经存在了 , 会有四种情况会直接 return -> PS:M171_02_03
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// Not found -> check parent.
String nameToLookup = originalBeanName(name);
if (parentBeanFactory instanceof AbstractBeanFactory) {
return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
nameToLookup, requiredType, args, typeCheckOnly);
}
else if (args != null) {
// Delegation to parent with explicit args.
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else if (requiredType != null) {
// No args -> delegate to standard getBean method.
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
else {
return (T) parentBeanFactory.getBean(nameToLookup);
}
}

// 如果为类型检查而获取实例,而不是实际使用 , 则将指定的bean标记为已经创建 -> PS:M171_02_04
if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}

try {
// RootBeanDefinition的获取和检查 LV171_001
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);

// 阶段三 : 依赖检查 , 保证初始化当前bean所依赖的bean
// 对于属性 LV171_001:mbd , 通过 getDependsOn 获取所有依赖
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
// 循环所有的依赖 , 分别调用 registerDependentBean + getBean 进行递归操作
for (String dep : dependsOn) {
if (isDependent(beanName, dep)) {
throw new BeanCreationException(.....);
}
registerDependentBean(dep, beanName);
try {
getBean(dep);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(.....);
}
}
}

// 阶段四 : 三种不同的类型获得 Bean 实例
// 判断 Bean 的类型不同创建 Bean -> PS:M171_02_05
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

else {
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, () -> {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(....);
}
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}

// 阶段五 : 此时 Bean 已经准备完成了 , 此处检查所需的类型是否与实际bean实例的类型匹配
// 如果实例不匹配 , 则需要转换, 转换后直接返回
if (requiredType != null && !requiredType.isInstance(bean)) {
try {
T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
if (convertedBean == null) {
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
return convertedBean;
}
catch (TypeMismatchException ex) {
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
}
// 如果上面没有返回 , 则直接发返回原本的Bean
return (T) bean;
}

参考与感谢

写这个之前 , 还跑过去再读了一遍 , 很感谢死磕系列开启了 IOC 的源码学习

-> @ topjava.cn/article/139…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Rust 官方发布 2021 的规划

发表于 2021-05-17

我们很高兴地宣布,Rust语言的第三版,即Rust 2021,计划在10月发布。 Rust 2021包含一些小的变化,但预计会对Rust在实践中的感觉有重大改进。

什么是版本?

Rust 1.0的发布确立了“稳定而不停滞 “作为Rust的核心交付价值。 自1.0发布以来,Rust的规则是,一旦一个功能被发布到稳定版,我们就会承诺在未来的所有版本中支持该功能。

然而,有些时候,能够对语言做一些不向后兼容的小改动是很有用的。 最明显的例子是引入一个新的关键字,这将使同名的变量失效。 例如,Rust的第一个版本没有async 和await 关键字。在后来的版本中突然将这些词改为关键字,会破坏像let async = 1; 。

版本是我们用来解决这个问题的机制。 当我们想要发布一个否则会向后不兼容的功能时,我们会将其作为新的Rust_版本的_一部分。 版本是可选择的,因此现有的板块在明确迁移到新版本之前不会看到这些变化。 这意味着,即使是最新版本的Rust仍然_不会_将async 作为一个关键字,除非选择2018版或更高版本。这个选择是作为_每个板块_ 的一部分Cargo.toml 。由cargo new 创建的新板块总是被配置为使用最新稳定版本。

版本是不会分裂生态系统

版本最重要的规则是,一个版本的cockate可以与其他版本编译的cockate进行无缝的互操作。这确保了迁移到一个较新的版本的决定是一个 “私人决定”,该板块可以在不影响其他板块的情况下做出。

对板条箱互操作性的要求意味着我们可以在一个版本中做一些限制。 一般来说,在一个版本中发生的变化往往是 “皮毛 “的。 所有的Rust代码,无论版本如何,最终都会被编译成编译器中相同的内部表示。

版本迁移很容易,而且基本上是自动化的

我们的目标是让crack很容易升级到新的版本。 当我们发布新的版本时,我们也提供工具来自动迁移。 它对你的代码进行必要的小改动,使其与新的版本兼容。 例如,当迁移到Rust 2018时,它改变了任何名为async ,以使用等效的原始标识符语法。r#async 。

自动迁移不一定是完美的:可能会有一些角落的情况,仍然需要手动修改。 该工具努力避免改变语义,以免影响代码的正确性或性能。

除了工具化,我们还维护了一个版本迁移指南,涵盖了一个版本的变化。 这个指南将描述变化,并给出人们可以了解更多信息的指针。 它还将涵盖人们应该注意的任何角落案例或细节。 该指南既可以作为版本的概述,也可以在人们遇到自动化工具化问题时作为快速故障排除参考。

Rust 2021 计划有哪些变化?


在过去的几个月里,Rust 2021工作组审议了许多关于新版本中包含哪些内容的建议。 我们很高兴地宣布最终的版本变化清单。 每个功能都必须满足两个标准才能进入这个清单。 首先,它们必须得到相关Rust团队的批准。 其次,它们的实施必须足够深入,我们有信心在计划的里程碑前及时完成。

对前奏的补充

标准库的前奏是包含每个模块中自动导入的所有东西的模块。它包含常用的项目,如Option 、Vec 、drop 、Clone 。

Rust编译器会优先考虑任何手动导入的项目,而不是来自前奏的项目,以确保对前奏的添加不会破坏任何现有的代码。例如,如果你有一个名为example 的板块或模块,包含一个pub struct Option; ,那么use example::*; 将使Option 明确地引用来自example ;而不是来自标准库。

然而,在前奏中添加_特质_会以一种微妙的方式破坏现有的代码。如果std‘sTryInto 也被导入,那么使用MyTryInto 特质对x.try_into() 的调用可能会变得含糊不清,无法编译,因为它提供了一个同名的方法。这就是我们还没有在前奏中添加TryInto 的原因,因为有很多代码会这样破坏。

作为一个解决方案,Rust 2021将使用一个新的前奏。 它与当前的前奏相同,只是新增加了三个内容。

  • std::convert::TryInto
  • std::convert::TryFrom
  • std::iter::FromIterator

默认的Cargo特性解析器

自Rust 1.51.0以来,Cargo对一个新的特性解析器提供了选择支持,可以通过resolver = "2" 在Cargo.toml 。

从Rust 2021开始,这将是默认的。也就是说,在Cargo.toml 中写入edition = "2021" 将意味着resolver = "2" 。

新的特性解析器不再合并以多种方式依赖的crates的所有请求的特性。 详情见Rust 1.51的公告。

用于数组的IntoIterator

直到Rust 1.53,只有对数组的_引用_实现了IntoIterator 。这意味着你可以在&[1, 2, 3] 和&mut [1, 2, 3] ,但不能直接在[1, 2, 3] 。

1
2
3
less复制代码for &e in &[1, 2, 3] {} // Ok :)

for e in [1, 2, 3] {} // Error :(

这是一个长期存在的问题,但解决方案并不像看起来那么简单。仅仅添加特质实现就会破坏现有的代码。今天,array.into_iter() 已经可以编译了,因为根据方法调用语法的工作原理,它隐含地调用了(&array).into_iter() 。添加特质实现将改变其含义。

通常我们把这种类型的破坏(添加特性实现)归类为 “轻微的 “和可以接受的。 但在这种情况下,有太多的代码会被破坏。

有人多次建议 “只在Rust 2021中为数组实现IntoIterator “。然而,这根本不可能。你不可能让一个特质的实现存在于一个版本,而不存在于另一个版本,因为版本可以混合。

相反,我们决定在_所有_版本中添加特质实现(从Rust 1.53.0开始),但添加一个小黑客,以避免在Rust 2021之前出现故障。 在Rust 2015和2018的代码中,编译器仍然会像以前一样将array.into_iter()解析为(&array).into_iter() ,就像特质实现不存在一样。这_只_适用于.into_iter() 方法调用语法。它不影响任何其他语法,如for e in [1, 2, 3] ,iter.zip([1, 2, 3]) 或IntoIterator::into_iter([1, 2, 3]) 。 这些将在_所有_版本中开始工作。

虽然这需要一个小的黑客来避免破坏,这是一个耻辱,但我们对这个解决方案将版本之间的差异保持在绝对最小的程度感到非常满意。 因为这个黑客只存在于旧版本中,在新版本中没有增加复杂性。

闭包中的不连续捕获

闭包会自动捕获你在其主体中引用的任何东西。例如,|| a + 1 会自动捕获周围环境中对a 的引用。

目前,这适用于整个结构,即使只使用一个字段。例如,|| a.x + 1 捕获对a 的引用,而不仅仅是a.x 。 在某些情况下,这是一个问题。当结构的一个字段已经被借用(可变)或移出时,其他字段不能再用于闭包,因为这将捕获整个结构,这不再可用。

1
2
3
4
5
6
7
8
ini复制代码let a = SomeStruct::new();

drop(a.x); // Move out of one field of the struct

println!("{}", a.y); // Ok: Still use another field of the struct

let c = || println!("{}", a.y); // Error: Tries to capture all of `a`
c();

从Rust 2021开始,闭包将只捕获它们使用的字段。 因此,上面的例子在Rust 2021中可以正常编译。

这种新的行为只在新版本中激活,因为它可以改变字段被丢弃的顺序。 至于所有版本的变化,自动迁移是可用的,它将更新你的闭包,这一点很重要。它可以在闭包内插入let _ = &a; ,强制整个结构像以前一样被捕获。

panic!() 宏的一致性

panic!() 宏是 Rust 最知名的宏之一。然而,它有一些微妙的惊喜,由于向后兼容,我们不能随便改变。

1
2
go复制代码panic!("{}", 1); // Ok, panics with the message "1"
panic!("{}"); // Ok, panics with the message "{}"

panic!() 宏只在调用一个以上的参数时使用字符串格式化。 当调用一个参数时,它甚至不看那个参数。

1
2
3
ini复制代码let a = "{";
println!(a); // Error: First argument must be a format string literal
panic!(a); // Ok: The panic macro doesn't care

(它甚至接受非字符串,如panic!(123) ,这是不常见的,而且很少有用)。

一旦隐式格式参数稳定下来,这将是一个特别的问题。这个特性将使println!("hello {name}") 成为println!("hello {}", name) 的简称。然而,panic!("hello {name}") 不会像预期的那样工作,因为panic!() 不会将单个参数作为格式字符串处理。

为了避免这种混乱的情况,Rust 2021采用了更加一致的panic!() 宏。新的panic!() 宏将不再接受任意表达式作为唯一的参数。它将和println!() 一样,总是将第一个参数处理为格式字符串。 由于panic!() 将不再接受任意的有效载荷。panic_any() 将是用格式化字符串以外的东西进行恐慌的唯一方法。

此外,在Rust 2021中,core::panic!() 和std::panic!() 将是相同的。目前,这两者之间存在一些历史差异,在打开或关闭#![no_std] 时,会有明显的差异。

保留语法

为了给未来一些新的语法腾出空间,我们决定为前缀标识符和字面意义保留语法:prefix#identifier,prefix"string",prefix'c', 和prefix#123, 其中prefix 可以是任何标识符。(除了那些已经有意义的,如b'…' 和r"…" 。

这是一个突破性的变化,因为目前宏可以接受hello"world" ,他们会将其视为两个单独的标记:hello 和"world" 。不过,(自动)修复很简单。只要插入一个空格:hello "world" 。

除了将这些变成标记化错误外,RFC还没有给任何前缀附加含义。 给特定前缀分配含义是留给未来的建议的,由于现在保留了这些前缀,所以不会有破坏性变化。

这些是你在未来可能看到的一些新的前缀。

  • f"" ,作为一个格式字符串的缩写。例如,f"hello {name}" ,作为相当于format_args!() 的调用的缩写。
  • c"" 或z"" 表示空尾的 C 字符串。
  • k#keyword ,以允许编写在当前版本中尚不存在的关键字。例如,虽然async 在2015版中不是一个关键字,但这个前缀将允许我们在2015版中接受k#async 作为替代,同时我们等待2018版将async 保留为一个关键字。

将两个警告提升为硬错误

在Rust 2021中,两个现有的行文将成为硬错误。 这些行文在旧版本中仍是警告。

  • bare-trait-objects: 在Rust 2021中,使用dyn 关键字来识别特质对象将是强制性的。
  • ellipsis-inclusive-range-patterns :Rust 2021中不再接受已被废弃的... ,该语法用于包容性范围模式。它已被..= 所取代,该语法与表达式一致。

Or patterns in macro_rules

从Rust 1.53.0开始,模式被扩展到支持| ,嵌套在模式的任何地方。这使得你可以写Some(1 | 2) ,而不是Some(1) | Some(2) 。由于以前根本不允许这样做,这不是一个突破性的变化。

然而,这个变化也影响到了macro_rules 宏。这种宏可以接受使用:pat 片段指定符的模式。目前,:pat _不_匹配| ,因为在Rust 1.53之前,不是所有的模式(在所有的嵌套层)都可以包含一个| 。接受类似A | B 的模式的宏,如 matches!() $($_:pat)|+因为我们不想破坏任何现有的宏,所以我们_没有_在Rust 1.53.0中改变:pat 的含义以包括| 。

相反,我们将作为Rust 2021的一部分进行修改。在新版本中,:pat 片段指定器_将_匹配A | B 。

由于有时人们仍然希望在没有| 的情况下匹配单一的模式变体,所以增加了指定的片段:pat_param ,以保留旧的行为。这个名字是指它的主要使用情况:封闭参数中的模式。

下一步是什么?

我们的计划是在9月前完成这些修改的合并和全面测试,以确保2021年的版本能够进入Rust 1.56.0。

然而,请注意,Rust是一个由志愿者运营的项目。 我们优先考虑在Rust上工作的每个人的个人福祉,而不是我们可能设定的任何最后期限和期望。 这可能意味着在必要时推迟一个版本,或者放弃一个被证明太难或压力太大而不能及时完成的功能。

也就是说,我们正在按计划进行,许多困难的问题已经被解决了,这要感谢所有为Rust 2021做出贡献的人!💛


你可以期待7月份关于新版本的另一个公告。 届时,我们希望所有的变化和自动迁移都能实现,并准备好进行公开测试。

我们将很快在 “Inside Rust “博客上发布一些关于这个过程的细节和被拒绝的建议。

如果你真的等不及了,许多功能已经可以在 Rust Nightly上使用-Zunstable-options --edition=2021。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何开发一款游戏:游戏开发流程及所需工具

发表于 2021-05-17

游戏作为娱乐生活的一个方面,参与其中的人越来越多,而大部分参与其中的人都是以玩家的身份。

他们热爱一款游戏,或是被游戏的故事情节、炫丽的场景、动听的音乐所艳羡,亦或是被游戏中角色扮演、炫酷的技能、有趣的任务所吸引,然而他们中的大多数可能并不了解如此一款好玩的游戏是如何打造出来的。

对于想来这个行业尝试的新人们,先对游戏开发制作有个整体的了解也是非常必要的。

接下来我将从几个方面来分别进行阐述。

基础知识
游戏,说白了就是一个程序,这个程序或在 PC 上或在移动设备上运行,玩家通过与这个程序交互来达到娱乐性的目的。我们先了解一下游戏中用到的各种引擎以及游戏相关术语。

游戏引擎

游戏引擎是游戏研发的主程序接口,它为开发者提供了各种开发游戏的的工具,即可编辑游戏系统和实时图像系统的核心组件,其目的就在于让开发者可以快速的做出游戏而不必从零开始。

游戏引擎包含渲染引擎、物理引擎、碰撞检测系统、网络引擎、音效引擎、脚本引擎、动画及场景管理等。

渲染引擎:是对游戏中的对象和场景起到渲染的效果,游戏中的角色都是通过渲染引擎将它的模型、动画、光影、特效等所有效果实时计算出来并展示到屏幕;

物理引擎:让对象运动遵循特定的规律,比如当角色跳起的时候,系统内定的重力值将决定它弹跳的高度及下落的速率;

碰撞检测系统:可以探测各物体的边缘,当两个 3D 物体在一起的时候,系统可以防止它们相互穿过;

网络引擎:是负责玩家与设备间的通信,处理来自键盘、鼠标及其它外设信号。若游戏联网,它也用来管理客户端与服务器间的通信;

Lua 引擎:是 Lua 的服务器引擎,lua 是一种轻量级的嵌入式脚本语言,在网游开发中应用广泛。

总的来说,一个游戏是引擎和资源组成的,资源包括图象、声音、动画等,游戏引擎就像一个发动机,控制着游戏的运行,它按游戏设计规则依次调用游戏资源。

游戏名词

CD-key:游戏的序列号或防盗密码;

BugFree:测试管理平台,是一款基于 Web 的开源错误追踪工具;

Ping:从客户端发送数据到服务器到接收到服务器反馈数据的时间,以 ms 计,若 Ping 值高会感觉延迟;

Proxy Server:代理服务器,代理网络用户去取得网络信息;

PU:付费用户;

RU:注册用户;

AU:活跃用户;

DAU:平均每日活跃用户;

CCU:同时在线人数;

PCU:最高同时在线人数;

ACU:平均同时在线人数;

ARPPU:付费玩家平均收入;

封测:限定用户数量的游戏测试,用来对技术和游戏产品进行初步的验证,用户规模较小;

内测:面向一定数量用户进行的内部游戏测试,多用于检测游戏压力和功能有无漏洞;

公测:对所有用户公开的开放性的网络游戏测试。

游戏的种类

游戏的分类方法很多,可以按终端、内容、摄像类型、玩家格斗对象、玩家人数等来分,其中按内容来分最直观,它可以根据游戏的元素迅速锚定游戏范围。

按终端分:主机游戏 (电视机游戏)、客户端游戏、网页游戏、手机游戏;

按摄影类型分:2D 游戏、2.5 游戏、3D 游戏;

按格斗对象分:PVE:PlayerVsEnvironment、PVP:PlayerVsPlayer;

按玩家人数分:单机游戏(Singe-Player Game)、多人游戏(Muti-Player Game)、大型多人在线(Massive Multiplayer Online Game)。

image.png

游戏的开发流程
游戏开发从狭义上讲就是程序部门进行相关游戏程序的编写,从广义上讲,是整个游戏制作过程,这其中包括多个部门的人员配备。下图是一个一般性的游戏开发团队。

image.png

整个团队包含四个部门,即策划、美术、程序、制作人,各个部门负责不同的工作,协调完成整个游戏的开发。

策划是团队的灵魂,也分执行策划、数据策划、表现策划、资源策划等,他们主要对游戏剧情、背景进行分析设计,对游戏中的各种规则进行描述及公式确定,对各种资料表格进行维护,对游戏中的特效、动作等进行收集并提出需求,进行 UI 设计及模型相关配置等。

程序是团队的骨肉,也可细分为主程序、客户端引擎、服务器引擎、3D 程序、AI 程序、脚本程序、数据库程序等,他们主要负责确定程序的数据结构,确定策划方案的完成方法,将策划提出的各种需求用程序来实现,并为游戏开发过程提供良好的编辑工具。

美术是团队的皮肤,可细分为人物原画、人物建模、材质贴图、人物动作、场景动画等,他们主要负责整个游戏的视觉风格,以及人物模型动作等的设计等。

制作人主要进行游戏的外部统筹,市场调研、游戏开发进度、游戏版权、游戏宣传、游戏发布及音乐音效素材的管理都是制作人工作的范畴。

下图是某国外游戏研发团队的组织架构图,可以参考了解一下。

image.png

游戏开发的各个时期

对于游戏制作人来说,每个游戏从产生要消亡要经历各个阶段,下面是普遍适用的典型范例,但并不是每个游戏都要经历所有的时期。

概念时期:就是整个游戏概念的确定,要做什么样的游戏,主题线索是什么;

原型开发时期:这个时期要制作游戏的原型,用来体验游戏的设计概念,从而纠正和改善不足的地方;

推广时期:此时是游戏开发方向出版方推广产品,向投资方展示游戏的设计概念、主要卖点、产品如何适应市场的需求、产品开发的可行性及具体的实现方案;

准备时期:这个时期主要处理游戏项目所涉及的商务及法律方面的事务,比如游戏专利、剧本版权、品牌商标等,从而组织开发团队制作大致的方案,确定游戏开发所需要的工具及其它细节问题;

制作时期: 这个时期是游戏制作的主体时期,完成 3D 模型的制作,场景制作,过场动画、画面渲染及音效录制等,游戏引擎和资源在此时期将被完全整合到一起。

质量保证时期:这个时期是游戏的 QA 或测试时期,主要用来保证游戏的各项功能是否完好,从而发现和修复各种 Bug 和错误;

母盘生成时期:这个时期是将游戏存盘交由平台厂商测试检测的时期,每个平台厂商的测试标准不尽相同,这个时期中也需要不断地测试改进游戏,修复 Bug,准备市场投放。

运营维护时期:这个时期是游戏发布后持续运营,在运营过程中发现问题,修复并更新升级的过程,这是一个长期的过程。

项目流程

一部游戏完整的开发过程,归纳起来可分为五步,如下图所示。

image.png

市场调研可以分为三个小部分,

1)调研前进行 “头脑风暴”,让尽量多的人想出尽量多的创意点子并做好记录,从而在市场调研过程中一一确认,不符合的排除;

2)撰写策划草案,从而让项目小组中的每一个成员对开发的项目有一个大体的认识,并且对目标明确;

3)对每一个草案都进行市场调研和分析,决定是否要开发这个游戏。市场调研主要从两个方面入手,即目标客户(玩家)和开发成本。

需求分析主要是撰写需求分析书,这主要包括三个方面:

1)策划需求

策划的分工:包括剧本、数值、界面、执行等方面;

进度控制:要时刻注意时间和开发进度的控制,需要写一个专门的项目进度汇总表。

2)美术需求

场景:包括游戏地图、小场景等方面;

人物:包括玩家角色、重要 NPC(玩家队友、提供任务的 NPC、主线剧情 NPC 等)、次要 NPC(路人、村民等)、怪物、BOSS 等;

动画:动画方面估计每个公司的需求都不尽相同。如果公司能力有限,动画的制作可以考虑外包的方式;

道具:主要需要考虑是否采取纸娃娃系统;

全身像:人物的全身像方面;

静画 &CG:游戏中可能出现的静画和 CG 的需求,没有则不需要写;

人物头像:人物的头像制作需求,其中包括人物的表情方面,包括喜、怒、哀、乐和悲等多种表情;

界面:界面的需求,包括主界面、各项子界面、屏幕界面、开头界面、END 界面、保存和载入界面等方面;

动态物件:包括游戏中可能出现的火把、光影等方面;

卷轴:又称为滚动条。根据游戏的情况来定具体的需求;

招式图:根据游戏开发的具体情况决定是否有此需求;

编辑器图素:各种编辑器的图素需求,例如关卡编辑器、地图编辑器等方面;

粒子特效:3D 粒子特效的需求;

宣传画:包括游戏的宣传画、海报等方面的制作需求;

游戏包装:游戏客户端的封面包装的制作;

说明书插图:游戏说明书内附插图的制作需求;

盘片图鉴:游戏客户端盘片上的图鉴的制作需求;

官方网站:游戏官方网站的制作需求。

3)程序需求

地图编辑器:包括编辑器的功能需求、各种数据的需求等;

粒子编辑器:关于粒子编辑器的需求;

内镶小游戏:包括游戏内部各种小游戏的需求;

功能函数:包括游戏中可能会出现的各种程序功能、技术参数、数据、碰撞检测、AI 等方面的需求;

系统需求:包括升级系统、道具系统、招式系统等系统导入器的需求。

项目开发步骤就是将整个游戏项目的资源通过引擎组织起来,对游戏的架构、功能及各逻辑模块进行充分的整合。

这就要明确游戏开发的日程和进度安排,这也是充分利用各种开发工具让开发效率大大提升的根本所在。

测试发布流程主要包括两次大型正规的测试,即 Alpha 测试和 Beta 测试,其中前者意味着游戏的功能和流程完整,QA 会为游戏定制测试计划,测试人员将发现的 Bug 提交到数据库,开发和设计人员对相应的错误进行修复。

后者意味着游戏中的各种资源已完成,产品已定型,后期只是修复 Bug。在这两次测试修复后,得到待发布的 Release 版。

Gold Release 流程主要是开发游戏的各种补丁包、游戏的升级版本,以及官方的各种礼包和插件等。

游戏开发所用的工具
选择正确的工具,可以为游戏项目节省开支,提高工作质量,降低项目风险,让整个项目团队成员集中注意力,从而把游戏做得好玩。

程序工具软件

OpenGL ES——OpenGL 长期以来都是行业内 2D/3D 图形高质表现的标准,它适用于各种设备。OpenGL ES 提供了在软件应用程序和软件图像引擎间的底层 API 接口;

IncrediBuild——这个开发工具极大的提升了 VS/VC 的编译和版本生成速度,有效降低增量构建所需要花费的时间,它主要是采用分布式编译技术,在公司内网可以调用其它计算机的资源进行快速编译。这是开发人员不可多得的一款好工具;

VS2013——微软的 VS 集成开发环境多年来都是游戏制作的基本软件,界面友好,功能齐全,可以极大的提升编码速度和工作流;

Visual Assist X——这是一个插件,引入了强大的编辑功能,完全整合在 C++IDE 环境中,可以极大的提升开发人员的工作进程,不过有的 IDE 环境已经整合了这款插件,自己不用手动安装了;

Direct X——它是微软在过去建立的众多行业标准之一,它是一种视窗技术,可以让你在玩游戏或观看视频过程中图像和音效有更高的品质,它包含多个配套组件,如 Direct3D、DirectSound、DirectPlay、DirectInput 等。

美术制作工具

美术制作工具要远多于程序软件,因此在游戏开发过程中,选择美术软件时要慎重考虑,以方便项目的顺利进展。

Maya——它是行业内首选的 3D 动画制作软件之一,它功能十分强大,可用于高端电脑构图,可以处理几乎所有的 3D 制作工作。

比如模型构建、动画制作、描绘渲染、电影特效等。但其缺点也在于其多边形建模工具不太理想;

3D Studio Max——它是游戏开发中 3D 程序开发的主流引导者,其多边形建模工具是所有 3D 程序中最棒的工具,用它进行开发效率也特别高;

PhotoShop CS——该软件在游戏制作中被广泛应用,是游戏制作的必备软件,它在游戏开发的各个时期都会用到,包括前期制作到最终完成并市场推广。美术人员用它来做出游戏环境和角色的设定,策划也用它来画关卡规划和界面示意图;

FaceGen Modeller——这是一款 3D 头脸创作工具,它可以为游戏制作多个角色,从而快速做出人物脸部及头部模型,形态非常逼真;

Zbrush——这款工具的特点在于使艺术模型呈现传统艺术创作的过程,它可以辅助制作人员做出逼真的环境多边模型,是地图场景的绝佳工具;

Granny——可以作为游戏的一个批量输出工具,它能够完成所有艺术素材,包括模型、渲染和过场动画的植入。它可以生成法线和纹理贴图,更是一款引擎解释工具。

游戏组件工具

游戏组件是指游戏的基本环境架构,比如描绘、场景和几何构型的构建,也称为中间件。

Havok——这是目前比较先进的物理引擎,它能让游戏模拟现实,可以将游戏做出非常逼真的效果;

Gamebryo——这是一款能够帮助开发人员快速制作原型版的工具,功能强大,运行稳定,是比较好的 3D 实时图形引擎,其强大的渲染引擎和动作处理系统使其在商业上获得巨大的成功;

Quazal——它属于网络建筑中间件,主要用于制作大型多人在线游戏,其它类似的中间件有 Big World。

音效工具

音效作为游戏里的重要组成部分,选择合适的工具也非常重要。作为游戏开发人员,关键要了解各种工具的使用限制,有很多的专业音效制作工具,包括 Nuendo、Vegas、Logic、ProTools、Peak、GameCODA、SoundForge 等。

场景构建工具:

Unreal Engine——这是一款比较完型的游戏开发引擎,它提供了比较全能的关卡编辑器、过场动画系统、3D 图形及 AI;

Source——这款引擎为人物角色动画提供了新技术,先进的 AI、光影渲染、实景图象都非常棒,引擎也包含了先进的物理引擎。

日常管理工具
游戏开发过程中所涉及的事务比较多,(手游,页游等都一样)内容也比较繁杂,用好日常管理工具可以有效提升工作效率。下面是几个用得比较多的工具:

MicroSoft Excel——利用它进行开发进度管理,开发人员可以非常轻松地跟踪管理多个游戏开发部门的进度,开发人员必须要对其十分熟悉,才能用的得心应手;

日常工作增量进程报告 (daily delta reports)——一个项目成功的关键就是运用日常工作进程报告,在这个过程中,每一名团队成员每天上交一份个人当日工作完成情况清单。这种进程报告的方式可以简明扼要、方便有效地跟踪项目进程;

源码控制报告和版本控制报告——目前大部分项目研发用的版本控件工具是 SVN、Perforce、Git 等,在使用版本控制软件前,一定要花一定的时间来熟悉软件的功能和使用方法,这对于游戏研发人员非常关键,否则就会犯些不必要的错误,从而导致工作效率下降;

运用 WiKi——它是协作性文档,是自由讨论和创造性工具,是最佳管理设计性文档的方法,当团队无法建立一个内部局域网来管理各种记录和设计进程或建立局域网工作量过大时,WiKi 就是你最佳的选择。

好了,关于游戏开发的相关知识,我就介绍到这里。游戏开发涉及的知识太多太多,我在这里只是概括性的做了一个引入,希望对您有些许的帮助,文章内容不免有很多不足之处,还请各位大侠多多指教。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

真的,搞懂 Kafka 看这一篇就够了!

发表于 2021-05-17

大家好,我是云祁。

有人说世界上有三个伟大的发明:火,轮子,以及 Kafka。

发展到现在,Apache Kafka 无疑是很成功的,Confluent 公司曾表示世界五百强中有三分之一的企业在使用 Kafka。今天便和大家分享一下 Kafka 相关知识点,高性能、持久化、多副本备份、横向扩展……

万字长文,做好准备,建议先收藏再看!

1、为什么有消息系统

  1. 解耦合
  2. 异步处理 例如电商平台,秒杀活动。一般流程会分为:1: 风险控制、2:库存锁定、3:生成订单、4:短信通知、5:更新数据
  3. 通过消息系统将秒杀活动业务拆分开,将不急需处理的业务放在后面慢慢处理;流程改为:1:风险控制、2:库存锁定、3:消息系统、4:生成订单、5:短信通知、6:更新数据
  4. 流量的控制 :1. 网关在接受到请求后,就把请求放入到消息队列里面 2.后端的服务从消息队列里面获取到请求,完成后续的秒杀处理流程。然后再给用户返回结果。优点:控制了流量 缺点:会让流程变慢

2、Kafka核心概念

生产者:Producer 往Kafka集群生成数据

消费者:Consumer 往Kafka里面去获取数据,处理数据、消费数据Kafka的数据是由消费者自己去拉去Kafka里面的数据

主题:topic

分区:partition 默认一个topic有一个分区(partition),自己可设置多个分区(分区分散存储在服务器不同节点上)

3、Kafka的集群架构

Kafka集群中,一个kafka服务器就是一个broker,Topic只是逻辑上的概念,partition在磁盘上就体现为一个目录。

Consumer Group:消费组 消费数据的时候,都必须指定一个group id,指定一个组的id假定程序A和程序B指定的group id号一样,那么两个程序就属于同一个消费组。

特殊: 比如,有一个主题topicA程序A去消费了这个topicA,那么程序B就不能再去消费topicA(程序A和程序B属于一个消费组);再比如程序A已经消费了topicA里面的数据,现在还是重新再次消费topicA的数据,是不可以的,但是重新指定一个group id号以后,可以消费。不同消费组之间没有影响,消费组需自定义,消费者名称程序自动生成(独一无二)。

Controller:Kafka节点里面的一个主节点,借助zookeeper。

4、Kafka磁盘顺序写保证写数据性能

**kafka写数据:**顺序写,往磁盘上写数据时,就是追加数据,没有随机写的操作。经验: 如果一个服务器磁盘达到一定的个数,磁盘也达到一定转数,往磁盘里面顺序写(追加写)数据的速度和写内存的速度差不多生产者生产消息,经过kafka服务先写到os cache 内存中,然后经过sync顺序写到磁盘上。

5、Kafka零拷贝机制保证读数据高性能

消费者读取数据流程:

  1. 消费者发送请求给kafka服务
  2. kafka服务去os cache缓存读取数据(缓存没有就去磁盘读取数据)
  3. 从磁盘读取了数据到os cache缓存中
  4. os cache复制数据到kafka应用程序中
  5. kafka将数据(复制)发送到socket cache中
  6. socket cache通过网卡传输给消费者

图片

kafka linux sendfile技术 — 零拷贝

1.消费者发送请求给kafka服务 2.kafka服务去os cache缓存读取数据(缓存没有就去磁盘读取数据) 3.从磁盘读取了数据到os cache缓存中 4.os cache直接将数据发送给网卡 5.通过网卡将数据传输给消费者

图片

6、Kafka日志分段保存

Kafka中一个主题,一般会设置分区;比如创建了一个topic_a,然后创建的时候指定了这个主题有三个分区。其实在三台服务器上,会创建三个目录。服务器1(kafka1)创建目录topic_a-0:。目录下面是我们文件(存储数据),kafka数据就是message,数据存储在log文件里。.log结尾的就是日志文件,在kafka中把数据文件就叫做日志文件 。一个分区下面默认有n多个日志文件(分段存储),一个日志文件默认1G。

图片

服务器2(kafka2):创建目录topic_a-1: 服务器3(kafka3):创建目录topic_a-2:

7、Kafka二分查找定位数据

Kafka里面每一条消息,都有自己的offset(相对偏移量),存在物理磁盘上面,在position Position:物理位置(磁盘上面哪个地方)也就是说一条消息就有两个位置:offset:相对偏移量(相对位置)position:磁盘物理位置稀疏索引: Kafka中采用了稀疏索引的方式读取索引,kafka每当写入了4k大小的日志(.log),就往index里写入一个记录索引。其中会采用二分查找。

图片

8、高并发网络设计(先了解NIO)

网络设计部分是kafka中设计最好的一个部分,这也是保证Kafka高并发、高性能的原因,对kafka进行调优,就得对kafka原理比较了解,尤其是网络设计部分

Reactor网络设计模式1:

图片

Reactor网络设计模式2:

图片

Reactor网络设计模式3:

图片

Kafka超高并发网络设计:

图片

图片

9、Kafka冗余副本保证高可用

在kafka里面分区是有副本的,注:0.8以前是没有副本机制的。创建主题时,可以指定分区,也可以指定副本个数。副本是有角色的:leader partition:1、写数据、读数据操作都是从leader partition去操作的。2、会维护一个ISR(in-sync- replica )列表,但是会根据一定的规则删除ISR列表里面的值 生产者发送来一个消息,消息首先要写入到leader partition中 写完了以后,还要把消息写入到ISR列表里面的其它分区,写完后才算这个消息提交 follower partition:从leader partition同步数据。

10、优秀架构思考-总结

Kafka — 高并发、高可用、高性能 高可用:多副本机制 高并发:网络架构设计 三层架构:多selector -> 多线程 -> 队列的设计(NIO) 高性能:写数据:

  1. 把数据先写入到OS Cache
  2. 写到磁盘上面是顺序写,性能很高

读数据:

  1. 根据稀疏索引,快速定位到要消费的数据
  2. 零拷贝机制 减少数据的拷贝 减少了应用程序与操作系统上下文切换

11、Kafka生产环境搭建

11.1 需求场景分析

电商平台,需要每天10亿请求都要发送到Kafka集群上面。二八反正,一般评估出来问题都不大。10亿请求 -> 24 过来的,一般情况下,每天的12:00 到早上8:00 这段时间其实是没有多大的数据量的。80%的请求是用的另外16小时的处理的。16个小时处理 -> 8亿的请求。16 * 0.2 = 3个小时 处理了8亿请求的80%的数据

也就是说6亿的数据是靠3个小时处理完的。我们简单的算一下高峰期时候的qps6亿/3小时 =5.5万/s qps=5.5万

10亿请求 * 50kb = 46T 每天需要存储46T的数据

一般情况下,我们都会设置两个副本 46T * 2 = 92T Kafka里面的数据是有保留的时间周期,保留最近3天的数据。92T * 3天 = 276T我这儿说的是50kb不是说一条消息就是50kb不是(把日志合并了,多条日志合并在一起),通常情况下,一条消息就几b,也有可能就是几百字节。

11.2 物理机数量评估

1)首先分析一下是需要虚拟机还是物理机 像Kafka mysql hadoop这些集群搭建的时候,我们生产里面都是使用物理机。2)高峰期需要处理的请求总的请求每秒5.5万个,其实一两台物理机绝对是可以抗住的。一般情况下,我们评估机器的时候,是按照高峰期的4倍的去评估。如果是4倍的话,大概我们集群的能力要准备到 20万qps。这样子的集群才是比较安全的集群。大概就需要5台物理机。每台承受4万请求。

场景总结:搞定10亿请求,高峰期5.5万的qps,276T的数据,需要5台物理机。

11.3 磁盘选择

搞定10亿请求,高峰期5.5万的qps,276T的数据,需要5台物理机。1)SSD固态硬盘,还是需要普通的机械硬盘**SSD硬盘:性能比较好,但是价格贵 SAS盘:某方面性能不是很好,但是比较便宜。SSD硬盘性能比较好,指的是它随机读写的性能比较好。适合MySQL这样集群。**但是其实他的顺序写的性能跟SAS盘差不多。kafka的理解:就是用的顺序写。所以我们就用普通的【机械硬盘】就可以了。

2)需要我们评估每台服务器需要多少块磁盘 5台服务器,一共需要276T ,大约每台服务器 需要存储60T的数据。我们公司里面服务器的配置用的是 11块硬盘,每个硬盘 7T。11 * 7T = 77T

77T * 5 台服务器 = 385T。

场景总结:

搞定10亿请求,需要5台物理机,11(SAS) * 7T

11.4 内存评估

搞定10亿请求,需要5台物理机,11(SAS) * 7T

我们发现kafka读写数据的流程 都是基于os cache,换句话说假设咱们的os cashe无限大那么整个kafka是不是相当于就是基于内存去操作,如果是基于内存去操作,性能肯定很好。内存是有限的。1) 尽可能多的内存资源要给 os cache 2) Kafka的代码用 核心的代码用的是scala写的,客户端的代码java写的。都是基于jvm。所以我们还要给一部分的内存给jvm。Kafka的设计,没有把很多数据结构都放在jvm里面。所以我们的这个jvm不需要太大的内存。根据经验,给个10G就可以了。

NameNode: jvm里面还放了元数据(几十G),JVM一定要给得很大。比如给个100G。

假设我们这个10请求的这个项目,一共会有100个topic。100 topic * 5 partition * 2 = 1000 partition 一个partition其实就是物理机上面的一个目录,这个目录下面会有很多个.log的文件。.log就是存储数据文件,默认情况下一个.log文件的大小是1G。我们如果要保证 1000个partition 的最新的.log 文件的数据 如果都在内存里面,这个时候性能就是最好。1000 * 1G = 1000G内存. 我们只需要把当前最新的这个log 保证里面的25%的最新的数据在内存里面。250M * 1000 = 0.25 G* 1000 =250G的内存。

250内存 / 5 = 50G内存 50G+10G = 60G内存

64G的内存,另外的4G,操作系统本生是不是也需要内存。其实Kafka的jvm也可以不用给到10G这么多。评估出来64G是可以的。当然如果能给到128G的内存的服务器,那就最好。

我刚刚评估的时候用的都是一个topic是5个partition,但是如果是数据量比较大的topic,可能会有10个partition。

总结:搞定10亿请求,需要5台物理机,11(SAS) * 7T ,需要64G的内存(128G更好)

11.5 CPU压力评估

评估一下每台服务器需要多少cpu core(资源很有限)

我们评估需要多少个cpu ,依据就是看我们的服务里面有多少线程去跑。线程就是依托cpu 去运行的。如果我们的线程比较多,但是cpu core比较少,这样的话,我们的机器负载就会很高,性能不就不好。

评估一下,kafka的一台服务器 启动以后会有多少线程?

Acceptor线程 1 processor线程 3 6~9个线程 处理请求线程 8个 32个线程 定时清理的线程,拉取数据的线程,定时检查ISR列表的机制 等等。所以大概一个Kafka的服务启动起来以后,会有一百多个线程。

cpu core = 4个,一遍来说,几十个线程,就肯定把cpu 打满了。cpu core = 8个,应该很轻松的能支持几十个线程。如果我们的线程是100多个,或者差不多200个,那么8 个 cpu core是搞不定的。所以我们这儿建议:CPU core = 16个。如果可以的话,能有32个cpu core 那就最好。

结论:kafka集群,最低也要给16个cpu core,如果能给到32 cpu core那就更好。2cpu * 8 =16 cpu core 4cpu * 8 = 32 cpu core

总结:搞定10亿请求,需要5台物理机,11(SAS) * 7T ,需要64G的内存(128G更好),需要16个cpu core(32个更好)

11.6 网络需求评估

评估我们需要什么样网卡?一般要么是千兆的网卡(1G/s),还有的就是万兆的网卡(10G/s)

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码高峰期的时候 每秒会有5.5万的请求涌入,5.5/5 = 大约是每台服务器会有1万个请求涌入。
我们之前说的,
10000 * 50kb = 488M  也就是每条服务器,每秒要接受488M的数据。数据还要有副本,副本之间的同步
也是走的网络的请求。488 * 2 = 976m/s
说明一下:
   很多公司的数据,一个请求里面是没有50kb这么大的,我们公司是因为主机在生产端封装了数据
   然后把多条数据合并在一起了,所以我们的一个请求才会有这么大。
   
说明一下:
   一般情况下,网卡的带宽是达不到极限的,如果是千兆的网卡,我们能用的一般就是700M左右。
   但是如果最好的情况,我们还是使用万兆的网卡。
   如果使用的是万兆的,那就是很轻松。

11.7 集群规划

请求量 规划物理机的个数 分析磁盘的个数,选择使用什么样的磁盘 内存 cpu core 网卡就是告诉大家,以后要是公司里面有什么需求,进行资源的评估,服务器的评估,大家按照我的思路去评估

一条消息的大小 50kb -> 1kb 500byte 1Mip 主机名 192.168.0.100 hadoop1 192.168.0.101 hadoop2 192.168.0.102 hadoop3

主机的规划:kafka集群架构的时候:主从式的架构:controller -> 通过zk集群来管理整个集群的元数据。

  1. zookeeper集群 hadoop1 hadoop2 hadoop3
  2. kafka集群 理论上来讲,我们不应该把kafka的服务于zk的服务安装在一起。但是我们这儿服务器有限。所以我们kafka集群也是安装在hadoop1 haadoop2 hadoop3

12、kafka运维

12.1 常见运维工具介绍

KafkaManager — 页面管理工具

12.2 常见运维命令

场景一:topic数据量太大,要增加topic数

一开始创建主题的时候,数据量不大,给的分区数不多。

1
2
css复制代码kafka-topics.sh --create --zookeeper hadoop1:2181,hadoop2:2181,hadoop3:2181 --replication-factor 1 --partitions 1 --topic test6
kafka-topics.sh --alter --zookeeper hadoop1:2181,hadoop2:2181,ha

broker id:

hadoop1:0 hadoop2:1 hadoop3:2 假设一个partition有三个副本:partition0:a,b,c

a:leader partition b,c:follower partition

ISR:{a,b,c}如果一个follower分区 超过10秒 没有向leader partition去拉取数据,那么这个分区就从ISR列表里面移除。

场景二:核心topic增加副本因子

如果对核心业务数据需要增加副本因子 vim test.json脚本,将下面一行json脚本保存

1
css复制代码{“version”:1,“partitions”:[{“topic”:“test6”,“partition”:0,“replicas”:[0,1,2]},{“topic”:“test6”,“partition”:1,“replicas”:[0,1,2]},{“topic”:“test6”,“partition”:2,“replicas”:[0,1,2]}]}

执行上面json脚本:

1
css复制代码kafka-reassign-partitions.sh --zookeeper hadoop1:2181,hadoop2:2181,hadoop3:2181 --reassignment-json-file test.json --execute

场景三:负载不均衡的topic,手动迁移vi topics-to-move.json

1
2
3
4
css复制代码{“topics”: [{“topic”: “test01”}, {“topic”: “test02”}], “version”: 1} // 把你所有的topic都写在这里


kafka-reassgin-partitions.sh --zookeeper hadoop1:2181,hadoop2:2181,hadoop3:2181 --topics-to-move-json-file topics-to-move.json --broker-list “5,6” --generate

把你所有的包括新加入的broker机器都写在这里,就会说是把所有的partition均匀的分散在各个broker上,包括新进来的broker此时会生成一个迁移方案,可以保存到一个文件里去:expand-cluster-reassignment.json

1
2
3
lua复制代码kafka-reassign-partitions.sh --zookeeper hadoop01:2181,hadoop02:2181,hadoop03:2181 --reassignment-json-file expand-cluster-reassignment.json --execute

kafka-reassign-partitions.sh --zookeeper hadoop01:2181,hadoop02:2181,hadoop03:2181 --reassignment-json-file expand-cluster-reassignment.json --verify

这种数据迁移操作一定要在晚上低峰的时候来做,因为他会在机器之间迁移数据,非常的占用带宽资源–generate: 根据给予的Topic列表和Broker列表生成迁移计划。generate并不会真正进行消息迁移,而是将消息迁移计划计算出来,供execute命令使用。–execute: 根据给予的消息迁移计划进行迁移。–verify: 检查消息是否已经迁移完成。

场景四:如果某个broker leader partition过多

正常情况下,我们的leader partition在服务器之间是负载均衡。hadoop1 4 hadoop2 1 hadoop3 1

现在各个业务方可以自行申请创建 topic,分区数量都是自动分配和后续动态调整的, kafka本身会自动把leader partition均匀分散在各个机器上,这样可以保证每台机器的读写吞吐量都是均匀的 但是也有例外,那就是如果某些broker宕机,会导致 leader partition 过于集中在其他少部分几台broker上, 这会导致少数几台broker的读写请求压力过高,其他宕机的 broker 重启之后都是folloer partition,读写请求很低,造成集群负载不均衡有一个参数,auto.leader.rebalance.enable,默认是true, 每隔300秒(leader.imbalance.check.interval.seconds)检查leader负载是否平衡 如果一台broker上的不均衡的leader超过了10%,leader.imbalance.per.broker.percentage, 就会对这个broker进行选举 配置参数:auto.leader.rebalance.enable 默认是true leader.imbalance.per.broker.percentage: 每个broker允许的不平衡的leader的比率。如果每个broker超过了这个值,控制器会触发leader的平衡。这个值表示百分比。10% leader.imbalance.check.interval.seconds:默认值300秒

13、Kafka生产者

13.1 生产者发送消息原理

图片

13.2 生产者发送消息原理—基础案例演示

图片

13.3 如何提升吞吐量

如何提升吞吐量:参数一:buffer.memory:设置发送消息的缓冲区,默认值是33554432,就是32MB 参数二:compression.type:默认是none,不压缩,但是也可以使用lz4压缩,效率还是不错的,压缩之后可以减小数据量,提升吞吐量,但是会加大producer端的cpu开销 参数三:batch.size:设置batch的大小,如果batch太小,会导致频繁网络请求,吞吐量下降;如果batch太大,会导致一条消息需要等待很久才能被发送出去,而且会让内存缓冲区有很大压力,过多数据缓冲在内存里,默认值是:16384,就是16kb,也就是一个batch满了16kb就发送出去,一般在实际生产环境,这个batch的值可以增大一些来提升吞吐量,如果一个批次设置大了,会有延迟。一般根据一条消息大小来设置。如果我们消息比较少。配合使用的参数linger.ms,这个值默认是0,意思就是消息必须立即被发送,但是这是不对的,一般设置一个100毫秒之类的,这样的话就是说,这个消息被发送出去后进入一个batch,如果100毫秒内,这个batch满了16kb,自然就会发送出去。

13.4 如何处理异常

  1. LeaderNotAvailableException:这个就是如果某台机器挂了,此时leader副本不可用,会导致你写入失败,要等待其他follower副本切换为leader副本之后,才能继续写入,此时可以重试发送即可;如果说你平时重启kafka的broker进程,肯定会导致leader切换,一定会导致你写入报错,是LeaderNotAvailableException。
  2. NotControllerException:这个也是同理,如果说Controller所在Broker挂了,那么此时会有问题,需要等待Controller重新选举,此时也是一样就是重试即可。
  3. NetworkException:网络异常 timeout a. 配置retries参数,他会自动重试的 b. 但是如果重试几次之后还是不行,就会提供Exception给我们来处理了,我们获取到异常以后,再对这个消息进行单独处理。我们会有备用的链路。发送不成功的消息发送到Redis或者写到文件系统中,甚至是丢弃。

13.5 重试机制

重试会带来一些问题:

  1. 消息会重复有的时候一些leader切换之类的问题,需要进行重试,设置retries即可,但是消息重试会导致,重复发送的问题,比如说网络抖动一下导致他以为没成功,就重试了,其实人家都成功了.
  2. 消息乱序消息重试是可能导致消息的乱序的,因为可能排在你后面的消息都发送出去了。所以可以使用”max.in.flight.requests.per.connection”参数设置为1, 这样可以保证producer同一时间只能发送一条消息。两次重试的间隔默认是100毫秒,用”retry.backoff.ms”来进行设置 基本上在开发过程中,靠重试机制基本就可以搞定95%的异常问题。

13.6 ACK参数详解

producer端设置的 request.required.acks=0;只要请求已发送出去,就算是发送完了,不关心有没有写成功。性能很好,如果是对一些日志进行分析,可以承受丢数据的情况,用这个参数,性能会很好。request.required.acks=1;发送一条消息,当leader partition写入成功以后,才算写入成功。不过这种方式也有丢数据的可能。request.required.acks=-1;需要ISR列表里面,所有副本都写完以后,这条消息才算写入成功。ISR:1个副本。1 leader partition 1 follower partition kafka服务端:min.insync.replicas:1, 如果我们不设置的话,默认这个值是1 一个leader partition会维护一个ISR列表,这个值就是限制ISR列表里面 至少得有几个副本,比如这个值是2,那么当ISR列表里面只有一个副本的时候。往这个分区插入数据的时候会报错。设计一个不丢数据的方案:数据不丢失的方案:1)分区副本 >=2 2)acks = -1 3)min.insync.replicas >=2 还有可能就是发送有异常:对异常进行处理

13.7 自定义分区

分区:1、没有设置key我们的消息就会被轮训的发送到不同的分区。2、设置了keykafka自带的分区器,会根据key计算出来一个hash值,这个hash值会对应某一个分区。如果key相同的,那么hash值必然相同,key相同的值,必然是会被发送到同一个分区。但是有些比较特殊的时候,我们就需要自定义分区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arduino复制代码public class HotDataPartitioner implements Partitioner {
private Random random;
@Override
public void configure(Map<String, ?> configs) {
random = new Random();
}
@Override
public int partition(String topic, Object keyObj, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
String key = (String)keyObj;
List partitionInfoList = cluster.availablePartitionsForTopic(topic);
//获取到分区的个数 0,1,2
int partitionCount = partitionInfoList.size();
//最后一个分区
int hotDataPartition = partitionCount - 1;
return !key.contains(“hot_data”) ? random.nextInt(partitionCount - 1) : hotDataPartition;
}
}

如何使用:配置上这个类即可:props.put(”partitioner.class”, “com.zhss.HotDataPartitioner”);

13.8 综合案例演示

14.1 消费组概念 groupid相同就属于同一个消费组 1)每个consumer都要属于一个consumer.group,就是一个消费组,topic的一个分区只会分配给 一个消费组下的一个consumer来处理,每个consumer可能会分配多个分区,也有可能某个consumer没有分配到任何分区 2)如果想要实现一个广播的效果,那只需要使用不同的group id去消费就可以。topicA: partition0、partition1 groupA:consumer1:消费 partition0 consuemr2:消费 partition1 consuemr3:消费不到数据 groupB: consuemr3:消费到partition0和partition1 3)如果consumer group中某个消费者挂了,此时会自动把分配给他的分区交给其他的消费者,如果他又重启了,那么又会把一些分区重新交还给他

14、Kafka消费者

14.1 消费组概念

groupid相同就属于同一个消费组 1)每个consumer都要属于一个consumer.group,就是一个消费组,topic的一个分区只会分配给 一个消费组下的一个consumer来处理,每个consumer可能会分配多个分区,也有可能某个consumer没有分配到任何分区 2)如果想要实现一个广播的效果,那只需要使用不同的group id去消费就可以。topicA: partition0、partition1 groupA:consumer1:消费 partition0 consuemr2:消费 partition1 consuemr3:消费不到数据 groupB: consuemr3:消费到partition0和partition1 3)如果consumer group中某个消费者挂了,此时会自动把分配给他的分区交给其他的消费者,如果他又重启了,那么又会把一些分区重新交还给他

14.2 基础案例演示

图片

14.3 偏移量管理

  1. 每个consumer内存里数据结构保存对每个topic的每个分区的消费offset,定期会提交offset,老版本是写入zk,但是那样高并发请求zk是不合理的架构设计,zk是做分布式系统的协调的,轻量级的元数据存储,不能负责高并发读写,作为数据存储。
  2. 现在新的版本提交offset发送给kafka内部topic:__consumer_offsets,提交过去的时候, key是group.id+topic+分区号,value就是当前offset的值,每隔一段时间,kafka内部会对这个topic进行compact(合并),也就是每个group.id+topic+分区号就保留最新数据。
  3. __consumer_offsets可能会接收高并发的请求,所以默认分区50个(leader partitiron -> 50 kafka),这样如果你的kafka部署了一个大的集群,比如有50台机器,就可以用50台机器来抗offset提交的请求压力. 消费者 -> broker端的数据 message -> 磁盘 -> offset 顺序递增 从哪儿开始消费?-> offset 消费者(offset)

14.4 偏移量监控工具介绍

  1. web页面管理的一个管理软件(kafka Manager) 修改bin/kafka-run-class.sh脚本,第一行增加JMX_PORT=9988 重启kafka进程
  2. 另一个软件:主要监控的consumer的偏移量。就是一个jar包 java -cp KafkaOffsetMonitor-assembly-0.3.0-SNAPSHOT.jar com.quantifind.kafka.offsetapp.OffsetGetterWeb –offsetStorage kafka \(根据版本:偏移量存在kafka就填kafka,存在zookeeper就填zookeeper) –zk hadoop1:2181 –port 9004 –refresh 15.seconds –retain 2.days。

14.5 消费异常感知

heartbeat.interval.ms:consumer心跳时间间隔,必须得与coordinator保持心跳才能知道consumer是否故障了, 然后如果故障之后,就会通过心跳下发rebalance的指令给其他的consumer通知他们进行rebalance的操作 session.timeout.ms:kafka多长时间感知不到一个consumer就认为他故障了,默认是10秒 max.poll.interval.ms:如果在两次poll操作之间,超过了这个时间,那么就会认为这个consume处理能力太弱了,会被踢出消费组,分区分配给别人去消费,一般来说结合业务处理的性能来设置就可以了。

14.6 核心参数解释

fetch.max.bytes:获取一条消息最大的字节数,一般建议设置大一些,默认是1M 其实我们在之前多个地方都见到过这个类似的参数,意思就是说一条信息最大能多大?

  1. Producer 发送的数据,一条消息最大多大, -> 10M
  2. Broker 存储数据,一条消息最大能接受多大 -> 10M
  3. Consumer max.poll.records: 一次poll返回消息的最大条数,默认是500条 connection.max.idle.ms:consumer跟broker的socket连接如果空闲超过了一定的时间,此时就会自动回收连接,但是下次消费就要重新建立socket连接,这个建议设置为-1,不要去回收 enable.auto.commit: 开启自动提交偏移量 auto.commit.interval.ms: 每隔多久提交一次偏移量,默认值5000毫秒 _consumer_offset auto.offset.reset:earliest 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费 topica -> partition0:1000 partitino1:2000 latest 当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据 none topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常

14.7 综合案例演示

引入案例:二手电商平台(欢乐送),根据用户消费的金额,对用户星星进行累计。订单系统(生产者) -> Kafka集群里面发送了消息。会员系统(消费者) -> Kafak集群里面消费消息,对消息进行处理。

14.8 group coordinator原理

面试题:消费者是如何实现rebalance的?— 根据coordinator实现

  1. 什么是coordinator 每个consumer group都会选择一个broker作为自己的coordinator,他是负责监控这个消费组里的各个消费者的心跳,以及判断是否宕机,然后开启rebalance的
  2. 如何选择coordinator机器 首先对groupId进行hash(数字),接着对__consumer_offsets的分区数量取模,默认是50,_consumer_offsets的分区数可以通过offsets.topic.num.partitions来设置,找到分区以后,这个分区所在的broker机器就是coordinator机器。比如说:groupId,“myconsumer_group” -> hash值(数字)-> 对50取模 -> 8 __consumer_offsets 这个主题的8号分区在哪台broker上面,那一台就是coordinator 就知道这个consumer group下的所有的消费者提交offset的时候是往哪个分区去提交offset,
  3. 运行流程 1)每个consumer都发送JoinGroup请求到Coordinator, 2)然后Coordinator从一个consumer group中选择一个consumer作为leader, 3)把consumer group情况发送给这个leader, 4)接着这个leader会负责制定消费方案, 5)通过SyncGroup发给Coordinator 6)接着Coordinator就把消费方案下发给各个consumer,他们会从指定的分区的 leader broker开始进行socket连接以及消费消息

图片

14.9 rebalance策略

consumer group靠coordinator实现了Rebalance

这里有三种rebalance的策略:range、round-robin、sticky

比如我们消费的一个主题有12个分区:p0,p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11 假设我们的消费者组里面有三个消费者

  1. range策略 range策略就是按照partiton的序号范围 p03 consumer1 p47 consumer2 p8~11 consumer3 默认就是这个策略;
  2. round-robin策略 就是轮询分配 consumer1:0,3,6,9 consumer2:1,4,7,10 consumer3:2,5,8,11 但是前面的这两个方案有个问题:12 -> 2 每个消费者会消费6个分区

假设consuemr1挂了:p0-5分配给consumer2,p6-11分配给consumer3 这样的话,原本在consumer2上的的p6,p7分区就被分配到了 consumer3上。

  1. sticky策略 最新的一个sticky策略,就是说尽可能保证在rebalance的时候,让原本属于这个consumer 的分区还是属于他们,然后把多余的分区再均匀分配过去,这样尽可能维持原来的分区分配的策略

consumer1:0-3 consumer2: 4-7 consumer3: 8-11 假设consumer3挂了 consumer1:0-3,+8,9 consumer2: 4-7,+10,11

15、Broker管理

15.1 Leo、hw含义

  1. Kafka的核心原理
  2. 如何去评估一个集群资源
  3. 搭建了一套kafka集群 -》 介绍了简单的一些运维管理的操作。
  4. 生产者(使用,核心的参数)
  5. 消费者(原理,使用的,核心参数)
  6. broker内部的一些原理

核心的概念:LEO,HW LEO:是跟offset偏移量有关系。

LEO:在kafka里面,无论leader partition还是follower partition统一都称作副本(replica)。

每次partition接收到一条消息,都会更新自己的LEO,也就是log end offset,LEO其实就是最新的offset + 1

HW:高水位 LEO有一个很重要的功能就是更新HW,如果follower和leader的LEO同步了,此时HW就可以更新 HW之前的数据对消费者是可见,消息属于commit状态。HW之后的消息消费者消费不到。

15.2 Leo更新

图片

15.3 hw更新

图片

15.4 controller如何管理整个集群

1: 竞争controller的 /controller/id 2:controller服务监听的目录:/broker/ids/ 用来感知 broker上下线 /broker/topics/ 创建主题,我们当时创建主题命令,提供的参数,ZK地址。/admin/reassign_partitions 分区重分配 ……图片

15.5 延时任务

kafka的延迟调度机制(扩展知识) 我们先看一下kafka里面哪些地方需要有任务要进行延迟调度。第一类延时的任务:比如说producer的acks=-1,必须等待leader和follower都写完才能返回响应。有一个超时时间,默认是30秒(request.timeout.ms)。所以需要在写入一条数据到leader磁盘之后,就必须有一个延时任务,到期时间是30秒延时任务 放到DelayedOperationPurgatory(延时管理器)中。假如在30秒之前如果所有follower都写入副本到本地磁盘了,那么这个任务就会被自动触发苏醒,就可以返回响应结果给客户端了, 否则的话,这个延时任务自己指定了最多是30秒到期,如果到了超时时间都没等到,就直接超时返回异常。第二类延时的任务:follower往leader拉取消息的时候,如果发现是空的,此时会创建一个延时拉取任务 延时时间到了之后(比如到了100ms),就给follower返回一个空的数据,然后follower再次发送请求读取消息, 但是如果延时的过程中(还没到100ms),leader写入了消息,这个任务就会自动苏醒,自动执行拉取任务。

海量的延时任务,需要去调度。

15.6 时间轮机制

  1. 什么会有要设计时间轮?Kafka内部有很多延时任务,没有基于JDK Timer来实现,那个插入和删除任务的时间复杂度是O(nlogn), 而是基于了自己写的时间轮来实现的,时间复杂度是O(1),依靠时间轮机制,延时任务插入和删除,O(1)
  2. 时间轮是什么?其实时间轮说白其实就是一个数组。tickMs:时间轮间隔 1ms wheelSize:时间轮大小 20 interval:timckMS * whellSize,一个时间轮的总的时间跨度。20ms currentTime:当时时间的指针。a:因为时间轮是一个数组,所以要获取里面数据的时候,靠的是index,时间复杂度是O(1) b:数组某个位置上对应的任务,用的是双向链表存储的,往双向链表里面插入,删除任务,时间复杂度也是O(1) 举例:插入一个8ms以后要执行的任务 19ms 3.多层级的时间轮 比如:要插入一个110毫秒以后运行的任务。tickMs:时间轮间隔 20ms wheelSize:时间轮大小 20 interval:timckMS * whellSize,一个时间轮的总的时间跨度。20ms currentTime:当时时间的指针。第一层时间轮:1ms * 20 第二层时间轮:20ms * 20 第三层时间轮:400ms * 20

图片

Respect ~

推荐阅读:

通透!数据仓库领域常见建模方法及实例演示

橙心优选-数据仓库高级工程师面经

关于构建与优化数据仓库架构与模型设计

全面解读数据中台、数据仓库和数据湖

架构师 | 数据仓库建设灵魂10问

图片

我是「云祁」,一枚热爱技术、会写诗的大数据开发猿,欢迎大家关注呀!

云祁QI

云祁QI 人生,海海,破浪前行。

本文使用 文章同步助手 同步

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

三、SpringBoot框架之日志

发表于 2021-05-17
  1. 日志框架

日志的起源:

① 在系统开发过程中将关键的数据输出到控制台;

② 系统中过多的控制台输出语句不便于维护,将控制台输出语句整合成日志jar包 xxxlogging.jar;

③ 对日志jar包升级产生新的日志jar包,xxxlogging-update.jar 面临对系统中已经使用的日志jar包替换问题;

④ 参考JDBC和数据库驱动之间的关系(JDBC只是调用接口,各大数据库厂商提供实现类)创建日志门面(日志的抽象层)logging-abstract.jar;

⑤ 项目中使用日志只需要导入具体的日志实现即可,日志框架都实现了日志抽象层;

市面上常见的日志框架:

JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j……

框架简介:

日志框架名称 所属类型 简介
JCL(Jakarta Commons Logging) 日志门面 Apache提供的一个通用日志API
SLF4j(Simple Logging Facade for Java) 日志门面 SLF4J提供了统一的记录日志的接口
jboss-logging 日志门面 jboss-logging是一款类似于slf4j的日志框架
Log4j(Log for java) 日志实现 Apache基金会一款优秀的开源日志框架
JUL(java.util.logging) 日志实现 JDK中提供的日志输出
Log4j2 日志实现 Apache基金会推出的Log4J的升级版,支持插件式结构
Logback 日志实现 Logback是由log4j创始人设计的又一个开源日志框架
  • 在使用日志框架时,选择一个日志门面作为调用接口,选择一个日志框架实现作为具体实现;
  • Spring Boot的底层Spring FrameWork框架默认使用的JCL框架;
  • 而Spring Boot 中Starter整合的日志门面是SLF4j,日志实现选用的是logback;

关于以上框架的详细介绍请参考:常用日志框架比较

  1. SLF4J框架

SLF4J(Simple Logging Facade for Java)即简单日志门面,不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志系统。

SLF4J 是一个日志抽象层,允许你使用任何一个日志系统,并且可以随时切换其他日志框架还不需要修改已经写好的程序。SLF4J只负责制定日志标准,并不是日志系统的具体实现。SLF4J只负责两件事情:

① 提供日志接口;

② 提供获取具体日志对象的方法;

2.1 SLF4j框架的使用

我们在使用日志调用日志中的方法时,应当调用日志抽象层的方法,而不是日志的具体实现类。

SLF4J框架使用步骤:

① 导入SLF4J的jar包和logback的实现jar包;

② 使用SLF4J提供的方法使用日志;

官方代码示例:

1
2
3
4
5
6
7
8
9
java复制代码import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}
  • 我们仅导入 slf4j-api.jar 包是不能实现日志操作的。SLF4J只提供日志操作接口,不提供具体的操作方法,如果想使用SLF4J作为日志的操作的门面还需要导入具体的日志实现框架;

例如:使用SLF4J框架和Logback框架共同完成日志操作,则需要导入 slf4j-api.jar logback-classic.jar logback-core.jar 包,即:使用SLF4J框架作为日志操作的门面,而Logback框架作为日志操作的具体实现类;

  • 如果使用了不是直接实现SLF4J框架的日志实现类则需要引入中间适配包进行整合调用;

例如:使用SLF4J框架和log4j框架完成日志操作,除了依赖 slf4-api.jar log4j.jar 包外还需要依赖适配包 slf4j-log412.jar;

SLF4J框架与其他日志框架整合时需要依赖的jar包关系如下图所示:

每种日志实现框架都有自己的配置文件,在使用SLF4J后,配置文件还是书写成日志实现框架本身的配置文件。

SLF4J的官方文档:SLF4J使用手册

2.2 SLF4j框架统一日志输出

当我们在进行框架整合时会遇到不同的框架使用的日志输出框架不统一的问题,需要我们进行日志框架的统一。

例如:我们的系统开发计划使用slf4j和logback日志框架做日志输出,我们的项目使用SSH框架。但是Spring Framework框架默认的日志输出是commons-logging框架,Hibernate框架默认的日志输出是jboss-logging框架,这时就需要我们对SLF4j框架的日志统一输出,屏蔽系统中其他日志输出框架。

如何让系统中所有的日志都统一到slf4j:

① 将系统中其他日志框架先排除出去;

② 用中间包来替换原有的日志框架;

③ 我们导入slf4j其他的实现;

例如:在项目中使用slf4j作为日志的门面框架,logback作为日志的实现框架,需要导入slf4j和logback相关的jar包。但项目中整合的Spring Frame框架使用的日志框架为JCL,此时需要将JCL日志框架在项目中排除掉,引入中间

包 jcl-over-slfj4.jar 替换原来JCL的jar包。这样项目中的日志输出也就统一到slf4j为日志的门面框架,logback为日志的实现框架了。

使用slf4j统一日志框架图示:

相关链接:slf4j解决遗留问题

  1. Spring Boot与日志

3.1 Spring Boot中日志的配置

pom.xml 中导入 了spring-boot-starter-web 依赖;

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

spring-boot-starter-web 依赖中包含 spring-boot-stater 依赖;

1
2
3
4
5
6
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>

spring-boot-stater 依赖中包含 spring-boot-starter-logging 依赖用于配置日志相关;

1
2
3
4
5
6
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>

spring-boot-starter-logging 依赖的构成

image-20201020094345703

Spring Boot底层使用 slf4j + logback 作为日志输出,并对整合的其他框架中默认的日志框架进行替换,实现日志输出框架的统一。

3.2 Spring Boot解决日志框架冲突

pom.xml 中书写了父类的依赖关系指向 spring-boot-starter-parent-xxx.pom 对 Spring Boot 框架进行配置;

1
2
3
4
5
6
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

spring-boot-starter-parent-xxx.pom 指定了 Spring Boot 中引用所有框架的依赖包 spring-boot-dependencies.pom;

1
2
3
4
5
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.4.RELEASE</version>
</parent>

spring-boot-dependencies.pom 中指定jar包的版本以及对框架中默认的日志框架排除;

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-console</artifactId>
<version>${activemq.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

也可以在导入框架时排除默认的日志框架,例如:排除 Spring Boot 中默认的日志框架配置;

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

Spring Boot排除其他框架中默认的日志框架时,在 pom.xml 文件中使用 <exclusion></exclusion> 标签进行依赖框架的排除。

Spring Boot中日志的配置总结:

① Spring Boot底层采用 slf4j + logback 的方式进行日志记录;

② Spring Boot将其他的日志框架替换成了 slf4j 框架;

③ 日志中间替换包实际上还是调用了 slf4j 的方法;

④ 当我们引入其他框架时,需要将框架中默认的日志框架移除掉;

一句话总结:Spring Boot 能自动适配所有日志,并且底层采用 slf4j 和 logback 框架作为日志记录,当我们引入其他框架时,需要将框架中的默认日志框架移除掉。

  1. 日志的使用

4.1 默认的日志配置

Spring Boot默认帮我们配置好日志,可以直接使用。

  • 日志的输出级别,由高到低依次为:trace<debug < info < warn < error;
  • 日志的输出级别可以调整,设置日志输出后,日志只会在当前设置级别和以后的级别生效。

例如:设置了 info 级别的日志输出,则 trace 级别和 debug 级别的日志不会再输出;

  • Spring Boot默认的日志输出级别是 info 级别(root 级别);

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
class Springboot03LoggingApplicationTests {

Logger logger = LoggerFactory.getLogger(getClass());

/**
* 日志的输出级别:由高到低 trace<debug<info<warn<error
* 可以调整日志的输出级别;设置日志输出级别后,日志只会在当前级别和以后的级别生效
*/
@Test
void contextLoads() {
logger.trace("trace级别日志。。。。。");
logger.debug("debug级别日志。。。。。");
// Spring Boot默认输出info级别的日志,没有指定日志级别则使用Spring Boot默认的级别(root级别)
logger.info("info级别日志。。。。。");
logger.warn("warn级别日志。。。。。");
logger.error("error级别日志。。。。。");
}

}

运行后输出的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vbnet复制代码  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.4.RELEASE)

2020-10-20 14:46:25.049 INFO 17748 --- [ main] .b.s.Springboot03LoggingApplicationTests : Starting Springboot03LoggingApplicationTests on YOGA-S740 with PID 17748 (started by Bruce in E:\workspace\workspace_idea03\demo-springBoot\springboot03_logging)
2020-10-20 14:46:25.051 INFO 17748 --- [ main] .b.s.Springboot03LoggingApplicationTests : No active profile set, falling back to default profiles: default
2020-10-20 14:46:26.248 INFO 17748 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2020-10-20 14:46:26.520 INFO 17748 --- [ main] .b.s.Springboot03LoggingApplicationTests : Started Springboot03LoggingApplicationTests in 1.745 seconds (JVM running for 2.833)

2020-10-20 14:46:26.716 INFO 17748 --- [ main] .b.s.Springboot03LoggingApplicationTests : info级别日志。。。。。
2020-10-20 14:46:26.716 WARN 17748 --- [ main] .b.s.Springboot03LoggingApplicationTests : warn级别日志。。。。。
2020-10-20 14:46:26.716 ERROR 17748 --- [ main] .b.s.Springboot03LoggingApplicationTests : error级别日志。。。。。

可以通过 application.properties 文件中修改Spring Boot的日志默认配置,此文件也可以修改Spring Boot其他配置,详细请查看Spring Boot之配置。

① logging.level:指定日志的输出级别,可以为项目中的每个包和类单独配置日志的输出级别;

② logging.file.path:指定日志的输出路径;

③ logging.file.name:指定日志的输出名称和路径,当 path 和 name 属性同时配置时,path 配置不会生效;

④ logging.pattern.console:指定控制台输出的样式;

logging.file.path 和 logging.file.file 输出的区别:

logging.file.name logging.file.path Example Description
(none) (none) 只在控制台输出日志信息
指定文件名 (none) my.log 指定输出的日志文件名称。名称可以是一个确切的位置或相对于当前项目目录。
(none) 指定目录 /var/log 输出 spring.log 日志文件到指定的目录,名称可以是一个确切的位置或相对于当前项目目录。

日志样式输出配置项:

配置项 配置内容
%d 表示日期时间配置
%thread 表示线程名称
%-5level 表示从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg 表示日志消息
%n 表示换行

日志输出样式示例:%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} -%msg%n

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
properties复制代码# 配置日志的输出级别,可以为每个包和类单独配置日志输出级别
logging.level.cn.bruce=trace

# path指定日志的输出目录,会在磁盘根路径下创建spring文件夹,并在目录下生成spring.log文件记录日志信息
# 不加/则在项目目录下创建文件夹,并在文件夹内生成日志输出文件spring.log
#logging.file.path=/spring
# name指定输出文件的名称和路径,文件位置可以自由指定。path和name同时书写时只有name会生效
logging.file.name=E:/spring/demo-spring.log

# 指定控制台输出的日志样式
# %d表示日期时间,
# %thread表示线程名,
# %‐5level:级别从左显示5个字符宽度
# %logger{50} 表示logger名字最长50个字符,否则按照句点分割。
# %msg:日志消息,
# %n是换行符

logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} --> [%thread] --> %-5level --> %logger{50} --> %msg%n

# 指定文件输出日志样式
logging.pattern.file=%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} === %msg%n

4.2 指定日志配置

我们可以给日志框架指定配置文件,只要将配置文件放在在类路径下即可。SpringBoot就不会使用默认配置了。

不同日志框架对应的配置文件:

Logging System Customization
Logback logback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
Log4j2 log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging) logging.properties
  • logback.xml:配置文件会直接被日志框架识别;
  • logback-spring.xml:日志框架不直接加载日志的配置项,由 Spring Boot 解析日志解析,可以使用 Spring Boot 的高级 Profile 功能。更多参考前文 Spring Boot 配置相关的内容。

注意:如果使用 logback.xml 仍然使用 Spring Boot 的高级 Profile 功能会报错。

Spring Boot官方文档中关于日志的配置:Spring Boot的日志配置

  1. 切换日志框架

Spring Boot 允许我们替换默认的 slf4j + logback 日志框架结构,可以切换成其他的日志配置框架。例如:我们将日志输出框架切换为 slf4j+log4j 的方式。

修改pom.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- 在SpringBoot框架中排除logback日志框架 -->
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
<!-- 在SpringBoot框架中排除logback转换框架 -->
<exclusion>
<artifactId>log4j-to-slf4j</artifactId>
<groupId>org.apache.logging.log4j</groupId>
</exclusion>
</exclusions>
</dependency>

<!-- 引入log4j适配包,将logback框架替换为log4j框架 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>

实际开发中并不建议切换到log4j框架,log4j框架存在性能问题,建议使用logback或log4j2日志框架。

切换成 log4j2 作为日志输出框架

修改 pom.xml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- 排除SpringBoot中logging的starter -->
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

<!-- 导入log4j的starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

切换成log4j2框架后的依赖关系

image-20201020170741477

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《Spring 手撸专栏》 开篇介绍,我要带新人撸 Spr

发表于 2021-05-17

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、前言

不正经!写写面经,去撸Spring源码啦🌶?

是的,在写了4篇关于Spring核心源码的面经内容后,我决定要去手撸一个Spring了。为啥这么干呢?因为所有我想写的内容,都希望它是以理科思维理解为目的的学会,而不是靠着硬背记住。而目前面经中涉及到的每一篇Spring源码内容分析,在即使去掉部分非主流逻辑后,依然会显得非常庞大。对有经验的老司机尚可阅读几遍接受,但就新人来讲只能放入收藏夹吃灰啦!

读过我整理的《Java面经手册》的小伙伴会知晓,这是一本以面试题为入口讲解 Java 核心内容的技术书籍,书中内容极力的向你证实代码是对数学逻辑的具体实现。当你仔细阅读书籍时,会发现Java中有大量的数学知识,包括:扰动函数、负载因子、拉链寻址、开放寻址、斐波那契(Fibonacci)散列法还有黄金分割点的使用等等。

所以在编写面经手册关于 Spring 系列时,我也希望它是一项有益于程序员真正成长的技术资料和价值汇总,而不仅仅是对一些列繁杂内容的罗列。那么从借鉴 tiny-spring、mini-spring 以及对我对Spring的学习和常折腾开发中间件的经验上,来编写一款适合自己沉淀也满足于大家学习的Spring资料。

傅哥的面经都是”假“的,一上来就学数学、撸源码、挖核心! 好!既然你这么说,接下来我们定义目标、计划,开始撸源码!

二、目标

本仓库以 Spring 源码学习为目的,通过带着读者一点点手写简化版 Spring 框架,了解 Spring 核心原理,为后续再深入学习 Spring 打下基础。

在手写的过程中会剔除 Spring 源码中繁杂的内容,摘取整体框架中的核心逻辑,简化代码实现过程,保留核心功能,例如:IOC、AOP、Bean生命周期、上下文、作用域、资源处理等内容实现。

所有的内容实现都会由简开始,一步步带着大家实现,最终所有的内容完成后,在提供一个相对完整的 small-spring,在这个过程中只要你能跟着走下来,那么最后你一定可以较容易的阅读 Spring 源码了。

三、计划

原定这周已经准备了 Spring AOP 筛选通知器的相关文章,源码已经撸好了。但发现这样发下去我估计阅读量是要劈叉,多数都进收藏夹。

写一篇文章最大的希望是与读者互动起来,不怕你提提意见,就怕你不给三连!所有读者给出的留言、评论、点赞、分享,都是下一篇文章的120迈动力的开始,所以这篇文章的源码撸完后,决定该把 Spring 整理整理了,不仅让我自己有一个学习的过程沉淀感,也让读者能真的学会这部分内容。背,那是八股文,懂,才能涨姿势!

讲道理,其实我也是一个乐于手撸源码的主,因为从源码的学习中我可以拿到一部分在业务系统开发过程中,不太可能接触到的技术内容。而这部分从源码中学到的技术内容又可以复用到业务系统开发中,例如我写过的很多中间件以及设计模式,都来自于对框架源码的内容的挖掘和运用。

那 Spring 框架源码撸多少了?

截止到目前为止,已经写好了四个章节的案例源码,主要包括:容器创建、Bean的定义和注册、构造实例、属性填充,因为最开始的内容比较简单,所以写起来也会比较快。目录结构如下,后续继续补充章节:

  • 第 1 章:开篇介绍,手写Spring能给你带来什么?
  • 第 2 章:Spring Bean 容器创建 | small-spring-step-01
  • 第 3 章:Spring Bean 的定义和注册 | small-spring-step-02
  • 第 4 章:Spring Bean 构造函数实例化策略以及Cglib动态生成使用 | small-spring-step-03
  • 第 5 章:给 Bean 对象填充属性信息 | small-spring-step-04
  • 第 6 章:待归档…

站在我的角度撸源码要比写文章快,哪怕是非常简单的知识点,也要做既不繁杂冗余的介绍,也要能把知识的广度和深度讲清楚。所以在这个过程中我也会阅读不少资料以及官网上的文档,最终把相对那些符合当前章节有价值的内容,展示给读者学习,同时这也是个人对技术内容的一个积累。

四、源码

本章节是整个 Spring 手撸专栏的开篇,所以这里先把源码地址以及学习使用方式交代给读者,方便后续大家在后续可以顺利的学习到这部分内容。

  • 源码目录:github.com/fuzhengwei/… - 汇总文章、源码、visio、xmind、ppt等包括创作过程中的整理内容,方便读者学习
  • 源码实现:github.com/small-sprin… - 拆解实现步骤,搭建组织工程,展示每一个章节的具体源码实现过程,如果你愿意也可以参与到工程建设中

五、总结

  • 当你阅读 Spring 源码时你会看到各种的嵌套、递归、代理,以及可能连想调试时都不清楚断点要打在哪里,运行起来的程序跳来跳去。最终导致自己也就看不下去这份源码了!这是因为 Spring 发展的太久了,它为了满足不同的场景,已经做了太多的补充和优化,所以我们要做的是剥丝抽茧,体现核心,把最直接相干的内容体现出来进行学习,才更容易理解。
  • 在源码学习的过程中,小傅哥会和你一起从最简单、最简单的Bean容器开始,可能有些时候某些章节内容并不会太多,不过我会帮你建立一些知识关联,尽可能让你在这个学习过程中,收获更多。
  • 那么本章节关于 Spring 手撸专栏的开篇介绍就到这了,接下来你可以阅读到文章、获取到源码,直至我们把所有的内容全部完成,到时候就可以开发出一个相对完整的 Spring 框架了。希望在这个过程中你能和我一直坚持学习打卡!

六、系列推荐

  • 《Java 面经手册》PDF,全书 417 页 11.5 万字,完稿&发版!
  • Spring Bean IOC、AOP 循环依赖解读
  • 关于 Spring 中 getBean 的全流程源码解析
  • Spring IOC 特性有哪些,不会读不懂源码!
  • 你说,怎么把Bean塞到Spring容器?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你看到的所有地址都不是真的 虚拟地址与物理地址

发表于 2021-05-17

先解释下一个困扰了我很久的问题:虚拟地址(vitural address)和逻辑地址(logical address)的区别。

大部分操作系统的书籍要么写的是虚拟地址,要么写的是逻辑地址,看的我一脸懵逼。

在《深入理解 Linux 内核》这本书中终于找到了确切的答案,这里我就不写出来了,扣概念的话这俩确实是有些区别的,不过对于我们日常使用以及理解操作系统来说的话,暂且可以把虚拟地址和逻辑地址理解为同一个意思。

你看到的所有地址都不是真的

下面这段 C 代码摘录自《操作系统导论 - [美] 雷姆兹·H.阿帕希杜塞尔》,依次打印出 main 函数的地址,由 malloc(类似于 Java 中的 new 操作)返回的堆空间分配的值,以及栈上一个整数的地址:

得到以下输出:

我们需要知道的是,所有这些打印出来的地址都是虚拟的,在物理内存中这些地址并不真实存在,它们最终都将由操作系统和 CPU 硬件翻译成真正的物理地址,然后才能从真实的物理位置获取该地址的值。

OK,上述就当作一个引子,让各位对物理地址和虚拟地址有个直观的理解,下面正文开始。

物理寻址 Physical Addressing

物理地址的概念很好理解,你可以把它称为真正的地址。《深入理解计算机系统 - 第 3 版》中给出的物理地址(physical address)的定义如下:

计算机系统的主存被组织成一个由 M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址。

比如说,第一个字节的物理地址是 0,接下来的字节地址是 1,再下一个是 2,以此类推,给定这种简单的结构,CPU 访问内存的最自然的方式就是使用这样的物理地址。我们把这种方式称为物理寻址(physical addressing)。

举个例子,比如说当程序执行了一条加载指令,指令内容是从物理地址 4 中读取 4 字节字传送到某个寄存器中。

物理寻址过程如下:当 CPU 执行到这条指令时,会生成物理地址 4,然后通过内存主线,把它传递给内存,内存取出从物理地址 4 处开始的 4 字节字,并将它返回给 CPU,CPU 会将它存放到指定的寄存器中。看下图:

其实不难发现,物理寻址这种方式,每一个程序都直接访问物理内存,其实是存在重大缺陷的:

1)首先,用户程序可以寻址内存的任意一个字节,它们就可以很容易地破坏操作系统,从而使系统慢慢地停止运行。

2)再次,这种寻址方式使得操作系统中同时运行两个或以上的程序几乎是不可能的。

举个例子,我们打开了三个相同的程序(计算器),都执行到某一步。比方说,用户在这三个程序的界面上分别输入了 10、100、1000,其对应的指令就是把用户输入的数字保存在内存中的某个地址中。如果这个位置只能保存一个数,那应该保存哪个呢?这不就冲突了吗?

再举个例子,摘自《现代操作系统 - 第 3 版》:

一个程序给物理内存地址 1000 赋值也就是存入了一些数据后,另一个程序也同样给这个地址赋值,那么第二个程序的赋值会覆盖掉第一个程序所赋的值,这会造成两个程序同时崩溃。

当然了,我们也说了是几乎不可能,不是完全不可能,还是有一些方法可以在物理寻址这种方式下实现多个程序并发运行的。

最简单的方法就是:首先,将空闲的进程存储在磁盘上,这样当它们不运行时就不会占用内存,然后,让一个程序(或者说进程)单独占用全部内存运行一小段时间,当发生上下文切换的时候,就停止这个进程,并将它所有的状态信息保存在磁盘上,再加载其他进程的状态信息,然后运行一段时间…… 只要在某一个时间内存中只有一个程序,那么就不会发生上述所说的地址冲突。这就实现了一种比较粗糙的并发。

为什么说他是粗糙的呢,因为这种方法有一个问题:将全部的内存信息保存到磁盘太慢了!特别是当内存增长的时候。

因此,我们考虑把进程对应的内存一直留在物理内存中,在发生上下文切换的时候就切换到特定的区域。

如下图所示,有 3 个进程(A、B、C),每个进程拥有从 512KB 物理内存中切出来给它们的一小部分内存,可以理解为这 3 个进程共享物理内存:

显然,这种方式是存在一定安全隐患的。毕竟如果各个进程之间可以随意读取、写入内容的话那就乱套了。

那么如何对每个进程使用的地址进行保护(protection)呢?继续使用物理内存模型肯定是不行了,因此操作系统创造了一个新的内存抽象,引入了一个新的内存模型,那就是虚拟地址空间,很多书中都会直接称呼为 “地址空间(Address Space)”。

虚拟寻址 Virtual Addressing

我先通俗地解释下虚拟地址空间和虚拟地址的概念,直接上书中的定义读起来有点生涩。

就是说每个进程的栈啊、堆啊、代码段啊等等它们的实际物理内存地址对于这个进程来说是不可见的,谁也不能直接访问这个物理地址。

那我们怎么去访问这个进程呢?

操作系统会给每个进程分配一个虚拟地址空间(vitural address),每个进程包含的栈、堆、代码段这些都会从这个地址空间中被分配一个地址,这个地址就被称为虚拟地址。底层指令写入的地址也是虚拟地址。

每个进程都拥有一个自己的地址空间,并且独立于其他进程的地址空间。也就是说一个进程中的虚拟地址 28 所对应的物理地址与另一个进程中的虚拟地址 28 所对应的物理地址是不同的,这样就不会发生冲突了。

可以这么理解,物理地址就是一个仓库,虚拟地址就是一个门牌,比方说一共有三十个门牌,那么所有的进程都能看见这三十个门牌,但是他们看见的某个相同门牌,指向的并不是同一个仓库。

OK,下面再来看《现代操作系统 - 第 3 版》书中对于地址空间的解释,应该很容易理解了:

地址空间是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间(除了在一些特殊情况下进程需要共享它们的地址空间外)。

地址空间的概念非常通用,并且在很多场合中出现。比如电话号码,在美国和很多其他国家,一个本地电话号码通常是一个 7 位的数字。因此,电话号码的地址空间是从 0 000 000 到 9 999 999。

地址空间也可以是非数字的,以 “.com” 结尾的网络域名的集合也是地址空间。这个地址空间是由所有包含 2~63 个字符并且后面跟着 “.com” 的字符串组成的,组成这些字符串的字符可以是字母、数字和连字符。

到现在你应该已经明白地址空间的概念了,它是很简单的。

有了虚拟地址空间后,CPU 就可以通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到内存之前会先被转换成合适的物理地址,这个虚拟地址到物理地址的转换过程称为 地址翻译/地址转换(address translation)。

地址翻译需要 CPU 硬件和操作系统的密切合作:CPU 上的内存管理单元(Memory Management Unit,MMU)就是专门用来进行虚拟地址到物理地址的转换的,不过 MMU 需要借助存放在内存中的查询表,而这张表的内容正是由操作系统进行管理的。

那么,上述这一套 CPU 生成虚拟地址并进行地址翻译的流程就是虚拟寻址(virtual addressing)。举个例子,看下图:

References

  • 《操作系统导论 - [美] 雷姆兹·H.阿帕希杜塞尔》
  • 《现代操作系统 - 第 3 版》
  • 《深入理解计算机系统 - 第 3 版》

🎉 关注公众号 | 飞天小牛肉,即时获取更新

  • 博主东南大学硕士在读,携程 Java 后台开发暑期实习生,利用课余时间运营一个公众号『 飞天小牛肉 』,2020/12/29 日开通,专注分享计算机基础(数据结构 + 算法 + 计算机网络 + 数据库 + 操作系统 + Linux)、Java 技术栈等相关原创技术好文。本公众号的目的就是让大家可以快速掌握重点知识,有的放矢。关注公众号第一时间获取文章更新,成长的路上我们一起进步
  • 并推荐个人维护的开源教程类项目: CS-Wiki(Gitee 推荐项目,现已累计 1.7k+ star), 致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习 ~ 😊
  • 如果各位小伙伴春招秋招没有拿得出手的项目的话,可以参考我写的一个项目「开源社区系统 Echo」Gitee 官方推荐项目,目前已累计 700+ star,基于 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + … 并提供详细的开发文档和配套教程。公众号后台回复 Echo 可以获取配套教程,目前尚在更新中。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

史上最详Android版kotlin协程入门进阶实战(五)

发表于 2021-05-17

banners_twitter.png
由于文章涉及到的只是点比较多、内容可能过长,可以根据自己的能力水平和熟悉程度分阶段跳着看。如有讲述的不正确的地方劳烦各位私信给笔者,万分感谢image.png

由于时间原因,笔者白天工作只有晚上空闲时间才能写作,所以更新频率应该在2-3天一篇,当然我也会尽量的利用时间,争取能够提前发布。为了方便阅读将本文章拆分个多个章节,根据自己需要选择对应的章节,现在也只是目前笔者心里的一个大概目录,最终以更新为准:

Kotlin协程基础及原理系列

  • 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
  • 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
  • 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
  • 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
  • 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装

Flow系列

  • Kotlin协程之Flow使用(一)
  • Kotlin协程之Flow使用(二)
  • Kotlin协程之Flow使用(三)

扩展系列

  • 封装DataBinding让你少写万行代码
  • ViewModel的日常使用封装

本章前言

本章节中除了会对协程做讲解外,不会对其他引入的框架做讲解。文章是基于用户已经对这些框架有一定的入门基础上,对与框架如何结合kotlin协程的使用做一个引导。整个篇幅会有些长,我们会在结合使用的同时,做一些架构上的封装,也是为了方便后续在实战的时候,大家能更方便、直观的理解代码。

笔者也只是一个普普通通的开发者,架构上的设计不一定合理,大家可以自行吸收文章精华,去糟粕。

kotlin协程的使用封装

在上一章节中,我们已经了解了协程在Activity、Fragment、Lifecycle、Viewmodel的基础使用,以及如何简单的自定义一个协程。本章节中,我们主要是做一些基础的封装工作。我们将在上一章节的内容基础上,引入DataBinding、LiveData、Flow等做一些基础封装。比如:Base类的定义,协程的使用封装,常用扩展函数等。

我们先引入本章节所使用到的相关库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码    // Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
// 协程Android支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"

implementation "androidx.activity:activity-ktx:1.2.2"
implementation "androidx.fragment:fragment-ktx:1.3.3"

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

// ok http
implementation "com.squareup.okhttp3:okhttp:4.9.0"
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'

// retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

现在我们就可以开始做一些基础的封装工作,同时在app的bulid.gradle文件中开启dataBinding的使用

1
2
3
4
5
6
kotlin复制代码android {
buildFeatures {
dataBinding = true
}
//省略...
}

文章中基于DataBinding的使用,可以参考 封装DataBinding让你少写万行代码

ViewModel的使用可以参考ViewModel的日常使用封装

这两篇文章是为了本章节专门写的扩展性阅读。

最近因为电脑炸机了,信号输出不稳定,开始以为是显卡坏了,折腾了几天还是没整好,最后发现是主板被腐蚀导致线路故障,当前用的主板停产很久了,最后只能找个兼容的,等了几天才到货,最终也导致本章节稍微延后了几天。电子产品太脆弱了,一定要注意防摔、防磕碰、防腐蚀!废话不多说,下面进入我们今天的正题。

image.png

协程的常用环境

在实际的开发过程中,我们经常需要把耗时处理移到非主线程上执行,等耗时操作异步完成以后,再回到主线程上刷新界面。基于这些需求,我们大致可以把使用协程的环境分为下面五种环境:

  • 网络请求
  • 回调处理
  • 数据库操作
  • 文件操作
  • 其他耗时操作

下面我们首先对网络请求这块进行处理。目前市面上大多数APP的在处理网络请求时候,都是使用的RxJava结合Retrofit、OkHttp进行网络请求处理。我们最终的目的也是使用协程结合Retrofit、okHttp进行网络请求处理。

我们在这里只是针对Retrofit、OkHttp结合协程、ViewModel、LiveData使用讲解,如果需要了解Retrofit和okHttp的原理,可以看看其他作者的原理分解文章。

协程在网络请求下的封装及使用

为了演示效果,笔者在万维易源申请了一面免费的天气API,我们使用的接口地址:

1
kotlin复制代码http[s]://route.showapi.com/9-2?showapi_appid=替换自己的值&showapi_sign=替换自己的值

此接口返回的通用数据格式,其中showapi_res_body返回的json内容比较多,笔者从中挑选了我们主要关注的几个字段:

参数名称 类型 描述
showapi_res_body String 消息体的JSON封装,所有应用级的返回参数将嵌入此对象 。
showapi_res_code int 查看错误码
showapi_res_error String 错误信息的展示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JSON复制代码{
"showapi_res_error":"",
"showapi_res_code":0,
"showapi_res_body":{
"time":"20210509180000", //预报发布时间
"now":{
"wind_direction":"西风", //风向
"temperature_time":"01:30", //获得气温的时间
"wind_power":"0级", //风力
"aqi":"30", //空气指数,越小越好
"sd":"40%", //空气湿度
"weather_pic":"http://app1.showapi.com/weather/icon/day/00.png", //天气小图标
"weather":"晴", //天气
"rain":"0.0", //降水量(mm)
"temperature":"15" //气温
}
}
}

当然我们还需要一个接收数据的对象,为了避免和其他库容易弄混淆,我们命名为CResponse,这个结构大家都很熟悉:

1
2
3
4
5
6
7
8
KOTLIN复制代码data class CResponse<T>(
@SerializedName("showapi_res_code")
val code: Int,
@SerializedName("showapi_res_error")
val msg: String? = null,
@SerializedName("showapi_res_body")
val data: T
)

由于API返回的字段名称实在是不符合笔者的胃口,而且用起来也不美观。所以笔者通过Gson的注解SerializedName将属性进行重命名。我们在实际开发中常常也会遇到这种问题,同样可以通过这种方法进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
KOTLIN复制代码data class Weather(
val now: WeatherDetail,
val time: String
)

data class WeatherDetail(
val aqi: String,
val rain: String,
val sd: String,
val temperature: String,
@SerializedName("temperature_time")
val temperatureTime: String,
val weather: String,
@SerializedName("weather_pic")
val weatherPic: String,
@SerializedName("wind_direction")
val windDirection: String,
@SerializedName("windPower")
val windPower: String
)

然后我们创建一下okHttp、Retrofit。在Retrofit2.6版本以后我们不再需要引入Retrofit的coroutine-adapter适配器库,我们直接使用即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
KOTLIN复制代码object ServerApi {
val service: CoroutineService by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
build()
}

private fun build():CoroutineService{
val retrofit = Retrofit.Builder().apply {
baseUrl(HttpConstant.HTTP_SERVER)
client(OkHttpClientManager.mClient)
addConverterFactory(ScalarsConverterFactory.create())
addConverterFactory(GsonConverterFactory.create())
}.build()
return retrofit.create(CoroutineService::class.java)
}
}

object HttpConstant {
internal val HTTP_SERVER = "https://route.showapi.com"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
KOTLIN复制代码object OkHttpClientManager {

val mClient: OkHttpClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
buildClient()
}

private fun buildClient(): OkHttpClient {
val logging = HttpLoggingInterceptor()
logging.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
return OkHttpClient.Builder().apply {
addInterceptor(CommonInterceptor())
addInterceptor(logging)
followSslRedirects(true)
}.build()
}
}

由于我们在调用的天气API接口的时候showapi_appid和showapi_sign必传的值,所以我们增加了一个CommonInterceptor拦截器来统一处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
KOTLIN复制代码class CommonInterceptor : Interceptor {

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val oldRequest = chain.request()
val httpUrl = oldRequest.url
val host = httpUrl.host
if (HttpConstant.HTTP_SERVER != host ) {
return chain.proceed(oldRequest)
}
val urlBuilder = httpUrl.newBuilder()
//这里填写自己申请appid和sign
urlBuilder.addQueryParameter("showapi_appid", SHOW_API_APPID)
urlBuilder.addQueryParameter("showapi_sign", SHOW_API_SIGN)

val request = oldRequest
.newBuilder()
.url(urlBuilder.build())
.build()
return chain.proceed(request)
}
}

为了方便快速演示,笔者从请求的参数列表中只抽取了一个用来演示,接下来我们定义我们在请求需要通过Retrofit使用的接口CoroutineService:

请求参数 类型 描述
area String 要查询的地区名称 。
1
2
3
4
5
6
KOTLIN复制代码interface CoroutineService {
@FormUrlEncoded
@POST("/9-2")
suspend fun getWeather(
@Field("area") area: String
): CResponse<Weather>

可以看到我们在使用Retrofit结合协程使用时,我们只需要在函数前增加suspend关键字就可以,同时返回结果可以直接定义为,我们需要从请求结果中解析出来的数据对象,而不再是像以前一样定义为Call<T>。

到此为止,我们基于基础数据的定义已经结束了,下面我们将正式进入我们今天的主题。为了更加清晰的理解,笔者这里不会采用直接一步到位的方式。那样可能会有很多人阅读理解起来有困难。笔者将会对请求过程进行一步一步的封装,这里需要一点耐心。

image.png

我们先创建一个Repository来请求数据:

1
2
3
4
5
6
7
KOTLIN复制代码class WeatherRepository {
suspend fun getWeather(
area: String
): CResponse<Weather>{
return ServerApi.service.getWeather(area)
}
}

同时在创建一个MainViewModel来使用Repository

1
2
3
4
5
6
7
8
9
10
11
12
KOTLIN复制代码class MainViewModel(private val repository: WeatherRepository):ViewModel() {

private val _weather:MutableLiveData<Weather> = MutableLiveData()
val mWeather: LiveData<Weather> = _weather

fun getWeather( area: String){
requestMain {
val result = repository.getWeather(area)
_weather.postValue(result.data)
}
}
}

现在我们就可以在MainActivity中创建MainViewModel来调用方法获取天气数据。我们在创建ViewModel对象的时候不再使用
ViewModelProviders.of(this).get(MainViewModel::class.java) 这种方式。而是使用activity-ktx库中的viewModels方法去创建:

1
2
3
4
5
6
7
8
KOTLIN复制代码public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}

这个方法需要我们传入一个Factory,我们自己定义一个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
KOTLIN复制代码object ViewModelUtils {
fun provideMainViewModelFactory(
): MainViewModelFactory {
return MainViewModelFactory(MainRepository())
}
}

class MainViewModelFactory(
private val repository: MainRepository
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(repository) as T
}
}

接下来我们在MainActivity使用,通过使用ViewModelUtils获取MainViewModelFactory,然后使用viewModels进行创建我们需要的viewModel对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
KOTLIN复制代码class MainActivity : BaseActivity<ActivityMainBinding>() {
private val viewModel by viewModels<MainViewModel> {
ViewModelUtils.provideMainViewModelFactory()
}
override fun initObserve() {
viewModel.mWeather.observe(this) {
mBinding.contentTv.text = "$it"
}
}

override fun ActivityMainBinding.initBinding() {
this.mainViewModel = viewModel
}
}

initObserve是我们在BaseActivity中定义的抽象方法。我们只在activity_main.xml简单定义了一个Textview来显示数据,虽然在XML中引入了mainViewModel,但是为演示过程,我们没有使用DataBinding直接做数据绑定。而在实际开发中应该是使用DataBinding直接在XML中数据绑定。

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
XML复制代码<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>
<variable
name="mainViewModel"
type="com.carman.kotlin.coroutine.request.viewmodel.MainViewModel" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">

<TextView
android:id="@+id/content_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="这里显示获取到的数据"
android:textColor="@color/black"
android:textSize="18sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

image.png

我们成功的请求到数据,并且显示在我们界面上。但是有个问题上我们现在的请求是没有做异常处理的。现在我们处理下请求过程中的异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
KOTLIN复制代码    fun getWeather(area: String){
requestMain {
val result = try {
repository.getWeather(area)
} catch (e: Exception) {
when(e) {
is UnknownHostException -> {
//...
}
//... 各种需要单独处理的异常
is ConnectException -> {
//...
}
else ->{
//...
}
}
null
}
_weather.postValue(result?.data)
}
}

这种做法虽然处理了异常,但是非常丑陋,而且我们需要在每一个请求地方写的时候,那将会是一个噩梦般的诅咒。
接下来将会是我们的重点内容,笔者将会封装出三种形式的调用,开始的时候对应的场景使用即可。

image.png

高阶函数方式

这个时候我们需要创建一个BaseRepository来进行封装处理,我们通过onSuccess获取成功的结果,通过onError来处理针对此次请求的特有异常,以及通过onComplete来处理执行完成的操作:

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
KOTLIN复制代码open class BaseRepository {

suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend () -> CResponse<T>,
noinline onSuccess: ((T?) -> Unit)? = null,
noinline onError: ((Exception)-> Unit) ? = null,
noinline onComplete: (() -> Unit)? = null ){
try {
val response = block()
onSuccess?.invoke(response?.data)
} catch (e: Exception) {
e.printStackTrace()
when (e) {
is UnknownHostException -> {
//...
}
//... 各种需要单独处理的异常
is ConnectException -> {
//...
}
else -> {
//...
}
}
onError?.invoke(e)
}finally {
onComplete?.invoke()
}
}
}

这个时候我们再修改一下WeatherRepository中的getWeather方法,我们需要通过launchRequest来包裹一下请求就可以:

1
2
3
4
5
6
7
8
9
10
KOTLIN复制代码    suspend fun getWeather(
area: String,
onSuccess: (Weather?) -> Unit,
onError: (Exception) -> Unit,
onComplete: () -> Unit,
){
launchRequest({
ServerApi.service.getWeather(area)
}, onSuccess,onError, onComplete)
}

然后我们修改一下MainViewModel中的getWeather方法,我们在处理异常的位置处理此次接口特有的异常即可,同时可以在请求结束后做一些收尾工作:

1
2
3
4
5
6
7
8
9
10
11
12
KOTLIN复制代码    fun getWeather(area: String) {
requestMain {
repository.getWeather(area, {
_weather.postValue(it)
}, {
it.printStackTrace()
Log.d(Companion.TAG, "异常提示处理")
}, {
Log.d(TAG, "请求结束,处理收尾工作")
})
}
}

同时除第一个执行请求的参数外,后面三个参数都可以传入为空实现。避免在不需要处理成功,异常,执行完成等操作的时候,出现这种影响美观的代码。假如我们通过sendData服务器发送一个数据,这个数据是否处理成功我们不需要关心,这个时候我们就可以如下操作:

1
2
3
4
5
6
7
KOTLIN复制代码    fun sendData(data: String) {
requestMain {
repository.launchRequest({
repository.sendData(data)
})
}
}

我们再回过头来看看launchRequest方法,我们在处理请求返回的结果时候直接就返回response。但是实际开发中我们一般在请求接口返回数据的时候,是需要判断接口数据状态code值是成功的时候才能返回数据。

我们本例中这个状态值是0。这个时候我们需要处理一下增加一个处理response的方法.我们再修改一下launchRequest方法:

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
KOTLIN复制代码    suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend () -> CResponse<T>,
noinline onSuccess: ((T?) -> Unit)? = null,
noinline onError: ((Exception) -> Unit)? = null,
noinline onComplete: (() -> Unit)? = null
) {
try {
val response = block()
when (response.code) {
HttpConstant.OK -> {
val isListType = T::class.isSubclassOf(List::class)
if (response.data == null && isListType) {
onSuccess?.invoke(Collections.EMPTY_LIST as? T)
} else {
onSuccess?.invoke(response?.data)
}
}
else -> onError?.invoke(CException(response))
}
} catch (e: Exception) {
e.printStackTrace()
when (e) {
is UnknownHostException -> {
}
//... 各种需要单独处理的异常
is ConnectException -> {
}
else -> {
}
}
onError?.invoke(e)
} finally {
onComplete?.invoke()
}
}

可以看到我们在处理response的时候,我们先通过判断返回的诗句类型是否为List集合类型。如果是集合类型且数据返回了一个null的时候,我们就尝试把一个的空集合转换为结果。

1
2
3
4
5
6
KOTLIN复制代码 val isListType = T::class.isSubclassOf(List::class)
if (response.data == null && isListType) {
onSuccess?.invoke(Collections.EMPTY_LIST as? T)
} else {
onSuccess?.invoke(response?.data)
}

多状态函数返回值方式

上面的封装方式我们是通过kotlin的高阶函数去实现的。假如我们想直接通过请求结果的时候,再结合其他请求处理数据通知界面刷新的时候,上面就显得很麻烦,而且好像又走到的无限嵌套的坑里。

image.png

这个时候我们就需要直接通过函数返回值来处理。现在我们首先的创建一个DataResult来封装一下返回结果,我们将返回的数据分成成功或者失败两种:

1
2
3
4
KOTLIN复制代码sealed class DataResult<out T> {
data class Success<out T>(val data: T) : DataResult<T>()
data class Error(val exception: Exception) : DataResult<Nothing>()
}

然然后在创建一个launchRequestForResult把之前的launchRequest代码拷贝过来稍作修改:

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
KOTLIN复制代码    suspend inline fun <reified T : Any> launchRequestForResult(
noinline block: suspend () -> CResponse<T>
): DataResult<T> {
return try {
val response = block()
if (0 == response.code) {
val isListType = T::class.isSubclassOf(List::class)
if (response.data == null && isListType) {
DataResult.Success(Collections.EMPTY_LIST as? T) as DataResult<T>
} else {
DataResult.Success(response.data)
}
} else {
DataResult.Error(CException(response))
}
} catch (e: Exception) {
when (e) {
is UnknownHostException -> {
}
//... 各种需要单独处理的异常
is ConnectException -> {
}
else -> {
}
}
DataResult.Error(e)
}
}

我们在WeatherRepository中再增加getWeather方法,通过launchRequestForResult来处理请求:

1
2
3
4
5
KOTLIN复制代码    suspend fun getWeather(area: String): DataResult<Weather> {
return launchRequestForResult {
ServerApi.service.getWeather(area)
}
}

然后我们同时我们也在MainViewModel中增加一个getWeatherForResult方法,这个时候我们就可以按我们的常规的编写代码顺序处理结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
KOTLIN复制代码    fun getWeatherForResult(area: String) {
requestMain {
val result = repository.getWeather(area)
when(result){
is DataResult.Success ->{
_weather.postValue(result.data)
}
is DataResult.Error ->{
Log.d(TAG, "${(result?.exception}")
}
}
}
}

当然,这种方式处理起来还是相对有些繁琐,因为当我们有多个请求是,我们需要写多个when来判断结果。那如果我们也不想写这些模板代码又该如何处理呢

直接返回值的方式

这个时候我们就需要在launchRequestForResult的基础上进一步的处理:

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
KOTLIN复制代码    suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend () -> CResponse<T>
): T? {
return try {
block()
} catch (e: Exception) {
e.printStackTrace()
when (e) {
is UnknownHostException -> {
}
//... 各种需要单独处理的异常
is ConnectException -> {
}
else -> {
}
}
throw e
}?.run {
if (0 == code) {
val isListType = T::class.isSubclassOf(List::class)
return if (data == null && isListType) {
Collections.EMPTY_LIST as? T
} else {
data
}
} else {
throw CException(this)
}
}
}

因为考虑到实际开发环境中,我们还是可能需要在外部处理异常提示的所以在这里还是通过throw重新抛出异常。如果外部不想处理非内接口CException异常,可以参考下面直接在catch返回null即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
KOTLIN复制代码    suspend inline fun <reified T : Any> launchRequest(
crossinline block: suspend () -> CResponse<T>
): T? {
return try {
block()
} catch (e: Exception) {
null
}?.run {
if (0 == code) {
val isListType = T::class.isSubclassOf(List::class)
if (data == null && isListType) {
Collections.EMPTY_LIST as? T
} else {
data
}
} else {
throw CException(this)
}
} ?: let {
null
}
}

同样在WeatherRepository中再增加getWeather方法,通过获取返回值的launchRequest来处理请求:

1
2
3
4
5
KOTLIN复制代码    suspend fun getWeather(area: String): Weather? {
return launchRequest{
ServerApi.service.getWeather(area)
}
}

因为我们在launchRequest重新抛出了异常,所以我们需要在请求的地方捕获一下:

1
2
3
4
5
6
7
8
9
KOTLIN复制代码    fun getWeather(area: String) {
requestMain {
val weather = try {
repository.getWeather(area)
} catch (e: Exception) {
//二次异常处理...
}
}
}

image.png

上面的三种方式算上一种抛砖引玉,其实我们还可以进一步的通过抽象ViewModel来统一处理内部接口请求异常。

如果您有更好的方法或者思路想法,欢迎交流。架构的演进以及代码的封装需要不断的学习和沟通,每一次知识交流与碰撞都是有意义的。

需要源码的看这里:demo源码

原创不易。如果您喜欢这篇文章,您可以动动小手点赞收藏image.png。

关联文章
Kotlin协程基础及原理系列

  • 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
  • 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
  • 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
  • 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
  • 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装

Flow系列

  • Kotlin协程之Flow使用(一)
  • Kotlin协程之Flow使用(二)
  • Kotlin协程之Flow使用(三)

扩展系列

  • 封装DataBinding让你少写万行代码
  • ViewModel的日常使用封装

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ViewModel的日常使用封装 ViewModel基本使用

发表于 2021-05-17

Kotlin协程基础及原理系列

  • 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
  • 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
  • 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
  • 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
  • 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装

Flow系列

  • Kotlin协程之Flow使用(一)
  • Kotlin协程之Flow使用(二)
  • Kotlin协程之Flow使用(三)

扩展系列

  • 封装DataBinding让你少写万行代码
  • ViewModel的日常使用封装

笔者也只是一个普普通通的开发者,设计不一定合理,大家可以自行吸收文章精华,去糟粕。

ViewModel基本使用

在上一篇文中《封装DataBinding让你少写万行代码》中我们已经学会了如何封装DataBinding进行代码优化。本章节我们将在上一篇文章的基础上讲解对ViewModel的使用封装。

ViewModel基本使用

我们都知道基础的创建ViewModel方式是通过下面三种方式进行创建的:

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
KOTLIN复制代码class MainActivity05:AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
/** 或者携带实例化工厂
val viewModel = ViewModelProvider(this,MainViewModelFactory(MainRepository())).get(MainViewModel::class.java)
或者
val viewModel = ViewModelProvider(viewModelStore,MainViewModelFactory(MainRepository())).get(MainViewModel::class.java) */
viewModel.mUser.observe(this) {
Log.d("mUser","$it")
}
}
}

class MainViewModel:ViewModel() {
private val _user:MutableLiveData<User> = MutableLiveData(User(1,"测试"))
val mUser: LiveData<User> = _user
}

//创建一个最简单的ViewModel工厂
class MainViewModelFactory(
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel() as T
}
}

因为我们只是为了演示所以MainViewModel我们只是简单创建了一个mUser: LiveData<User>对象供于MainActivity进行observe。

在上面基础上我们通过引入lifecycle-extensions这个扩展库。

1
KOTLIN复制代码    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

我们通过它提供的方法ViewModelProviders这种方式创建ViewModel:

1
2
3
4
5
6
7
8
9
10
11
KOTLIN复制代码class MainActivity:AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
viewModel.mUser.observe(this) {
Log.d("mUser","$it")
}
}
}

这种通过ViewModelProviders这种方式创建ViewModel,是基于lifecycle-extensions这个扩展库提供的方法的,我们可以看到实际上ViewModelProviders最终也是通过ViewModelProvider去创建。

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
KOTLIN复制代码/**
* Utilities methods for {@link ViewModelStore} class.
* @deprecated Use the constructors for {@link ViewModelProvider} directly.
*/
@Deprecated
public class ViewModelProviders {
@Deprecated
@NonNull
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
return new ViewModelProvider(activity);
}

@Deprecated
@NonNull
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity,
@Nullable Factory factory) {
if (factory == null) {
factory = activity.getDefaultViewModelProviderFactory();
}
return new ViewModelProvider(activity.getViewModelStore(), factory);
}

@Deprecated
@NonNull
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment) {
return new ViewModelProvider(fragment);
}

@Deprecated
@NonNull
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) {
if (factory == null) {
factory = fragment.getDefaultViewModelProviderFactory();
}
return new ViewModelProvider(fragment.getViewModelStore(), factory);
}
}

这个方法在早期的版本中是使用到最多的,目前网上很多文章也还是使用的这种写法。但是我们可以看到在高版本的lifecycle-extensions库中ViewModelProviders已经被废弃。

通过KTX扩展库方式创建ViewModel

由于ViewModelProviders已经被废弃了,同时上面的写法确实也稍微有点繁琐。所以为了进一步简化,我们使用到下面ktx库中的方法:

1
2
Groovy复制代码 implementation "androidx.activity:activity-ktx:1.2.2"
implementation "androidx.fragment:fragment-ktx:1.3.3"

我们通过这2个扩展库,使用viewModels就可以非常快速的创建出我们所需要的ViewModel,

1
2
3
4
5
6
7
8
9
10
11
KOTLIN复制代码class MainActivity:AppCompatActivity() {
private val viewModel:MainViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.mUser.observe(this) {
Log.d("mUser","$it")
}
}
}

而在Fragment中我们可以使用activityViewModels或者viewModels来创建。

1
2
3
4
5
KOTLIN复制代码class HomeFragment:Fragment() {
val activityViewModel:MainViewModel by activityViewModels()
//或者
val viewModel:MainViewModel by viewModels()
}

可以看到viewModels其实是ComponentActivity的一个内联函数,通过在viewModels函数中调用ViewModelLazy来获取指定的ViewModel,同时viewModels可以传入用于创建ViewModel的工厂factoryProducer:

1
2
3
4
5
6
7
8
9
KOTLIN复制代码public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}

return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}

我们通过观察ViewModelLazy实现可以看到,ViewModelLazy最终还是调用的ViewModelProvider方法去创建,这就是传说中的kotlin语法糖,好甜。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
KOTLIN复制代码public class ViewModelLazy<VM : ViewModel> (
private val viewModelClass: KClass<VM>,
private val storeProducer: () -> ViewModelStore,
private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
private var cached: VM? = null

override val value: VM
get() {
val viewModel = cached
return if (viewModel == null) {
val factory = factoryProducer()
val store = storeProducer()
ViewModelProvider(store, factory).get(viewModelClass.java).also {
cached = it
}
} else {
viewModel
}
}

override fun isInitialized(): Boolean = cached != null
}

这里需要提一下的是在Fragment中通过activityViewModels创建的ViewModel,得到的将会这个Fragment所在的Acivity的ViewModel,后面的步骤流程基本就是都是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
KOTLIN复制代码public inline fun <reified VM : ViewModel> Fragment.viewModels(
noinline ownerProducer: () -> ViewModelStoreOwner = { this },
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)

public inline fun <reified VM : ViewModel> Fragment.activityViewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> = createViewModelLazy(
VM::class, { requireActivity().viewModelStore },
factoryProducer ?: { requireActivity().defaultViewModelProviderFactory }
)

public fun <VM : ViewModel> Fragment.createViewModelLazy(
viewModelClass: KClass<VM>,
storeProducer: () -> ViewModelStore,
factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}

可以看到在activityViewModels方法中实际是调用了requireActivity来进行创建。如果我们需要实现activity和Fragment的数据共享就可以通过这种方式去创建。

image.png

通过反射创建ViewModel

虽然经过上面引入KTX库来处理,可能还是不能满足某些人的胃口。毕竟实际开发环境往往存在各种各样的问题,导致不符合实际的开发使用。没关系,我们还可以通过反射的方式再进一步的去处理。

通过上面演进,我们知道不管外部初始化如何变化,最终的方式还是调用ViewModelProvider去创建。这时我们可以参考封装DataBinding的方式,通过反射去创建我们的ViewModel。

我们先增加一个ComponentActivity的扩展方法,通过它去获取我们在BaseActivity中得到泛型ViewModel的具体类型:

1
2
3
4
5
KOTLIN复制代码inline fun <VM: ViewModel> ComponentActivity.createViewModel(position:Int): VM {
val vbClass = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.filterIsInstance<Class<*>>()
val viewModel = vbClass[position] as Class<VM>
return ViewModelProvider(this).get(viewModel)
}

然后我们就可以在BaseActivity中通过createViewModel创建我们需要的ViewModel,这里的BaseActivity中的ViewDataBinding不明白的可以去看看封装DataBinding。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
KOTLIN复制代码abstract class BaseActivity<VB : ViewDataBinding,VM: ViewModel> : AppCompatActivity(), BaseBinding<VB> {
protected val mBinding: VB by lazy(mode = LazyThreadSafetyMode.NONE) {
getViewBinding(layoutInflater)
}
protected lateinit var viewModel:VM

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
viewModel = createViewModel(1)
mBinding.initBinding()
initObserve()
}
abstract fun initObserve()
}

修改一下我们的MainActivity:

1
2
3
4
5
6
7
8
9
10
11
12
KOTLIN复制代码class MainActivity : BaseActivity<ActivityMainBinding, MainViewModel>() {

override fun initObserve() {
viewModel.mUser.observe(this){
Log.d("MainViewModel","user: $it") //TestViewModel: user: User(id=1, name=测试)
}
}

override fun ActivityMainBinding.initBinding() {
//数据绑定...
}
}

此时我们成功创建了MainViewModel,通过这种方式节省我们编写大量ViewModel模板代码的时间。无形中有增加了摸鱼吹水的时间。

image.png

但是这个还有个问题,上面的方式只能支持没有参数的ViewModel。如果需要创建的ViewModel需要传入参数怎么办。比如我们将MainViewModel修改为需要一个MainRepository:

1
2
3
4
kotlin复制代码class MainViewModel(private val repository: MainRepository):ViewModel() {
private val _user: MutableLiveData<User> = MutableLiveData(User(1,"测试"))
val mUser: LiveData<User> = _user
}

这个时候如果我们还是使用上面的方式,那我们的程序将会崩溃报错,提示无法创建实例化对象:

1
KOTLIN复制代码java.lang.RuntimeException: Cannot create an instance of class com.carman.kotlin.coroutine.request.viewmodel.MainViewModel

这个时候我们需要创建一个MainViewModelFactory来创建我们的MainViewModel:

1
2
3
4
5
6
7
8
KOTLIN复制代码class MainViewModelFactory(
private val repository: MainRepository
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return MainViewModel(repository) as T
}
}

这个时候我们又该如何把MainViewModelFactory和上面的反射方法结合到一起使用呢。虽然我在使用ViewModelProvider可以传入一个Factory,如我们在最开始MainActivity直接创建使用的时候:

1
KOTLIN复制代码ViewModelProvider(this,MainViewModelFactory(MainRepository())).get(MainViewModel::class.java)

但是我们现在是通过反射的方式去创建,当我们有很多ViewModel的时候,我们是不知道具体使用哪一个去创建。所以这个时候我们需要创建一个ViewModelUtils的工具类,然后提取扩展函数createViewModel方法,我们进一步修改,增加一个factory参数,通过factory去判断是否需要使用工厂创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
KOTLIN复制代码object ViewModelUtils {
fun <VM : ViewModel> createViewModel(
activity: ComponentActivity,
factory: ViewModelProvider.Factory? = null,
position: Int
): VM {
val vbClass =
(activity.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.filterIsInstance<Class<*>>()
val viewModel = vbClass[position] as Class<VM>
return factory?.let {
ViewModelProvider(
activity,
factory
).get(viewModel)
} ?: let {
ViewModelProvider(activity).get(viewModel)
}
}
}

我们再修改一下BaseActivity在构造方法中增加一个factory参数。把我们之前直接调用createViewModel扩展函数修改为使用ViewModelUtils的createViewModel方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
KOTLIN复制代码abstract class BaseActivity<VB : ViewDataBinding, VM : ViewModel>(
private val factory: ViewModelProvider.Factory? = null
) : AppCompatActivity(), BaseBinding<VB> {
protected val mBinding: VB by lazy(mode = LazyThreadSafetyMode.NONE) {
getViewBinding(layoutInflater)
}
protected lateinit var viewModel:VM

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
viewModel = ViewModelUtils.createViewModel(this, factory, 1)
mBinding.initBinding()
initObserve()
}
abstract fun initObserve()
}

这个时候我们再修改一下MainActivity,我们使用provideMainViewModelFactory方法用于获取MainViewModel的建造工厂MainViewModelFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
KOTLIN复制代码class MainActivity : BaseActivity<ActivityMainBinding, MainViewModel>(provideMainViewModelFactory()) {

override fun initObserve() {
viewModel.mUser.observe(this){
Log.d("MainViewModel","user: $it")
}
}

override fun ActivityMainBinding.initBinding() {
this.mainViewModel = viewModel
}
}

fun provideMainViewModelFactory(
): MainViewModelFactory {
return MainViewModelFactory(MainRepository())
}

这个时候我们再运行我们的程序,一切恢复正常运行。这里我们已经对Activity中创建ViewModel进行处理了,那我们在Fragment中又改如何创建呢。

image.png

这个时候我们就需要参考上面Fragment-ktx中的activityViewModels和viewModels方法,也将Fragment中创建ViewModel分为2种形式,同时也区分是否需要通过factory工厂去获取实例化对象:

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
KOTLIN复制代码fun <VM : ViewModel> createViewModel(
fragment: Fragment,
factory: ViewModelProvider.Factory? = null,
position: Int
): VM {
val vbClass =
(fragment.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.filterIsInstance<Class<*>>()
val viewModel = vbClass[position] as Class<VM>
return factory?.let {
ViewModelProvider(
fragment,
factory
).get(viewModel)
} ?: let {
ViewModelProvider(fragment).get(viewModel)
}
}

fun <VM : ViewModel> createActivityViewModel(
fragment: Fragment,
factory: ViewModelProvider.Factory? = null,
position: Int
): VM {
val vbClass =
(fragment.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments.filterIsInstance<Class<*>>()
val viewModel = vbClass[position] as Class<VM>
return factory?.let {
ViewModelProvider(
fragment.requireActivity(),
factory
).get(viewModel)
} ?: let {
ViewModelProvider(fragment.requireActivity()).get(viewModel)
}
}

现在我们再修改一下BaseFragment,与在Activity不同的是,由于Fragment是存在选择与Activity共享一个ViewModel,或者自己创建一个私有ViewModel。所以我们增加了一个shareViewModel参数来控制我们选择使用哪一种方式创建:

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
KOTLIN复制代码abstract class BaseFragment<VB : ViewDataBinding, VM : ViewModel>(
private val shareViewModel: Boolean = false,
private val factory: ViewModelProvider.Factory? = null
) : Fragment(), BaseBinding<VB> {
protected lateinit var mBinding: VB
private set
protected lateinit var viewModel: VM
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBinding = getViewBinding(inflater, container)
viewModel = if (shareViewModel) ViewModelUtils.createActivityViewModel(this, factory, 1)
else ViewModelUtils.createViewModel(this, factory, 1)
return mBinding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mBinding.initBinding()
}

override fun onDestroy() {
super.onDestroy()
if (::mBinding.isInitialized) {
mBinding.unbind()
}
}
}

到现在为止,ViewModel在开发中常用的三种封装使用方式就讲完了。我们接下来讲一讲通过注解的方式去创建,可能有些小伙伴使用过Dagger,但是我们这里要说的不是使用Dagger,而是基于Dagger封装专门为Android环境下的使用的Android Jetpack组件之一Hilt。

image.png

通过Hilt注解创建ViewModel

我们先引入Hilt依赖:

1
KOTLIN复制代码 classpath 'com.google.dagger:hilt-android-gradle-plugin:2.35.1'
1
2
3
kotlin复制代码plugins {
id 'dagger.hilt.android.plugin'
}
1
2
KOTLIN复制代码 implementation "com.google.dagger:hilt-android:2.35.1"
kapt "com.google.dagger:hilt-android-compiler:2.35.1"

笔者这里只是作为扩展阅读,不是详细讲解Hilt如何使用,如果想了解详细使用可以去官网看看使用 Hilt 实现依赖项注入或者有兴趣的可以在地下留言,笔者到时候再找个时间单独写一篇Hil的入门何使用t。

我们创建一个DemoApplication不做任何实现,只是增加一个@HiltAndroidApp注解,它会触发Hilt的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。

1
2
KOTLIN复制代码@HiltAndroidApp
class DemoApplication : Application() { ... }

然后在我们需要使用@HiltViewModel对的MainViewModel进行注解,同时需要使用@Inject来执行字段注入

1
2
3
4
5
KOTLIN复制代码@HiltViewModel
class MainViewModel @Inject constructor(private val repository: MainRepository):ViewModel() {
private val _user: MutableLiveData<User> = MutableLiveData(User(1,"测试"))
val mUser: LiveData<User> = _user
}

同时我们也需要再MainRepository中使用@Inject注解进行字段注入:

1
2
KOTLIN复制代码class MainRepository @Inject constructor():BaseRepository(){
}

然后在我们的MainActivity中使用AndroidEntryPoint注解。@AndroidEntryPoint为项目中的每个Android 类生成一个单独的 Hilt 组件。这些组件可以从它们各自的父类接收依赖项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
KOTLIN复制代码@AndroidEntryPoint
class MainHiltActivity : BaseVBActivity<ActivityMainBinding>(){
val viewModel:MainViewModel by viewModels()

override fun initObserve() {
viewModel.mUser.observe(this){
Log.d("MainViewModel","user: $it") //MainViewModel: user: User(id=1, name=测试)
}
}

override fun ActivityMainBinding.initBinding() {
this.mainViewModel = viewModel
}

}

我们这里是通过与KTX扩展库结合使用,虽然创建MainViewModel的时候需要传入构建工厂,但是我们通过使用Hilt注解来帮助我们注入。

image.png

需要源码的看这里:demo源码
原创不易。如果您喜欢这篇文章,您可以动动小手点赞收藏image.png。

关联文章
Kotlin协程基础及原理系列

  • 史上最详Android版kotlin协程入门进阶实战(一) -> kotlin协程的基础用法
  • 史上最详Android版kotlin协程入门进阶实战(二) -> kotlin协程的关键知识点初步讲解
  • 史上最详Android版kotlin协程入门进阶实战(三) -> kotlin协程的异常处理
  • 史上最详Android版kotlin协程入门进阶实战(四) -> 使用kotlin协程开发Android的应用
  • 史上最详Android版kotlin协程入门进阶实战(五) -> kotlin协程的网络请求封装

Flow系列

  • Kotlin协程之Flow使用(一)
  • Kotlin协程之Flow使用(二)
  • Kotlin协程之Flow使用(三)

扩展系列

  • 封装DataBinding让你少写万行代码
  • ViewModel的日常使用封装

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…667668669…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%