深入剖析 Git 对象底层原理

一、引言

在我们日常使用 Git 时,通常的操作是:

  • 在写完一段代码后,执行 git add命令,将这段代码添加到暂存区中
  • 然后再执行 git commitgit push 命令,将 本地 Git 版本库中的提交同步到服务器中的版本库中

Git 在中间做了什么,它如何存储不同的文件和内容,以及如何区分不同分支下的文件版本呢?日常操作对这些自动的操作都是无感的。 但是如果哪天一旦上述操作中出现了错误,需要找回自己的代码时,如果不懂 Git 其内部存储原理,是没法找回的,因此为了避免这种情况,就有必要去了解其内部的存储------Git 对象的原理。

二、Git 对象

2.1. Git 对象概述

我们知道,Git 是一个内容寻址文件系统,其核心部分是一个键值对数据库。 当我们向 Git 仓库中插入任意类型的内容时,它会返回一个唯一的键。我们可以通过该键在任意时刻再次取回插入的内容。 比如我们初始化 GitDemo ,发现 Git 对 objects 目录进行了初始化,并创建了 pack 和 info 子目录,但均为空:

shell 复制代码
$ git init GitDemo
Initialized empty Git repository in D:/GitDemo/.git/
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

然后创建一个 readme.txt 文本,执行 git add后会发现在 .git/objects 中新增了一个文件夹 89 和文件 dab47ae90ebdfee4e6cb3d64708cd73e9c5472

shell 复制代码
$ echo 'read me please' > readme.txt
$ git add readme.txt
$ find .git/objects -type f
.git/objects/89/dab47ae90ebdfee4e6cb3d64708cd73e9c5472

查看其文件内容,类型和大小:

shell 复制代码
$ git cat-file -p 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
read me please
$ git cat-file -t 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
blob
$ git cat-file -s 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
15

这个键值为 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472的对象就是 Git 对象中的 blob 对象。而且 Git 中所有的对象都存储在 .git/objects目录(也叫做对象库)中。 这个键值是一个 SHA-1 的哈希值,由 40 个十六进制的数字组成。它是通过一个将待存储的数据外加一个头部信息一起做 SHA 算法运算而得到的校验和。40 个十六进制数字就相当于 160 比特,当用 SHA-1 对不同对象进行区分和识别时,冲突的概率就会极低,不用存储文件的具体类型,用 blob 和 SHA-1 就足以分辨不同文件内容了。 下面来看看 Git 对象的类型:

2.2. Git 对象类型

2.2.1. Blob 对象

1. Blob 对象的定义和作用

Blob(Binary Large Object,二进制大对象)是Git中的一种对象类型,用来指代某些可以包含任意数据的变量或文件。它是Git对文件内容的一种抽象表示。每个文件在Git仓库中都被表示为一个独立的Blob对象。Blob对象保存了文件的原始二进制数据,无论文件是文本文件还是二进制文件,Git都以Blob对象的形式存储它们。 比如在上一节中的 readme.txt 文本,在 Git 中就是以 blob 对象存储的:

shell 复制代码
$ git cat-file -t 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
blob
$ git cat-file -p 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
read me please
  • **当在Git仓库中添加、修改或删除某个文件时,Git会创建一个新的Blob对象来存储这个文件的内容。**这样就可以跟踪文件的变化历史,并且可以在需要时恢复到特定的文件版本。

比如我们修改 readme.txt 文本,会发现有两个 blob 对象存储 readme.txt 的两个版本:

shell 复制代码
//新增一行文本: reading
$ vi readme.txt
$ git add readme.txt
//原来版本的readme.txt内容还存在:
$ git cat-file -p 89dab47ae90ebdfee4e6cb3d64708cd73e9c5472
//新版本的readme.txt内容
$ git cat-file -p b0530c9
read me please
reading
  • 因为在修改内容后创建了新的 Blob 对象,因此 Git 可以使用 Blob 对象来进行文件比较操作。通过比较两个Blob对象的哈希值,Git可以快速确定文件内容是否发生了变化,从而进行版本控制和合并操作。

2. Blob 对象的存储方式

Blob对象在Git中的存储方式是使用对象哈希值来进行索引和存储。具体的存储方式如下:

  1. 当在 Git 仓库中添加文件并执行 git add时,Git 就会提取该内容,然后将内容进行 SHA-1 哈希计算,得到一个40个十六进制字符的哈希值。这个哈希值就是Blob对象的唯一标识符,也就是我们上节提到的键。
  2. 而后如果Blob对象是新的,Git会将它以哈希值(上面由 SHA-1 哈希计算得到的标识符)为文件名存储在对象数据库中(也就是 .git/objects 目录下)。
  3. 存储时,Git将Blob对象的内容写入一个临时文件,并将该文件的路径与哈希值相关联。(一般来说取前两位作为文件目录,剩下的 38 位作为文件名。比如 readme.txt 文件:目录是 89,文件名为 dab47ae90ebdfee4e6cb3d64708cd73e9c5472。)
  4. 此外如果 blob 对象过大,Git会对存储的Blob对象进行压缩,并将压缩后的数据写入真正的对象文件中。这些压缩的文件存储在 .git/objects/pack

2.2.2. Tree 对象

1. Tree 对象的定义和作用

Tree 对象是Git中的一种对象类型,用于表示文件和目录的组织结构。每当向Git仓库中添加一个目录时,Git会创建一个新的Tree对象来表示该目录的结构。Tree对象包含了目录中的文件和子目录的元数据,以及它们对应的Blob或Tree对象的哈希值。 比如我们接着在 GitDemo 仓库中添加目录 lib和文件 readme2.txt 并提交后,当前目录为:

shell 复制代码
│  readme.txt
│
└─lib
      readme2.txt

在 git 中的存储如下:

shell 复制代码
$ git cat-file -p master^{tree}
040000 tree dbff68a947c7cc60653ff64260b372a405939ae2    lib
100644 blob b0530c9b7360a8cea0e4af86475cac70a2985138    readme.txt

master^{tree} 语法表示 master 分支上最新的提交所指向的树对象。lib 子目录(所对应的那条树对象记录)并不是一个数据对象,而是一个指针,其指向的是另一个树对象:

shell 复制代码
$ git cat-file -p dbff68a947c7cc
//模式  对象类型   对象的SHA-1值                          文件名
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    readme2.txt

从上面可以发现,一个 Tree 对象包含一条或多条树对象记录。每条记录含有一个指向数据对象或者子目录对象的 SHA-1 指针,以及相应的模式、类型、对象名(SHA-1 值)、文件名信息:

  1. 模式(Mode):模式表示文件或目录的类型和权限。它是一个八进制数字,通常以4位表示。常见的模式包括:
  • 100644:表示普通文件,即Blob对象。
  • 100755:表示可执行文件,即Blob对象。
  • 040000:表示目录,即Tree对象。
  1. 类型(Type):类型表示列表项所指向的对象类型,即是Blob对象还是Tree对象。
  2. 对象名(Object Name):对象名是对应的Blob或Tree对象的哈希值。它是一个40个十六进制字符的字符串,用于唯一标识对象。
  3. 文件名或目录名(File/Directory Name):文件名或目录名是列表项所代表的文件或目录的名称。

如果记录中的类型为 Blob,表示该项是一个文件;如果该类型为 Tree,表示该项是一个子目录。

2. Tree 对象的存储方式

和 blob 对象的存储方式类似,Tree对象在Git中的存储方式是使用对象哈希值来进行索引和存储。当执行 git add 时 Git 内部的操作有:

  1. 构建树对象:当你在Git仓库中添加、修改或删除文件时,Git会根据当前目录结构构建一个Tree对象。该Tree对象包含了目录中的文件和子目录的元数据,以及它们对应的Blob或Tree对象的哈希值。
  2. 哈希计算:Git会对Tree对象的内容进行哈希计算,生成一个40个十六进制字符的哈希值。这个哈希值就是Tree对象的唯一标识符。
  3. 检查存储:Git会检查对象数据库中是否已存在具有相同哈希值的Tree对象。如果存在,则直接引用已有的对象;如果不存在,则进行存储。
  4. 存储对象:如果Tree对象是新的,Git会将它以哈希值为文件名存储在对象数据库中。对象数据库通常位于".git/objects"目录下。存储时,Git将Tree对象的内容以特定的格式写入一个临时文件,并将该文件的路径与哈希值相关联。
  5. 压缩和索引:Git会对存储的Tree对象进行压缩,并将压缩后的数据写入真正的对象文件中。同时,Git会更新索引文件,将Tree对象的哈希值与文件路径进行映射。

2.2.3. Commit 对象

1. Commit 对象的定义和作用

Commit对象是Git中的一种对象类型,用于记录代码仓库中的提交操作(也就是执行 git commit命令)。每个Commit对象代表一个特定的提交操作,包含了提交的元数据和指向代码快照的引用。通过Commit对象,Git能够跟踪代码修改的历史,并实现版本控制和代码回溯等功能。 如下图所示,每个 Commit 对象就是一个 version 版本,Commit 对象通过指向代码快照(也就是一个 Tree 对象)的引用,记录了代码仓库在某个特定时间点的状态。 我们再次引用上面 Tree 对象中的 GitDemo 仓库案例,此时仓库中有如下文件和目录:

shell 复制代码
│  readme.txt
│
└─lib
      readme2.txt

可以通过 git cat-file master命令查看此时的 commit 对象:

shell 复制代码
//master就是一个指向commit对象的指针, 其内部存储Commit对象的SHA-1值
$ cat .git/refs/heads/master
2b2af66549827bd6a466fe43081f406c2a12900b
$ git cat-file -p 2b2af665498
tree 2503e9e0c4f774fc5ce298f4972f0e6d3a800d6f
parent 7b34a1e750918570ed610ee1f228e83b43a1192e
author wangJw <wangJw@163.com> 1705458723 +0800
committer wangJw <wangJw@163.com> 1705458723 +0800

second commit

从上面可知,一个 commit 对象由这样几个部分组成:

  1. 指向代码快照的引用(tree):Commit对象包含一个指向代码快照的引用,通常是指向一个Tree对象。该Tree对象记录了提交时代码仓库中文件和目录的状态。
  2. 父提交(parent):Commit对象可以有一个或多个父提交,指向前一个或多个Commit对象。这构成了提交历史的链式结构。通常,一个Commit对象的父提交是它之前的一个Commit对象,除非进行了分支合并等操作。
  3. 作者(author):Commit对象记录了提交的作者信息,包括姓名和电子邮件。作者是指实际进行代码更改的人。
  4. 提交者(Committer):Commit对象还包含了提交者信息,通常与作者相同。提交者是指将更改提交到代码仓库的人。
  5. 提交消息(Commit Message):也就是 second commit 中的内容, Commit对象包含了提交时附加的可选消息,用于描述提交的目的、更改的内容、修复的问题等。提交消息可以提供其他开发者和团队成员了解提交的背景和目的。
  6. Commit对象的哈希值(SHA-1 值):每个Commit对象都有一个唯一的哈希值,用于标识该对象。

加上 commit 对象和 master 指针,可以完善在 tree 对象中的图:

2. Commit 对象的存储方式

当执行 git commit命令提交代码时,commit 对象随之创建,Git 的内部操作有:

  1. 内容提取:执行git commit命令提交代码时,Git会提取提交的相关信息,包括作者、提交者、提交时间、提交消息和父提交等。
  2. 创建对象:Git会将提交信息和父提交的引用等数据组合成一个Commit对象。
  3. 哈希计算:Git会对Commit对象的内容进行SHA-1哈希计算,得到一个40个十六进制字符的哈希值。这个哈希值就是Commit对象的唯一标识符。
  4. 检查存储:Git会检查对象数据库中是否已存在具有相同哈希值的Commit对象。如果存在,则直接引用已有的对象;如果不存在,则进行存储。
  5. 存储对象:如果Commit对象是新的,Git会将它以哈希值为文件名存储在对象数据库中。对象数据库通常位于 .git/objects 目录下。存储时,Git将Commit对象的内容写入一个临时文件,并将该文件的路径与哈希值相关联。
  6. 压缩和索引:Git会对存储的Commit对象进行压缩,并将压缩后的数据写入真正的对象文件中。同时,Git会更新索引文件,将Commit对象的哈希值与文件路径进行映射。

2.2.4. Tag 对象

1. Tag 对象的定义和作用

Tag对象是Git中的一种对象类型,用于给特定的提交打上标记。Tag对象的主要作用是标记代码仓库中的特定提交或里程碑。它可以用于记录发布版本、重要的里程碑、稳定的代码快照等。git 标签分为两种类型:轻量标签和附注标签。

轻量标签

轻量标签是指向提交对象的引用:

shell 复制代码
//创建轻量级标签
$ git tag firstTag
//查看标签:
$ git tag
firstTag

当创建了 firstTag 后,会在.git/refs/tags 目录下创建一个名为 firstTag 的文件,其内容指向当前的 commit 对象的 SHA-1 值

shell 复制代码
$ cat .git/refs/tags/firstTag
2b2af66549827bd6a466fe43081f406c2a12900b
//轻量标签指向提交对象的引用
$ git cat-file -t firstTag
commit
$ git cat-file -p firstTag
tree 2503e9e0c4f774fc5ce298f4972f0e6d3a800d6f
parent 7b34a1e750918570ed610ee1f228e83b43a1192e
author wangJw <wangJw@163.com> 1705458723 +0800
committer wangJw <wangJw@163.com> 1705458723 +0800

second commit

我们发现,轻量标签 firstTag 中的内容只有一个指向提交对象的 SHA-1 值。没有其他内容,因此无法知道何人,什么时间创建的标签。在团队开发中很容易发生混淆,因此可以用另外一种打标签的方式:附注标签

附注标签

附注标签则是仓库中的一个独立对象,使用带参数 -a-m <msg>git tag 命令:

shell 复制代码
//创建一个空提交:
$ git commit --allow-empty -m "empty commit for tagTest"
[master 8a4678f] empty commit for tagTest
//创建一个附注标签
$ git tag -m "secondTag" secondTag
//查看所有标签
$ git tag
firstTag
secondTag

这个时候再来看看 .git/refs/tags中的 secondTag 标签内容:

shell 复制代码
//查看该标签的类型
$ git cat-file -t secondTag
tag
//再来看看secondTag标签的内容
$ git cat-file -p secondTag
object 8a4678fae181c16c6f4ff0e6a618991128d86da2
type commit
tag secondTag
tagger wangJw <wangJw@163.com> 1705480524 +0800

secondTag

主要由这样几个部分组成:

  1. 标签指向的提交对象(object):附注标签对象中包含一个指向特定提交的引用。此处 object 中的值为我上面的提交对象的 SHA-1 值
  2. 标签指向对象类型(type):指向的提交对象的类型
  3. 标签名称(tag):附注标签对象包含一个唯一的标签名称,用于标识和引用该标签。
  4. 标签作者(tagger):附注标签对象记录了标签的作者信息,包括姓名和电子邮件地址。作者是指创建该标签的人。
  5. 标签消息(Tag Message):其中的 "secondTag"内容,附注标签对象包含一个可选的标签消息,用于描述标签的目的、里程碑或其他相关信息。标签消息可以提供其他开发者和团队成员了解标签的背景和用途。

我们再来看看 Tag 对象的存储方式

2. Tag 对象的存储方式

当执行带有 -a-m <msg>git tag命令时,Git 就会由如下操作:

  1. 创建对象:当你执行创建标签的操作(如git tag命令)时,Git会创建一个Tag对象。
  2. 内容提取:Tag对象包含标签的名称、类型、标签指向的提交、标签作者、标签创建时间、标签消息等信息。
  3. 哈希计算:Git会对Tag对象的内容进行SHA-1哈希计算,得到一个40个十六进制字符的哈希值。这个哈希值就是Tag对象的唯一标识符。
  4. 检查存储:Git会检查对象数据库中是否已存在具有相同哈希值的Tag对象。如果存在,则直接引用已有的对象;如果不存在,则进行存储。
  5. 存储对象:如果Tag对象是新的,Git会将它以哈希值为文件名存储在对象数据库中。对象数据库通常位于 .git/objects 目录下。存储时,Git将Tag对象的内容写入一个临时文件,并将该文件的路径与哈希值相关联。
  6. 压缩和索引:Git会对存储的Tag对象进行压缩,并将压缩后的数据写入真正的对象文件中。同时,Git会更新索引文件,将Tag对象的哈希值与文件路径进行映射。

三. Git 对象的存储

3.1 SHA-1 算法如何生成哈希值

SHA-1(Secure Hash Algorithm 1)是一种用于生成哈希值的加密算法。该算法将任意长度的输入经过散列运算转换为固定长度的输出。这个固定长度的输出就叫做对应输入内容的数字摘要或者哈希值。 那么对于 Git 对象中的 SHA-1 哈希值是如何生成的? 在 《Pro Git 2nd》这本书提到,SHA-1 哈希值是通过将待存储的数据+一个头部信息(header)一起做 SHA-1 校验运算而得到的。 而在头部信息由这些部分组成:

  • 对象类型字符串,比如"blob", "tree", "commit", "tag"
  • 空格
  • 数组内容的字节数
  • 空字节(null byte)

Git 会将上述的头部信息和文件原始数据拼接,来计算出 SHA-1 校验和。在 Linux 中有 sha1sum 命令可以生成 SHA1 哈希值,下面来验证一下我们生成的 SHA1 哈希值和 Git 是不是相同的:

shell 复制代码
//当前目录结构
│  a.txt
│
└─b
     c.txt

3.1.1. blob 对象的 SHA1 哈希值

先来看看 blob 对象,也就是 a.txt 对应的文件内容的 SHA1 哈希值生成过程,

shell 复制代码
//a.txt中的内容为:
$ cat a.txt
123
//字符数为3
$ git cat-file blob HEAD:a.txt | wc -c
3

其头部信息为 blob 3\000 在文件内容上加上头部信息,然后对新文件内容执行 SHA-1 哈希算法:

shell 复制代码
$ (printf "blob 3\000"; git cat-file blob HEAD:a.txt) | sha1sum
d800886d9c86731ae5c4a62b0b77c437015e00d2 *-

查看在 Git 仓库中是否找到该 SHA-1 值对应的 blob 对象

shell 复制代码
$ git cat-file -p d80088
123

说明执行 sha1 算法和 Git 操作算法得到的结果一致,验证了 Git 中 SHA-1 哈希值的生成过程

3.1.2. commit 对象的 SHA1 哈希值

此时在提交链最末端的 commit 对象内容是:

shell 复制代码
$ git cat-file commit master
tree 46bda27c4834d428a388841808fdaa7ca15a7bc1
parent 61b04b17412e1d9639db2a6b1b4e83319473a14a
author wangJW <1w@163.com> 1679818066 +0800
committer wangJW <1w@163.com> 1679818066 +0800

second commit

根据头部信息的组成,需要知道 commit 中的字符数:

shell 复制代码
$ git cat-file commit HEAD | wc -c
218

然后加上空格以及空字符串:commit 218\000,然后与 commit 对象内容拼接,将拼接后的内容计算 SHA1 校验和:

shell 复制代码
$ (printf "commit 218\000"; git cat-file commit HEAD) | sha1sum
2514fb61430ad5beea4f80e2548f1fbdfd97d74d *-

再来看看 HEAD 文件中对应的 Commit 对象以及其内容是不是与上面的 SHA1 相符:

shell 复制代码
$ cat .git/HEAD
2514fb61430ad5beea4f80e2548f1fbdfd97d74d
$ git cat-file -p 2514fb6
tree b79d07773ea2d47125f1e7078bbc8113a74a2fa7
parent 61b04b17412e1d9639db2a6b1b4e83319473a14a
author wangJW <1w@163.com> 1705493204 +0800
committer wangJW <1w@163.com> 1705493204 +0800

second commit

从结果可知,说明 Git 内部就是采用头部信息+内容利用 SHA1 算法得到的哈希值。 再来看看 tree 对象

3.1.3 tree 对象的 SHA1 哈希值

直接拿上面 commit 对象中的 tree 对象来做实验,首先查看 tree 对象中的内容和其中的字节数

shell 复制代码
$ git cat-file -p b79d0777
100644 blob d800886d9c86731ae5c4a62b0b77c437015e00d2    a.txt
040000 tree ceb3bfbba0a2f151a88628549113aa5c1be65bf5    b
//此时就是对应HEAD指针指向的树
$ git cat-file tree HEAD^{tree} | wc -c
61

然后根据头部信息+tree 对象内容信息计算 SHA-1 值:

shell 复制代码
$ (printf "tree 61\000"; git cat-file tree HEAD^{tree}) | sha1sum
b79d07773ea2d47125f1e7078bbc8113a74a2fa7 *-

发现此时计算出的 SHA-1 值和 commit 对象所指向的值完全相同,再次验证 SHA-1 生成方式。最后再来看看 tag 对象

3.1.4 tag 对象的 SHA1 哈希值

首先创建一个 tag 对象:

shell 复制代码
$ git tag -m "firstTag" firstTag
//创建成功
$ git tag
firstTag

获取这个 tag 对象的字节数,并执行 SHA1 哈希算法

shell 复制代码
$ git cat-file tag firstTag | wc -c
136

$ (printf "tag 136\000"; git cat-file tag firstTag) | sha1sum
d0c8f7e57f23b368152094bf3e57e70b3569cb13 *-

从 tag 对象的执行结果说明,SHA1 哈希值生成方式正确。

3.2 Git 对象的存储位置

从前面查看 blob 对象内容时提到过,在 Git 中的对象存储在 Git 仓库的 .git/objects 目录下。 在下列情况中,会触发 Git 存储对象的操作:

  1. git add:在执行 git add 命令暂存某个文件 时,Git 将会将文件的内容转换为一个 Blob 对象,并将该对象存储在本地对象数据库中。这个操作将文件添加到暂存区(Staging Area),为接下来的提交做准备。
  2. git commit:执行 git commit 命令时,Git 首先会创建一个新的 Commit 对象。这个 Commit 对象包含了提交的元数据信息,如作者、提交时间、提交信息等。同时,Git 会创建一个对应的根目录的 Tree 对象,记录了当前提交时仓库中所有文件的快照。最后,Git 将这个 Commit 对象存储在本地对象数据库中,并将当前分支指向该 Commit 对象,表示当前的工作状态。
  3. git tag:执行创建附注标注命令时,Git 会创建一个 Tag 对象,该对象包含标签的元数据信息,并指向一个特定的 Commit 对象。这个 Tag 对象会被存储在本地对象数据库中,以便后续引用。
  4. git merge:执行 git merge 命令时,Git 会创建一个新的 Commit 对象,该对象包含合并的元数据信息,并引用两个或多个合并的分支的 Commit 对象。Git 会将这个新的 Commit 对象存储在本地对象数据库中,并将当前分支指向该新的 Commit 对象。

四、 总结

本文通过 .git 目录角度解析 Git对象

  1. Git 对象主要有以下四种类型:Blob存储文件内容,Tree记录文件结构,Commit记录历史,Tag添加标签。
  2. Git 通过提取对象内容加头信息,使用 SHA-1 算法生成哈希值作为唯一 ID
  3. Git 对象存储于 .git/objects目录下,其中对象 ID 值前两位作为目录名,后 38 位作为文件名
  4. 在在执行暂存(add)、提交(commit)、合并(merge)、打标签(tag)等操作时都会触发 Git 对象的存储

参考资料

《Git 权威指南》

《Git Pro》

相关推荐
GraduationDesign7 分钟前
基于SpringBoot的蜗牛兼职网的设计与实现
java·spring boot·后端
颜淡慕潇17 分钟前
【K8S问题系列 | 20 】K8S如何删除异常对象(Pod、Namespace、PV、PVC)
后端·云原生·容器·kubernetes
customer0821 分钟前
【开源免费】基于SpringBoot+Vue.JS安康旅游网站(JAVA毕业设计)
java·vue.js·spring boot·后端·kafka·开源·旅游
程序猿online41 分钟前
nvm安装使用,控制node版本
开发语言·前端·学习
web Rookie1 小时前
React 中 createContext 和 useContext 的深度应用与优化实战
前端·javascript·react.js
男孩121 小时前
react高阶组件及hooks
前端·javascript·react.js
m0_748251721 小时前
DataOps驱动数据集成创新:Apache DolphinScheduler & SeaTunnel on Amazon Web Services
前端·apache
珊珊来吃1 小时前
EXCEL中给某一列数据加上双引号
java·前端·excel
搬码后生仔2 小时前
将 ASP.NET Core 应用程序的日志保存到 D 盘的文件中 (如 Serilog)
后端·asp.net
Suwg2092 小时前
《手写Mybatis渐进式源码实践》实践笔记(第七章 SQL执行器的创建和使用)
java·数据库·笔记·后端·sql·mybatis·模板方法模式