0%

谷歌学术

  • 站在巨人的肩膀上 & 纯粹的学术网站
  • 摘要甚至全文检索 —> No.1
  • 邮件订阅领域作者 —> 获取作者 / 文献引用最新进展
  • BibTeX 引用文献导出 —> LaTeX 参考文献数据库

检索词对比:知网 | Google 学术搜索

1
2
3
4
5
6
7
8
9
10
11
Photovoltaic module recycling
End-of-life
Silicon PV modules
Physical Recovery
Low temperatures
Aluminum back field

青海省科技厅应用基础研究项目(2022-ZJ-768)
西宁市科技计划项目(2021-Y-01)

一种微晶多孔泡沫玻璃管的制备方法

知网补充:

  • 基金号检索 —> 相关主题
  • 专利检索 —> 开启搜索开关

谷歌专利

  • Google Patents:Search and read the full text of patents from around the world
  • 专利免费下载,PDF 格式
  • 数据统计功能

image.png

1
2
泡沫玻璃 制备
光伏组件回收

文献管理

Zotero:Zotero | Your personal research assistant

示意图

Figma / Sketch

Visio

LaTeX

拓展资料

参考资料

Obsidian_to_Anki 插件教程

对 Anki 进行设置:工具 —> 插件 —> 设置 AnkiConnect 插件

Anki 插件设置

1
2
3
4
5
6
7
8
9
10
11
{
"apiKey": null,
"apiLogPath": null,
"webBindAddress": "127.0.0.1",
"webBindPort": 8765,
"webCorsOrigin": "http://localhost",
"webCorsOriginList": [
"http://localhost",
+ "app://obsidian.md"
]
}

修改完之后如下:

1
2
3
4
5
6
7
8
9
10
11
{
"apiKey": null,
"apiLogPath": null,
"webBindAddress": "127.0.0.1",
"webBindPort": 8765,
"webCorsOrigin": "http://localhost",
"webCorsOriginList": [
"http://localhost",
"app://obsidian.md"
]
}

Ob 笔记设置

安装 Obsidian 插件:Export to Anki

笔记开头声明牌组:TARGET DECK: Test

普通卡 Neuracache

正则表达式:

1
((?:[^\n][\n]?)+) #flashcard ?\n*((?:\n(?:^.{1,3}$|^.{4}(?<!<!--).*))+)

参考资料:Neuracache flashcard style · Pseudonium/Obsidian_to_Anki Wiki · GitHub

卡片语法示例

1
2
3
4
TARGET DECK: Test

卡片正面内容 #flashcard
卡片背面内容

如下,若添加进 Anki 成功的话,会自动在卡片的下方添加上一个 html 注释

1
2
3
4
5
TARGET DECK: Test

卡片正面内容 #flashcard
卡片背面内容
<!--ID: 1691487007086-->

如果想要清除 Anki 中的卡片,在 ID 上一行添加 DELETE

1
2
3
这是卡片正面 #flashcard
this is the back of the card

  • DELETE 需要大写,同步之后,Anki 中对应的卡片就被自动删除了,Ob 中的笔记还在,只是将这条笔记下方的 ID 被删除了(也就是说下次同步时,还会自动往 Anki 中添加这条笔记)
  • 如果 Anki 中删除了笔记,而 Ob 中没有删除的话,那么同步 Ob 时,也不会往 Anki 中添加卡片了,因为这条笔记下面的 ID 还没有被删除

挖空卡 Cloze

正则表达式:

1
((?:.+\n)*(?:.*{.*)(?:\n(?:^.{1,3}$|^.{4}(?<!<!--).*))*)

参考资料:Cloze Paragraph style · Pseudonium/Obsidian_to_Anki Wiki · GitHub

注意:将正则表达式粘贴到 Cloze 类型的模板当中

高亮挖空卡

正则表达式:

1
((?:.+\n)*(?:.*==.*)(?:\n(?:^.{1,3}$|^.{4}(?<!<!--).*))*)

参考资料:Cloze Paragraph style · Pseudonium/Obsidian_to_Anki Wiki · GitHub

1
this is the ==highlight== note

注意:如果是需要开启 Highlight-cloze style,那么需要同时打开 CurlyCloze 和 CurlyCloze - Highlights to Cloze 两关开关

需求

输出一份 PDF 文件,左侧为英文歌词,右侧为中文歌词翻译,尽量保持排版美观。文末附中英文歌词

实现

代码中较难理解的部分已添加注释

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
\documentclass[12pt]{ctexart}
\usepackage{geometry}
\usepackage{multicol}
\usepackage{fontspec}
\usepackage{eso-pic}
\usepackage{tikz}
\usepackage{paracol}
\usepackage{fancyhdr}
\usepackage{hyperref}

% 设置页面大小和边距
\geometry{a4paper,left=1.5cm,right=1.5cm,top=2cm,bottom=2cm}

% 设置背景为网格
\AddToShipoutPictureBG{%
\begin{tikzpicture}[overlay,remember picture]
\draw[step=5mm, color=gray!10, line width=0.2mm] (current page.south west) grid (current page.north east);
\end{tikzpicture}%
}

% 设置页眉和页脚样式
\pagestyle{fancy}
\fancyhf{}
\renewcommand{\headrulewidth}{0pt}
\cfoot{\thepage}
\rfoot{\href{https://coffeelize.top/}{Design By Coffeelize}}

% 设置字体
\setmainfont{Times New Roman}
\newfontfamily\zhfont{Source Han Serif CN}

% 取消段落首行缩进
\setlength{\parindent}{0pt}

% 设置列间距和列宽比例
\setlength{\columnsep}{0.5cm}
\columnratio{0.6}

\begin{document}

\section*{The Wandering Star}

\hspace{1em}

\begin{paracol}{2}

\begin{leftcolumn*}
Soothsayer's lured to her star \\
Server of heaven's silent wanderers \\
Oh holy night, summon up the ghost \\
The hard of hearing always listen closest \\
The hard of hearing always listen closest

\hspace{2em}

Lo lo lo lo lo, lo lo and behold \\
It has been foretold lo lo lo lo long ago \\
Lo lo lo lo lo, lo lo and behold \\
I guess you never know what you never know

\hspace{2em}

Saturn, Jupiter confer, breaker of the wall in the morning \\
Aries, Pisces, either one \\
You will never know firsthand what day you're born on

\hspace{2em}

Lo lo lo lo lo, lo lo and behold \\
It has been foretold lo lo lo lo long ago \\
Lo lo lo lo lo, lo lo and behold \\
I guess you never know what you never know

\hspace{2em}

Wandering star\\
Wandering star\\
Wandering star\\
Wandering star

\end{leftcolumn*}

\switchcolumn

% 右侧中文
\zhfont

被诱至她的星辰的预言家\\
天堂的寂静漫游者的仆人\\
哦,神圣的夜晚,召唤幽灵\\
听力不佳的人总是最专注地倾听\\
听力不佳的人总是最专注地倾听

\hspace{2em}

啦啦啦啦啦,啦啦看哪\\
这早已被预言啦啦啦啦很久以前\\
啦啦啦啦啦,啦啦看哪\\
我猜你永远不会知道你从未知道的事情

\hspace{2em}

土星,木星交谈,清晨的墙壁破碎者\\
白羊座,双鱼座,任何一个\\
你永远不会亲自知道你是在哪一天出生的

\hspace{2em}

啦啦啦啦啦,啦啦看哪\\
这早已被预言啦啦啦啦很久以前\\
啦啦啦啦啦,啦啦看哪\\
我猜你永远不会知道你从未知道的事情

\hspace{2em}

漫游的星星\\
漫游的星星\\
漫游的星星\\
漫游的星星

\end{paracol}

\end{document}

image.png

注:中文翻译来源于 GPT 翻译

The Wandering Star 中英文歌词

这里提供 The Wandering Star 中英文歌词,如果大家有更好的排版方案,可以使用如下的歌词进行排版


Soothsayer’s lured to her star
Server of heaven’s silent wanderers
Oh holy night, summon up the ghost
The hard of hearing always listen closest
The hard of hearing always listen closest

Lo lo lo lo lo, lo lo and behold
It has been foretold lo lo lo lo long ago
Lo lo lo lo lo, lo lo and behold
I guess you never know what you never know

Saturn, Jupiter confer, breaker of the wall in the morning
Aries, Pisces, either one
You will never know firsthand what day you’re born on

Lo lo lo lo lo, lo lo and behold
It has been foretold lo lo lo lo long ago
Lo lo lo lo lo, lo lo and behold
I guess you never know what you never know

Wandering star
Wandering star
Wandering star
Wandering star


被诱至她的星辰的预言家
天堂的寂静漫游者的仆人
哦,神圣的夜晚,召唤幽灵
听力不佳的人总是最专注地倾听
听力不佳的人总是最专注地倾听

啦啦啦啦啦,啦啦看哪
这早已被预言啦啦啦啦很久以前
啦啦啦啦啦,啦啦看哪
我猜你永远不会知道你从未知道的事情

土星,木星交谈,清晨的墙壁破碎者
白羊座,双鱼座,任何一个
你永远不会亲自知道你是在哪一天出生的

啦啦啦啦啦,啦啦看哪
这早已被预言啦啦啦啦很久以前
啦啦啦啦啦,啦啦看哪
我猜你永远不会知道你从未知道的事情

漫游的星星
漫游的星星
漫游的星星
漫游的星星

有用的资料

在线 LaTeX 格式化工具:Online Latex Formatter

最终流程图

  • 简明流程:Win (obsidian) <— 坚果云 —> Mac (obsidian) <— 软链接 —> Hexo
  • 实现功能:Win 的 Obsidian 做笔记,或者 Mac 的 Obsidian 做笔记 —> md 格式的文件 —> 通过 hexo 的 3 条命令上传至博客

image.png

Why Obsidian

博客文章都是使用 Markdown 格式完成的,在 Markdown 编辑体验上,Obsidian 的体验要远远好于 VS Code。毕竟术业有专攻,Obsidian 主打 Markdown 这种垂直领域的文件编辑,而 VS Code 作为通用代码编辑器,不奢求太多体验,大抵能用就行

Obsidian 丰富的插件、轻量和自定义程度高的快捷键这几点已经足以秒杀大多数具有 Markdown 编辑功能的编辑器了

面临的问题

Yaml 字段问题

Hexo 的 URL 使用 abbrlink 永久链接,而这个永久链接是使用 CRC32 算法和十六进制表示法生成的,在 Ob 中自然没有现成的工具可以实现这个功能 —> 那么转换思路,abbrlink 只要唯一就好,完全可以使用时间戳来代替这种复杂的计算

至于时间戳,可以使用 Ob 核心插件中的 模板 插件来完成,创建文件后即可自动添加 abbrlink

image.png

同理,date 和 title 字段也可以使用模板来完成,简易的 YAML 字段如下:

1
2
3
4
5
title: {{NAME}}
categories:
date: {{DATE:YYYY-MM-DD HH:mm:ss}}
abbrlink: {{DATE:YYYYMMDDHHmmss}}
tag:

图片上传问题

习惯使用 SM.MS 这类的图床工具,在 Ob 中插入图片还需要额外手动将文中图片一张张上传到图床中,过程简单但又浪费时间,操作过程也毫无意义 —> 需要一个更加自动化的流程

既然锁定了图床这类工具,必然和 image 有关,思路也很清晰,去 Ob 的插件市场搜索关键词 image 即可

image.png

参考资料:

图片压缩问题

问题也很清晰,在 图片压缩指南 | 智朋的个人博客 中已经介绍过了几款工具,推荐使用 caesium,可这仍然无法接入自动化流程 —> 需要将图片上传至图床前自动压缩图片 —> 也就指向了 Picgo 上传工具

image.png

image.png

注:图床配置名是根据 图床设置 自己设置的,并非一定为 “SM”

参考资料:GitHub - juzisang/picgo-plugin-compress: Image compression plugin for PicGo

经测试,同样一张截图,系统直接保存图片 127KB,通过插件压缩上传后 39KB,压缩效果可以,主要是实现了自动化

实际上,对于非博客中的文章,如自己的一些笔记,当然还是插入本地图片更方便。插件作者在设计上增加了一个控制开关,在 MD 文件的 YAML 区添加 image-auto-upload: true 才实现图片粘贴上传功能 —> 因此,可以利用 QuickAdd + 模板 (配置好 YAML 区) 来实现「只有某一种模板的文件,向其中粘贴图片的时候才会自动上传到图床中」

1
2
3
4
5
6
title: {{NAME}}
categories:
date: {{DATE:YYYY-MM-DD HH:mm:ss}}
abbrlink: {{DATE:YYYYMMDDHHmmss}}
image-auto-upload: true
tag:

文件同步问题

面临的问题:Ob 资源文件夹路径和 hexo 文件夹路径不在一块,如何将 hexo 中包含 markdown 文件的文件夹导入到 Ob 资源库中是一个问题

尝试 1:使用 rsync 命令工具

1
rsync -avzh --delete /Users/wuzhipeng/Documents/ZhPObsidian/ZhPObsidian/_posts  /Users/wuzhipeng/ZhPblog/source

选项说明

  • -a 或 --archive:以归档模式同步文件,包括保留文件权限、时间戳等元数据。
  • -v 或 --verbose:显示详细的输出信息。
  • -z 或 --compress:使用压缩传输来减少数据传输量。
  • -h 或 --human-readable:以易读的格式显示输出信息。
  • -n 或 --dry-run:模拟同步操作,不实际同步文件。
  • -P 或 --progress:显示同步进度。
  • --delete:在目标目录中删除不存在于源文件夹中的文件。
  • --exclude:排除指定的文件或目录。
  • --include:只包含指定的文件或目录。
  • --bwlimit:限制带宽使用。

的确可以同步,但这样做有几个明显的缺点:

  • 单向同步:如果双方都做了修改,那么需要左右来回同步一次,容易出错
  • 同步错误:虽然同步是成功的,但是有报错的出现且暂时无法解决,用起来不放心
1
rsync error: some files could not be transferred (code 23) at /AppleInternal/Library/BuildRoots/c2cb9645-dafc-11ed-aa26-6ec1e3b3f7b3/Library/Caches/com.apple.xbs/Sources/rsync/rsync/main.c(996)

尝试 2:软链接

1
ln -s /Users/wuzhipeng/Documents/ZhPObsidian/ZhPObsidian/_posts  /Users/wuzhipeng/ZhPblog/source

没想到 Ob 支持软链接,这样 hexo 和 Obsidian 访问的就是同一文件目录,不涉及同步问题,这样也很稳定,目前使用的就是这种方案

注意事项:

  • Ob 中的一个文件夹软链接到 Hexo 中,需要首先删除 Hexo 中的对应文件夹,比如以上中的 _posts 文件夹

购买 Obsidian 官方同步之后

仍然是使用软链接方案,删除软链接文件夹,将 Obsidian 本地库的对应文件夹路径粘贴替换一下即可。目前软链接的文件夹有:_postsaboutdownloadnotes

image.png

1
ln -s /Users/wuzhipeng/Documents/SynZhPObsidian/SynZhPObsidan/_posts  /Users/wuzhipeng/ZhPblog/source
1
ln -s /Users/wuzhipeng/Documents/SynZhPObsidian/SynZhPObsidan/download  /Users/wuzhipeng/ZhPblog/source
1
ln -s /Users/wuzhipeng/Documents/SynZhPObsidian/SynZhPObsidan/about  /Users/wuzhipeng/ZhPblog/source
1
ln -s /Users/wuzhipeng/Documents/SynZhPObsidian/SynZhPObsidan/notes  /Users/wuzhipeng/ZhPblog/source

图片排版

两图并排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
\documentclass{ctexart}
\usepackage{coffeelize}
\usepackage{graphicx}
\usepackage{subfigure}

\begin{document}

\begin{figure}[htbp]
\centering
\subfigure[subfig:1]{
\includegraphics[width=0.48\textwidth]{example-image-A}
}
\subfigure[subfig:2]{
\includegraphics[width=0.48\textwidth]{example-image-A}
}
\caption{两张图片并列}
\label{fig:subfigure_example1}
\end{figure}

\end{document}

01-两图并排.png

三图并排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
\documentclass{ctexart}
\usepackage{coffeelize}
\usepackage{graphicx}
\usepackage{subfigure}

\begin{document}

\begin{figure}[htbp]
\centering
\subfigure[subfig:1]{
\includegraphics[width=0.3\textwidth]{example-image-A}
}
\subfigure[subfig:2]{
\includegraphics[width=0.3\textwidth]{example-image-A}
}
\subfigure[subfig:3]{
\includegraphics[width=0.3\textwidth]{example-image-A}
}
\caption{两张图片并列}
\label{fig:subfigure_example1}
\end{figure}

\end{document}

02-三图并排.png

四图并排

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
\documentclass{ctexart}
\usepackage{coffeelize}
\usepackage{graphicx}
\usepackage{subfigure}

\begin{document}

\begin{figure}[htbp]
\centering
\subfigure[subfig:1]{
\includegraphics[width=0.2\textwidth]{example-image-A}
}
\subfigure[subfig:2]{
\includegraphics[width=0.2\textwidth]{example-image-A}
}
\subfigure[subfig:3]{
\includegraphics[width=0.2\textwidth]{example-image-A}
}
\subfigure[subfig:4]{
\includegraphics[width=0.2\textwidth]{example-image-A}
}
\caption{两张图片并列}
\label{fig:subfigure_example1}
\end{figure}

\end{document}

03-四图并排.png

两行四图并排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
\documentclass{ctexart}
\usepackage{coffeelize}
\usepackage{graphicx}
\usepackage{subfigure}

\begin{document}

\begin{figure}[htbp]
\centering
\subfigure[图1]{\includegraphics[width=0.48\linewidth]{example-image-A}}\quad
\subfigure[图2]{\includegraphics[width=0.48\linewidth]{example-image-A}}
\\
\subfigure[图3]{\includegraphics[width=0.48\linewidth]{example-image-A}}\quad
\subfigure[图4]{\includegraphics[width=0.48\linewidth]{example-image-A}}
\caption{两列并排的四张小图}
\label{fig:subfigure_example4}
\end{figure}

\end{document}

04-两行四图并排.png

两行六图并排

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
\documentclass{ctexart}
\usepackage{coffeelize}
\usepackage{graphicx}
\usepackage{subfigure}

\begin{document}

\begin{figure}[htbp]
\centering
\subfigure[图1]{\includegraphics[width=0.3\linewidth]{example-image-A}}\quad
\subfigure[图2]{\includegraphics[width=0.3\linewidth]{example-image-A}}\quad
\subfigure[图3]{\includegraphics[width=0.3\linewidth]{example-image-A}}
\\
\subfigure[图4]{\includegraphics[width=0.3\linewidth]{example-image-A}}\quad
\subfigure[图5]{\includegraphics[width=0.3\linewidth]{example-image-A}}\quad
\subfigure[图6]{\includegraphics[width=0.3\linewidth]{example-image-A}}
\caption{两列并排的六张小图}
\label{fig:subfigure_example4}
\end{figure}

\end{document}

05-两行六图并排.png

本文转载自:StackExchange 和它的游戏规则 | 与非的零空间,转载前已征得原作者同意

我们都明白一个道理:没有规矩,不成方圆。这句话是在讲规则的重要性,规则定好了,怎么玩也玩不脱,规则没定好或者干脆没有规则,那大家就没法再愉快地玩耍了。问题就在于好的游戏规则不好制定。这很像编程,一个程序只有在合理的逻辑关系下才能跑通,跑起来还不够,如果写代码的时候没有考虑周全,迟早会碰到 bug;当事关人类行为之时,潜在的 bug 就更多了,比如自私、虚荣、懒惰…… 如何能找到一套解决方案,即使不能消除这些 bug,至少也要将之压到最低,是每一个人类社区的管理者都要考虑的问题,这个社区往大了说,可以是一个国家,往小了说,可能仅仅是一家问答网站。

数量与质量

问答类网站是近年来发展势头很猛的一类在线社区,这类网站的崛起是伴随着互联网整体信息量的冗余而愈发显著的。用 亚伦・斯沃茨 的话说,就是互联网的当下问题不是大众能否发出声音,而是发声的源头太多,人们想要从这些庞杂的信息中提取出有用的东西出来是一件极其困难的事情。搜索引擎的出现为找到正确信息提供了有力支持,另一类寻找信息的工具就是问答网站,一问一答,从来都是人类交流的最有效手段,相比搜索引擎查找出来的近似结果,一个精准问题所引出的有针对性的回答势必更有帮助。

目前问答网站已经有很多,国内最大的当属百度知道,可是用过百度知道的人都不免发现,这个网站的回答质量不是一般的良莠不齐,而且普遍水准不高。国内比较靠谱的问答网站可能是近年来兴起的知乎,这个网站上线以来给人的感觉是干货比较多,原创也不少,与国内网络社区普遍的浮躁氛围迥异,因此被寄予厚望。我也是这个网站开放注册后的首批用户之一,可是两年多用下来,我也和网站的其他老用户一样,明显感到知乎的问答质量在逐渐降低。

有人说是因为随着知乎注册用户的增多,大量低水平用户涌入导致。一开始我也觉得是这样,那些问问题不经过大脑思考,回答问题只知道复制粘贴的人,说句不好听的,正如夏洛克所言,「拉低了整条街的智商」。可是当我后来发现了 Stackoverflow 这个网站后,我才明白关键原因并不在于用户水平,国外也有大量的低端用户,同样也是神棍伴民科齐飞,大忽悠与脑残粉横行。而 Stackoverflow 自 2008 年成立以来,始终保持超高问答水准的原因,在于它在上线之时,就制定了一套用心良苦的游戏规则。

制度的力量

Stackoverflow 是一个编程领域的问答社区,在其之上还有一个母站,叫做 StackExchange,管理着几十个大大小小的问答社区(参见 这里),Stackoverflow 是其中成立最早、规模最大的一个子站,其他的所有子站在形式上都与 Stackoverflow 保持一致,它们的主题各异,但社区制度是同一的,是一种基于声誉值(reputation)的激励制度,这里简述如下:

  • 新注册用户只有 1 声誉,此时你除了提问啥也干不了;
  • 当你提出来的问题被人顶起一次,可以获得 5 声誉;
  • 积累够 15 声誉时,你才能去顶别人的问题及回答;
  • 当你的回答被顶一次时,可以获得 10 声誉;
  • 如果你的回答被提问者采纳为答案,则可获得 15 声誉,同时提问者也可获得 2 声誉;
  • 如果你的回答被反对,则声誉 -2,同时反对你的人声誉也跟着 -1;
  • 如果你的问题被反对,则声誉 -2;
  • 如果你的问答被至少六个人标记为垃圾,则一次性减去 100 声誉。

此外还有一些额外的奖惩制度,比如我刚刚使用 Stackoverflow 时,问了一些没有认真准备的问题,结果很快就被人踩了,我心想这不行啊,本来就不多的声誉值再踩几下就没了…… 于是我立即删除了自己的问题,令我没有想到的是,不仅之前被扣的声誉值又返还给我,网站还额外奖励了 2 声誉,奖励的理由是我接纳了本社区的「同行评审」意见,主动删除低质量问题。当时我就惊呆了,「同行评审」这个词我以前仅仅在涉及到学术出版时才会遇见啊。

你可能会问,那么利用它这个漏洞不就可以无限刷声誉了吗,严谨的设计者当然不会犯这样的错误,当我问下一个问题时,网站在最醒目的地方告诫我,由于我问了过多的低水平问题(- -III),这个帐号正处于危险状态,我的下一个问题必须内容翔实有针对性,否则如果再被人反对,将永久失去提问资格。这个警告确实让我认真了起来,我于是拿出写论文的诚意,写了一个既有代码片段又有若干示意图的问题。果然,我的诚意也换来了别人的诚意,这个问题在很短时间内就获得了多人的回复,完美解决了我的疑问,同时也解除了我的帐户危机。

那么,我如果很讨厌某个用户,能否通过不断踩他的问题和答案让他的账户变成危险状态呢,这在其他的网络社区可是屡见不鲜的事情。然而在 StackExchange 则没有可能,它有一整套防止私人攻击的规则,比如你要是反对了某人的答案,则除了扣他 2 声誉,你自己也要扣去 1 声誉,这样你在踩别人的时候,必然会三思而后行;如果你真的对某人怀恨在心,只要是他的问题你就反对,那怎么办?StackExchange 不允许这种情况发生,你根本无法完成这样的操作。甚至不仅是持续反对某人,你也无法持续赞同某人,这样的行为被称为「投票欺诈」(voting fraud),系统会严格禁止此类行为,以保证问答的客观性。

自治、自制

由于 StackExchange 是一个知识型社区,它最关心的必然是网站内容的质量,所以如何规避低端问答一定是网站设计者首要考虑的问题。很多同类社区在发展初期由于比较小众,用户水平较高,即使出现低质量内容,管理员也能及时控制;然而一旦用户增多,内容覆盖范围扩大,这个问题就没法解决了。首先管理员根本来不及控制不断涌现的内容,其次他们也未必有那个水平去做判断。

StackExchange 采用基于声誉值的管理制度则比较完善地解决了这个问题。初级用户不能点赞的设定,表明了网站对普通网民的不信任,这种不信任是合理的,毕竟有那么多曾经优秀的社区都被网络水军们给糟蹋了;你如果仅仅是来寻找答案的,那没问题,网站的一切内容全部公开,甚至做了 SEO,你用 Google 就能搜到 StackExchange 的任何一个问答。但你如果想对 StackExchange 有所贡献,就不那么简单了,你必须证明自己具备这个资格,如何证明?提出好问题。只有好问题才会吸引人来作答,才会被人顶起,才能获得最初的声誉。

StackExchange 对问问题有着严格的规定,比如提问前必须搜索已有问答,避免问重复问题,避免问与社区主题无关的问题,避免问范围模糊的问题;此外问题描述要详实具体,给出例证,要具有可重现性;语言方面还要过关,不能词不达意,也不能啰啰嗦嗦…… 这么多规定简直像是对学术论文的要求,规则制定者一定笃信只有好的问题才能引出好的答案。那么这么严苛的规定不怕把用户吓跑吗?不怕,因为吓跑的本来就不是 StackExchange 的目标用户群,这毕竟是个知识型社区,不是普通的社交网络,用户数量从来都不是它关心的,高质量用户数量才是它关心的。事实上,StackExchange 的游戏规则非但没有吓跑用户,反而正在吸引越来越多的人参与其中。

在我看来,这种用户自治的游戏规则是最高明的管理策略,社区建设者只用把平台搭建好,剩下的工作就交给广大用户,而真正愿意参与这个社区的用户都必然是认真对待的,声誉值越高的用户,责任感也越强,想要获得高的声誉值,就得懂得自制,按着游戏规则玩,慢慢积累。

你可能会奇怪,既然问个问题都如此麻烦,为何我还要在上面花费时间?原因有二:

首先,对于这种业界公认的知识型社区,由于它的干货比重远远超出同类网站,其用户的声誉值便也有了独特的含金量,就好比一个项目被 fork 的次数之于 GitHub,声誉值在这里可以说明一切。你若能在自己的简历中写上 Stackoverflow 声誉值过万,任何一家用人单位都不会视而不见。

其次,并非所有人做什么事都要有所回报,这有点类似去问那些编辑维基百科的志愿者为何要花自己时间去做这种费力不讨好的事情。现在互联网上各种社区层出不穷,质量也是参差不齐,好不容易出现一个用户都那么认真对待的社区,当然倍受欢迎,因为这上面的讨论氛围是无可比拟的,一个问题问出去,可能几分钟内就有人回复,有的是指出你提问的不足之处,有的是给出可能有帮助的参考资料,甚至还有帮你纠正英语语法错误的…… 在这样的社区中,可能要比平时多花几倍时间才能完成一次提问,但你所收获的将更多。

这,才是真正的「人人为我,我为人人。」

参考资料

常用快捷键

  • 标尺:Ctrl + R
  • 反选:Ctrl + Shift + I
  • 参考线:从标尺处拖拉可新建参考线;
  • 显示 / 隐藏参考线:Ctrl + ;
  • 内容填充:框选,右键,填充
  • 复制图层:Ctrl + J
  • 将框选的图层复制到新文档:先框选 –> Ctrl + J –> Ctrl + N –> 从剪切板导入。这样做的目的是框选的图层长宽多大,新文档的长宽就是多大,不会有大量多余的空白部分
  • 按比例裁剪:裁剪前可设置裁剪的长宽比(Visio 中似乎没有这个功能,就只能在 PS 中操作了)
  • 对图形进行变换操作:Ctrl + T
  • 对图形进行连续相同的变换操作:Ctrl + Shift + T
  • 取消选区:Ctrl + D
  • 恢复选区:Ctrl + Shift + D

内部类

内部类的基本使用

内部类概念:在一个类中定义一个类

比如,在一个类 A 的内部定义一个类 B,类 B 就被称为内部类。内部类定义格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
class 外部类名{
修饰符 class 内部类名{

}
}
*/

class Outer {
//此处的Inner就是所说的内部类
public class Inner {

}
}

内部类的访问特点

  • 内部类可以直接访问外部类的成员,包括私有
  • 外部类要访问内部类的成员,必须创建对象

1、示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test1Inner {
public static void main(String[] args) {
//此处编译器报错
Inner i = new Inner();
}
}

class Outer {
private int a = 10;
//内部类
class Inner {
int num = 10;

public void show(){
System.out.println("Inner..show");
}
}
}

以上代码中,为什么这里编译器会报错呢:

1
Inner i = new Inner();

因为类名没有写全 –> 为什么呢?举例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test1Inner {
public static void main(String[] args) {
Inner i = new Inner();
}
}

class Outer {
private int a = 10;
//内部类
class Inner {

}
}

class Outer2{
class Inner{

}
}

此时创建 Inner 对象时,编译器都懵掉了,你到底是想要创建那个类中的 Inner 类 –> 需要我们正确书写格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test1Inner {
public static void main(String[] args) {
//创建内部类的格式
//外部类名.内部类类名 对象名 = new 外部类对象().new 内部类对象;
Outer.Inner i = new Outer().new Inner();
}
}

class Outer {
private int a = 10;
//内部类
class Inner {

}
}

emm,创建内部类为什么格式那么复杂:-)。如下是演示内部类变量、内部类方法的访问方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test1Inner {  
public static void main(String[] args) {
//创建内部类的格式
//外部类名.内部类类名 对象名 = new 外部类对象().new 内部类对象;
Outer.Inner i = new Outer().new Inner();
//访问内部类变量
System.out.println(i.num);
//访问内部类方法
i.show();
}

}

class Outer {
private int a = 10;
//内部类
class Inner {
int num = 10;
public void show(){
System.out.println("Inner...show");
}
}
}

内部类是可以直接使用外部类中的成员变量的,代码如下:

可以看到,调用内部类方法时,可以访问到外部类中的 a 变量(包括私有的成员变量)

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
public class Test1Inner {  
public static void main(String[] args) {
//创建内部类的格式
//外部类名.内部类类名 对象名 = new 外部类对象().new 内部类对象;
Outer.Inner i = new Outer().new Inner();
//访问内部类变量
System.out.println(i.num);
//访问内部类方法
i.show();
}

}

class Outer {
//外部类中私有的成员变量
private int a = 10;
//内部类
class Inner {
int num = 10;
public void show(){
System.out.println("Inner...show");
//内部类可以直接访问外部类的成员变量(包括私有的成员变量)
System.out.println("外部类中的成员变量" + a);
}
}
}

程序输出

1
2
3
10
Inner...show
外部类中的成员变量10

成员内部类

按照内部类在类中定义的位置不同,可以分为如下两种形式

  • 在类的成员位置:成员内部类
  • 在类的局部位置:局部内部类

外界创建成员内部类格式

1
2
//外部类名.内部类名 对象名 = 外部类对象.内部类对象;
Outer.Inner oi = new Outer().new Inner();

成员内部类,也属于(成员),既然是成员就可以被一些修饰符所修饰,比如 private 和 static

私有成员内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test2Innerclass {  
public static void main(String[] args) {
//此处编译错误
Outer.Inner oi = new Outer().new Inner();
}
}

class Outer {
//私有成员内部类
private class Inner {
public void show(){
System.out.println("inner..show");
}
}
}

此处,创建成员内部类对象时编译错误
因为 private 是同一类中可见 –> 也就是说只在 Outer 类中是可见的。那么外界如何创建这个内部类对象呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test2Innerclass {  
public static void main(String[] args) {
Outer outer = new Outer();
//调用外部类中的方法,利用类中的方法来间接访问内部类
outer.method();
}
}

class Outer {
//私有成员内部类
private class Inner {
public void show(){
System.out.println("inner..show");
}
}

public void method(){
//因为和Inner类都属于Outer类,所以可以直接这样创建
Inner i = new Inner();
//访问类中的方法
i.show();
}
}

小结

私有成员内部类访问:在自己所在的外部类中创建对象访问

静态成员内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test3Innerclass {  
public static void main(String[] args) {
// 外部类名.内部类名 对象名 = new 外部类名.内部类名();
Outer.Inner oi = new Outer.Inner();
oi.show();
}
}

class Outer {
//静态成员内部类
static class Inner {
public void show(){
System.out.println("inner..show");
}
}
}
1
2
Outer.Inner oi = new Outer().new Inner(); //错误,这是没有加static修饰符时的方式
Outer.Inner oi = new Outer.Inner(); //正确
  • 因为 Inner 不是 private 修饰的,所以外部可以访问
  • 因为 Inner 是 static 修饰的,可以不用创建类的对象『new Outer ()』来访问,通过类『Outer』直接就可以访问了

对于 Staic 修饰的其实很好调用,比如静态内部类中还有一个静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test3Innerclass {  
public static void main(String[] args) {
// 外部类名.内部类名 对象名 = new 外部类名.内部类名();
Outer.Inner oi = new Outer.Inner();
//类名一路调用即可
Outer.Inner.method();
}
}

class Outer {
//静态成员内部类
static class Inner {
public static void method(){
System.out.println("inner..method");
}
}
}
1
2
//类名一路调用即可,都是Static的,无需创建对象,直接类名调用
Outer.Inner.method();

小结

静态成员内部类访问:

1
外部类名.内部类名 对象名 = new 外部类名.内部类名(); 

静态成员内部类中的静态方法:

1
外部类名.内部类名.方法名();

局部内部类

局部内部类定义位置:局部内部类是在方法中定义的类,所以外界是无法直接访问的,需要在方法内部创建对象并使用

该类可以直接访问外部类的成员,也可以访问方法内地局部变量

1、代码展示:调用局部内部类中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test4Innerclass {  
public static void main(String[] args) {
Outer o = new Outer();
o.method();
}
}

class Outer {

public void method(){

//写在method当中的局部内部类
class Inner {
public void show(){
System.out.println("show...");
}
}

//方法内部可以访问Inner类
Inner inner = new Inner();
inner.show();
}
}

因为局部内部类的访问范围仅仅是方法体中的范围,范围如下图所示

01-局部内部类的访问范围.png

2、代码展示:局部内部类访问不同变量

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
public class Test4Innerclass {
public static void main(String[] args) {
Outer o = new Outer();
o.method();
}
}

class Outer {
// 类中的成员变量
int a = 10;

public void method(){
// 方法中的局部变量
int b = 20;

//写在method当中的局部内部类
class Inner {
public void show(){
System.out.println("show...");
// 可以访问(嵌在里边的可以访问外边的)
System.out.println(a);
// 可以访问方法中的局部变量
System.out.println(b);
}
}

//方法内部可以访问Inner类
Inner inner = new Inner();
inner.show();
}
}

小结

  • 局部内部类,外界是无法直接使用,需要在方法内部创建对象并使用
  • 该类可以直接访问外部类的成员,也可以访问方法内的局部变量
  • 局部内部类我们平时是很少编写的,因为局部内部类实在是太受限了;在看源码的过程中也很少会见到局部内部类,讲他的作用是为后面的匿名内部类打基础。因为匿名内部类属于一种特殊的局部内部类

匿名内部类

匿名内部类本质上说一个特殊的局部内部类(定义在方法内部)
前提:需要存在一个接口或类

格式如下:

1
2
3
4
5
6
7
8
9
10
/*
new 类名或接口名(){
重写方法;
}
*/

new Inter(){
@Override
public void method(){}
}

1、代码演示:正常使用接口中的方法需要几步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test5Innerclass {
public static void main(String[] args) {
InterImpl ii = new InterImpl();
ii.show();
}
}

interface Inner{
void show();
}

class InterImpl implements Inner{
@Override
public void show() {
System.out.println("InterImpl 重写的show方法");
}
}

1)创建实现类,通过 implements 关键字去实现接口
2)重写方法
3)创建实现类对象
4)调用重写后的方法

可以看到,想要使用接口中的方法还是比较复杂的。而如果是通过匿名内部类,就可以化简为 1 步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test5Innerclass {  
public static void main(String[] args) {
- InterImpl ii = new InterImpl();
- ii.show();
//匿名内部类
+ new Inner(){
+ @Override
+ public void show() {
+ System.out.println("匿名内部类中的show方法");
+ }
+ }.show();
}
}

interface Inner{
void show();
}

-class InterImpl implements Inner{
- @Override
- public void show() {
- System.out.println("InterImpl 重写的show方法");
- }
-}

我们重点分析这几行代码

1
2
3
new Inner(){
//『实现接口的类』中需要重写的方法
}.show();

可以认为

02-正常方式访问接口中的方法VS匿名内部类方式.png

匿名内部类的理解:将继承(或实现)、方法重写、创建对象这三步放在了一步都当完成

1
2
new Inner(){}; --> 相当于创建了一个实现了接口的实现类对象
new Inner(){}.show(); --> 实现类对象调用方法

2、代码演示:如果接口中有多个方法,匿名内部类最多只能调用其中的一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test5Innerclass {
public static void main(String[] args) {

//匿名内部类
new Inner(){
@Override
public void show1() {
System.out.println("匿名内部类中的show1方法");
}

@Override
public void show2() {
System.out.println("匿名内部类中的show2方法");
}
//此处只能调用一次方法,就相当于"对象.方法",而不能"对象.方法1.方法2"
}.show1();
}
}

interface Inner{
void show1();
void show2();
}

此时,我就想调用其中的多个方法呢,怎么办

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
public class Test5Innerclass {  
public static void main(String[] args) {

//Inner i 接口的引用;new Inner(){} 实现类对象 --> 父类的引用指向了一个子类的对象(多态)
Inner i = new Inner(){
@Override
public void show1() {
System.out.println("匿名内部类中的show1方法");
}

@Override
public void show2() {
System.out.println("匿名内部类中的show2方法");
}
};

i.show1();
i.show2();
}
}

interface Inner{
void show1();
void show2();
}

案例小结:如果说一个接口当中有多个方法,如果使用匿名内部类的方式将其中的多个方法都进行调用,可以在匿名内部类前面通过一个父类或父接口的引用去接收一下,这样就能以多态的形式将匿名内部类接受过来。通过引用就可以调其中的方法了

匿名内部类在开发中的使用

当方法的形式参数是接口或者抽象类时,可以将匿名内部类作为实际参数进行传递

1、代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestSwimming {  
public static void main(String[] args) {

//调用方法(参数要求为实现类对象,这里我们使用匿名内部类的方式)
goSwimming(new Swimming(){
@Override
public void swim() {
System.out.println("GoGoGo!");
}
});
}

//实际参数为接口的方法
public static void goSwimming(Swimming swimming){
swimming.swim();
}
}

interface Swimming {
void swim();
}

此处蓝色标记的就是创建了一个匿名内部类,将其作为 goSwimming () 方法的参数

03-匿名内部类作为方法参数.png

既然匿名内部类作为方法参数,其格式比较固定,编译器也为我们提供了代码提示:只要输入 new 父接口名,根据提示回车即可自动生成代码段

04-匿名内部类作为方法参数代码快速生成.gif

Lambda 表达式

Lambda 表达式可以认为是对匿名内部类的优化,在了解这一块知识前,请先熟悉匿名内部类

匿名内部类和 Lambda 表达式

1、代码演示:匿名内部类和 Lambda 表达式的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestSwimming {
public static void main(String[] args) {

//1、使用匿名内部类的方式
goSwimming(new Swimming() {
@Override
public void swim() {
System.out.println("铁汁, 我们去游泳吧");
}
});

//2、使用Lambda表达式,此处可以理解为是对匿名内部类的优化
goSwimming(() -> System.out.println("铁汁, 我们去游泳吧"));
}

//使用接口的方法
public static void goSwimming(Swimming swimming) {
swimming.swim();
}
}

interface Swimming {
void swim();
}

小结:Lambda 表达式可以使关注点更加明确,为什么这么说呢,请看一下分析:

1、对于匿名内部类的方式(面向对象思想,以什么形式去做)
1)方法要一个接口,我得给个接口的实现类对象
2)创建匿名内部类对象,重新方法
3)方法要干嘛呢,其实就是打印一句话

而其实,我们想要做的仅仅是打印一句话,也就是想要完成第 3 步,而第 1 步和第 2 步都是附加的操作,是 “不得不” 才这样写的

2、对于 Lambda 表达式(函数式编程思想,更多关注做什么)

Lambda 表达式的标准格式

05-匿名内部类与Lambda表达式.png

组成 Lambda 表达式的三要素:形式参数、箭头、代码块

1
(形式参数) -> {代码块}
  • 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可
  • ->:表示将小括号中的形式参数传到大括号的代码块中进行处理
  • 代码块:是我们具体要做到事情,也就是方法体中的内容

Lambda 表达式的使用前提:

  • 有一个接口(也就表明 Lambda 只能操作接口,不能操作类)
  • 接口中有且仅有一个抽象方法

Lambda 带参数无返回值

1、代码示例:

  • 接口中有且只有一个方法,方法有一个参数,无返回值
  • 采用匿名内部类和 Lambda 表达式分别进行实现
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
public class StringHandlerDemo {  

public static void main(String[] args) {
// 匿名内部类的实现方式
useStringHandler(new StringHandler() {
@Override
public void printMessage(String msg) {
System.out.println("我是匿名内部类" + msg);
}
});

// Lambda实现方式
useStringHandler((String msg)->{System.out.println("我是Lambda表达式" + msg);});
useStringHandler( (msg) -> System.out.println("我是Lambda表达式" + msg));
useStringHandler( msg -> System.out.println("我是Lambda表达式" + msg));
}

public static void useStringHandler(StringHandler stringHandler){
stringHandler.printMessage("coffeelize");
}
}

interface StringHandler {
//带参数无返回值
void printMessage(String msg);
}

Lambda 无参数有返回值

如果 Lambda 所操作的接口中的方法,有返回值,一定要通过 return 语句,否则会出现编译错误

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
public class RandomNumHandlerDemo {  

public static void main(String[] args) {
//匿名内部类实现方式
useRandomNumHandler(new RandomNumHandler() {
@Override
public int getNumber() {
Random r = new Random();
//产生一个1-10的随机数
int num = r.nextInt(10) + 1;
return num;
}
});

//Lambda表达式实现方式
useRandomNumHandler( () -> {
Random r = new Random();
int num = r.nextInt(10) + 1;
// 注意: 如果lambda所操作的接口中的方法, 有返回值, 一定要通过return语句,否则会出现编译错误
return num;
} );
}

public static void useRandomNumHandler(RandomNumHandler randomNumHandler){
int result = randomNumHandler.getNumber();
System.out.println(result);
}
}

interface RandomNumHandler {
//无参数有返回值
int getNumber();
}

Lambda 带参数带返回值

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
public class CalculatorDemo {  

public static void main(String[] args) {
//匿名内部类的实现方式
useCalculator(new Calculator() {
@Override
public int calc(int a, int b) {
return a + b;
}
});

//Lambda表达式实现方式
useCalculator((int a, int b)->{
return a + b;
});

useCalculator( (a,b) ->
a + b
);
}

public static void useCalculator(Calculator calculator){
int result = calculator.calc(10,20);
System.out.println(result);
}
}

interface Calculator {
//带参数,带返回值
int calc(int a, int b);
}

我们可以看到,以上代码中,第二种 Lambda 的书写更加简练,关于省略规则请往下看

1
2
3
4
5
6
7
8
//Lambda表达式实现方式  
useCalculator((int a, int b)->{
return a + b;
});

useCalculator( (a,b) ->
a + b
);

省略规则

  • 参数类型可以省略,但是有多个参数的情况下,不能只省略一个
    • 为什么可以省略呢?因为在接口中的方法中已经定义了参数类型,可以推导出来参数类型,所以可以省略
  • 如果参数有且仅有一个,那么小括号可以省略
  • 如果代码块的语句只有一条,可以省略大括号和分号,甚至是 return
    • 若有返回参数,要省略,大括号、分号和 return 一起省略

初识 Docker

项目部署的问题

微服务虽然具备各种各样的优势,但服务的拆分通用给部署带来了很大的麻烦

  • 分布式系统中,依赖的组件非常多,不同组件之间部署时往往会产生一些冲突
    • 依赖关系复杂,容易出现兼容性问题
  • 在数百上千台服务中重复部署,环境不一定一致,会遇到各种问题
    • 开发、测试、生产环境有差异

Docker 解决依赖兼容问题

Docker 如何解决依赖的兼容问题的呢,采用了两个手段:

  • 将应用的 Libs(函数库)、Deps(依赖)、配置与应用一起 打包
  • 将每个应用放到一个 隔离 容器去运行,避免互相干扰

01-应用放在隔离带容器中运行.png

这样打包好的应用包中,既包含应用本身,也保护应用所需要的 Libs、Deps,无需再操作系统上安装这些,自然就不存在不同应用之间的兼容问题了

Docker 解决操作系统环境差异

虽然解决了不同应用的兼容问题,但是开发、测试等环境会存在差异,操作系统版本也会有差异,怎么解决这些问题呢?

02-操作系统结构.png

  • 计算机硬件:例如 CPU、内存、磁盘等
  • 系统内核(内核与硬件交互,提供操作硬件指令):所有 Linux 发行版的内核都是 Linux,例如 CentOS、Ubuntu、Fedora 等。内核可以与计算机硬件交互,对外提供 内核指令,用于操作计算机硬件
  • 系统应用(系统应用封装内核指令为函数,便于程序员调用):操作系统本身提供的应用、函数库。这些函数库是对内核指令的封装,使用更加方便

Ubuntu 和 CentOS 都是基于 Linux 内核,无非是系统应用不同,提供的函数库有差异。此时,如果将一个 Ubuntu 版本的 MySQL 应用安装到 CentOS 系统,MySQL 在调用 Ubuntu 函数库时,会发现找不到或者不匹配,就会报错了,那 Docker 如何解决不同系统环境问题的呢

  • Docker 将用户程序与所需要调用的系统 (比如 Ubuntu) 函数库一起打包
  • Docker 运行到不同操作系统时,直接基于打包的函数库,借助于操作系统的 Linux 内核来运行

那么就可以认为 Docer 打包好的程序包可以应用在任何 Linux 内核的操作系统上

03-Docker打包好的程序包可以运行在任一Linux内核的系统上.png

总结

Docker 是一个快速交付应用、运行应用的技术,具备下列优势:

  • 可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意 Linux 操作系统
  • 运行时利用沙箱机制形成隔离容器,各个应用互不干扰
  • 启动、移除都可以通过一行命令完成,方便快捷

Docker 架构

镜像和容器

  • 镜像(Image,硬盘中的文件):Docker 将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像
  • 容器(Container,相当于进程):镜像中的应用程序运行后形成的进程就是容器,只是 Docker 会给容器进程做隔离,对外不可见

镜像都是只读的,这样可以防止容器对镜像数据的写入,造成数据污染;如果容器需要写数据,可以从镜像中拷贝一份数据到自己的空间中,在本空间中进行读写操作

04-镜像和容器.png

DockerHub

DockerHub 是一个官方的 Docker 镜像的托管平台。这样的平台称为 Docker Registry,国内也有类似于 DockerHub 的公开服务,比如 网易云镜像服务阿里云镜像库

我们一方面可以将自己的镜像共享到 DockerHub,另一方面也可以从 DockerHub 拉取镜像
05-DockerHub.png

Docker 架构

Docker 是一个 CS 架构的程序,由两部分组成

  • 服务端 (server):Docker 守护进程,负责处理 Docker 指令,管理镜像、容器等
  • 客户端 (client):通过命令或 RestAPI 向 Docker 服务端发送指令。可以在本地或远程向服务端发送指令

06-Docker架构.png

安装 Docker

Docker 分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。这里主要介绍 CentOS 安装 Docker

1.1、卸载(可选)

如果之前安装过旧版本的 Docker,可以使用下面命令卸载

1
2
3
4
5
6
7
8
9
10
11
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine \
docker-ce

\ 表示命令没有结束还需要继续往下读(换行),命令太长可以通过 \ 提高可读性

1.2、安装 yum-utils 工具

1
2
3
yum install -y yum-utils \
device-mapper-persistent-data \
lvm2 --skip-broken

1.3、设置下载的镜像源

1
2
3
4
5
6
7
# 设置docker镜像源
yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo
yum makecache fast

1.4、安装 docker

1
yum install -y docker-ce

至此 docker 安装完毕

启动 docker

Docker 应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦,因此建议大家直接关闭防火墙。启动 docker 前,一定要关闭防火墙!

1、关闭防火墙

1
2
3
4
5
6
# 关闭防火墙
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld
# 查看防火墙状态
systemctl status firewalld

2、启动 Docker

1
2
3
4
5
systemctl start docker  # 启动docker服务

systemctl stop docker # 停止docker服务

systemctl restart docker # 重启docker服务

3、可通过查看 Docker 状态或 Docker 版本查看是否已经启动

1
2
3
systemctl status docker
//或者
docker -v

配置镜像

docker 官方镜像仓库网速较差,我们需要设置国内镜像服务,可参考阿里云的镜像加速文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

1
2
3
4
5
6
7
8
9
10
11
sudo mkdir -p /etc/docker

//将内容写入json文件中
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://5vycoa8o.mirror.aliyuncs.com"]
}
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

Docker 基本操作

镜像操作

镜像名称

镜名称一般分两部分组成:[repository]:[tag],在没有指定 tag 时,默认是 latest,代表最新版本的镜像。如这里的 mysql 就是 repository,5.7 就是 tag,合一起就是镜像名称,代表 5.7 版本的 MySQL 镜像

07-镜像名称的组成.png

镜像命令

常见的镜像操作命令如图

08-常用镜像指令.png

  • 从镜像服务器拉去镜像:docker pull
  • 从本地文件构建镜像:docker build
  • 查看本地存在哪些镜像:docker images
  • 删除本地镜像:docker rmi
  • 推送镜像到镜像服务器:docker push
  • 将镜像打包成一个压缩包:docker save
  • 加载压缩包为镜像:docker load

查看 docker 帮助文档

1
2
3
4
//查看docker命令及简介
docker --help
//查看具体的某一命令,比如这里详细查看images命令的功能
docker images --help

案例:从 DockerHub 中拉取镜像

需求:从 DockerHub 中拉取一个 nginx 镜像并查看

1、首先去镜像仓库搜索 nginx 镜像,比如 Docker Hub Container Image Library | App Containerization

09-DockerHub拉取镜像.png

1
2
//这里官方示例中没有指定版本,那么默认就是最新版
docker pull nginx

10-DockerHub搜索镜像.png

2、查看镜像

1
docker images

可以查看到本地中已经有 Nginx 镜像了

1
2
3
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 12 months ago 141MB

案例:通过压缩包导出导入镜像

需求:利用 docker save 将 nginx 镜像导出磁盘,然后再通过 load 加载回来

1、利用 docker xx –help 命令查看 docker save 和 docker load 的语法

1
docker save --help

通过帮助文档可得知 save 的命令格式为:

1
docker save -o [保存的目标文件名称] [镜像名称]

2、使用 docker save 导出镜像到磁盘

1
docker save -o nginx.tar nginx:latest

-o 表示选项,注意此时镜像在本地还是有的

3、使用 docker load 加载镜像

3.1)先删除本地的 nginx 镜像

1
docker rmi nginx:latest

3.2)加载压缩包镜像

1
docker load -i nginx.tar

容器操作

容器相关命令

11-容器常用指令.png

容器有三个状态:

  • 运行:进程正常运行
  • 暂停:进程暂停,CPU 不再运行,并不释放内存
  • 停止:进程终止,回收进程占用的内存、CPU 等资源

容器操作的命令:

  • docker run:创建并运行一个容器,处于运行状态
  • docker pause:让一个运行的容器暂停
  • docker unpause:让一个容器从暂停状态恢复运行
  • docker stop:停止一个运行的容器
  • docker start:让一个停止的容器再次运行
  • docker rm:删除一个容器
  • docker ps:查看所有运行的容器及状态
  • docker logs:查看容器运行日志
  • docker exec:进入容器执行命令

案例:创建运行一个容器

需求:创建并运行 nginx 容器的命令

可以去官网搜索 Nginx,并查看其文档:nginx - Official Image | Docker Hub,比如官网中给了如下运行命令示例:

1
docker run --name some-nginx -d -p 8080:80 some-content-nginx

这里以如下命令进行命令解读:

1
docker run --name containerName -p 80:80 -d nginx
  • docker run :创建并运行一个容器
  • –name : 给容器起一个名字,比如叫做 mn
  • -p :将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口;此处宿主机端口不做要求,但容器端口基本上取决于容器本身(软件监听的端口可能就是某一个端口)
  • -d:后台运行容器
  • nginx:镜像名称,例如 nginx,没有写标签 tag 说明是最新版 latest

![[Pasted image 20230110202450.png]]

因为容器是隔离的,所以用户无法直接通过 80 端口来访问到容器,需要将容器的端口与宿主机端口映射。端口映射就相当于将原本隔离的容器暴露出一个小窗口,通过这个小窗口来对容器进行访问

容器创建完成后,会生成一个唯一 ID

1
2
[root@localhost ~]# docker run --name mn -p 80:80 -d nginx
b8ae9bcbdde97a1ef9b055e44470427cd937571c4f2fdb5cb7a710c3d9a828e7

通过访问宿主机 80 端口,就可访问 docker 中的 Nginx 服务了

1
192.168.119.128:80

通过 logs 命令可以查看容器日志

1
2
3
4
5
6
7
8
//命令格式
docker logs [OPTIONS] CONTAINER

//docker logs 容器名
docker logs mn

//持续跟踪日志,通过Ctrl+C可以停止跟踪
docker logs -f mn

案例:操作容器

需求:进入 Nginx 容器,修改 HTML 文件内容,添加 “coffeelize 欢迎您”

1、进入容器(容器是运行的)

1
docker exec -it mn bash

命令解读:

  • docker exec :进入容器内部,执行一个命令
  • -it : 给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
  • mn :要进入的容器的名称
  • bash:进入容器后执行的命令,bash 是一个 linux 终端交互命令

注意:exec 命令可以进入容器修改文件,但是在容器内修改文件是不推荐的,修改了是没有记录(日志的),之后都不知道进行了哪些修改操作

2、进入 nginx 的 HTML 所在目录 /usr/share/nginx/html

容器内部会模拟一个独立的 Linux 文件系统,看起来如同一个 linux 服务器一样

1
2
3
root@b8ae9bcbdde9:/# ls
bin dev docker-entrypoint.sh home lib64 mnt proc run srv tmp var
boot docker-entrypoint.d etc lib media opt root sbin sys usr

我们进入 Nginx 的目录(至于如何找到这个目录的可能需要在 DockerHub 查看 Nginx 的文档了),可以发现目录下包含 index.html

1
cd /usr/share/nginx/html

3、修改 index.html 的内容

容器内没有 vi 命令,无法直接修改,我们用下面的命令来修改

1
sed -i -e 's#Welcome to nginx#coffeelize欢迎您#g' -e 's#<head>#<head><meta charset="utf-8">#g' index.html

4、验证

访问虚拟机 80 端口,输出页面如下,说明修改成功

12-修改成功.png

5、退出容器

1
exit

6、停止容器

1
2
3
4
5
6
docker stop mn

//查看运行中的docker
docker ps
//查看所有容器(包括停止的)
docker ps -a

7、启动容器

1
docker start mn

8、删除容器

1
2
//停掉容器之后删除容器 或者 强制删除运行中的程序
docker rm -f mn

数据卷

在之前的 nginx 案例中,修改 nginx 的 html 页面时,需要进入 nginx 内部。并且因为没有编辑器,修改文件也很麻烦,这就是因为容器与数据(容器内文件)耦合带来的后果。要解决这个问题,必须将 数据与容器解耦,这就要用到数据卷了

13-容器与数据耦合度高.png

数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录

14-数据卷示意图.png

一旦完成数据卷挂载,对容器的一切操作都会作用在数据卷对应的宿主机目录了,这样,我们操作宿主机的 /var/lib/docker/volumes/html 目录,就等于操作容器内的 /usr/share/nginx/html 目录了;多个容器可以挂在同一个卷,就可以 “共享” 修改操作了;如果哪一天将容器删除了,没关系,数据卷还在,将新容器在挂载到这个数据卷上就可以了访问之前的数据了

数据卷操作命令

数据卷操作的基本语法如下

1
docker volume [COMMAND]
  • create:创建一个 volume
  • inspect:显示一个或多个 volume 的信息
  • ls:列出所有的 volume
  • prune:删除未使用的 volume
  • rm:删除一个或多个指定的 volume

案例:创建和查看数据卷

需求:创建一个数据卷,并查看数据卷在宿主机的目录位置

1、创建数据卷

1
docker volume create html

2、查看所有数据卷

1
docker volume ls

3、查看数据卷详细信息卷

1
docker volume inspect html

返回信息如下,其中重点关注 Mountpoint 挂载点

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost ~]# docker volume inspect html
[
{
"CreatedAt": "2023-01-10T21:32:34+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/html/_data",
"Name": "html",
"Options": {},
"Scope": "local"
}
]

4、小结

数据卷的作用:将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全

挂载数据卷

我们在创建容器时,可以通过 -v 参数来挂载一个数据卷到某个容器内目录,命令格式如下:

1
2
3
4
5
docker run \
--name mn \
-v html:/root/html \
-p 8080:80
nginx \

这里的 -v 就是挂载数据卷的命令:

  • docker run:创建并运行容器
  • --name mn:给容器起个名字叫 mn
  • -v html:/root/html :把 html 数据卷挂载到容器内的 /root/html 这个目录中
  • -p 8080:80:吧宿主机的 8080 端口映射到容器内的 80 端口
  • nginx:镜像名称

案例:给 nginx 挂载数据卷

需求:创建一个 nginx 容器,修改容器内的 html 目录内的 index.html 内容
分析:上个案例中,我们进入 nginx 容器内部,已经知道 nginx 的 html 目录所在位置 /usr/share/nginx/html ,我们需要把这个目录挂载到 html 这个数据卷上,方便操作其中的内容

0、查看容器是否在运行,并且已经提前创建好了 html 数据卷

1
2
# 检查容器是否在运行
docker ps -a

1、创建容器并挂载数据卷到容器内的 HTML 目录

1
docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx

2、进入 html 数据卷所在位置,并修改 HTML 内容

1
2
3
4
5
6
7
8
# 查看html数据卷的位置
docker volume inspect html
# 通过查看挂载点可知如下目录,进入该目录
cd /var/lib/docker/volumes/html/_data
# 查看目录下有哪些文件
ls
# 修改文件,此处可通过FinalShell使用本地的高级编辑工具来打开编辑
vi index.html

3、在做数据卷挂在时,如果要创建数据卷不存在,docker 会为我们自动创建数据卷

比如在我们使用如下命令前,docker 中是没有 html 数据卷的,一样可以正常使用如下命令,因为 docker 会为我们自动创建 html 数据卷

1
docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx

案例:给 MySQL 挂载本地目录

容器不仅仅可以挂载数据卷,也可以直接挂载到宿主机目录上。关联关系如下

  • 带数据卷模式:宿主机目录 –> 数据卷 –> 容器内目录
  • 直接挂载模式:宿主机目录 –> 容器内目录

15-容器直接挂在到宿主机.png

目录挂载与数据卷挂载的语法是类似的:

  • -v [宿主机目录]:[容器内目录]
  • -v [宿主机文件]:[容器内文件]

案例需求:创建并运行一个 MySQL 容器,将宿主机目录直接挂载到容器

1、将课前资料中的 mysql.tar 文件上传到虚拟机的 tmp 目录,通过 load 命令加载为镜像

1
2
3
4
5
6
7
8
cd /
cd tmp/

//rz或FinalShell上传mysql.tar

docker load -i mysql.tar
//查看镜像是否导入,mysql的版本为5.7.25
docker images

2、创建目录 /tmp/mysql/data

1
2
//-p表示多级目录创建
mkdir -p /tmp/mysql/data

3、创建目录 /tmp/mysql/conf,将课前资料提供的 hmy.cnf 文件上传到 /tmp/mysql/conf

1
2
3
4
//-p表示多级目录创建
mkdir -p /tmp/mysql/conf

//rz或FinalShell上传hmy.cnf

hmy.cnf 的文件内容为

1
2
3
4
5
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000

4、去 DockerHub 查阅资料 mysql | Docker Hub,创建并运行 MySQL 容器,要求:

1)挂载 /tmp/mysql/data 到 mysql 容器内数据存储目录
2)挂载 /tmp/mysql/conf/hmy.cnf 到 mysql 容器的配置文件
3)设置 MySQL 密码

官网上给定的运行示例如下

1
docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

其中 -e 表示运行环境,后面可以直接设置 mysql 密码;-d 表示后台运行;tag 为版本号,其中还缺少了端口号的设置,我们对这个命令进行修改

1
2
3
4
5
6
7
8
docker run \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123 \
-p 3306:3306 \
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/mysql \
-d \
mysql:5.7.25

但是此时有个报错 (bind: address already in use),因为之前我们已经在虚拟机中运行 MySQL 了,也就是已经占用了宿主机的 3306 端口,这里我们改成 3305 试一下

1
2
3
4
5
6
7
8
9
10
11
12
//删除刚才创建的mysql容器
docker rm mysql

//再次执行如下命令,注意端口改为了3305
docker run \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123 \
-p 3305:3306 \
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/mysql \
-d \
mysql:5.7.25

注意:此处的 /etc/mysql/conf.d 目录,可以合并添加我们创建的 hmy.cnf 配置,而不是将 MySQL 默认的配置文件完全覆盖掉(因为我们创建的配置文件只包含了默认配置的少数配置信息,替换掉默认配置的话配置就不全了)

5、测试 MySQL 连接

通过 Navicat 测试可正常连接
16-Navicat连接成功.png

6、小节

数据卷挂载与目录直接挂载的比较:

  • 数据卷挂载耦合度低,由 docker 来管理目录,但是目录较深,不好找
  • 目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看

Dockerfile 自定义镜像

镜像结构

常见的镜像在 DockerHub 就能找到,但是我们自己写的项目就必须自己构建镜像了,而要自定义镜像,就必须先了解镜像的结构才行

镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。我们以 MySQL 为例,来看看镜像的组成结构

17-镜像结构.png

  • 基础镜像(BaseImage):应用依赖的系统函数库、环境、配置、文件等
  • 入口(Entrypoint):镜像运行入口,一般是程序启动的脚本和参数
  • 层(Layer):在 BaseImage 基础上添加安装包、依赖、配置等,每次操作都形成新的层

Dockerfile

Dockerfile 是一个文本文件,其中包含一个个的 ** 指令 (Instruction)**,用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层 Layer。更新详细语法说明,可参考官网文档: https://docs.docker.com/engine/reference/builder

18-Dockerfile指令.png

构建 Java 项目

基于 Ubuntu 构建 Java 项目

需求:基于 Ubuntu 镜像构建一个新镜像,运行一个 java 项目

1、新建一个空文件夹 docker-demo

1
2
cd /tmp/
mkdir docker-demo

2、拷贝课前资料中的 docker-demo.jar 文件到 docker-demo 这个目录
3、拷贝课前资料中的 jdk8.tar.gz 文件到 docker-demo 这个目录
4、拷贝课前资料提供的 Dockerfile 到 docker-demo 这个目录

Dockerfile 中的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local

# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar

# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8

# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin

# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar

5、进入 docker-demo

1
cd docker-demo

6、运行命令

1
docker build -t javaweb:1.0 .

-t 表示 tag;javaweb 为镜像名称;注意命令后面还有个 .,表示 dockerfile 所在的目录(构建时告知 dockerfile 在哪)

可以看到 dockerfile 共有 9 个指令,也就分为了 9 个 step,每个指令执行都会创建出一个层

7、通过命令查看构建好的镜像

1
2
3
4
5
6
7
[root@localhost docker-demo]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
javaweb 1.0 c94aff541e94 42 seconds ago 722MB
nginx latest 605c77e624dd 12 months ago 141MB
redis latest 7614ae9453d1 12 months ago 113MB
ubuntu 16.04 b6f507652425 16 months ago 135MB
mysql 5.7.25 98455b9624a9 3 years ago 372MB

可以看到我们基于 ubuntu 构建的(配置好 java 环境的)javaweb 项目的镜像已经构建好了

可以通过命令来运行镜像(8090 端口在 dockerfile 中已经声明暴露了端口)

1
docker run --name web -p 8090:8090 -d javaweb:1.0

浏览器访问如下地址,可以发现我们的项目(之前我们的 docker-demo.jar 项目)正常跑起来了

1
http://192.168.119.128:8090/hello/count

19-项目运行成功.png

虚拟机中运行 docker –> docker 中运行 ubuntu –> ubuntu 中运行 docker-demo java 项目😂,虚拟机内存开始吃紧了

20-虚拟机内存吃紧.png

小结

分析:其实我们的 java 项目真正只用到了如下一行

1
COPY ./docker-demo.jar /tmp/app.jar

dockerfile 文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local

# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar

# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8

# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin

# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar

那么我们之后在构建 Java 项目镜像时,可以先构建如下不会改变的层做一个镜像,然后在基于这个镜像来构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local

# 拷贝jdk
COPY ./jdk8.tar.gz $JAVA_DIR/

# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8

# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin

而实际上,有人也已经构建好了这个镜像了,我们直接拿来用就行,镜像名为 java:8-alpine

1
2
3
4
5
6
7
8
# 指定基础镜像
FROM java:8-alpine
# 拷贝java项目的包
COPY ./app.jar /tmp/app.jar
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar

DockerCompose

Docker Compose 可以基于 Compose 文件帮我们快速部署分布式应用,而无需手动一个个创建和运行容器

初识 DockerCompose

Compose 文件是一个文本文件,通过指令定义集群中的每个容器如何运行。格式如下(相当于把 docker run 中的所有指令转换为了 Compose 指令了):

1
2
3
4
5
6
7
8
9
10
11
12
13
version: "3.8"
services:
  mysql:
    image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123
    volumes:
     - "/tmp/mysql/data:/var/lib/mysql"
     - "/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf"
  web:
    build: .
    ports:
     - "8090:8090"

上面的 Compose 文件就描述一个项目,其中包含两个容器

  • mysql:一个基于 mysql:5.7.25 镜像构建的容器,并且挂载了两个目录
    • 为什么没有定义端口呢:因为 MySQL 运行在微服务当中,供内部使用无需对外开放
    • 无需定义后台运行,默认就是后台运行
  • web:一个基于 docker build 临时构建的镜像容器,映射端口时 8090
    • 为什么没有指定镜像:因为通过 build 就可以构建镜像

安装 DockerCompose

1、下载

1
2
# 安装
curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

或者通过本地文件准备好的文件直接上传,上传至 /usr/local/bin/ 目录

2、修改文件权限

1
2
# 修改权限
chmod +x /usr/local/bin/docker-compose

3、Base 自动补全命令

之后使用 Docker Compose 时就会有补全提示

1
2
# 补全命令
curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose

此时可能会报:拒绝连接的错误,需要执行如下命令修改 hosts 文件

1
echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts

案例:利用 DockerCompose 部署

需求:将之前学习的 cloud-demo 微服务集群利用 DockerCompose 部署

1、查看课前资料提供的 cloud-demo 文件夹,里面已经编写好了 docker-compose 文件

课前资料提供的 cloud-demo 文件夹,里面已经编写好了 docker-compose 文件,而且每个微服务都准备了一个独立的目录。对于每一个微服务目录,其中都包含一个 Dockerfile 文件和对于的微服务 jar 包。最外层包含 docker-compose.yml 配置文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: "3.2"

services:
nacos:
image: nacos/nacos-server
environment:
MODE: standalone
ports:
- "8848:8848"
mysql:
image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- "$PWD/mysql/data:/var/lib/mysql"
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
userservice:
build: ./user-service
orderservice:
build: ./order-service
gateway:
build: ./gateway
ports:
- "10010:10010"

这几个微服务(mysql、userservice、orderservice 以及 gateway)中,只有网关暴露了端口,因为网关是外部访问微服务的入口。其他微服务都需要注册到 Nacos 服务中

MySQL 微服务中需要的表和数据课程资料也已经为我们准备好了

2、修改自己的 cloud-demo 项目,将数据库、nacos 地址都命名为 docker-compose 中的服务名

因为微服务将来要部署为 docker 容器,而容器之间互联不是通过 IP 地址,而是通过容器名。这里我们将 order-service、user-service、gateway 服务的 mysql、nacos 地址都修改为基于容器名的访问

比如 user-service 中的 bootstrap 配置文件

1
2
3
4
5
6
7
8
9
10
11
spring:  
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
- server-addr: localhost:8848 # Nacos地址
+ server-addr: nacos:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名

application.yml 配置文件中

1
2
- url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
+ url: jdbc:mysql://mysql:3306/cloud_user?useSSL=false

同理,order-service 和 gateway 微服务的配置文件也这样修改

3、使用 maven 打包工具,将项目中的每个微服务都打包为 app.jar

为什么都打包成 app.jar 呢 –> 因为我们在微服务目录下的 Dockerfile 文件里是这样配置的,我们配置的名称都是 app.jar

1
2
3
FROM java:8-alpine
COPY ./app.jar /tmp/app.jar
ENTRYPOINT java -jar /tmp/app.jar

那么,既然各个微服务打包完成都需要叫这个 app.jar 名字,我们是否可以修改配置文件实现项目打包自动叫这个名字呢 –> 可以的,在各个微服务的 pom 文件中添加如下配置

1
2
3
4
5
6
7
8
9
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

通过 Maven 的 Package 来打包

4、将打包好的 app.jar 拷贝到 cloud-demo 中的每一个对应的微服务子目录中

21-项目部署前准备.gif

5、将 cloud-demo 上传至虚拟机 (tmp 目录),利用 docker-compose up -d 来部署

1
2
3
4
5
6
7
cd /
cd /tmp/

//上传cloud-demo文件夹

cd cloud-demo/
docker-compose up -d
  • up:表示创建并执行容器
  • down:停止并删除容器
  • 其他命令可以通过 help 命令查看

6、查看打包好的镜像和运行的容器

1
2
docker images
docker ps
1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost cloud-demo]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cloud-demo_gateway latest 3ce691e26939 About a minute ago 185MB
cloud-demo_orderservice latest b81195944331 About a minute ago 187MB
cloud-demo_userservice latest 2dc6d8c88bdc About a minute ago 184MB
javaweb 1.0 c94aff541e94 3 hours ago 722MB
nginx latest 605c77e624dd 12 months ago 141MB
redis latest 7614ae9453d1 12 months ago 113MB
ubuntu 16.04 b6f507652425 16 months ago 135MB
nacos/nacos-server latest bdf60dc2ada3 17 months ago 1.05GB
mysql 5.7.25 98455b9624a9 3 years ago 372MB
java 8-alpine 3fd9dd82815c 5 years ago 145MB

虚拟机 2G 内存快要炸了😳,开始借用交换内存了

7、通过查看日志可发现 order-service 有报错

1
2
3
4
5
6
7
docker-compose logs -f

//退出日志
Ctrl+C

//重启微服务(微服务启动前Nacos已经启动完成)
docker-compose restart gateway userservice orderservice

原因是因为 Nacos 微服务启动晚于 order-service,导致报错。关键是报错之后没有进行重新启动 –> 因此,我们最好是先启动 Nacos 微服务,之后再启动 order-service 等系列微服务

浏览器访问如下,均可正常接收到数据

1
2
http://192.168.119.128:10010/user/2?authorization=admin
http://192.168.119.128:10010/order/102?authorization=admin

至此,DockerCompose 部署微服务完成

8、删除掉这些容器吧,虚拟机要炸了

1
2
//删除通过docker-compose部署的容器,同时删除镜像
docker-compose down --rmi all
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@localhost cloud-demo]# docker-compose down --rmi all
Stopping cloud-demo_nacos_1 ... done
Stopping cloud-demo_userservice_1 ... done
Stopping cloud-demo_gateway_1 ... done
Stopping cloud-demo_mysql_1 ... done
Stopping cloud-demo_orderservice_1 ... done
Removing cloud-demo_nacos_1 ... done
Removing cloud-demo_userservice_1 ... done
Removing cloud-demo_gateway_1 ... done
Removing cloud-demo_mysql_1 ... done
Removing cloud-demo_orderservice_1 ... done
Removing network cloud-demo_default
Removing image nacos/nacos-server
Removing image mysql:5.7.25
Removing image cloud-demo_userservice
Removing image cloud-demo_orderservice
Removing image cloud-demo_gateway

Docker 镜像仓库

简化版镜像仓库

Docker 官方的 Docker Registry 是一个基础版本的 Docker 镜像仓库,具备仓库管理的完整功能,但是没有图形化界面

1
2
3
4
5
6
docker run -d \
--restart=always \
--name registry \
-p 5000:5000 \
-v registry-data:/var/lib/registry \
registry

命令中挂载了一个数据卷 registry-data 到容器内的 /var/lib/registry 目录,这是私有镜像库存放数据的目录,访问如下链接可以查看当前私有镜像服务中包含的镜像

1
http://yourip:5000/v2/_catalog

带有图形化界面版本

操作此步骤前,需要先完成 Docker 信任地址配置

使用 DockerCompose 部署带有图象界面的 DockerRegistry,命令如下

1
2
3
4
5
6
7
8
9
cd /
cd /tmp/
mkdir registry-ui
cd registry-ui
touch docker-compose.yml
//修改这个yml文件,内容如下代码块

//修改yml文件完成后执行docker-compose
docker-compose up -d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
version: '3.0'
services:
registry:
image: registry
volumes:
- ./registry-data:/var/lib/registry
ui:
image: joxit/docker-registry-ui:static
ports:
- 8080:80
environment:
- REGISTRY_TITLE=coffeelize私有仓库
- REGISTRY_URL=http://registry:5000
depends_on:
- registry

通过访问如下地址,即可访问我们创建的带有图形界面的 Docker 镜像仓库了

1
192.168.119.128:8080

22-建立私有仓库.png

配置 Docker 信任地址

我们的私服采用的是 http 协议,默认不被 Docker 信任,所以需要做一个配置

1
2
3
4
5
6
7
8
# 打开要修改的文件
vi /etc/docker/daemon.json
# 添加内容:
"insecure-registries":["http://192.168.119.128:8080"]
# 重加载
systemctl daemon-reload
# 重启docker
systemctl restart docker

在添加内容时,注意多个配置之间别把逗号忘加了

1
2
3
4
{
"registry-mirrors": ["https://5vycoa8o.mirror.aliyuncs.com"],
+ "insecure-registries":["http://192.168.119.128:8080"]
}

在私有镜像仓库中推送或拉去镜像

推送镜像到私有镜像服务必须先 tag,步骤如下:

1、重新 tag 本地镜像(重命名镜像,并且以镜像仓库地址为前缀),名称前缀为私有仓库的地址:192.168.119.128:8080/

1
docker tag nginx:latest 192.168.119.128:8080/nginx:1.0 

利用 tag 命令,可以将一个镜像重命名,这里我们对之前下载的最新版 Nginx 镜像进行操作

23-推送镜像前重命名镜像.png

此时,查看本地的镜像,就可以找到我们打包并且重命名后的镜像了,可以发现这两个镜像的 ID(605c77e624dd)其实是一样的

1
2
192.168.119.128:8080/nginx   1.0        605c77e624dd   12 months ago   141MB
nginx latest 605c77e624dd 12 months ago 141MB

2、推送镜像

1
docker push 192.168.119.128:8080/nginx:1.0 

24-推送镜像到私有仓库.png

3、拉取镜像

1
docker pull 192.168.150.101:8080/nginx:1.0