在UE5中，使用`GConfig`类操作配置文件的核心方法
在UE5中，使用GConfig类操作配置文件的核心方法如下：一、基础操作流程创建配置文件路径// 获取项目配置目录（自动创建） FString ConfigPath = FPaths::ProjectConfigDir() / "MyConfig.ini"; // 或直接指定路径 FString CustomPath = FPaths::ProjectDir() / "Config/MyConfig.ini";写入配置数据void UMyFunctionLibrary::WriteConfig( const FString& Section, const FString& Key, const FString& Value, const FString& ConfigPath ) { GConfig->SetString(*Section, *Key, *Value, *ConfigPath); GConfig->Flush(true, *ConfigPath); // 强制刷新到磁盘 }读取配置数据FString UMyFunctionLibrary::ReadConfig( const FString& Section, const FString& Key, const FString& ConfigPath ) { FString Result; GConfig->GetString(*Section, *Key, Result, *ConfigPath); return Result; }二、高级配置方法自动配置类（推荐）UCLASS(config=MyConfig, defaultconfig) class UMyConfig : public UObject { GENERATED_BODY() UPROPERTY(Config, EditAnywhere) float TestValue = 1.0f; UPROPERTY(Config, EditAnywhere) FString TestString = "Default"; };自动保存路径：ProjectName/Config/DefaultMyConfig.ini（默认配置）运行时路径：ProjectName/Saved/Config/Windows/MyConfig.ini【3】全局配置标记UPROPERTY(globalconfig) int32 GlobalSetting = 42;优先级高于局部配置保存在DefaultEngine.ini根目录【9】三、蓝图集成方案创建蓝图库类UCLASS(BlueprintType) class UConfigManager : public UBlueprintFunctionLibrary { GENERATED_BODY() UFUNCTION(BlueprintCallable, Category="Config") static void SaveConfig(FString Section, FString Key, FString Value) { UConfigManager::WriteConfig(Section, Key, Value, FPaths::ProjectConfigDir() / "MyConfig.ini"); } // 对应读取函数... };通过#include "Config/ConfigManager.h"暴露给蓝图【7】四、打包配置设置Build.cs配置public class YourProjectBuild : ModuleRules { public YourProjectBuild(ReadOnlyTargetRules Target) : base(Target) { RuntimeDependencies.Add(Path.Combine(ModuleDirectory, "../../Config", "MyConfig.ini")); } }确保配置文件包含在发布包中【8】五、注意事项路径规范ProjectConfigDir()：ProjectName/Config/GeneratedConfigDir()：ProjectName/Saved/Config/【3】【4】刷新机制GConfig->Flush(true)：强制写入磁盘GConfig->Flush(false)：仅更新内存缓存类型支持基础类型：FString/int32/float复杂类型需序列化处理六、完整示例// MyConfigManager.h #pragma once #include "CoreMinimal.h" #include "UObject/NoExportTypes.h" #include "MyConfigManager.generated.h" UCLASS() class MYPROJECT_API UMyConfigManager : public UObject { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = "Config") static void WriteConfig(FString Section, FString Key, FString Value); UFUNCTION(BlueprintCallable, Category = "Config") static FString ReadConfig(FString Section, FString Key); }; // MyConfigManager.cpp #include "MyConfigManager.h" #include "GameFramework/Game.h" #include "Misc/FileHelper.h" void UMyConfigManager::WriteConfig(FString Section, FString Key, FString Value) { FString ConfigPath = FPaths::ProjectConfigDir() / "MyConfig.ini"; GConfig->SetString(*Section, *Key, *Value, *ConfigPath); GConfig->Flush(true, *ConfigPath); } FString UMyConfigManager::ReadConfig(FString Section, FString Key) { FString ConfigPath = FPaths::ProjectConfigDir() / "MyConfig.ini"; FString Result; GConfig->GetString(*Section, *Key, Result, *ConfigPath); return Result; }七、典型应用场景游戏内设置保存音量/画质等用户偏好使用globalconfig标记实现跨项目同步【9】编辑器扩展通过UDeveloperSettings继承实现可视化配置面板【3】热更新配置动态加载Config/目录下的配置文件建议优先使用UCLASS(config)自动配置方案，复杂场景结合GConfigAPI实现精细控制。配置文件默认使用UTF-8编码，需注意跨平台兼容性处理。
网站历经 300 天的上线运行，现发布演示！站长将以最低成本维护本站，力求稳定存活长久本站大部分源码已开源至 GitHub 社区域名：gal.saop.cc源码：github.com/AdingApkgg/vns社群：t.me/FullDiveSAO
我正在一步步解释 Git 的方方面面。在使用 Git 近 15 年后，我已经非常习惯于 Git 的特性，很容易忘记它令人困惑的地方。因此，我在 Mastodon 上进行了调查：你有觉得哪些 Git 术语很让人困惑吗？我计划写篇博客，来解读 Git 中一些奇怪的术语，如：“分离的 HEAD 状态”，“快速前移”，“索引/暂存区/已暂存”，“比 origin/main 提前 1 个提交”等等。我收到了许多有洞见的答案，我在这里试图概述其中的一部分。下面是这些术语的列表：HEAD 和 “heads”“分离的 HEAD 状态”在合并或变基时的 “ours” 和 “theirs”“你的分支已经与 'origin/main' 同步”HEAD^、HEAD~、HEAD^^、HEAD~~、HEAD^2、HEAD~2.. 和 ...“可以快速前移”“引用”、“符号引用”refspecs“tree-ish”“索引”、“暂存的”、“已缓存的”“重置”、“还原”、“恢复”“未跟踪的文件”、“追踪远程分支”、“跟踪远程分支”检出reflog合并、变基和遴选rebase –onto提交更多复杂的术语我已经尽力讲解了这些术语，但它们几乎覆盖了 Git 的每一个主要特性，这对一篇博客而言显然过于繁重，所以在某些地方可能会有一些粗糙。HEAD 和 “heads”有些人表示他们对 HEAD 和 refs/heads/main 这些术语感到困惑，因为听起来像是一些复杂的技术内部实现。以下是一个快速概述：“heads” 就是 “分支”。在 Git 内部，分支存储在一个名为 .git/refs/heads 的目录中。（从技术上讲，官方 Git 术语表 中明确表示分支是所有的提交，而 head 只是最近的提交，但这只是同一事物的两种不同思考方式）HEAD 是当前的分支，它被存储在 .git/HEAD 中。我认为，“head 是一个分支，HEAD 是当前的分支” 或许是 Git 中最奇怪的术语选择，但已经设定好了，想要更清晰的命名方案已经为时已晚，我们继续。“HEAD 是当前的分支” 有一些重要的例外情况，我们将在下面讨论。“分离的 HEAD 状态”你可能已经看到过这条信息：$ git checkout v0.1 You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. [...] （消息译文：你处于 “分离 HEAD” 的状态。你可以四处看看，进行试验性的更改并提交，你可以通过切换回一个分支来丢弃这个状态下做出的任何提交。）这条信息的实质是：在 Git 中，通常你有一个已经检出的 “当前分支”，例如 main。存放当前分支的地方被称为 HEAD。你做出的任何新提交都会被添加到你的当前分支，如果你运行 git merge other_branch，这也会影响你的当前分支。但是，HEAD 不一定必须是一个分支！它也可以是一个提交 ID。Git 会称这种状态（HEAD 是提交 ID 而不是分支）为 “分离的 HEAD 状态”例如，你可以通过检出一个标签来进入分离的 HEAD 状态，因为标签不是分支如果你没有当前分支，一系列事情就断链了：git pull 根本就无法工作（因为它的全部目的就是更新你的当前分支）除非以特殊方式使用 git push，否则它也无法工作git commit、git merge、git rebase 和 git cherry-pick 仍然可以工作，但它们会留下“孤儿”提交，这些提交没有连接到任何分支，因此找到这些提交会很困难你可以通过创建一个新的分支或切换到一个现有的分支来退出分离的 HEAD 状态在合并或变基中的 “ours” 和 “theirs”遇到合并冲突时，你可以运行 git checkout --ours file.txt 来选择 “ours” 版本中的 file.txt。但问题是，什么是 “ours”，什么是 “theirs” 呢？我总感觉此类术语混淆不清，也因此从未用过 git checkout --ours，但我还是查找相关资料试图理清。在合并的过程中，这是如何运作的：当前分支是 “ours”，你要合并进来的分支是 “theirs”，这样看来似乎很合理。$ git checkout merge-into-ours # 当前分支是 “ours” $ git merge from-theirs # 我们正要合并的分支是 “theirs” 而在变基的过程中就刚好相反 —— 当前分支是 “theirs”，我们正在变基到的目标分支是 “ours”，如下：$ git checkout theirs # 当前分支是 “theirs” $ git rebase ours # 我们正在变基到的目标分支是 “ours” 我以为之所以会如此，因为在操作过程中，git rebase main 其实是将当前分支合并到 main （它类似于 git checkout main; git merge current_branch），尽管如此我仍然觉得此类术语会造成混淆。这个精巧的小网站 对 “ours” 和 “theirs” 的术语进行了解释。人们也提到，VSCode 将 “ours”/“theirs” 称作 “当前的更改”/“收到的更改”，同样会引起混淆。“你的分支已经与 origin/main 同步”此信息貌似很直白 —— 你的 main 分支已经与源端同步！但它实际上有些误导。可能会让你以为这意味着你的 main 分支已经是最新的，其实不然。它真正的含义是 —— 如果你最后一次运行 git fetch 或 git pull 是五天前，那么你的 main 分支就是与五天前的所有更改同步。因此，如果你没有意识到这一点，它对你的安全感其实是一种误导。我认为 Git 理论上可以给出一个更有用的信息，像是“与五天前上一次获取的源端 main 是同步的”，因为最新一次获取的时间是在 reflog 中记录的，但它没有这么做。HEAD^、HEAD~、HEAD^^、HEAD~~、HEAD^2、HEAD~2我早就清楚 HEAD^ 代表前一次提交，但我很长一段时间都困惑于 HEAD~ 和 HEAD^ 之间的区别。我查询资料，得到了如下的对应关系：HEAD^ 和 HEAD~ 是同一件事情（指向前 1 个提交）HEAD^^^、HEAD~~~ 和 HEAD~3 是同一件事情（指向前 3 个提交）HEAD^3 指向提交的第三个父提交，它与 HEAD~3 是不同的这看起来有些奇怪，为什么 HEAD~ 和 HEAD^ 是同一个概念？以及，“第三个父提交”是什么？难道就是父提交的父提交的父提交？（剧透：并非如此）让我们一起深入探讨一下！大部分提交只有一个父提交。但是合并提交有多个父提交 - 因为它们合并了两个或更多的提交。在 Git 中，HEAD^ 意味着 “HEAD 提交的父提交”。但是如果 HEAD 是一个合并提交，那 HEAD^ 又代表怎么回事呢？答案是，HEAD^ 指向的是合并提交的第一个父提交，HEAD^2 是第二个父提交，HEAD^3 是第三个父提交，等等。但我猜他们也需要一个方式来表示“前三个提交”，所以 HEAD^3 是当前提交的第三个父提交（如果当前提交是一个合并提交，可能会有很多父提交），而 HEAD~3 是父提交的父提交的父提交。我想，从我们之前对合并提交 “ours”/“theirs” 的讨论来看，HEAD^ 是 “ours”，HEAD^2 是 “theirs”。.. 和 ...这是两个命令：git log main..testgit log main...test我从没用过 .. 和 ... 这两个命令，所以我得查一下 man git-range-diff。我的理解是比如这样一个情况：A - B main \ C - D test main..test 对应的是提交 C 和 Dtest..main 对应的是提交 Bmain...test 对应的是提交 B，C，和 D更有挑战的是，git diff 显然也支持 .. 和 ...，但它们在 git log 中的意思完全不同？我的理解如下：git log test..main 显示在 main 而不在 test 的更改，但是 git log test...main 则会显示 两边 的改动。git diff test..main 显示 test 变动 和 main 变动（它比较 B 和 D），而 git diff test...main 会比较 A 和 D（它只会给你显示一边的差异）。有关这个的更多讨论可以参考 这篇博客文章。“可以快速前移”在 git status 中，我们会经常遇到如下的信息：$ git status On branch main Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded. (use "git pull" to update your local branch) （消息译文：你现在处于 main 分支上。你的分支比 origin/main 分支落后了 2 个提交，可以进行快速前进。 (使用 git pull 命令可以更新你的本地分支)）但“快速前移” 到底是何意？本质上，它在告诉我们这两个分支基本如下图所示（最新的提交在右侧）：main: A - B - C origin/main: A - B - C - D - E 或者，从另一个角度理解就是：A - B - C - D - E (origin/main) | main 这里，origin/main 仅仅多出了 2 个 main 不存在的提交，因此我们可以轻松地让 main 更新至最新 —— 我们所需要做的就是添加上那 2 个提交。事实上，这几乎不可能出错 —— 不存在合并冲突。快速前进式合并是个非常棒的事情！这是合并两个分支最简单的方式。运行完 git pull 之后，你会得到如下状态：main: A - B - C - D - E origin/main: A - B - C - D - E 下面这个例子展示了一种不能快速前进的状态。A - B - C - X (main) | - - D - E (origin/main)此时，main 分支上有一个 origin/main 分支上无的提交（X），所以无法执行快速前移。在此种情况，git status 就会如此显示：$ git status Your branch and 'origin/main' have diverged, and have 1 and 2 different commits each, respectively. （你的分支和 origin/main 分支已经产生了分歧，其中各有 1 个和 2 个不同的提交。）“引用”、“符号引用”在使用 Git 时，“引用” 一词可能会使人混淆。实际上，Git 中被称为 “引用” 的实例至少有三种：分支和标签，例如 main 和 v0.2HEAD，代表当前活跃的分支诸如 HEAD^^^ 这样的表达式，Git 会将其解析成一个提交 ID。确切说，这可能并非 “引用”，我想 Git 将其称作 “版本参数”，但我个人并未使用过这个术语。个人而言，“符号引用” 这个术语颇为奇特，因为我觉得我只使用过 HEAD（即当前分支）作为符号引用。而 HEAD 在 Git 中占据核心位置，多数 Git 核心命令的行为都基于 HEAD 的值，因此我不太确定将其泛化成一个概念的实际意义。refspecs在 .git/config 配置 Git 远程仓库时，你可能会看到这样的代码 +refs/heads/main:refs/remotes/origin/main 。[remote "origin"] url = git@github.com:jvns/pandas-cookbook fetch = +refs/heads/main:refs/remotes/origin/main我对这段代码的含义并不十分清楚，我通常只是在使用 git clone 或 git remote add 配置远程仓库时采用默认配置，并没有动机去深究或改变。“tree-ish”在 git checkout 的手册页中，我们可以看到：git checkout [-f|--ours|--theirs|-m|--conflict= ] [<tree-ish>] [--] <pathspec>... 那么这里的 tree-ish 是什么意思呢？其实当你执行 git checkout THING . 时，THING 可以是以下的任一种：一个提交 ID（如 182cd3f）对一个提交 ID 的引用（如 main 或 HEAD^^ 或 v0.3.2）一个位于提交内的子目录（如 main:./docs）可能就这些？对我个人来说，“提交内的目录”这个功能我从未使用过，从我的视角看，tree-ish 可以解读为“提交或对提交的引用”。“索引”、“暂存”、“缓存”这些术语都指向的是同一样东西（文件 .git/index，当你执行 git add 时，你的变动会在这里被暂存）：git diff --cachedgit rm --cachedgit diff --staged文件 .git/index尽管它们都是指向同一个文件，但在实际使用中，这些术语的应用方式有所不同：很显然，--index 和 --cached 并不总是表示同一种意思。我自己从未使用 --index，所以具体细节我就不展开讨论了，但是你可以在 Junio Hamano（Git 的主管维护者）的博客文章 中找到详细解释。相应的手册页没有给出特别有帮助的描述：git reset: “重置当前 HEAD 到指定的状态”git revert: “还原某些现有的提交”git restore: “恢复工作树文件”虽然这些简短的描述为你详细说明了哪个名词受到了影响（“当前 HEAD”，“某些提交”，“工作树文件”），但它们都预设了你已经知道在这种语境中，“重置”、“还原”和“恢复”的准确含义。
第一章 程序漫谈 程序漫谈1.1 硬件、软件、程序1.2 计算机语言1.3 语言和实现语言的工具 1.3.1 机器语言 1.3.2 汇编语言 1.3.3 高级语言 1.3.4 语言实现工具 1.3.4.1 C++ Builder 的基ᴀ功能 1.3.4.2 VCL vs. MFC1.1 硬件硬件、、、、软件软件、、、、程序程序对于计算机，也许你是老鸟，也许你是菜鸟……但不管怎样，如果你此时此地你要学习编程，那么你应该多多少少知道点什么叫硬件什么叫软件——反正我不管你懂不懂，为了面子，我很不乐意你问我什么叫硬件什么叫软件——我做在这凳子半个小时了，一直想不出如何给二者下个定义。 美国一个电脑神童说：“凡是摔到地上会坏的就是硬件。”我深感不妥，众所周知，如果把硬盘摔到地上，那么硬盘坏了，里头的那些数据——都是软件——也一样地坏得让你我心疼。 倘若按字面上理解，那就更加的矛盾重重：硬盘硬是硬件；软盘软也是硬件。 还一种说法是：看得见摸得着的为硬件，看不见摸不着的为软件。刚觉得它说得不错，但马上我就发觉了它的破绽：我现在用的 Word2000，它就在屏幕上，界面美观，操作方便……无奈之下，我搬出金山词霸，它说：“硬件：计算机及其它直接参与数据运算或信息交流的物理设备”。挺好。硬件就是设备。平常我们生活中的各种设备，洗衣机，冰箱，电视，还有螺丝刀，钳子，都是硬件。 软件呢？“软件：控制计算机硬件功能及其运行的指令、例行程序和符号语言”。指令、程序和符号语言是什么且不说，至少我们得知：软件是用来控制硬件的运行的。 这就好办点。我们可以打比方：譬如汽车，其ᴀ身自然是硬件，但关于驾驶车的那一套技术，及有关交通规则，我们可称为软件，因为后者控制了前者的运行方式。 （不传之秘笈：如果你英语一般，学习编程时，别忘装上金山词霸。并且装上后立即上网升级） 现在来谈“指令、程序、和符号语言”。我想交通方面的“软件”确实就是这些东西。我不会驾车，但曾多次看到警察在我上班坐的班车前用指头一指，就令我们的司机脸色发青。之后，一套既定的处罚程序被执行。很快，਀说我们的司机又在学习那些用来表示“单行”、“只许右拐”、“不许停车”、“禁鸣”等奇奇怪怪的符号语言了…… 事实上，说软件看不见摸不着其实也正确。因为它们是思想，精神，规则，逻辑。ᴀ身是抽象的，确实不可触及。但软件总是要有载体来存放，要有表达或表现方式，这些使得它们变得形象具体起来。在此意义上，说软件是摔在地上坏不了的东西，也相当行得通，神童毕竟是神童。 最后，什么是程序？我决定斗胆来给它下个定义： 程序是一组按照一定的逻辑的进行组合的指令。 因此，在以后的学习过程上，很多时候我们会觉得程序就是指令；同样很多时候，我们觉得程序就是逻辑。 当然，更多的时候，我们并不区分程序和软件二者。也许前者更趋于抽象，而后者趋于具体。比如我们在写那些表达我们的思想逻辑时，我们喜欢说“写程序”；而当程序完成，可以待价而沽时，我们称它为软件。1.2 计算机语言 计算机语言程序用计算机语言写成。编程的实质就是你用计算机语言来表达你解决问题的逻辑。 那么，什么叫计算机语言？ 先不必去解释。因为，计算机是机器，机器不是生物，它怎么能有语言？小猫小狗有语言我尚可相信，机器也有语言，还要我们去学习，这似乎有渎人类之尊严。 如果我不把这个结解开，可能部分特别在意人类尊严的学生对学习编程从此产生心理障碍，无法继续学习…… 狭义上，我们讲的语言，汉语英语广东话，它是语言，有声音。小鸟之间吱吱喳喳，大抵也是语言。但其实语言二字虽都带口，却不是说非得有声才称为语言：哑语无声，但它也是语言。广义上讲，语言是沟通、交流的一种手段。基于此，我们认为所有的机器或工具，也就包括计算机，都有它们自己的语言。比如锤子，它的语言是敲打；比如螺丝刀，它的语言是拧，如果你非要拧锤子，非要敲打螺丝刀，那么结果就像你用法语和广东佬交谈，用粤语和法国佬说话一样莫名其妙。 一般地，越复杂的机器，人类与其沟通的语言也越复杂。譬如汽车，你想驾驭它，你就必须去驾校参加学习。想一想，开车的时候，我们的确是在和车进行沟通。如果你俩之间的沟通出现差错——你心里右转，手却一个劲向左转方向盘，向机器发出了错误的命令——这将多么可怕！ 至此，我们的心理障碍可以消除了。小猫小狗有语言是因为它们聪明，而机器有语言却是因为它们的笨：它们笨，没办法像动物一样可以通过培训来理会人类的意愿，所以，让人类来反过来为它们定一套沟通的规则，然后人自己去学会这些语言，从而可以方便控制机器。 再所以，我们推论，凡是机器语言都是笨笨的语言。机器语言可以分低级语言和高级语言，但无论何者，都是笨得可爱——学得越多你就会越发现它的笨和可爱。另外，当我说越复杂的机器，其语言也越复杂时，我用“一般地”加以修饰。这是因为，发明和发展机器的智者们会为机器制造出越来越高级的语言，这些高级语言，最终越来越接近人类的自然语言。就像计算机，我们有信心相信，终有一天，它能਀懂我们的语言——这就是流传在程序员中的一个梦。当程序员熬红眼敲打数万行代码时，他们便会想起这个梦：闭上双眼，伸腰，对 PC 说：“BEGIN……”；深呼吸一次，然后说：“END”。张眼时发现计算机已完成了所有工作…… （不传秘笈：程序员必备之工具：日产乐敦牌眼药水一瓶，用于预防角膜炎；韩产 777 牌指甲刀一枚，用于铰除因击键刺激而疯长的指甲；国产肛泰若干，治疗因长期坐姿不当而导致的痔疮） 回到计算机。它是机器，也是人类有史以来，继发明使用火、电、电子这些改善人类生活的工具后，最为重要，最为先进，最为广泛使用的工具。它的机器语言之复杂程度可想而知，已经复杂到必须成为一门大学的专业课程。然而别忘了我们前面的结论，语言只是沟通的手段。在这个意义上，当你用鼠标或键盘在计算机上进行输入时，只要你输入的是正确操作，我们都认为你在使用计算机语言，因为你确实是在用一种特定的方式或动作，进行和计算机的交流。 当然，这里的课程并不特意教你任何有关计算机的基ᴀ操作。计算机的基ᴀ操作主要是指如何使用计算机内已有的软件产品，比如 Windows ᴀ身（操作系统是软件，称为系统软件）；比如办公系统 MS Office或 WPS Office（这些实现工作生活中具体应用需求的称为应用软件）；比如游戏（一种特定的，只拿来玩的应用，称为游戏软件）。但我们不同，我们学的是如何编写软件。也就是说，我们将是发明人，设计师，创造者；而他们（到今天仍拒不学习编程的家伙）都只是使用者。（我突然有些不安：这么说其实纯属煽情。公平地讲，任何人的任凭创造都是别人的劳动成果之上，任何人也都在创造自己的杰作） 程序（或软件）是用计算机语言写出来的。写一个程序，大致是这么一个过程：人有一个问题或需求，想用计算机解决……人想出解决问题或实现需求的思路……人将思路抽象成数学方法和逻辑表达或某种流程的模式……程序员将数学方法，逻辑表达中的数据和流程用计算机语言表达，称为代码……用计算机高级语言写成的代码被语言的实现工具（VC，VB，Delphi，或 C++ Builder）转换成计算机的最低级机器语言。这就完成了人与机器在程序制定上的最后沟通。 可见，你的思路是先用人类自己的语言思考，然后用一门计算机语言写成代码，最终，需要一个语言工具来将它转换成机器可以理解的机器语言。我们要学的就是一门承上启下的计算机语言。这样语言有很多：BASIC，Pascal ,C, C++, Java, C#……我们学 C 和 C++。它是使用最多的语言。有关 C，C++的更多特点，我们将在下一节谈到。 尽管你完全可以直接用最低级的计算机语言——机器语言——来写代码，那样就不需要语言工具了，但在这里你要弄清了，我们不是教机器语言。下一节，你会明白用机器直接能懂的语言——不妨称之为原始的机器语言——写软件，在今天是多么的不现实。
大家应该都知道 Mybatis 源码也是对 Jbdc 的再一次封装，不管怎么进行包装，还是会有获取链接、preparedStatement、封装参数、执行这些步骤的。配置解析过程 String resource = "mybatis-config.xml"; //1.读取resources下面的mybatis-config.xml文件 InputStream inputStream = Resources.getResourceAsStream(resource); //2.使用SqlSessionFactoryBuilder创建SqlSessionFactory SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //3.通过sqlSessionFactory创建SqlSession SqlSession sqlSession = sqlSessionFactory.openSession();Resources.getResourceAsStream (resource) 读取文件public static InputStream getResourceAsStream(String resource) throws IOException { return getResourceAsStream(null, resource); } //loader赋值为null public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException { InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader); if (in == null) { throw new IOException("Could not find resource " + resource); } return in; } //classLoader为null public InputStream getResourceAsStream(String resource, ClassLoader classLoader) { return getResourceAsStream(resource, getClassLoaders(classLoader)); } //classLoader类加载 InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) { for (ClassLoader cl : classLoader) { if (null != cl) { //加载指定路径文件流 InputStream returnValue = cl.getResourceAsStream(resource); // now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource if (null == returnValue) { returnValue = cl.getResourceAsStream("/" + resource); } if (null != returnValue) { return returnValue; } } } return null; }总结：主要是通过 ClassLoader.getResourceAsStream () 方法获取指定的 classpath 路径下的 Resource 。通过 SqlSessionFactoryBuilder 创建 SqlSessionFactory//SqlSessionFactoryBuilder是一个建造者模式 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); } //XMLConfigBuilder也是建造者模式 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } //接下来进入XMLConfigBuilder构造函数 public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) { this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props); } //接下来进入this后,初始化Configuration private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { super(new Configuration()); ErrorContext.instance().resource("SQL Mapper Configuration"); this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; } //其中parser.parse()负责解析xml,build(configuration)创建SqlSessionFactory return build(parser.parse());parser.parse () 解析 xmlpublic Configuration parse() { //判断是否重复解析 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; //读取配置文件一级节点configuration parseConfiguration(parser.evalNode("/configuration")); return configuration; } private void parseConfiguration(XNode root) { try { //properties 标签,用来配置参数信息，比如最常见的数据库连接信息 propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); loadCustomLogImpl(settings); //实体别名两种方式:1.指定单个实体;2.指定包 typeAliasesElement(root.evalNode("typeAliases")); //插件 pluginElement(root.evalNode("plugins")); //用来创建对象(数据库数据映射成java对象时) objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 //数据库环境 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); //数据库类型和Java数据类型的转换 typeHandlerElement(root.evalNode("typeHandlers")); //这个是对数据库增删改查的解析 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }总结:parseConfiguration 完成的是解析 configuration 下的标签private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { //解析<package name=""/> if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); //包路径存到mapperRegistry中 configuration.addMappers(mapperPackage); } else { //解析<mapper url="" class="" resource=""></mapper> String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); //读取Mapper.xml文件 InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }总结：通过解析 configuration.xml 文件，获取其中的 Environment、Setting, 重要的是将下的所有解析出来之后添加到 Configuration,Configuration 类似于配置中心，所有的配置信息都在这里。mapperParser.parse () 对 Mapper 映射器的解析public void parse() { if (!configuration.isResourceLoaded(resource)) { //解析所有的子标签 configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); //把namespace（接口类型）和工厂类绑定起来 bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); } //这里面解析的是Mapper.xml的标签 private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); //对其他命名空间缓存配置的引用 cacheRefElement(context.evalNode("cache-ref")); //对给定命名空间的缓存配置 cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); //是最复杂也是最强大的元素，用来描述如何从数据库结果集中来加载对象 resultMapElements(context.evalNodes("/mapper/resultMap")); //可被其他语句引用的可重用语句块 sqlElement(context.evalNodes("/mapper/sql")); //获得MappedStatement对象(增删改查标签) buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } } //获得MappedStatement对象(增删改查标签) private void buildStatementFromContext(List<XNode> list) { if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); } //获得MappedStatement对象(增删改查标签) private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { //循环增删改查标签 for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { //解析insert/update/select/del中的标签 statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } } public void parseStatementNode() { //在命名空间中唯一的标识符,可以被用来引用这条语句 String id = context.getStringAttribute("id"); //数据库厂商标识 String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } String nodeName = context.getNode().getNodeName(); SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; //flushCache和useCache都和二级缓存有关 //将其设置为true后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); //将其设置为 true 后，将会导致本条语句的结果被二级缓存缓存起来，默认值：对 select 元素为 true boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); // Include Fragments before parsing XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); //会传入这条语句的参数类的完全限定名或别名 String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); // Parse selectKey after includes and remove them. processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: <selectKey> and <include> were parsed and removed) KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); //从这条语句中返回的期望类型的类的完全限定名或别名 String resultType = context.getStringAttribute("resultType"); Class<?> resultTypeClass = resolveClass(resultType); //外部resultMap的命名引用 String resultMap = context.getStringAttribute("resultMap"); String resultSetType = context.getStringAttribute("resultSetType"); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); String resultSets = context.getStringAttribute("resultSets"); builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); } public MappedStatement addMappedStatement( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) { if (unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved"); } id = applyCurrentNamespace(id, false); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) .resource(resource) .fetchSize(fetchSize) .timeout(timeout) .statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resultSets(resultSets) .resultMaps(getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) .useCache(valueOrDefault(useCache, isSelect)) .cache(currentCache); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); if (statementParameterMap != null) { statementBuilder.parameterMap(statementParameterMap); } MappedStatement statement = statementBuilder.build(); //持有在configuration中 configuration.addMappedStatement(statement); return statement; } public void addMappedStatement(MappedStatement ms){ //ms.getId = mapper.UserMapper.getUserById //ms = MappedStatement等于每一个增删改查的标签的里的数据 mappedStatements.put(ms.getId(), ms); } //最终存放到mappedStatements中,mappedStatements存放的是一个个的增删改查 protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection").conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and " + targetValue.getResource());解析 bindMapperForNamespace () 方法把 namespace（接口类型）和工厂类绑定起来private void bindMapperForNamespace() { //当前Mapper的命名空间 String namespace = builderAssistant.getCurrentNamespace(); if (namespace != null) { Class<?> boundType = null; try { //interface mapper.UserMapper这种 boundType = Resources.classForName(namespace); } catch (ClassNotFoundException e) { } if (boundType != null) { if (!configuration.hasMapper(boundType)) { configuration.addLoadedResource("namespace:" + namespace); configuration.addMapper(boundType); } } } } public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); } public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { //接口类型(key)->工厂类 knownMappers.put(type, new MapperProxyFactory<>(type)); MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } }生成 SqlSessionFactory 对象XMLMapperBuilder.parse () 方法，是对 Mapper 映射器的解析里面有两个方法：（1）configurationElement () 解析所有的子标签，最终解析 Mapper.xml 中的 insert/update/delete/select 标签的 id (全路径) 组成 key 和整个标签和数据连接组成 MappedStatement 存放到 Configuration 中的 mappedStatements 这个 map 里面。（2）bindMapperForNamespace () 是把接口类型 (interface mapper.UserMapper) 和工厂类存到放 MapperRegistry 中的 knownMappers 里面。SqlSessionFactory 的创建public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }直接把 Configuration 当做参数，直接 new 一个 DefaultSqlSessionFactory。SqlSession 会话的创建过程mybatis 操作的时候跟数据库的每一次连接，都需要创建一个会话，我们用 openSession () 方法来创建。这个会话里面需要包含一个 Executor 用来执行 SQL。Executor 又要指定事务类型和执行器的类型。创建 Transaction (两种方式)属性 产生工厂类 产生事务JDBC JbdcTransactionFactory JdbcTransactionMANAGED ManagedTransactionFactory ManagedTransaction如果配置的是 JDBC, 则会使用 Connection 对象的 commit ()、rollback ()、close () 管理事务。如果配置成 MANAGED, 会把事务交给容器来管理，比如 JBOSS,Weblogic。SqlSession sqlSession = sqlSessionFactory.openSession(); public SqlSession openSession() { //configuration中有默认赋值protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }<environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments>创建 Executor//ExecutorType是SIMPLE,一共有三种SIMPLE(SimpleExecutor)、REUSE(ReuseExecutor)、BATCH(BatchExecutor) private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { //xml中的development节点 final Environment environment = configuration.getEnvironment(); //type配置的是Jbdc所以生成的是JbdcTransactionFactory工厂类 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); //Jdbc生成JbdcTransactionFactory生成JbdcTransaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); //创建CachingExecutor执行器 final Executor executor = configuration.newExecutor(tx, execType); //创建DefaultSqlSession属性包括 Configuration、Executor对象 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }获得 Mapper 对象UserMapper userMapper = sqlSession.getMapper(UserMapper.class); public <T> T getMapper(Class<T> type) { return configuration.getMapper(type, this); }mapperRegistry.getMapper 是从 MapperRegistry 的 knownMappers 里面取的，knownMappers 里面存的是接口类型 (interface mapper.UserMapper) 和工厂类 (MapperProxyFactory)。public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }从 knownMappers 的 Map 里根据接口类型 (interface mapper.UserMapper) 取出对应的工厂类。public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }这里通过 JDK 动态代理返回代理对象 MapperProxy (org.apache.ibatis.binding.MapperProxy@6b2ea799)protected T newInstance(MapperProxy<T> mapperProxy) { //mapperInterface是interface mapper.UserMapper return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } UserMapper userMapper = sqlSession.getMapper(UserMapper.class);执行 SQLUser user = userMapper.getUserById(1);调用 invoke 代理方法由于所有的 Mapper 都是 MapperProxy 代理对象，所以任意的方法都是执行 MapperProxy 的 invoke () 方法public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { //判断是否需要去执行SQL还是直接执行方法 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); //这里判断的是接口中的默认方法Default等 } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } //获取缓存,保存了方法签名和接口方法的关系 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }调用 execute 方法这里使用的例子用的是查询所以走的是 else 分支语句。public Object execute(SqlSession sqlSession, Object[] args) { Object result; //根据命令类型走不行的操作command.getType()是select switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { //将参数转换为SQL的参数 Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break; case FLUSH: result = sqlSession.flushStatements(); break; default: throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; }调用 selectOne 其实是 selectListselectOne 查询一个和查询多个其实是一样的。public <T> T selectOne(String statement, Object parameter) { // Popular vote was to return null on 0 results and throw exception on too many. List<T> list = this.selectList(statement, parameter); if (list.size() == 1) { return list.get(0); } else if (list.size() > 1) { throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size()); } else { return null; } } public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { //从Configuration里的mappedStatements里根据key(id的全路径)获取MappedStatement 对象 MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }创建 CacheKey从 BoundSql 中获取 SQL 信息，创建 CacheKey。这个 CacheKey 就是缓存的 Key。public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { //创建缓存Key BoundSql boundSql = ms.getBoundSql(parameterObject); //key = -575461213:-771016147:mapper.UserMapper.getUserById:0:2147483647:select * from test_user where id = ?:1:development CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null) { flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }清空本地缓存public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } //queryStack 用于记录查询栈，防止递归查询重复处理缓存 //flushCache=true 的时候，会先清理本地缓存（一级缓存） if (queryStack == 0 && ms.isFlushCacheRequired()) { //清空本地缓存 clearLocalCache(); } List<E> list; try { queryStack++; list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { //如果没有缓存,会从数据库查询：queryFromDatabase() list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); //如果 LocalCacheScope == STATEMENT,会清理本地缓存 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }从数据库查询private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; //先在缓存用占位符占位 localCache.putObject(key, EXECUTION_PLACEHOLDER); try { //执行Executor 的 doQuery(),默认是SimpleExecutor list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { //执行查询后,移除占位符 localCache.removeObject(key); } //从新放入数据 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }执行 doQuerypublic <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }源码总结总体上来说，MyBatis 的源码还是比较简单的，只要大家踏下心来，花个两三天仔细研究下，基本上都能弄明白源码的主体脉络。
changeme1.安装依赖项sudo apt update sudo apt install build-essential bison libmysqlclient-dev libpcre3-dev libpq-dev \ libsqlite3-dev libssl-dev libz-dev libjemalloc-dev libicu-dev 2.签出 Git 存储库git clone https://github.com/fluffos/fluffos.git cd fluffos git checkout master #(or an release tag) 3.升级你的 cmakesudo pip install --upgrade cmake 4.开始构建mkdir build cd build cmake .. make -j `nproc` install 5.自动化脚本!/bin/bash更新系统软件包列表sudo apt update 安装软件包及依赖库sudo apt install git bison build-essential autoconf automake cmake gcc g++ libjemalloc-dev zlib1g-dev libbz2-dev libssl-dev libmysqlclient-dev libsqlite3-dev libpq-dev libpcre3-dev libevent-dev libicu-dev libdw-dev binutils-dev libgtest-dev telnet python3 -y 进入当前用户的 home 目录cd ~ 如果 fluffos 目录不存在，则从 gitee 克隆 fluffos 仓库if [ ! -d "fluffos" ]; then git clone https://gitee.com/mudren/fluffos.git fi 进入 fluffos 目录并拉取最新代码cd fluffos && git checkout . && git pull 如果 build 目录已存在，则删除if [ -d "build" ]; then rm -rf build fi 创建 build 目录并进入mrkdi build && cd build 记录开始时间starttime=`date +'%Y-%m-%d %H:%M:%S'` 编译 fluffos，使用多线程编译，开启 SQLite 数据库和默认数据库支持cmake -DPACKAGE_DB_SQLITE=2 -DPACKAGE_DB_DEFAULT_DB=2 .. && make -j$(nproc) install 记录结束时间endtime=`date +'%Y-%m-%d %H:%M:%S'` 计算编译时间start_seconds=$(date --date=" $starttime" +%s); end_seconds=$(date --date="$endtime" +%s); 输出编译时间echo Start: $starttime. echo End: $endtime. echo "Build Time: "$((end_seconds-start_seconds))"s." 复制驱动至系统目录sudo cp bin/driver /usr/local/games/ 自动化脚本去掉.txt后缀名即可执行
我们选出 2022 年 10 个最美的 Linux 发行版，在保证系统稳定的前提下，会给你良好的视觉体验。定制化拓展 Linux 发行版以满足需求是非常有意思的事。无论是 Ubuntu 还是 Fedora ，都有各种各样的工具去定制化 Linux 桌面。但是，也有很多不需要任何定制化而且令人眼前一亮的 Linux 发行版。它们的开发者使它们在安装后不需要再定制化就能有非常好的视觉效果。现在，我们编制了一份最美 Linux 发行版列表，你可以现在尝试来更新你电脑的视觉外观。2022 最美的 Linux 发行版1、Zorin OS最先介绍的 Linux 发行版是 Zorin OS。Zorin OS 使用基于 GNOME 的 Zorin 桌面 ，非常适合追求美观的桌面和生产力的新用户。Zorin OS 最有特点的功能之一是，它可以随意转化，让其看起来像其他操作系统桌面，这意味着只需要设置布局选项，就可以改变任务栏、应用菜单、停靠栏，给用户最大的的灵活性，安装即用。阅读有关 Zorin OS 更多信息2、elementary OSelementary OS 是基于 Ubuntu 长期稳定支持（LTS）发行版中最美观的一个。它使用吸引人的 Pantheon 桌面环境，其外观和使用体验受到了 macOS 的启发。elementary OS 非常适合从 macOS 进入 Linux 世界的新用户，因为他们会发现许多熟悉的东西，比如手势和窗口样式。但是，elementary OS 很多地方不能通过设置菜单进行定制，只能依靠外部脚本命令来进一步定制。不过系统默认桌面就已经十分美观了，能满足大部分人的需求。elementary OS 最具特色的是它精心设计的应用商店。应用商店提供各种类别的应用，特别是那些专为 elementary OS 定制而且外观精美、功能强大的应用。阅读有关 elementaryOS 更多信息3、深度操作系统第三个介绍的 Linux 发行版是深度操作系统。它基于 Debian ，由中国深度科技公司开发。它使用自研的基于 Qt 的深度桌面环境（DDE）。深度桌面的部件、配色方案、窗口样式、壁纸看起来都非常不错，给用户安装即享的视觉体验。深度操作系统精心打磨的视觉组件和 macOS 非常相似。并且由于是 Debian 的稳定分支，如果你想要一个外观精美且稳定可靠的 Linux 发行版，深度操作系统是最适合的选择。为什么深度操作系统体验很好？基于 Qt 的酷炫桌面原生部件及支持暗色主题定制化停靠栏的多种选择项透明度、窗口效果、光标主题、图标主题强调色阅读有关深度操作系统更多信息4、CutefishOS第四个介绍的 Linux 发行版是 CutefishOS 。这个基于 Debian 和 Ubuntu 的 Linux 发行版具有一个原生开发的 Cutefish 桌面。 CutefishOS 还在开发中，但由于它的外观，它已经在用户群体中掀起波澜。（LCTT 译注：似乎也是国内团队开发的）CutefishOS 底层基于 Qt 和 KDE 框架构建。这个带有 Cutefish 桌面的高效 Linux 发行版在顶部提供了全局菜单功能。由于其目前正在开发中，定制选项仍在进行中。但在最新的发行版中， CutefishOS 已经支持深色模式、强调色、动画效果、停靠位置（左、右、下）等选项。如果你想体验完全不同的桌面风格，可以尝试 CutefishOS 。 你也可以查看下面提供的关于此桌面的完整评论和教程。Cutefish OS 测评5、Manjaro KDE Plasma 版Manjaro Linux KDE 版是现在外观最好看的 Linux 发行版之一。 Manjaro KDE 基于 Arch Linux ，采用了 KDE Plasma，并额外添加了一些调整工具和组件。 Manjaro 的绿色调色板给你以全新的外观和体验。你可以通过内置的 KDE 工具和设置进一步定制，并从 KDE 商店获得图标和主题。Manjaro KDE 是性能和外观与 Arch Linux 之强大的完美结合。对于 Arch Linux 新用户来说， Manjaro 是一个理想的起点。阅读有关 Manjaro KDE 更多信息6、Garuda Linux出名的 Garuda Linux 是榜单上的第六位。 Garuda Linux 基于 Arch Linux ，带有美观的桌面，其特点是为所有主流桌面环境定制了图标、主题。 Garuda Linux 使用针对硬件优化了性能的 Zen 内核，Garuda Linux 的外观和体验令人印象深刻。其 macOS 风格看起来非常的方便。 Neon 图标主题、可爱的调色板、模糊和透明度与全局菜单的结合是其自身的完美选择。Garuda 最主要的优点是支持所有桌面环境 — KDE Plasma、 GNOME、 Xfce、 LXQT、 MATE 等等。阅读有关 Garuda Linux 更多信息7、Linux Mint Cinnamon 版我们推荐 Linux Mint 是因为它非常的简洁、优美和稳定。它是目前最被广泛使用和出名的 Linux 发行版之一，或许其使用人数仅在 Ubuntu 之下。但和本列表中其他 Linux 发行版比较起来，它看起来不是很精美。不过，如果你喜欢看起来很棒的经典用户桌面，默认的 Cinnamon 桌面看起来干净且完美。Linux Mint Cinnamon 版对所有用户都非常友好，特别是第一次使用 Linux 或者从 Windows 转到 Linux 的用户。其默认的外观和体验和 Mint 的绿色调色板都能使人耳目一新。如果你无法确定用哪个既稳定又养眼的 Linux 发行版，那就毫不犹豫的选择 Linux Mint Cinnamon 版。阅读有关 Linux Mint 更多信息8、Nitrux OSNitrux Linux 基于 Debian ，其特点是一个名为 NX 桌面的 KDE Plasma 修改版。这个独特的 Linux 发行版有一套建立在 Maui 套件和 Qt 之上的 Nitrux 应用。 Nitrux 不使用 systemd，而使用 OpenRC 作为初始化系统。凭借其独特的功能和外观，Nitrux 是目前最好的 Linux 发行版之一。Nitrux OS 的默认外观设计得非常完美，它采用经过改进的 KDE Plasma 桌面，带有 Kvantum 主题引擎、图标主题、调色板、光标主题等。 Nitrux OS 背后的团队还开发了名为 Maui Shell 的独立桌面，这是一个美丽的融合型桌面，可以根据屏幕大小进行自我调整。如果你需要一款稳定方便的 KDE Plasma 桌面，那么 Nitrux OS 非常适合你。你不会失望的。阅读有关 Nitrux OS 更多信息9、优麒麟优麒麟是一个官方的 Ubuntu 版本，是专门为使用简体中文的中国人设计的。但它同时也支持其他语言。这个修改版的 Ubuntu 使用优麒麟用户界面（也称为 UKUI）。 UKUI 桌面使用 Qt 开发，支持 MATE 桌面组件。优麒麟看起来精致，就外观和设计而言，就像是 GNOME 和 KDE Plasma 的结合。优麒麟具有设计精美的图标集、底部任务栏，漂亮的应用程序视图、应用程序切换器，圆角窗口等精心制作的功能。阅读有关优麒麟更多信息10、Pop!_OSPop!_OS 是由生产计算机硬件的 System76 公司开发的。 这个基于 Ubuntu 的 Linux 发行版已经预装在所有 System76 硬件上。不过，你也可以从其官方仓库下载然后安装到你的机器上。Pop!_OS 的特点是在默认的 GNOME 桌面上带有额外的调整和配置。此桌面具有 GNOME 40 时代之前的特点，预配置了多个扩展和调整。比如你可以获得一个可以配置为在桌面中自由调整的底部停靠栏、一个用于启动应用程序的启动器、圆角窗口等诸如此类的功能。此桌面还有自动平铺和优化的键盘导航功能，可提高你的工作效率。其外观和感觉都很干净，设计精美，有调色板，内置了深色模式、圆角窗口，以及图标主题。阅读有关 Pop!_OS 更多信息
有时候你很忙。而有时候你只是需要看起来很忙，就像电影中的黑客一样。有一些开源工具就是干这个的。如果在你在消磨时光时看过谍战片、动作片或犯罪片，那么你就会清晰地在脑海中勾勒出黑客的电脑屏幕的样子。就像是在《黑客帝国》电影中，代码雨 一样的十六进制数字流，又或是一排排快速移动的代码。也许电影中出现一幅世界地图，其中布满了闪烁的光点和一些快速更新的图表。不可或缺的，也可能有 3D 旋转的几何形状。甚至，这一切都会显示在一些完全不符合人类习惯的数量荒谬的显示屏上。 在《剑鱼行动》电影中黑客就使用了七个显示屏。当然，我们这些从事计算机行业的人一下子就明白这完全是胡说八道。虽然在我们中，许多人都有双显示器（或更多），但一个闪烁的数据仪表盘、刷新的数据通常和专注工作是相互矛盾的。编写代码、项目管理和系统管理与日常工作不同。我们遇到的大多数情况，为了解决问题，都需要大量的思考，与客户沟通所得到一些研究和组织的资料，然后才是少许的 敲代码。然而，这与我们想追求电影中的效果并不矛盾，也许，我们只是想要看起来“忙于工作”而已。注：当然，我仅仅是在此胡诌。如果您公司实际上是根据您繁忙程度来评估您的工作时，无论您是蓝领还是白领，都需要亟待解决这样的工作文化。假装工作很忙是一种有毒的文化，对公司和员工都有害无益。这就是说，让我们找些乐子，用一些老式的、毫无意义的数据和代码片段填充我们的屏幕。（当然，数据或许有意义，但不是在这种没有上下文的环境中。）当然有一些用于此用途的有趣的图形界面程序，如 hackertyper.net 或是 GEEKtyper.com 网站（LCTT 译注：是在线假装黑客操作的网站），为什么不使用标准的 Linux 终端程序呢？对于更老派的外观，可以考虑使用 酷炫复古终端，这听起来确实如此：一个酷炫的复古终端程序。我将在下面的屏幕截图中使用酷炫复古终端，因为它看起来的确很酷。Genact我们来看下第一个工具——Genact。Genact 的原理很简单，就是慢慢地无尽循环播放您选择的一个序列，让您的代码在您外出休息时“编译”。由您来决定播放顺序，但是其中默认包含数字货币挖矿模拟器、Composer PHP 依赖关系管理工具、内核编译器、下载器、内存转储等工具。其中我最喜欢的是其中类似《模拟城市》加载显示。所以只要没有人仔细检查，你可以花一整个下午等待您的电脑完成进度条。Genact 发布了 支持 Linux、OS X 和 Windows 的版本。并且其 Rust 源代码 在 GitHub 上开源（遵循 MIT 许可证）。HollywoodHollywood 采取更直接的方法。它本质上是在终端中创建一个随机的数量和配置的分屏，并启动那些看起来很繁忙的应用程序，如 htop、目录树、源代码文件等，并每隔几秒将其切换。它被组织成一个 shell 脚本，所以可以非常容易地根据需求进行修改。Hollywood的 源代码 在 GitHub 上开源（遵循 Apache 2.0 许可证）。Blessed-contribBlessed-contrib 是我个人最喜欢的应用，实际上并不是为了这种表演而专门设计的应用。相反地，它是一个基于 Node.js 的终端仪表盘的构建库的演示文件。与其他两个不同，实际上我已经在工作中使用 Blessed-contrib 的库，而不是用于假装忙于工作。因为它是一个相当有用的库，并且可以使用一组在命令行显示信息的小部件。与此同时填充虚拟数据也很容易，所以可以很容易实现你在计算机上模拟《战争游戏》的想法。Blessed-contrib 的源代码在 GitHub 上（遵循 MIT 许可证）。当然，尽管这些工具很容易使用，但也有很多其他的方式使你的屏幕丰富。在你看到电影中最常用的工具之一就是Nmap，这是一个开源的网络安全扫描工具。实际上，它被广泛用作展示好莱坞电影中，黑客电脑屏幕上的工具。因此 Nmap 的开发者创建了一个 页面，列出了它出现在其中的一些电影，从《黑客帝国 2：重装上阵》到《谍影重重3》、《龙纹身的女孩》，甚至《虎胆龙威 4》。当然，您可以创建自己的组合，使用终端多路复用器（如 screen 或 tmux）启动您希望使用的任何数据切分程序。那么，您是如何使用您的屏幕的呢？
学习密码学背后的基本概念，主要是保密性、完整性和身份认证。安全通信正快速成为当今互联网的规范。从 2018 年 7 月起，Google Chrome 将对全部使用 HTTP 传输（而不是 HTTPS 传输）的站点开始显示“不安全”警告。虽然密码学已经逐渐变广为人知，但其本身并没有变得更容易理解。Let's Encrypt 设计并实现了一套令人惊叹的解决方案，可以提供免费安全证书和周期性续签；但如果不了解底层概念和缺陷，你也不过是加入了类似“货物崇拜cargo cult”的技术崇拜的程序员大军。安全通信的特性密码学最直观明显的目标是保密性confidentiality：消息message传输过程中不会被窥探内容。为了保密性，我们对消息进行加密：对于给定消息，我们结合一个密钥key生成一个无意义的乱码，只有通过相同的密钥逆转加密过程（即解密过程）才能将其转换为可读的消息。假设我们有两个朋友 Alice 和 Bob，以及他们的八卦nosy邻居 Eve。Alice 加密类似 "Eve 很讨厌" 的消息，将其发送给 Bob，期间不用担心 Eve 会窥探到这条消息的内容。对于真正的安全通信，保密性是不够的。假如 Eve 收集了足够多 Alice 和 Bob 之间的消息，发现单词 “Eve” 被加密为 "Xyzzy"。除此之外，Eve 还知道 Alice 和 Bob 正在准备一个派对，Alice 会将访客名单发送给 Bob。如果 Eve 拦截了消息并将 “Xyzzy” 加到访客列表的末尾，那么她已经成功的破坏了这个派对。因此，Alice 和 Bob 需要他们之间的通信可以提供完整性integrity：消息应该不会被篡改。而且我们还有一个问题有待解决。假如 Eve 观察到 Bob 打开了标记为“来自 Alice”的信封，信封中包含一条来自 Alice 的消息“再买一加仑冰淇淋”。Eve 看到 Bob 外出，回家时带着冰淇淋，这样虽然 Eve 并不知道消息的完整内容，但她对消息有了大致的了解。Bob 将上述消息丢弃，但 Eve 找出了它并在下一周中的每一天都向 Bob 的邮箱中投递一封标记为“来自 Alice”的信封，内容拷贝自之前 Bob 丢弃的那封信。这样到了派对的时候，冰淇淋严重超量；派对当晚结束后，Bob 分发剩余的冰淇淋，Eve 带着免费的冰淇淋回到家。消息是加密的，完整性也没问题，但 Bob 被误导了，没有认出发信人的真实身份。身份认证Authentication这个特性用于保证你正在通信的人的确是其声称的那样。信息安全还有其它特性，但保密性、完整性和身份验证是你必须了解的三大特性。加密和加密算法加密都包含哪些部分呢？首先，需要一条消息，我们称之为明文plaintext。接着，需要对明文做一些格式上的初始化，以便用于后续的加密过程（例如，假如我们使用分组加密算法block cipher，需要在明文尾部填充使其达到特定长度）。下一步，需要一个保密的比特序列，我们称之为密钥key。然后，基于私钥，使用一种加密算法将明文转换为密文ciphertext。密文看上去像是随机噪声，只有通过相同的加密算法和相同的密钥（在后面提到的非对称加密算法情况下，是另一个数学上相关的密钥）才能恢复为明文。（LCTT 译注：cipher 一般被翻译为密码，但其具体表达的意思是加密算法，这里采用加密算法的翻译）加密算法使用密钥加密明文。考虑到希望能够解密密文，我们用到的加密算法也必须是可逆的reversible。作为简单示例，我们可以使用 XOR。该算子可逆，而且逆算子就是本身（P ^ K = C; C ^ K = P），故可同时用于加密和解密。该算子的平凡应用可以是一次性密码本one-time pad，不过一般而言并不可行。但可以将 XOR 与一个基于单个密钥生成任意随机数据流arbitrary stream of random data的函数结合起来。现代加密算法 AES 和 Chacha20 就是这么设计的。我们把加密和解密使用同一个密钥的加密算法称为对称加密算法symmetric cipher。对称加密算法分为流加密算法stream ciphers和分组加密算法两类。流加密算法依次对明文中的每个比特或字节进行加密。例如，我们上面提到的 XOR 加密算法就是一个流加密算法。流加密算法适用于明文长度未知的情形，例如数据从管道或 socket 传入。RC4 是最为人知的流加密算法，但在多种不同的攻击面前比较脆弱，以至于最新版本 （1.3）的 TLS （“HTTPS” 中的 “S”）已经不再支持该加密算法。Efforts 正着手创建新的加密算法，候选算法 ChaCha20 已经被 TLS 支持。分组加密算法对固定长度的分组，使用固定长度的密钥加密。在分组加密算法领域，排行第一的是 先进加密标准Advanced Encryption Standard（AES），使用的分组长度为 128 比特。分组包含的数据并不多，因而分组加密算法包含一个工作模式，用于描述如何对任意长度的明文执行分组加密。最简单的工作模式是 电子密码本Electronic Code Book（ECB），将明文按分组大小划分成多个分组（在必要情况下，填充最后一个分组），使用密钥独立的加密各个分组。这里我们留意到一个问题：如果相同的分组在明文中出现多次（例如互联网流量中的 GET / HTTP/1.1 词组），由于我们使用相同的密钥加密分组，我们会得到相同的加密结果。我们的安全通信中会出现一种模式规律pattern，容易受到攻击。因此还有很多高级的工作模式，例如 密码分组链接Cipher Block Chaining（CBC），其中每个分组的明文在加密前会与前一个分组的密文进行 XOR 操作，而第一个分组的明文与一个随机数构成的初始化向量进行 XOR 操作。还有其它一些工作模式，在安全性和执行速度方面各有优缺点。甚至还有 Counter (CTR) 这种工作模式，可以将分组加密算法转换为流加密算法。除了对称加密算法，还有非对称加密算法asymmetric ciphers，也被称为公钥密码学public-key cryptography。这类加密算法使用两个密钥：一个公钥public key，一个私钥private key。公钥和私钥在数学上有一定关联，但可以区分二者。经过公钥加密的密文只能通过私钥解密，经过私钥加密的密文可以通过公钥解密。公钥可以大范围分发出去，但私钥必须对外不可见。如果你希望和一个给定的人通信，你可以使用对方的公钥加密消息，这样只有他们的私钥可以解密出消息。在非对称加密算法领域，目前 RSA 最具有影响力。非对称加密算法最主要的缺陷是，它们是计算密集型computationally expensive的。那么使用对称加密算法可以让身份验证更快吗？如果你只与一个人共享密钥，答案是肯定的。但这种方式很快就会失效。假如一群人希望使用对称加密算法进行两两通信，如果对每对成员通信都采用单独的密钥，一个 20 人的群体将有 190 对成员通信，即每个成员要维护 19 个密钥并确认其安全性。如果使用非对称加密算法，每个成员仅需确保自己的私钥安全并维护一个公钥列表即可。非对称加密算法也有加密数据长度限制。类似于分组加密算法，你需要将长消息进行划分。但实际应用中，非对称加密算法通常用于建立机密confidential、已认证authenticated的通道channel，利用该通道交换对称加密算法的共享密钥。考虑到速度优势，对称加密算法用于后续的通信。TLS 就是严格按照这种方式运行的。基础安全通信的核心在于随机数。随机数用于生成密钥并为确定性过程deterministic processes提供不可预测性。如果我们使用的密钥是可预测的，那我们从一开始就可能受到攻击。计算机被设计成按固定规则操作，因此生成随机数是比较困难的。计算机可以收集鼠标移动或键盘计时keyboard timings这类随机数据。但收集随机性（也叫信息熵entropy）需要花费不少时间，而且涉及额外处理以确保均匀分布uniform distribution。甚至可以使用专用硬件，例如熔岩灯lava lamps墙等。一般而言，一旦有了一个真正的随机数值，我们可以将其用作种子seed，使用密码安全的伪随机数生成器cryptographically secure pseudorandom number generator生成随机数。使用相同的种子，同一个随机数生成器生成的随机数序列保持不变，但重要的是随机数序列是无规律的。在 Linux 内核中，/dev/random 和 /dev/urandom 工作方式如下：从多个来源收集信息熵，进行无偏处理remove biases，生成种子，然后生成随机数，该随机数可用于 RSA 密钥生成等。其它密码学组件我们已经实现了保密性，但还没有考虑完整性和身份验证。对于后两者，我们需要使用一些额外的技术。首先是密码散列函数crytographic hash function，该函数接受任意长度的输入并给出固定长度的输出（一般称为摘要digest）。如果我们找到两条消息，其摘要相同，我们称之为碰撞collision，对应的散列函数就不适合用于密码学。这里需要强调一下“找到”：考虑到消息的条数是无限的而摘要的长度是固定的，那么总是会存在碰撞；但如果无需海量的计算资源，我们总是能找到发生碰撞的消息对，那就令人比较担心了。更严重的情况是，对于每一个给定的消息，都能找到与之碰撞的另一条消息。另外，哈希函数必须是单向的one-way：给定一个摘要，反向计算对应的消息在计算上不可行。相应的，这类条件被称为碰撞阻力collision resistance、第二原象抗性second preimage resistance和原象抗性preimage resistance。如果满足这些条件，摘要可以用作消息的指纹。理论上不存在具有相同指纹的两个人，而且你无法使用指纹反向找到其对应的人。如果我们同时发送消息及其摘要，接收者可以使用相同的哈希函数独立计算摘要。如果两个摘要相同，可以认为消息没有被篡改。考虑到 SHA-1 已经变得有些过时，目前最流行的密码散列函数是 SHA-256。散列函数看起来不错，但如果有人可以同时篡改消息及其摘要，那么消息发送仍然是不安全的。我们需要将哈希与加密算法结合起来。在对称加密算法领域，我们有消息认证码message authentication codes（MAC）技术。MAC 有多种形式，但哈希消息认证码hash message authentication codes（HMAC） 这类是基于哈希的。HMAC 使用哈希函数 H 处理密钥 K、消息 M，公式为 H(K + H(K + M))，其中 + 代表连接concatenation。公式的独特之处并不在本文讨论范围内，大致来说与保护 HMAC 自身的完整性有关。发送加密消息的同时也发送 MAC。Eve 可以任意篡改消息，但一旦 Bob 独立计算 MAC 并与接收到的 MAC 做比较，就会发现消息已经被篡改。在非对称加密算法领域，我们有数字签名digital signatures技术。如果使用 RSA，使用公钥加密的内容只能通过私钥解密，反过来也是如此；这种机制可用于创建一种签名。如果只有我持有私钥并用其加密文档，那么只有我的公钥可以用于解密，那么大家潜在的承认文档是我写的：这是一种身份验证。事实上，我们无需加密整个文档。如果生成文档的摘要，只要对这个指纹加密即可。对摘要签名比对整个文档签名要快得多，而且可以解决非对称加密存在的消息长度限制问题。接收者解密出摘要信息，独立计算消息的摘要并进行比对，可以确保消息的完整性。对于不同的非对称加密算法，数字签名的方法也各不相同；但核心都是使用公钥来检验已有签名。汇总现在，我们已经有了全部的主体组件，可以用其实现一个我们期待的、具有全部三个特性的体系system。Alice 选取一个保密的对称加密密钥并使用 Bob 的公钥进行加密。接着，她对得到的密文进行哈希并使用其私钥对摘要进行签名。Bob 接收到密文和签名，一方面独立计算密文的摘要，另一方面使用 Alice 的公钥解密签名中的摘要；如果两个摘要相同，他可以确信对称加密密钥没有被篡改且通过了身份验证。Bob 使用私钥解密密文得到对称加密密钥，接着使用该密钥及 HMAC 与 Alice 进行保密通信，这样每一条消息的完整性都得到保障。但该体系没有办法抵御消息重放攻击（我们在 Eve 造成的冰淇淋灾难中见过这种攻击）。要解决重放攻击，我们需要使用某种类型的“握手handshake”建立随机、短期的会话标识符session identifier。密码学的世界博大精深，我希望这篇文章能让你对密码学的核心目标及其组件有一个大致的了解。这些概念为你打下坚实的基础，让你可以继续深入学习。感谢 Hubert Kario、Florian Weimer 和 Mike Bursell 在本文写作过程中提供的帮助。