-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
341 lines (341 loc) · 292 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[git config全局和本地配置]]></title>
<url>%2F%2Fblog%2Fgit-config%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 前言程序开发人员在日常开发过程中,使用 git 可能会遇到这样一个情况: 公司项目使用公司账号提交git信息。 个人业余项目使用个人账户提交git信息。 也就是说,我们需要 git 根据不同项目使用不同的账号及邮箱提交相关记录,本文就是专门解决这个问题的。 本地设置本地设置有两种方式,命令方式 和 配置文件方式,两种方式选择任意一种,都可以配置当前git项目提交git信息的账号及邮箱。 命令方式对于本地 git repo 配置,先进入 本地git repo 目录,使用命令: 12git config user.name "your-username"git config user.email "your-email-address" 配置文件方式对于本地 git repo 配置,先进入 本地git项目 目录,编辑 .git/config 文件,增加以下信息: 123[user] name = your-username email = your-email-address 保存并退出。以上 命令方式 和 配置文件方式 两种方式,都可以在 本地git repo 目录下,通过以下命令查看本地项目的账号及邮箱是否更改成功。 1git config user.name 对于很多个 本地git repo 来说,每个 本地git repo 都要设置自定义化git本地账号及邮箱,有时我们不想每个都设置,我们可以使用git的全局配置,所有 本地git repo 都是使用全局配置! 全局设置全局设置有两种方式,命令方式 和 配置文件方式,两种方式选择任意一种,都可以配置全局git项目提交git信息的账号及邮箱。 命令方式在git服务器中,任意非 本地git repo 中,使用以下命令配置全局配置: 12git config --global user.name "your-username"git config --global user.email "your-email-address" 配置文件方式编辑 〜/.gitconfig,其内容与 .git/config 文件中的内容相同,即将 [user] 部分信息添加至 〜/.gitconfig 文件中,内容如下: 123[user] name = your-username email = your-email-address 保存并退出。任意非 本地git repo 中,在使用以下命令查看是否配置成功 1git config user.name 好了,这次git config全局及本地配置就到这里了!]]></content>
<categories>
<category>Git</category>
</categories>
<tags>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Vim命令速查表]]></title>
<url>%2F%2Fblog%2Fvim-cheatsheet%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 Vim命令速查表 个人Vim命令速查表。 vim-cheatsheet | Vim官网 | Github | Vim中文文档 Vim自定义配置文件 - vimrc Chrome浏览器神级插件 - Vimium 光标移动注:一般模式下,任意一个动作都可以重复。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647h # 光标左移,同 <Left> 键j # 光标下移,同 <Down> 键k # 光标上移,同 <Up> 键l # 光标右移,同 <Right> 键Ctrl+e # 向上滚动一行Ctrl+y # 向下滚动一行Ctrl+u # 向上滚动半屏 --> move up 1/2 a screenCtrl+d # 向下滚动半屏 --> move down 1/2 a screenCtrl+f # 向下滚动一屏 --> move forward one full screenCtrl+b # 向上滚动一屏 --> move back one full screen0 # 跳到行首(是数字零,不是字母O),效用等同于 <Home> 键^ # 跳到从行首开始第一个非空白字符$ # 跳到行尾,效用等同于 <End> 键gg # 跳到第一行,效用等同于 Ctrl+<Home>G # 跳到最后一行,效用等同于 Ctrl+<End>nG # 跳到第n行,比如 10G 是移动到第十行:n # 跳到第n行,比如 :10<回车> 是移动到第十行10% # 移动到文件 10% 处15| # 移动到当前行的 15列w # 跳到下一个单词开头 (word: 标点或空格分隔的单词)W # 跳到下一个单词开头 (WORD: 空格分隔的单词)e # 跳到下一个单词尾部 (word: 标点或空格分隔的单词)E # 跳到下一个单词尾部 (WORD: 空格分隔的单词)b # 跳到上一个单词开头 (word: 标点或空格分隔的单词)B # 跳到上一个单词开头 (WORD: 空格分隔的单词)ge # 跳到前一个单词结尾) # 向前移动一个句子(句号分隔)( # 向后移动一个句子(句号分隔)} # 向前移动一个段落(空行分隔){ # 向后移动一个段落(空行分隔)<enter> # 移动到下一行首个非空字符n<enter> # 光标向下移动 n 行+ # 移动到下一行首个非空字符(同回车键)- # 移动到上一行首个非空字符H # 移动到屏幕上部M # 移动到屏幕中部L # 移动到屏幕下部<S+Left> # 按住 SHIFT 按左键,向左移动一个单词<S+Right> # 按住 SHIFT 按右键,向右移动一个单词<S+Up> # 按住 SHIFT 按上键,向上翻页<S+Down> # 按住 SHIFT 按下键,向下翻页gm # 移动到行中gj # 光标向下移动一个屏幕行(忽略自动换行)gk # 光标向上移动一个屏幕行(忽略自动换行)zz # 调整光标所在行到屏幕中央zt # 调整光标所在行到屏幕上部zb # 调整光标所在行到屏幕下部 插入模式123456789i # 在光标处进入插入模式I # 在行首进入插入模式a # 在光标后进入插入模式A # 在行尾进入插入模式o # 在下一行插入新行并进入插入模式O # 在上一行插入新行并进入插入模式gi # 进入到上一次插入模式的位置<esc> # 退出插入模式Ctrl+[ # 退出插入模式(等价于 esc 键) 插入模式的命令注:由 i, I, a, A, o, O 等命令进入插入模式 1234567891011121314151617181920212223242526272829303132333435<Up> # 光标向上移动<Down> # 光标向下移动<Left> # 光标向左移动<Right> # 光标向右移动<S+Left> # 按住 SHIFT 按左键,向左移动一个单词<S+Right> # 按住 SHIFT 按右键,向右移动一个单词<S+Up> # 按住 SHIFT 按上键,向上翻页<S+Down> # 按住 SHIFT 按下键,向下翻页<PageUp> # 上翻页<PageDown> # 下翻页<Delete> # 删除光标处字符<BS> # Backspace 向后删除字符<Home> # 光标跳转行首<End> # 光标跳转行尾Ctrl+d # 减少缩进Ctrl+f # 减少缩进Ctrl+t # 增加缩进Ctrl+h # 删除前一个字符,相当于回格键backspaceCtrl+o # 临时退出插入模式,执行单条命令又返回插入模式Ctrl+u # 当前行删除到行首所有字符Ctrl+w # 删除光标前的一个单词Ctrl+\ Ctrl+O # 临时退出插入模式(光标保持),执行单条命令又返回插入模式Ctrl+R 0 # 插入寄存器(内部 0号剪贴板)内容,Ctrl+R 后可跟寄存器名Ctrl+R " # 插入匿名寄存器内容,相当于插入模式下 p粘贴Ctrl+R = # 插入表达式计算结果,等号后面跟表达式Ctrl+R : # 插入上一次命令行命令Ctrl+R / # 插入上一次搜索的关键字Ctrl+v {char} # 插入非数字的字面量Ctrl+v {code} # 插入用三位数字表示的ascii/unicode字符编码,如Ctrl+v 065Ctrl+v 065 # 插入 10进制 ascii 字符(两数字) 065 即 A字符Ctrl+v x41 # 插入 16进制 ascii 字符(三数字) x41 即 A字符Ctrl+v o101 # 插入 8进制 ascii 字符(三数字) o101 即 A字符Ctrl+v u1234 # 插入 16进制 unicode 字符(四数字)Ctrl+v U12345678 # 插入 16进制 unicode 字符(八数字)Ctrl+K {ch1} {ch2} # 插入 digraph(见 :h digraph),快速输入日文或符号等 自动补全在插入模式下,最常用的补全 123Ctrl+n # 插入模式下文字自动补全 Ctrl+P # 插入模式下文字自动补全Ctrl+e # 有补全列表时,终止这次补全,继续输入 智能补全 12345678910111213Ctrl+X # 进入补全模式Ctrl+X Ctrl+L # 整行补全Ctrl+X Ctrl+N # 插入模式下,根据当前文件里关键字补全Ctrl+X Ctrl+K # 根据字典补全Ctrl+X Ctrl+T # 根据同义词字典补全Ctrl+X Ctrl+F # 插入模式下补全文件名Ctrl+X Ctrl+I # 根据头文件内关键字补全Ctrl+X Ctrl+] # 根据标签补全Ctrl+X Ctrl+D # 补全宏定义Ctrl+X Ctrl+V # 补全vim命令Ctrl+X Ctrl+U # 用户自定义补全方式Ctrl+X Ctrl+S # 拼写建议,例如:一个英文单词Ctrl+X Ctrl+O # 插入下 Omnifunc 补全 文本编辑1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768r # 替换当前字符R # 进入替换模式,直至 ESC 离开s # 替换字符(删除光标处字符,并进入插入模式,前可接数量)S # 替换行(删除当前行,并进入插入模式,前可接数量)cc # 改写当前行(删除当前行并进入插入模式),同 Scw # 改写光标开始处的当前单词ciw # 改写光标所处的单词caw # 改写光标所处的单词,并且包括前后空格(如果有的话)c0 # 改写到行首c^ # 改写到行首(第一个非零字符)c$ # 改写到行末C # 改写到行尾(同c$)ci" # 改写双引号中的内容ci' # 改写单引号中的内容cib # 改写小括号中的内容cab # 改写小括号中的内容(包含小括号本身)ci) # 改写小括号中的内容ci] # 改写中括号中内容ciB # 改写大括号中内容caB # 改写大括号中的内容(包含大括号本身)ci} # 改写大括号中内容cit # 改写 xml tag 中的内容cis # 改写当前句子c2w # 改写下两个单词ct( # 改写到小括号前x # 删除当前字符,前面可以接数字,3x代表删除三个字符X # 向前删除字符dd # 删除(剪切)当前行 --> delete (cut) a line d0 # 删除(剪切)到行首d^ # 删除(剪切)到行首(第一个非零字符)d$ # 删除(剪切)到行末D # 删除(剪切)到行末(同 d$)dw # 删除(剪切)当前单词diw # 删除(剪切)光标所处的单词daw # 删除(剪切)光标所处的单词,并包含前后空格(如果有的话)di" # 删除双引号中的内容di' # 删除单引号中的内容dib # 删除小括号中的内容di) # 删除小括号中的内容dab # 删除小括号内的内容(包含小括号本身)di] # 删除中括号中内容diB # 删除大括号中内容di} # 删除大括号中内容daB # 删除大括号内的内容(包含大括号本身)dit # 删除 xml tag 中的内容dis # 删除当前句子d2w # 删除下两个单词dt( # 删除到小括号前dgg # 删除到文件头部dG # 删除到文件尾部d} # 删除下一段d{ # 删除上一段10d # 删除当前行开始的10行:10d # 删除第10行:1,10d # 删除1-10行J # 链接多行为一行. # 重复上一次操作~ # 替换大小写g~iw # 替换当前单词的大小写gUiw # 将单词转成大写guiw # 将当前单词转成小写guu # 全行转为小写gUU # 全行转为大写<< # 减少缩进>> # 增加缩进== # 自动缩进Ctrl+A # 增加数字Ctrl+X # 减少数字 复制粘贴copy 命令的格式为: 1:[range]copy{address} 参数说明: [range] 表示要复制的行范围,其中copy可缩写为 :co 或 :t {address} 表示复制的目标位置,这两个参数都可以缺省,用于表示Vim光标所在当前行。 例如,:5copy. 表示复制Vim当前打开的文件的第 5 行到当前行 (用 . 表示),即为第 5 行创建一份副本,并放到当前行下方。 下标列出了使用 copy 命令的缩写形式 t 进行文件复制的一些实例及用途,用于理解复制命令 copy 的用途。 12345:3,5t. # 把第 3 行到第 5 行的内容复制到当前行下方:t5 # 把当前行复制到第 5 行下方:t. # 复制当前行到当前行下方 (等价于普通模式下的 yyp):t$ # 把当前行复制到文本结尾:'<,'>t0 # 把高亮选中的行复制到文件开头 常用复制粘贴命令: 12345678910111213141516171819202122232425262728293031323334353637p # 粘贴到光标后P # 粘贴到光标前v # 开始标记y # 复制标记内容V # 开始按行标记Ctrl+V # 开始列标记y$ # 复制当前位置到本行结束的内容yy # 复制当前行 --> yank (copy) a lineY # 复制当前行,同 yyyiw # 复制当前单词3yy # 复制光标下三行内容v0 # 选中当前位置到行首v$ # 选中当前位置到行末viw # 选中当前单词vib # 选中小括号内的东西vi) # 选中小括号内的东西vi] # 选中中括号内的东西viB # 选中大括号内的东西vi} # 选中大括号内的东西vis # 选中句子中的东西vab # 选中小括号内的东西(包含小括号本身)va) # 选中小括号内的东西(包含小括号本身)va] # 选中中括号内的东西(包含中括号本身)vaB # 选中大括号内的东西(包含大括号本身)va} # 选中大括号内的东西(包含大括号本身):set paste # 允许粘贴模式(避免粘贴时自动缩进影响格式):set nopaste # 禁止粘贴模式"?yy # 复制当前行到寄存器 ? ,问号代表 0-9 的寄存器名称"?d3j # 删除光标下三行内容,并放到寄存器 ? ,问号代表 0-9 的寄存器名称"?p # 将寄存器 ? 的内容粘贴到光标后"?P # 将寄存器 ? 的内容粘贴到光标前:registers # 显示所有寄存器内容:[range]y # 复制范围,比如 :20,30y 是复制20到30行,:10y 是复制第十行:[range]d # 删除范围,比如 :20,30d 是删除20到30行,:10d 是删除第十行ddp # 交换两行内容:先删除当前行复制到寄存器,并粘贴"_[command] # 使用[command]删除内容,并且不进行复制(不会污染寄存器)"*[command] # 使用[command]复制内容到系统剪贴板(需要vim版本有clipboard支持) 文本编辑、复制粘贴中的内容可以简单总结为: 1234ci'、ci"、ci(、ci[、ci{、ci< # 分别更改这些配对标点符号中的文本内容di'、di"、di(或dib、di[、di{或diB、di< # 分别删除这些配对标点符号中的文本内容yi'、yi"、yi(、yi[、yi{、yi< # 分别复制这些配对标点符号中的文本内容vi'、vi"、vi(、vi[、vi{、vi< # 分别选中这些配对标点符号中的文本内容 cit、dit、yit、vit,分别操作一对标签之间的内容,编辑html、xml很好用! 另外,如果把上面的 i 改成 a 可以同时操作配对标点和配对标点内的内容。 移动文本1:[range]move{address} 参数说明: [range] 表示要移动的行范围 {address} 表示移动的目标位置,这两个参数都可以缺省 例如:123:m+1 # 下移1行:m-2 # 上移1行:8,10m2 # 把当前打开文件的第8~10行内容移动到第 2 行下方 文本对象1234567891011121314151617181920212223242526$ # 到行末0 # 到行首^ # 到行首非空字符iw # 整个单词(不包括分隔符)aw # 整个单词(包括分隔符)iW # 整个 WORD(不包括分隔符)aW # 整个 WORD(包括分隔符)is # 整个句子(不包括分隔符)ib # 小括号内ab # 小括号内(包含小括号本身)iB # 大括号内aB # 大括号内(包含大括号本身)i) # 小括号内a) # 小括号内(包含小括号本身)i] # 中括号内a] # 中括号内(包含中括号本身)i} # 大括号内a} # 大括号内(包含大括号本身)i' # 单引号内a' # 单引号内(包含单引号本身)i" # 双引号内a" # 双引号内(包含双引号本身)2i) # 往外两层小括号内2a) # 往外两层小括号内(包含小括号本身)2f) # 到第二个小括号处2t) # 到第二个小括号前 撤销与恢复1234u # 撤销命令可以组合,例如Nu N是任意整数,表示撤销N步操作,下同U # 撤销整行操作Ctrl+r # 撤销上一次 u 命令Ctrl+R # 回退前一个命令 查找替换一般模式下的查找命令: 1234567891011121314/pattern # 从光标处向文件尾搜索 pattern?pattern # 从光标处向文件头搜索 patternn # 向同一方向执行上一次搜索N # 向相反方向执行上一次搜索* # 向前搜索光标下的单词# # 向后搜索光标下的单词f<char> # 向后搜索当前行第一个为 <char> 的字符,2fv 可以找到第二个v字符F<char> # 向前搜索当前行第一个为 <char> 的字符t<char> # 向后搜索当前行第一个为 <char> 的字符前T<char> # 向前搜索当前行第一个为 <char> 的字符前; # 重复上次的字符查找命令(f/t命令), # 反转方向查找上次的字符查找命令(f/t命令)tx # 搜索当前行到指定 字符串 之前fx # 搜索当前行到指定 字符串 之处 一般模式下的替换命令: 1:[range]s[ubstitute]/{pattern}/{string}/[flags] 参数说明: pattern:就是要被替换掉的字串,可以用 regexp 來表示 string:將 pattern 由 string 所取代 [range]:有以下一些取值: [range]取值 含义 无 默认光标所在行 . 光标所在当前行 N 第 N 行 $ 最后一行 ‘a 标记 a 所在的行(之前要用 ma 做过标记) $-1 倒数第二行,可以对某一行加减某个数值获得确定的某行 1,10 第1~10行 1,$ 第一行到最后一行 1,. 第一行到当前行 .,$ 当前行到最后一行 ‘a,’b 标记 a 所在的行 到 标记 b 所在的行(之前要用 ma、mb 做过标记) % 所有行(和 1,$ 等价) ?str? 从当前位置向上搜索,找到第一个str的行(str可以是正则) /str/ 从当前位置向下搜索,找到第一个str的行(str可以是正则) 注意,上面的所有用于range的表示方法都可以通过 +、- 操作来设置相对偏移量。 [flags]有以下一些取值: [flags]取值 含义 g 对指定范围内的所有匹配项(global)进行替换 c 在替换前请求用户进行确认(confirm) e 忽略执行过程中的错误 i 不区分大小写 无 只在指定范围内的第一个匹配项进行替换 举例: 12345678910:s/p1/p2/g # 将当前行中全替换p1为p2:%s/p1/p2/g # 将当前文件中全替换p1为p2:%s/p1/p2/gc # 将当前文件中全替换p1为p2,并且每处询问你是否替换:10,20s/p1/p2/g # 将第10到20行中所有p1替换为p2:%s/1\\2\/3/123/g # 将“1\2/3” 替换为 “123”(特殊字符使用反斜杠标注):%s/\r//g # 删除 DOS 换行符 ^M:%s///gn # 统计某个模式的匹配个数:%s/^\s*$\n//g # 删除Vim打开文件中所有空白行:g/^\s*$/d # 删除Vim打开文件中所有空白行:%s/^M$//g # 删除Vim文件中显式的 ^M 符号(操作系统换行符问题) 可视模式注:Vim 可视模式下可以选择一块编辑区域,然后对选中的文件内容执行插入、删除、替换、改变大小写等操作。! 12345678910111213141516171819v # 切换到面向字符的可视模式V # 切换到面向行的可视模式Ctrl-V # 切换到面向列块的可视模式> # 增加缩进< # 减少缩进d # 删除高亮选中的文字x # 删除高亮选中的文字c # 改写文字,即删除高亮选中的文字并进入插入模式s # 改写文字,即删除高亮选中的文字并进入插入模式y # 拷贝文字~ # 转换大小写o # 跳转到标记区的另外一端O # 跳转到标记块的另外一端u # 标记区转换为小写U # 标记区转换为大写gv # 重选上次的高亮选区g Ctrl+G # 显示所选择区域的统计信息ggVG # 选择全文esc # 按esc键退出可视模式 此外: Vim normal 命令可以在命令行模式执行普通模式下的命令,当 normal 命令与 Vim 可视化模式结合时,只需很少的操作就能完成大量重复性工作。 注释命令多行注释123Ctrl+v # 进入命令行模式,按Ctrl + v进入可视模式,然后按j, 或者k选中多行,把需要注释的行标记起来I # 按大写字母I,再插入注释符,例如#、//esc # 按esc键就会全部注释了 取消多行注释 123Ctrl+v # 进入命令行模式,按Ctrl + v进入可视模式,按字母l横向选中列的个数,例如#、//(需要选中2列)j 或 k # 按字母j,或者k选中注释符号d # 按d键就可全部取消注释 复杂注释 1234567891011:起始行号,结束行号s/^/注释符/g(注意冒号) # 在指定的行首添加注释:起始行号,结束行号s/^注释符//g(注意冒号) # 在指定的行首取消注释:3,5 s/^/#/g # 注释第3-5行:3,5 s/^#//g # 解除3-5行的注释:1,$ s/^/#/g # 注释整个文档:1,$ s/^#//g # 取消注释整个文档:%s/^/#/g # 注释整个文档,此法更快:%s/^#//g # 取消注释整个文档 位置跳转1234567891011121314151617181920212223Ctrl+O # 跳转到上一个位置Ctrl+I # 跳转到下一个位置Ctrl+^ # 跳转到 alternate file (当前窗口的上一个文件)% # 跳转到 {} () [] 的匹配gd # 跳转到局部定义(光标下的单词的定义)gD # 跳转到全局定义(光标下的单词的定义)gf # 打开名称为光标下文件名的文件[[ # 跳转到上一个顶层函数(比如C语言以大括号分隔)]] # 跳转到下一个顶层函数(比如C语言以大括号分隔)[m # 跳转到上一个成员函数]m # 跳转到下一个成员函数[{ # 跳转到上一处未匹配的 {]} # 跳转到下一处未匹配的 }[( # 跳转到上一处未匹配的 (]) # 跳转到下一处未匹配的 )[c # 上一个不同处(diff时)]c # 下一个不同处(diff时)[/ # 跳转到 C注释开头]/ # 跳转到 C注释结尾`` # 回到上次跳转的位置'' # 回到上次跳转的位置`. # 回到上次编辑的位置'. # 回到上次编辑的位置 文件操作1234567891011121314151617181920212223242526:w # 保存文件(会修改文件的时间戳):w <filename> # 按名称保存文件:x # 保存文件并退出(不会修改文件的时间戳):e <filename> # 打开文件并编辑:saveas <filename> # 另存为文件:o <filename> # 在当前窗口打开另一个文件:r <filename> # 读取文件并将内容插入到光标后:r !dir # 将 dir 命令的输出捕获并插入到光标后:only # 关闭除光标所在的窗口之外的其他窗口:close # 关闭光标所在窗口的文件:q # 关闭光标所在的窗口并退出:q! # 强制退出:qa! # 关闭所有窗口(不保存):qall # 放弃所有操作并退出:wa # 保存所有文件:wall # 保存所有:wqall # 保存所有并退出。:cd <path> # 切换 Vim 当前路径:pwd # 显示 Vim 当前路径:new # 打开一个新的窗口编辑新文件:enew # 在当前窗口创建新文件:vnew # 在左右切分的新窗口中编辑新文件:tabnew # 在新的标签页中编辑新文件:version # 查看Vim版本ZZ # 保存文件(如果有改动的话),并关闭窗口ZQ # 不保存文件关闭窗口 打开文件12345678910111213vim filename # 打开或新建文件,并将光标置于第一行首vim + filename # 打开文件,并将光标置于最后一行首vim +n filename # 打开文件,并将光标置于第 n 行首vim -c cmd file # 在打开文件 file 前,先执行指定的Vim命令cmdvim -b file # 以二进制模式打开文件,该模式某些特殊字符 (如换行符 ^M) 都可以显示出来vim -d file1 file2 # 使用Vim同时打开 file1 和 file2 文件并diff两个文件的差异vim -r filename # 在上次正用 vim编辑时发生系统崩溃,恢复文件vim -R file # 以只读形式打开文件,但是仍然可以使用 :wq! 写入vim -M file # 强制性关闭修改功能,无法使用 :wq! 写入 vim -o file1 file2 # 终端中要打开vim文件时,横向分割显示多个文件vim -O file1 file2 # 终端中要打开vim文件时,纵向分割显示多个文件vim -x file # 以加密方式打开文件vim +/target file # 打开 file 并将光标移动到找到的第一个 target 字符串上 已打开文件操作12345678910:ls # 查案缓存列表:bn # 切换到下一个缓存:bp # 切换到上一个缓存:bd # 删除缓存:b 1 # 切换到1号缓存:b abc # 切换到文件名为 abc 开头的缓存:badd <filename> # 将文件添加到缓存列表:set hidden # 设置隐藏模式(未保存的缓存可以被切换走,或者关闭):set nohidden # 关闭隐藏模式(未保存的缓存不能被切换走,或者关闭)n Ctrl+^ # 切换缓存,先输入数字的缓存编号,再按 Ctrl + 6 窗口操作123456789101112131415161718192021222324252627282930313233:sp <filename> # 上下切分窗口并在新窗口打开文件 filename:vs <filename> # 左右切分窗口并在新窗口打开文件 filename:split # 将当前窗口再复制一个纵向窗口出来,内容同步,游标可以不同:vsplit # 将当前窗口再复制一个横向窗口出来,内容同步,游标可以不同Ctrl+W s # 上下切分窗口Ctrl+W v # 左右切分窗口Ctrl+W w # 循环切换到下一个窗口Ctrl+W W # 循环切换到上一个窗口Ctrl+W p # 跳到上一个访问过的窗口Ctrl+W c # 关闭当前窗口Ctrl+W o # 关闭其他窗口Ctrl+W h # 跳到左边的窗口Ctrl+W j # 跳到下边的窗口Ctrl+W k # 跳到上边的窗口Ctrl+W l # 跳到右边的窗口Ctrl+W + # 增加当前窗口的行高,前面可以加数字Ctrl+W - # 减少当前窗口的行高,前面可以加数字Ctrl+W < # 减少当前窗口的列宽,前面可以加数字Ctrl+W > # 增加当前窗口的列宽,前面可以加数字Ctrl+W = # 让所有窗口宽高相同Ctrl+W H # 将当前窗口移动到最左边Ctrl+W J # 将当前窗口移动到最下边Ctrl+W K # 将当前窗口移动到最上边Ctrl+W L # 将当前窗口移动到最右边Ctrl+W x # 交换窗口Ctrl+W f # 在新窗口中打开名为光标下文件名的文件Ctrl+W gf # 在新标签页中打开名为光标下文件名的文件Ctrl+W R # 旋转窗口Ctrl+W T # 将当前窗口移到新的标签页中Ctrl+W P # 跳转到预览窗口Ctrl+W z # 关闭预览窗口Ctrl+W _ # 纵向最大化当前窗口Ctrl+W | # 横向最大化当前窗口 标签页123456789101112131415161718:tabs # 显示所有标签页:tabe <filename> # 在新标签页中打开文件 filename:tabn # 下一个标签页:tabp # 上一个标签页:tabc # 关闭当前标签页:tabo # 关闭其他标签页:tabn n # 切换到第n个标签页,比如 :tabn 3 切换到第三个标签页:tabm n # 标签移动:tabfirst # 切换到第一个标签页:tablast # 切换到最后一个标签页:tab help # 在标签页打开帮助:tab drop <file> # 如果文件已被其他标签页和窗口打开则跳过去,否则新标签打开:tab split # 在新的标签页中打开当前窗口里的文件:tab ball # 将缓存中所有文件用标签页打开:set showtabline=? # 设置为 0 就不显示标签页标签,1会按需显示,2会永久显示ngt # 切换到第n个标签页,比如 2gt 将会切换到第二个标签页gt # 下一个标签页gT # 上一个标签页 书签12345678910:marks # 显示所有书签ma # 保存当前位置到书签 a ,书签名小写字母为文件内,大写全局'a # 跳转到书签 a所在的行`a # 跳转到书签 a所在位置`. # 跳转到上一次编辑的行'A # 跳转到全文书签 A[' # 跳转到上一个书签]' # 跳转到下一个书签'< # 跳到上次可视模式选择区域的开始'> # 跳到上次可视模式选择区域的结束 帮助信息12345678910111213141516171819:h tutor # 入门文档:h quickref # 快速帮助:h index # 查询 Vim 所有键盘命令定义:h summary # 帮助你更好的使用内置帮助系统:h Ctrl+H # 查询普通模式下 Ctrl+H 是干什么的:h i_Ctrl+H # 查询插入模式下 Ctrl+H 是干什么的:h i_<Up> # 查询插入模式下方向键上是干什么的:h pattern.txt # 正则表达式帮助:h eval # 脚本编写帮助:h function-list # 查看 VimScript 的函数列表 :h windows.txt # 窗口使用帮助:h tabpage.txt # 标签页使用帮助:h +timers # 显示对 +timers 特性的帮助:h :! # 查看如何运行外部命令:h tips # 查看 Vim 内置的常用技巧文档:h set-termcap # 查看如何设置按键扫描码:viusage # NORMAL 模式帮助:exusage # EX 命令帮助:version # 显示当前 Vim 的版本号和特性 外部命令123456789:!command # 执行一次性shell命令,如下命令::!pwd:shell # 启动一个交互的 shell 执行多个命令,exit命令退出并返回 Vim:!ls # 运行外部命令 ls,并等待返回:r !ls # 将外部命令 ls 的输出捕获,并插入到光标后:w !sudo tee % # sudo以后保存当前文件:call system('ls') # 调用 ls 命令,但是不显示返回内容:!start notepad # Windows 下启动 notepad,最前面可以加 silent:sil !start cmd # Windows 下当前目录打开 cmd:%!prog # 运行文字过滤程序,如整理 json格式 :%!python -m json.tool Quickfix 窗口12345678:copen # 打开 quickfix 窗口(查看编译,grep等信息):copen 10 # 打开 quickfix 窗口,并且设置高度为 10:cclose # 关闭 quickfix 窗口:cfirst # 跳到 quickfix 中第一个错误信息:clast # 跳到 quickfix 中最后一条错误信息:cc [nr] # 查看错误 [nr]:cnext # 跳到 quickfix 中下一个错误信息:cprev # 跳到 quickfix 中上一个错误信息 拼写检查1234567:set spell # 打开拼写检查:set nospell # 关闭拼写检查]s # 下一处错误拼写的单词[s # 上一处错误拼写的单词zg # 加入单词到拼写词表中zug # 撤销上一次加入的单词z= # 拼写建议 代码折叠123456789101112131415161718za # 切换折叠zA # 递归切换折叠zc # 折叠光标下代码zC # 折叠光标下所有代码zd # 删除光标下折叠zD # # 递归删除所有折叠zE # 删除所有折叠zf # 创建代码折叠zF # 指定行数创建折叠zi # 切换折叠zm # 所有代码折叠一层zr # 所有代码打开一层zM # 折叠所有代码,设置 foldlevel=0,设置 foldenablezR # 打开所有代码,设置 foldlevel 为最大值zn # 折叠 none,重置 foldenable 并打开所有代码zN # 折叠 normal,重置 foldenable 并恢复所有折叠zo # 打开一层代码zO # 打开光标下所有代码折叠 文档加密解密文档加密 加密方式打开文件时,并在屏幕左下角提示输入密码两次才可进行操作,保存文件退出后必须输入正常密码才能正确打开文件,否则会显示乱码。 123vim -x file_name # 输入加密密码 -> 确认密码! 注意:不修改内容也要保存。:wq,不然密码设定不会生效。:X # 命令行模式下,输入加密密码 -> 确认密码! 注意:不修改内容也要保存。:wq,不然密码设定不会生效。:set key=密码 # 命令行模式下,输入加密密码 -> 确认密码! 注意:不修改内容也要保存。:wq,不然密码设定不会生效。 文档解密 12:X # 命令行模式下,提示输入密码,不输入而是按 Enter。注意:不修改内容也要保存。:wq,不然解密设定不会生效。:set key= # 命令行模式下,设置key的密码为空。注意:不修改内容也要保存。:wq,不然密码设定不会生效。 宏录制12345qa # 开始录制名字为 a 的宏q # 结束录制宏@a # 播放名字为 a 的宏@@ # 播放上一个宏@: # 重复上一个ex命令(即冒号命令) 其他命令123456789101112131415161718192021222324252627282930313233343536373839Ctrl+X Ctrl+E # 插入模式下向上滚屏Ctrl+X Ctrl+Y # 插入模式下向下滚屏Ctrl+G # 显示正在编辑的文件名,以及大小和位置信息g Ctrl+G # 显示文件的:大小,字符数,单词数和行数,可视模式下也可用ga # 显示光标下字符的 ascii 码或者 unicode 编码g8 # 显示光标下字符的 utf-8 编码字节序gi # 回到上次进入插入的地方,并切换到插入模式K # 查询光标下单词的帮助Ctrl+PgUp # 上个标签页,GVim OK,部分终端软件需设置对应键盘码Ctrl+PgDown # 下个标签页,GVim OK,部分终端软件需设置对应键盘码Ctrl+R Ctrl+W # 命令模式下插入光标下单词Ctrl+Insert # 复制到系统剪贴板(GVIM)Shift+Insert # 粘贴系统剪贴板的内容(GVIM):set ff=unix # 设置换行为 unix:set ff=dos # 设置换行为 dos:set ff? # 查看换行设置:set nohl # 清除搜索高亮:set termcap # 查看会从终端接收什么以及会发送给终端什么命令:set guicursor= # 解决 SecureCRT/PenguiNet 中 NeoVim 局部奇怪字符问题:set t_RS= t_SH= # 解决 SecureCRT/PenguiNet 中 Vim8.0 终端功能奇怪字符:set fo+=a # 开启文本段的实时自动格式化:earlier 15m # 回退到15分钟前的文件内容:map # 来查看当前 Vim 配置的 map 快捷键:inoremap # 来查看当前 Vim 配置的 inoremap 快捷键:nnoremap # 来查看当前 Vim 配置的 nnoremap 快捷键:.!date # 在当前窗口插入时间:%!xxd # 开始二进制编辑:%!xxd -r # 保存二进制编辑:r !curl -sL {URL} # 读取 url 内容添加到光标后:g/^\s*$/d # 删除空行:g/green/d # 删除所有包含 green 的行:v/green/d # 删除所有不包含 green 的行:g/gladiolli/# # 搜索单词打印结果,并在结果前加上行号:g/ab.*cd.*efg/# # 搜索包含 ab,cd 和 efg 的行,打印结果以及行号:v/./,/./-j # 压缩空行:Man bash # 在 Vim 中查看 man,先调用 :runtime! ftplugin/man.vim 激活/fred\|joe # 搜索 fred 或者 joe/\<\d\d\d\d\> # 精确搜索四个数字/^\n\{3} # 搜索连续三个空行 查看命令历史命令行模式下:12:history # 查看所有命令行模式下输入的命令历史:history search或 / 或? # 查看搜索历史 普通模式下:123q/ # 查看使用/输入的搜索历史q? # 查看使用?输入的搜索历史q: # 查看命令行历史 寄存器查看寄存器值 12:reg # 查看所有寄存器值:reg "{register_name} # 查看指定寄存器值 调取寄存器值 123"{register_name} # 普通模式下调取寄存器值:Ctrl+r "寄存器名称 # 命令模式下(输入 Ctrl+r 后Vim会自动打出"寄存器引用符号)Ctrl+r 寄存器名称 # 插入模式下(无需输入寄存器引用符号") Vim寄存器分类 名称 引用方式 说明 无名寄存器 “” 默认寄存器,所有的复制和修改操作(x、s、d、c、y)都会将该数据复制到无名寄存器 字母寄存器 “a - “z 或 “A - “Z {register_name}只能是一位的26个英文字母,从a-z,A-Z寄存器内容将会合并到对应小写字母内容后边 复制专用寄存器 “0(数字0) 仅当使用复制操作(y)时,该数据将会同时被复制到无名寄存器和复制专用寄存器 逐级临时缓存寄存器 “1 - “9 所有不带范围(‘(’,‘)’,‘{’,‘}’)、操作涉及1行以上的删除修改操作(x、s、d、c)的数据都会复制到逐级临时缓存寄存器,并在新的数据加入时,逐级先后推移。1的数据复制到2,2的复制到3,最后的9寄存器内容将会被删除 黑洞寄存器 “_ 几乎所有的操作涉及的数据都会被复制到寄存器,如果想让操作的数据不经过寄存器,可以指定黑洞寄存器,数据到该寄存器就会消失掉,不能显示,也不存在 系统剪切板 “+ 或”* 与Vim外部的GUI交互数据时,需要使用专用的系统剪切板 表达式寄存器 “= 所有寄存器里最特殊的一个,用于计算表达式。输入完该寄存器应用后,会在命令行里提示“=”,按需输入表达式,结果将会显示到光标处 其他寄存器 Vim设置编辑Vim配置文件: 12:edit $MYVIMRC # 在Vim的命令模式下使用该命令打开 vim 配置文件:source $MYVIMRC # Vim 配置文件的改动后,使用该命令为vim配置文件加载新的配置选项,如果vimrc文件恰好是当前活动的缓冲区,则可把此命令简化为:so %。 Vim配置说明: 注:Vim配置可以在命令模式下单个设置,只在当前窗口生效!123456789101112131415161718192021222324252627282930syntax # 列出已经定义的语法项syntax clear # 清除已定义的语法规则syntax on # 允许语法高亮syntax off # 禁止语法高亮set history=200 # 记录 200 条历史命令set bs=? # 设置BS键模式,现代编辑器为 :set bs=eol,start,indentset sw=4 # 设置缩进宽度为 4set ts=4 # 设置制表符宽度为 4set noet # 设置不展开 tab 成空格set et # 设置展开 tab 成空格set winaltkeys=no # 设置 GVim 下正常捕获 ALT 键set nowrap # 关闭自动换行set ttimeout # 允许终端按键检测超时(终端下功能键为一串ESC开头的扫描码)set ttm=100 # 设置终端按键检测超时为100毫秒set term=? # 设置终端类型,比如常见的 xtermset ignorecase # 设置搜索是否忽略大小写set smartcase # 智能大小写,默认忽略大小写,除非搜索内容里包含大写字母set list # 设置显示制表符和换行符set nu # 设置显示行号,禁止显示行号可以用 :set nonuset number # 设置显示行号,禁止显示行号可以用 :set nonumberset relativenumber # 设置显示相对行号(其他行与当前行的距离)set paste # 进入粘贴模式(粘贴时禁用缩进等影响格式的东西)set nopaste # 结束粘贴模式set spell # 允许拼写检查set hlsearch # 设置高亮查找set ruler # 总是显示光标位置set nocompatible # 设置不兼容原始 vi 模式(必须设置在最开头)set incsearch # 查找输入时动态增量显示查找结果set insertmode # Vim 始终处于插入模式下,使用 Ctrl+o 临时执行命令set all # 列出所有选项设置情况 Vim插件插件 - vim-commentaryvim-commentary:批量注释工具, 可以注释多行和去除多行注释。 12345gcc # 注释当前行gc{motion} # 注释 {motion} 所标注的区域,比如 gcap 注释整段gci{ # 注释大括号内的内容gc # 在 Visual Mode 下面按 gc 注释选中区域:7,17Commentary # 注释 7 到 17 行 插件 - nerdtreenerdtree:该插件用于列出当前路径的目录树。 123456789101112131415161718192021222324252627? # 快速帮助文档o # 打开一个目录或者打开文件,创建的是buffer,也可以用来打开书签go # 打开一个文件,但是光标仍然留在NERDTree,创建的是buffert # 打开一个文件,创建的是Tab,对书签同样生效T # 打开一个文件,但是光标仍然留在NERDTree,创建的是Tab,对书签同样生效i # 水平分割创建文件的窗口,创建的是buffergi # 水平分割创建文件的窗口,但是光标仍然留在NERDTrees # 垂直分割创建文件的窗口,创建的是buffergs # 和gi,go类似x # 收起当前打开的目录X # 收起所有打开的目录e # 以文件管理的方式打开选中的目录D # 删除书签P # 大写,跳转到当前根路径p # 小写,跳转到光标所在的上一级路径K # 跳转到第一个子路径J # 跳转到最后一个子路径Ctrl+j和Ctrl+k # 在同级目录和文件间移动,忽略子目录和子文件C # 将根路径设置为光标所在的目录u # 设置上级目录为根路径U # 设置上级目录为跟路径,但是维持原来目录打开的状态r # 刷新光标所在的目录R # 刷新当前根路径I # 显示或者不显示隐藏文件f # 打开和关闭文件过滤器q # 关闭NERDTreeA # 全屏显示NERDTree,或者关闭全屏 插件 - asyncrun.vim12:AsyncRun ls # 异步运行命令 ls 结果输出到 quickfix 使用 :copen 查看:AsyncRun -raw ls # 异步运行命令 ls 结果不匹配 errorformat Vim模式12345普通模式 # 按 Esc 或 Ctrl+[ 进入,左下角显示文件名或为空插入模式 # 按i进入,左下角显示 --INSERT--可视模式 # 按v进入,左下角显示 --VISUAL--替换模式 # 按r或R开始,左下角显示 --REPLACE--命令行模式 # 按 : 或者 / 或者 ? 开始 网络资源 最新版本 https://github.com/vim/vim Windows 最新版 https://github.com/vim/vim-win32-installer/releases 插件浏览 http://vimawesome.com 正确设置ALT/BS键 http://www.skywind.me/blog/archives/2021 视频教程 http://vimcasts.org/ 中文帮助 http://vimcdoc.sourceforge.net/doc/help.html 中文版入门到精通 https://github.com/wsdjeg/vim-galore-zh_cn 五分钟脚本入门 http://www.skywind.me/blog/archives/2193 脚本精通 http://learnvimscriptthehardway.stevelosh.com/ 十六年使用经验 http://zzapper.co.uk/vimtips.html 配色方案 http://vimcolors.com/ TIPS 永远不要用 Ctrl+C 代替 完全不同的含义,容易错误中断运行的后台脚本 很多人使用 Ctrl+[ 代替 ,左手小指 Ctrl,右手小指 [ 熟练后很方便 某些终端中使用 Vim 8 内嵌终端如看到奇怪字符,使用 :set t_RS= t_SH= 解决 某些终端中使用 NeoVim 如看到奇怪字符,使用 :set guicursor= 解决 多使用 ciw, ci[, ci”, ci( 以及 diw, di[, di”, di( 命令来快速改写/删除文本 在行内左右移动光标时,多使用w b e或W B E,而不是h l或方向键,这样会快很多 Shift 相当于移动加速键, w b e 移动光标很慢,但是 W B E 走的很快 自己要善于总结新技巧,比如移动到行首非空字符时用 0w 命令比 ^ 命令更容易输入 在空白行使用 dip 命令可以删除所有临近的空白行,viw 可以选择连续空白 缩进时使用 >8j >} ap =i} == 会方便很多 插入模式下,当你发现一个单词写错了,应该多用 Ctrl+W 这比 快 y d c 命令可以很好结合 f t 和 /X 比如 dt) 和 y/end c d x 命令会自动填充寄存器 “1 到 “9 , y 命令会自动填充 “0 寄存器 用 v 命令选择文本时,可以用 o 掉头选择,有时很有用 写文章时,可以写一段代码块,然后选中后执行 :!python 代码块就会被替换成结果 搜索后经常使用 :nohl 来消除高亮,使用很频繁,可以 map 到 上 搜索时可以用 Ctrl+R Ctrl+W 插入光标下的单词,命令模式也能这么用 映射按键时,应该默认使用 noremap ,只有特别需要的时候使用 map 当你觉得做某事很低效时,你应该停下来,然后思考正确的高效方式来完成 用 y 复制文本后,命令模式中 Ctrl+R 然后按双引号 0 可以插入之前复制内容 Windows 下的 GVim 可以设置 set rop=type:directx,renmode:5 增强显示 书籍Vim实用技巧 Vim键盘图 参考 https://github.com/skywind3000/awesome-cheatsheets/blob/master/editors/vim.txt https://github.com/groenewege/vimrc/blob/master/vim_cheat_sheet.txt http://blog.g-design.net/post/4789778607/vim-cheat-sheet http://www.keyxl.com/aaa8263/290/VIM-keyboard-shortcuts.htm http://jmcpherson.org/editing.html http://www.fprintf.net/vimCheatSheet.html http://www.ouyaoxiazai.com/article/24/654.html http://bbs.it-home.org/thread-80794-1-1.html http://www.lpfrx.com/wp-content/uploads/2008/09/vi.jpg http://michael.peopleofhonoronly.com/vim/ https://github.com/hobbestigrou/vimtips-fortune/blob/master/fortunes/vimtips https://github.com/glts/vim-cottidie/blob/master/autoload/cottidie/tips]]></content>
<categories>
<category>Vim</category>
</categories>
<tags>
<tag>Vim</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring文件上传下载]]></title>
<url>%2F%2Fblog%2Fspring-ud%2F</url>
<content type="text"><![CDATA[注:本文原链接:https://www.cnblogs.com/chloneda/p/spring-ud.html 前言开发人员多少都会遇到文件的上传下载,特别是Java的Spring框架。那么我们能不能实现一些通用工具类,实现文件上传和下载的功能,可以有效地避免重复开发代码。 因此,我在网上查找了相关资料,并在此基础上加以改进,支持文件上传及下载功能,而且代码非常精简。如有问题,欢迎大家指正! 文件上传文件上传及下载需要两个依赖: 123456789101112<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.1.6.RELEASE</version></dependency><!--tomcat中也有servlet-api包,避免冲突添加<scope>provided</scope>--><dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope></dependency> Spring文件上传一般从请求中获取文件对象,或者直接上传文件获取文件对象。这里提供两个方法,可以满足以上两种情形!当然,如果想自定义文件上传路径,也可以在这两个方法的基础上再增加一个文件路径参数,具体还是看大家的需求情况吧! 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465package com.chloneda.utils;import org.springframework.web.multipart.MultipartFile;import org.springframework.web.multipart.MultipartHttpServletRequest;import org.springframework.web.multipart.commons.CommonsMultipartResolver;import javax.servlet.http.HttpServletRequest;import java.io.File;import java.io.IOException;import java.util.*;/** * @author chloneda * @description: 文件上传工具类 */public class RequestUtils { /** * 获取请求中的文件并上传 * * @param request 请求对象 * @param useNewName 是否使用原文件名称 */ public static void uploadFileFromRequest(HttpServletRequest request, boolean useNewName) throws IOException { CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(request.getSession().getServletContext()); /** 判断 request 是否有文件上传 */ if (multipartResolver.isMultipart(request)) { /** 向上转型获取更多功能 */ MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request; Iterator<String> iter = multiRequest.getFileNames(); while (iter.hasNext()) { MultipartFile multipartFile = multiRequest.getFile(iter.next()); if (multipartFile.isEmpty()) { System.out.println("文件未上传!"); continue; } uploadFileFromMultipartFile(multipartFile, request, useNewName); } } } /** * 获取文件并上传至指定目录 * * @param file 上传的文件 * @param useNewName 是否使用原文件名称 * @throws IOException */ public static void uploadFileFromMultipartFile( MultipartFile file, HttpServletRequest request, boolean useNewName) throws IOException { /** 获取服务器项目发布运行所在地址 */ String uploadDir = request.getSession().getServletContext().getRealPath("/") + "upload"; String originalName = file.getOriginalFilename(); /** 使用时间缀解决文件重名问题 */ if (useNewName) { long timestamp = System.currentTimeMillis(); originalName = timestamp + "-" + originalName; } String destFilePath = uploadDir + File.separator + originalName; file.transferTo(new File(destFilePath)); }} 文件下载文件下载需要知道文件所在路径,并通过文件流的响应方式返回给前端下载,具体代码如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081package com.chloneda.utils;import javax.servlet.http.HttpServletResponse;import java.io.*;import java.net.URLEncoder;import java.nio.charset.StandardCharsets;/** * @author chloneda * @description: 返回文件流至浏览器 */public class ResponseUtils { /** * 返回文件流至前端进行展示 * * @param srcFilePath * @param contentType 展示contentType一般是image/jpeg、jpg、gif、png等 * @param response */ public static void responseFileForShow( HttpServletResponse response, String srcFilePath, String contentType) { File file = checkFile(srcFilePath, false); responseFile(response, file, contentType, false); } /** * 返回文件流至前端进行下载 * * @param srcFilePath 返回文件的具体路径 * @param response */ public static void responseFileForLoad(HttpServletResponse response, String srcFilePath) { File file = checkFile(srcFilePath, false); responseFile(response, file, "application/octet-stream", true); } /** * 使用response返回文件流 * * @param response response进行流的返回 * @param file 要返回的具体文件 * @param contentType contentType类型 * @param isLoad true时下载,false时展示 */ private static void responseFile( HttpServletResponse response, File file, String contentType, boolean isLoad) { try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file)); BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream())) { response.setContentType(contentType); /** 如果是下载,指定文件名称 */ if (isLoad) { String fileName = file.getName(); /** 指定字符集解决下载文件名乱码 */ response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())); } byte[] buffer = new byte[8192]; int count = 0; while ((count = in.read(buffer, 0, 8192)) != -1) { out.write(buffer, 0, count); } out.flush(); } catch (IOException e) { e.printStackTrace(); } } private static File checkFile(String filePath, boolean create) { File file = new File(filePath); if (!file.exists()) { if (create) { file.mkdirs(); } else { throw new IllegalArgumentException("文件不存在:{}!" + filePath); } } return file; }} 小结文件的上传下载是比较普遍的功能需求,这里通过请求响应的方式实现,可以满足日常的开发需求!]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>Spring</tag>
</tags>
</entry>
<entry>
<title><![CDATA[pom.xml配置文件详解(Maven)]]></title>
<url>%2F%2Fblog%2Fmaven-pom%2F</url>
<content type="text"><![CDATA[注:本文转载自:https://blog.csdn.net/u012152619/article/details/51485297 注:博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 最近打算对Maven的pom.xml文件进行一下深入研究,做一下总结,惊喜的是网上已经有比较详细的资料,为避免重复造轮子,所以转载了这篇文章。 前言setting.xml主要用于配置maven的运行环境等一系列通用的属性,是全局级别的配置文件;而pom.xml主要描述了项目的maven坐标,依赖关系,开发者需要遵循的规则,缺陷管理系统,组织和licenses,以及其他所有的项目相关因素,是项目级别的配置文件。 基础配置一个典型的pom.xml文件配置如下:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd"> <!-- 模型版本。maven2.0必须是这样写,现在是maven2唯一支持的版本 --> <modelVersion>4.0.0</modelVersion> <!-- 公司或者组织的唯一标志,并且配置时生成的路径也是由此生成, 如com.winner.trade,maven会将该项目打成的jar包放本地路径:/com/winner/trade --> <groupId>com.winner.trade</groupId> <!-- 本项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 --> <artifactId>trade-core</artifactId> <!-- 本项目目前所处的版本号 --> <version>1.0.0-SNAPSHOT</version> <!-- 打包的机制,如pom,jar, maven-plugin, ejb, war, ear, rar, par,默认为jar --> <packaging>jar</packaging> <!-- 帮助定义构件输出的一些附属构件,附属构件与主构件对应,有时候需要加上classifier才能唯一的确定该构件 不能直接定义项目的classifer,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成的 --> <classifier>...</classifier> <!-- 定义本项目的依赖关系 --> <dependencies> <!-- 每个dependency都对应这一个jar包 --> <dependency> <!--一般情况下,maven是通过groupId、artifactId、version这三个元素值(俗称坐标)来检索该构件, 然后引入你的工程。如果别人想引用你现在开发的这个项目(前提是已开发完毕并发布到了远程仓库),--> <!--就需要在他的pom文件中新建一个dependency节点,将本项目的groupId、artifactId、version写入, maven就会把你上传的jar包下载到他的本地 --> <groupId>com.winner.trade</groupId> <artifactId>trade-test</artifactId> <version>1.0.0-SNAPSHOT</version> <!-- maven认为,程序对外部的依赖会随着程序的所处阶段和应用场景而变化,所以maven中的依赖关系有作用域(scope)的限制。 --> <!--scope包含如下的取值:compile(编译范围)、provided(已提供范围)、runtime(运行时范围)、test(测试范围)、system(系统范围) --> <scope>test</scope> <!-- 继承自该项目的所有子项目的默认依赖信息。这部分的依赖信息不会被立即解析,而是当子项目声明一个依赖(必须描述group ID和 artifact ID信息),如果group ID和artifact ID以外的一些信息没有描述,则通过group ID和artifact ID 匹配到这里的依赖,并使用这里的依赖信息。 --> <dependencyManagement> <dependencies> <!--参见dependencies/dependency元素 --> <dependency> ...... </dependency> </dependencies> </dependencyManagement> <!-- 设置指依赖是否可选,默认为false,即子项目默认都继承:为true,则子项目必需显示的引入,与dependencyManagement里定义的依赖类似 --> <optional>false</optional> <!-- 屏蔽依赖关系。 比如项目中使用的libA依赖某个库的1.0版,libB依赖某个库的2.0版,现在想统一使用2.0版,就应该屏蔽掉对1.0版的依赖 --> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <!-- 为pom定义一些常量,在pom中的其它地方可以直接引用 使用方式 如下 :${file.encoding} --> <properties> <file.encoding>UTF-8</file.encoding> <java.source.version>1.5</java.source.version> <java.target.version>1.5</java.target.version> </properties> ...</project> 一般来说,上面的几个配置项对任何项目都是必不可少的,定义了项目的基本属性。 这里有必要对一个不太常用的属性classifier做一下解释,因为有时候引用某个jar包,classifier不写的话会报错。 classifier元素用来帮助定义构件输出的一些附属构件。附属构件与主构件对应,比如主构件是 kimi-app-2.0.0.jar,该项目可能还会通过使用一些插件生成 如kimi-app-2.0.0-javadoc.jar (Java文档)、 kimi-app-2.0.0-sources.jar(Java源代码) 这样两个附属构件。这时候,javadoc、sources就是这两个附属构件的classifier,这样附属构件也就拥有了自己唯一的坐标。 classifier的用途 maven download javadoc / sources jar包的时候,需要借助classifier指明要下载那个附属构件 引入依赖的时候,有时候仅凭groupId、artifactId、version无法唯一的确定某个构件,需要借助classifier来进一步明确目标。比如JSON-lib,有时候会同一个版本会提供多个jar包,在JDK1.5环境下是一套,在JDK1.3环境下是一套: 引用它的时候就要注明JDK版本,否则maven不知道你到底需要哪一套jar包:123456<dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.4</version> <classifier>jdk15</classifier></dependency> 构建配置123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152<build> <!-- 产生的构件的文件名,默认值是${artifactId}-${version}。 --> <finalName>myPorjectName</finalName> <!-- 构建产生的所有文件存放的目录,默认为${basedir}/target,即项目根目录下的target --> <directory>${basedir}/target</directory> <!--当项目没有规定目标(Maven2叫做阶段(phase))时的默认值, --> <!--必须跟命令行上的参数相同例如jar:jar,或者与某个阶段(phase)相同例如install、compile等 --> <defaultGoal>install</defaultGoal> <!--当filtering开关打开时,使用到的过滤器属性文件列表。 --> <!--项目配置信息中诸如${spring.version}之类的占位符会被属性文件中的实际值替换掉 --> <filters> <filter>../filter.properties</filter> </filters> <!--项目相关的所有资源路径列表,例如和项目相关的配置文件、属性文件,这些资源被包含在最终的打包文件里。 --> <resources> <resource> <!--描述了资源的目标路径。该路径相对target/classes目录(例如${project.build.outputDirectory})。 --> <!--举个例子,如果你想资源在特定的包里(org.apache.maven.messages),你就必须该元素设置为org/apache/maven/messages。 --> <!--然而,如果你只是想把资源放到源码目录结构里,就不需要该配置。 --> <targetPath>resources</targetPath> <!--是否使用参数值代替参数名。参数值取自properties元素或者文件里配置的属性,文件在filters元素里列出。 --> <filtering>true</filtering> <!--描述存放资源的目录,该路径相对POM路径 --> <directory>src/main/resources</directory> <!--包含的模式列表 --> <includes> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <!--排除的模式列表 如果<include>与<exclude>划定的范围存在冲突,以<exclude>为准 --> <excludes> <exclude>jdbc.properties</exclude> </excludes> </resource> </resources> <!--单元测试相关的所有资源路径,配制方法与resources类似 --> <testResources> <testResource> <targetPath /> <filtering /> <directory /> <includes /> <excludes /> </testResource> </testResources> <!--项目源码目录,当构建项目的时候,构建系统会编译目录里的源码。该路径是相对于pom.xml的相对路径。 --> <sourceDirectory>${basedir}\src\main\java</sourceDirectory> <!--项目脚本源码目录,该目录和源码目录不同, <!-- 绝大多数情况下,该目录下的内容会被拷贝到输出目录(因为脚本是被解释的,而不是被编译的)。 --> <scriptSourceDirectory>${basedir}\src\main\scripts </scriptSourceDirectory> <!--项目单元测试使用的源码目录,当测试项目的时候,构建系统会编译目录里的源码。该路径是相对于pom.xml的相对路径。 --> <testSourceDirectory>${basedir}\src\test\java</testSourceDirectory> <!--被编译过的应用程序class文件存放的目录。 --> <outputDirectory>${basedir}\target\classes</outputDirectory> <!--被编译过的测试class文件存放的目录。 --> <testOutputDirectory> ${basedir}\target\test-classes </testOutputDirectory> <!--项目的一系列构建扩展,它们是一系列build过程中要使用的产品,会包含在running bulid‘s classpath里面。 --> <!--他们可以开启extensions,也可以通过提供条件来激活plugins。 --> <!--简单来讲,extensions是在build过程被激活的产品 --> <extensions> <!--例如,通常情况下,程序开发完成后部署到线上Linux服务器,可能需要经历打包、 --> <!--将包文件传到服务器、SSH连上服务器、敲命令启动程序等一系列繁琐的步骤。 --> <!--实际上这些步骤都可以通过Maven的一个插件 wagon-maven-plugin 来自动完成 --> <!--下面的扩展插件wagon-ssh用于通过SSH的方式连接远程服务器, --> <!--类似的还有支持ftp方式的wagon-ftp插件 --> <extension> <groupId>org.apache.maven.wagon</groupId> <artifactId>wagon-ssh</artifactId> <version>2.8</version> </extension> </extensions> <!--使用的插件列表 。 --> <plugins> <plugin> <groupId></groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.5.5</version> <!--在构建生命周期中执行一组目标的配置。每个目标可能有不同的配置。 --> <executions> <execution> <!--执行目标的标识符,用于标识构建过程中的目标,或者匹配继承过程中需要合并的执行目标 --> <id>assembly</id> <!--绑定了目标的构建生命周期阶段,如果省略,目标会被绑定到源数据里配置的默认阶段 --> <phase>package</phase> <!--配置的执行目标 --> <goals> <goal>single</goal> </goals> <!--配置是否被传播到子POM --> <inherited>false</inherited> </execution> </executions> <!--作为DOM对象的配置,配置项因插件而异 --> <configuration> <finalName>${finalName}</finalName> <appendAssemblyId>false</appendAssemblyId> <descriptor>assembly.xml</descriptor> </configuration> <!--是否从该插件下载Maven扩展(例如打包和类型处理器), --> <!--由于性能原因,只有在真需要下载时,该元素才被设置成true。 --> <extensions>false</extensions> <!--项目引入插件所需要的额外依赖 --> <dependencies> <dependency>...</dependency> </dependencies> <!--任何配置是否被传播到子项目 --> <inherited>true</inherited> </plugin> </plugins> <!--主要定义插件的共同元素、扩展元素集合,类似于dependencyManagement, --> <!--所有继承于此项目的子项目都能使用。该插件配置项直到被引用时才会被解析或绑定到生命周期。 --> <!--给定插件的任何本地配置都会覆盖这里的配置 --> <pluginManagement> <plugins>...</plugins> </pluginManagement> </build> pom里面的仓库与setting.xml里的仓库功能是一样的。主要的区别在于,pom里的仓库是个性化的。比如一家大公司里的setting文件是公用的,所有项目都用一个setting文件,但各个子项目却会引用不同的第三方库,所以就需要在pom里设置自己需要的仓库地址。 分发配置1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768<!--项目分发信息,在执行mvn deploy后表示要发布的位置。 --><!--有了这些信息就可以把网站部署到远程服务器或者把构件部署到远程仓库。 --><distributionManagement> <!--部署项目产生的构件到远程仓库需要的信息 --> <repository> <!--是分配给快照一个唯一的版本号(由时间戳和构建流水号),还是每次都使用相同的版本号 --> <!--参见repositories/repository元素 --> <uniqueVersion>true</uniqueVersion> <id> repo-id </id> <name> repo-name</name> <url>file://${basedir}/target/deploy </url> <layout /> </repository> <!--构件的快照部署到哪里,如果没有配置该元素,默认部署到repository元素配置的仓库 --> <snapshotRepository> <uniqueVersion /> <id /> <name /> <url /> <layout /> </snapshotRepository> <!--部署项目的网站需要的信息 --> <site> <!--部署位置的唯一标识符,用来匹配站点和settings.xml文件里的配置 --> <id> site-id </id> <!--部署位置的名称 --> <name> site-name</name> <!--部署位置的URL,按protocol://hostname/path形式 --> <url>scp://svn.baidu.com/banseon:/var/www/localhost/banseon-web </url> </site> <!--项目下载页面的URL。如果没有该元素,用户应该参考主页。 --> <!--使用该元素的原因是:帮助定位那些不在仓库里的构件(由于license限制)。 --> <downloadUrl /> <!--如果构件有了新的groupID和artifact ID(构件移到了新的位置),这里列出构件的重定位信息。 --> <relocation> <!--构件新的group ID --> <groupId /> <!--构件新的artifact ID --> <artifactId /> <!--构件新的版本号 --> <version /> <!--显示给用户的,关于移动的额外信息,例如原因。 --> <message /> </relocation> <!--给出该构件在远程仓库的状态。不得在本地项目中设置该元素,因为这是工具自动更新的。 --> <!--有效的值有:none(默认),converted(仓库管理员从Maven 1 POM转换过来), --> <!--partner(直接从伙伴Maven 2仓库同步过来),deployed(从Maven 2实例部署),verified(被核实时正确的和最终的)。 --> <status /> </distributionManagement> 仓库配置12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758<!--发现依赖和扩展的远程仓库列表。 --><repositories> <!--包含需要连接到远程仓库的信息 --> <repository> <!--如何处理远程仓库里发布版本的下载 --> <releases> <!--true或者false表示该仓库是否为下载某种类型构件(发布版,快照版)开启。 --> <enabled /> <!--该元素指定更新发生的频率。Maven会比较本地POM和远程POM的时间戳。 --> <!--这里的选项是:always(一直),daily(默认,每日), --> <!--interval:X(这里X是以分钟为单位的时间间隔),或者never(从不)。 --> <updatePolicy /> <!--当Maven验证构件校验文件失败时该怎么做: --> <!--ignore(忽略),fail(失败),或者warn(警告)。 --> <checksumPolicy /> </releases> <!--如何处理远程仓库里快照版本的下载。有了releases和snapshots这两组配置, --> <!--POM就可以在每个单独的仓库中,为每种类型的构件采取不同的策略。 --> <!--例如,可能有人会决定只为开发目的开启对快照版本下载的支持 --> <snapshots> <enabled /> <updatePolicy /> <checksumPolicy /> </snapshots> <!--远程仓库唯一标识符。可以用来匹配在settings.xml文件里配置的远程仓库 --> <id> repo-id </id> <!--远程仓库名称 --> <name> repo-name</name> <!--远程仓库URL,按protocol://hostname/path形式 --> <url>http://192.168.1.169:9999/repository/ </url> <!--用于定位和排序构件的仓库布局类型-可以是default(默认)或者legacy(遗留)。 --> <!--Maven 2为其仓库提供了一个默认的布局; --> <!--然而,Maven1.x有一种不同的布局。 --> <!--我们可以使用该元素指定布局是default(默认)还是legacy(遗留)。 --> <layout> default</layout> </repository> </repositories> <!--发现插件的远程仓库列表,这些插件用于构建和报表 --><pluginRepositories> <!--包含需要连接到远程插件仓库的信息.参见repositories/repository元素 --> <pluginRepository /> </pluginRepositories> profile配置12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667<!--在列的项目构建profile,如果被激活,会修改构建处理 --><profiles> <!--根据环境参数或命令行参数激活某个构建处理 --> <profile> <!--自动触发profile的条件逻辑。Activation是profile的开启钥匙。 --> <activation> <!--profile默认是否激活的标识 --> <activeByDefault>false</activeByDefault> <!--activation有一个内建的java版本检测,如果检测到jdk版本与期待的一样,profile被激活。 --> <jdk>1.7</jdk> <!--当匹配的操作系统属性被检测到,profile被激活。os元素可以定义一些操作系统相关的属性。 --> <os> <!--激活profile的操作系统的名字 --> <name>Windows XP</name> <!--激活profile的操作系统所属家族(如 'windows') --> <family>Windows</family> <!--激活profile的操作系统体系结构 --> <arch>x86</arch> <!--激活profile的操作系统版本 --> <version>5.1.2600</version> </os> <!--如果Maven检测到某一个属性(其值可以在POM中通过${名称}引用),其拥有对应的名称和值,Profile就会被激活。 --> <!-- 如果值字段是空的,那么存在属性名称字段就会激活profile,否则按区分大小写方式匹配属性值字段 --> <property> <!--激活profile的属性的名称 --> <name>mavenVersion</name> <!--激活profile的属性的值 --> <value>2.0.3</value> </property> <!--提供一个文件名,通过检测该文件的存在或不存在来激活profile。missing检查文件是否存在,如果不存在则激活profile。 --> <!--另一方面,exists则会检查文件是否存在,如果存在则激活profile。 --> <file> <!--如果指定的文件存在,则激活profile。 --> <exists>/usr/local/hudson/hudson-home/jobs/maven-guide-zh-to-production/workspace/</exists> <!--如果指定的文件不存在,则激活profile。 --> <missing>/usr/local/hudson/hudson-home/jobs/maven-guide-zh-to-production/workspace/</missing> </file> </activation> <id /> <build /> <modules /> <repositories /> <pluginRepositories /> <dependencies /> <reporting /> <dependencyManagement /> <distributionManagement /> <properties /> </profile> profile配置项在setting.xml中也有,是pom.xml中profile元素的裁剪版本,包含了id,activation, repositories, pluginRepositories和 properties元素。这里的profile元素只包含这五个子元素是因为setting.xml只关心构建系统这个整体(这正是settings.xml文件的角色定位),而非单独的项目对象模型设置。如果一个settings中的profile被激活,它的值会覆盖任何其它定义在POM中或者profile.xml中的带有相同id的profile。 pom.xml中的profile可以看做pom.xml的副本,拥有与pom.xml相同的子元素与配置方法。它包含可选的activation(profile的触发器)和一系列的changes。例如test过程可能会指向不同的数据库(相对最终的deployment)或者不同的dependencies或者不同的repositories,并且是根据不同的JDK来改变的。只需要其中一个成立就可以激活profile,如果第一个条件满足了,那么后面就不会在进行匹配。 报表配置123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354<!--描述使用报表插件产生报表的规范,特定的maven 插件能输出相应的定制和配置报表. --><!--当用户执行“mvn site”,这些报表就会运行,在页面导航栏能看到所有报表的链接。 --><reporting> <!--true,则网站不包括默认的报表。这包括“项目信息”菜单中的报表。 --> <excludeDefaults /> <!--所有产生的报表存放到哪里。默认值是${project.build.directory}/site。 --> <outputDirectory /> <!--使用的报表插件和他们的配置。 --> <plugins> <plugin> <groupId /> <artifactId /> <version /> <inherited /> <configuration> <links> <link>http://java.sun.com/j2se/1.5.0/docs/api/</link> </links> </configuration> <!--一组报表的多重规范,每个规范可能有不同的配置。 --> <!--一个规范(报表集)对应一个执行目标 。例如,有1,2,3,4,5,6,7,8,9个报表。 --> <!--1,2,5构成A报表集,对应一个执行目标。2,5,8构成B报表集,对应另一个执行目标 --> <reportSets> <!--表示报表的一个集合,以及产生该集合的配置 --> <reportSet> <!--报表集合的唯一标识符,POM继承时用到 --> <id>sunlink</id> <!--产生报表集合时,被使用的报表的配置 --> <configuration /> <!--配置是否被继承到子POMs --> <inherited /> <!--这个集合里使用到哪些报表 --> <reports> <report>javadoc</report> </reports> </reportSet> </reportSets> </plugin> </plugins> </reporting> 环境配置12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152<!--项目的问题管理系统(Bugzilla, Jira, Scarab,或任何你喜欢的问题管理系统)的名称和URL,本例为 jira --><issueManagement> <!--问题管理系统(例如jira)的名字, --> <system> jira </system> <!--该项目使用的问题管理系统的URL --> <url> http://jira.clf.com/</url> </issueManagement> <!--项目持续集成信息 --><ciManagement> <!--持续集成系统的名字,例如continuum --> <system /> <!--该项目使用的持续集成系统的URL(如果持续集成系统有web接口的话)。 --> <url /> <!--构建完成时,需要通知的开发者/用户的配置项。包括被通知者信息和通知条件(错误,失败,成功,警告) --> <notifiers> <!--配置一种方式,当构建中断时,以该方式通知用户/开发者 --> <notifier> <!--传送通知的途径 --> <type /> <!--发生错误时是否通知 --> <sendOnError /> <!--构建失败时是否通知 --> <sendOnFailure /> <!--构建成功时是否通知 --> <sendOnSuccess /> <!--发生警告时是否通知 --> <sendOnWarning /> <!--不赞成使用。通知发送到哪里 --> <address /> <!--扩展配置项 --> <configuration /> </notifier> </notifiers> </ciManagement> 项目信息配置123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157<!--项目的名称, Maven产生的文档用 --><name>banseon-maven </name> <!--项目主页的URL, Maven产生的文档用 --><url>http://www.clf.com/ </url> <!--项目的详细描述, Maven 产生的文档用。 当这个元素能够用HTML格式描述时 --><!--(例如,CDATA中的文本会被解析器忽略,就可以包含HTML标签),不鼓励使用纯文本描述。 --><!-- 如果你需要修改产生的web站点的索引页面,你应该修改你自己的索引页文件,而不是调整这里的文档。 --><description>A maven project to study maven. </description> <!--描述了这个项目构建环境中的前提条件。 --><prerequisites> <!--构建该项目或使用该插件所需要的Maven的最低版本 --> <maven /> </prerequisites> <!--项目创建年份,4位数字。当产生版权信息时需要使用这个值。 --><inceptionYear /> <!--项目相关邮件列表信息 --><mailingLists> <!--该元素描述了项目相关的所有邮件列表。自动产生的网站引用这些信息。 --> <mailingList> <!--邮件的名称 --> <name> Demo </name> <!--发送邮件的地址或链接,如果是邮件地址,创建文档时,mailto: 链接会被自动创建 --> <post> clf@126.com</post> <!--订阅邮件的地址或链接,如果是邮件地址,创建文档时,mailto: 链接会被自动创建 --> <subscribe> clf@126.com</subscribe> <!--取消订阅邮件的地址或链接,如果是邮件地址,创建文档时,mailto: 链接会被自动创建 --> <unsubscribe> clf@126.com</unsubscribe> <!--你可以浏览邮件信息的URL --> <archive> http:/hi.clf.com/</archive> </mailingList> </mailingLists> <!--项目开发者列表 --><developers> <!--某个项目开发者的信息 --> <developer> <!--SCM里项目开发者的唯一标识符 --> <id> HELLO WORLD </id> <!--项目开发者的全名 --> <name> banseon </name> <!--项目开发者的email --> <email> banseon@126.com</email> <!--项目开发者的主页的URL --> <url /> <!--项目开发者在项目中扮演的角色,角色元素描述了各种角色 --> <roles> <role> Project Manager</role> <role>Architect </role> </roles> <!--项目开发者所属组织 --> <organization> demo</organization> <!--项目开发者所属组织的URL --> <organizationUrl>http://hi.clf.com/ </organizationUrl> <!--项目开发者属性,如即时消息如何处理等 --> <properties> <dept> No </dept> </properties> <!--项目开发者所在时区, -11到12范围内的整数。 --> <timezone> -5</timezone> </developer> </developers> <!--项目的其他贡献者列表 --><contributors> <!--项目的其他贡献者。参见developers/developer元素 --> <contributor> <name /> <email /> <url /> <organization /> <organizationUrl /> <roles /> <timezone /> <properties /> </contributor> </contributors> <!--该元素描述了项目所有License列表。应该只列出该项目的license列表,不要列出依赖项目的license列表。 --><!--如果列出多个license,用户可以选择它们中的一个而不是接受所有license。 --><licenses> <!--描述了项目的license,用于生成项目的web站点的license页面,其他一些报表和validation也会用到该元素。 --> <license> <!--license用于法律上的名称 --> <name> Apache 2 </name> <!--官方的license正文页面的URL --> <url>http://www.clf.com/LICENSE-2.0.txt </url> <!--项目分发的主要方式: repo,可以从Maven库下载 manual, 用户必须手动下载和安装依赖 --> <distribution> repo</distribution> <!--关于license的补充信息 --> <comments> Abusiness-friendly OSS license </comments> </license> </licenses> <!--SCM(Source Control Management)标签允许你配置你的代码库,供Maven web站点和其它插件使用。 --><scm> <!--SCM的URL,该URL描述了版本库和如何连接到版本库。欲知详情,请看SCMs提供的URL格式和列表。该连接只读。 --> <connection>scm:svn:http://svn.baidu.com/banseon/maven/</connection> <!--给开发者使用的,类似connection元素。即该连接不仅仅只读 --> <developerConnection>scm:svn:http://svn.baidu.com/banseon/maven/ </developerConnection> <!--当前代码的标签,在开发阶段默认为HEAD --> <tag /> <!--指向项目的可浏览SCM库(例如ViewVC或者Fisheye)的URL。 --> <url> http://svn.baidu.com/banseon</url> </scm> <!--描述项目所属组织的各种属性。Maven产生的文档用 --><organization> <!--组织的全名 --> <name> demo </name> <!--组织主页的URL --> <url> http://www.clf.com/</url> </organization> Maven POM]]></content>
<categories>
<category>Maven</category>
</categories>
<tags>
<tag>Maven</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java基础系列-如何优雅地使用close()]]></title>
<url>%2F%2Fblog%2Fjava-close%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 前言本文尽量采用通俗易懂、循序渐进的方式,让大家真正优雅地使用close()方法! 问题场景平时我们使用资源后一般都会关闭资源,即close()方法,但这个步骤重复性很高,还面临上述执行顺序不明的风险,而且很多人还是不能正确合理地关闭资源。 我们来看看close()是怎么错误地关闭资源的? 错误的close()先来看看如下的错误关闭资源方式: 1234567891011121314151617181920212223242526272829303132333435package com.chloneda.jutils.test;import java.sql.*;/** * @author chloneda * @description: close()方法测试 * 错误的close() */public class CloseTest { public static void main(String[] args) { Connection conn = null; Statement st = null; ResultSet rs = null; try { //1.加载驱动程序 Class.forName("com.mysql.jdbc.Driver"); //2.获得数据库链接 conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/common", "root", "123456"); //3.通过数据库的连接操作数据库,实现增删改查 st = conn.createStatement(); rs = st.executeQuery("select * from new_table"); //4.处理数据库的返回结果 while (rs.next()) { System.out.println(rs.getString("id") + " " + rs.getString("name")); } //5.关闭资源 rs.close(); st.close(); conn.close(); } catch (Exception e) { e.printStackTrace(); } }} 上面代码的资源关闭写在了try代码块中,一旦close方法调用之前(比如3步骤)就抛出异常,那么关闭资源的代码就永远不会得到执行。 如果我们把关闭资源的代码放在finally中行不行呢?123456789101112131415161718192021222324try { //1.加载驱动程序 Class.forName("com.mysql.jdbc.Driver"); //2.获得数据库链接 conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/common", "root", "123456"); //3.通过数据库的连接操作数据库,实现增删改查 st = conn.createStatement(); rs = st.executeQuery("select * from new_table"); //4.处理数据库的返回结果 while (rs.next()) { System.out.println(rs.getString("id") + " " + rs.getString("name")); }} catch (Exception e) { e.printStackTrace();} finally { //5.关闭资源 try { rs.close(); st.close(); conn.close(); } catch (SQLException e) { e.printStackTrace(); }} 答案是不行!如果在 2步骤 的try中conn获得数据库链接抛出异常,那么conn仍然为null,此时进入finally代码块中,执行close()就报空指针异常了,关闭资源没有意义!因此,我们需要在close()之前判断一下conn等是否为空,只有不为空的时候才需要close。 常见的close()针对上述场景,得到常见的使用close()方式如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051/** * @author chloneda * @description: close()方法测试 * 常见的close() */public class CloseTest { public static void main(String[] args) { Connection conn = null; Statement st = null; ResultSet rs = null; try { //1.加载驱动程序 Class.forName("com.mysql.jdbc.Driver"); //2.获得数据库链接 conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/common", "root", "123456"); //3.通过数据库的连接操作数据库,实现增删改查 st = conn.createStatement(); rs = st.executeQuery("select * from new_table"); //4.处理数据库的返回结果 while (rs.next()) { System.out.println(rs.getString("id") + " " + rs.getString("name")); } } catch (Exception e) { e.printStackTrace(); } finally { //5.关闭资源 if (null != rs) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (null != st) { try { st.close(); } catch (SQLException e) { e.printStackTrace(); } } if (null != conn) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }} 这是常见的close()!但是finally代码块的代码重复性太高了,这还只是三个资源的关闭,如果有很多个资源需要在finally中关闭,那不是需要编写很多不优雅的代码?其实,关闭资源是没啥逻辑的代码,我们需要精简代码,减少代码重复性,优雅地编程! 使用AutoCloseable接口自从Java7以后,我们可以使用 AutoCloseable接口 (Closeable接口也可以)来优雅的关闭资源了 看看修改例子:123456789101112131415161718192021222324252627282930313233343536373839404142/** * @author chloneda * @description: close()方法测试 * 使用AutoCloseable接口 */public class CloseTest { public static void main(String[] args) { Connection conn = null; Statement st = null; ResultSet rs = null; try { //1.加载驱动程序 Class.forName("com.mysql.jdbc.Driver"); //2.获得数据库链接 conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/common", "root", "123456"); //3.通过数据库的连接操作数据库,实现增删改查 st = conn.createStatement(); rs = st.executeQuery("select * from new_table"); //4.处理数据库的返回结果 while (rs.next()) { System.out.println(rs.getString("id") + " " + rs.getString("name")); } } catch (Exception e) { e.printStackTrace(); } finally { //5.关闭资源,调用自定义的close()方法 close(rs); close(st); close(conn); } } public static void close(AutoCloseable closeable) { if (closeable != null) { try { closeable.close(); } catch (Exception e) { e.printStackTrace(); } } }} 上面的finally代码块的代码量是不是减少了许多,就单纯地调用的静态的 close(AutoCloseable closeable) 方法。为什么可以这样呢? 其实Connection、Statement、ResultSet三个接口都继承了AutoCloseable接口。所以只要涉及到资源的关闭,继承了AutoCloseable接口,实现了close()方法,我们都可以调用 close(AutoCloseable closeable) 方法进行资源关闭。 此外,java IO流的很多类都实现了 Closeable接口,而Closeable接口又继承自 AutoCloseable接口,也可以调用上面的 close(AutoCloseable closeable) 方法进行资源关闭。是不是一语惊醒梦中人啊? 使用try-with-resources其实Java7以后,还有一种关闭资源的方式,也就是 try-with-resources,这种方式也是我们推荐的!很优雅! 我们来看看它是怎么优雅地关闭资源的!123456789101112131415161718192021222324/** * @author chloneda * @description: close()方法测试 * 使用try-with-resources */public class CloseTest { public static void main(String[] args) throws ClassNotFoundException { //1.加载驱动程序 Class.forName("com.mysql.jdbc.Driver"); try (//2.获得数据库链接 Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/common", "root", "123456"); //3.通过数据库的连接操作数据库,实现增删改查 Statement st = conn.createStatement(); ResultSet rs = st.executeQuery("select * from new_table") ) { //4.处理数据库的返回结果 while (rs.next()) { System.out.println(rs.getString("id") + " " + rs.getString("name")); } } catch (Exception e) { e.printStackTrace(); } }} 这种方式就省略了finally,不必重复编写关闭资源的代码了!而且资源也得到了关闭!怎么验证这个问题?可以查看底下的 实际应用 章节! 其实try-with-resources关闭资源的操作,本质上是继承了java.lang.AutoCloseable接口,实现了close方法,所以使用try-with-resources能关闭资源。很神奇吧! 实际应用这个章节就是验证使用try-with-resources可以关闭资源的问题的! 上面我们说了使用try-with-resources关闭资源,只要是继承了java.lang.AutoCloseable接口 实现close()方法就可以使用! 我们自定义一个资源类,来看看实际应用吧!12345678910111213141516171819202122232425262728293031323334package com.chloneda.jutils.test;/** * @author chloneda * @description: 资源类, 实现AutoCloseable接口. */public class Resources implements AutoCloseable { public void useResource() { System.out.println("useResource:{} 正在使用资源!"); } @Override public void close() { System.out.println("close:{} 自动关闭资源!"); }}/** * @description: 使用try-with-resources自动关闭资源测试. */class AutoClosableTest { public static void main(String[] args) { /** 使用try-with-resource,自动关闭资源 */ try ( Resources resource = new Resources() ) { resource.useResource(); } catch (Exception e) { e.getMessage(); } finally { System.out.println("Finally!"); } }} 结果输出。123useResource:{} 正在使用资源!close:{} 自动关闭资源!Finally! 看到运行结果了吗?Resources类实现AutoCloseable接口,实现了close()方法,try-with-resources 就会自动关闭资源! 一旦Resources类没有继承java.lang.AutoCloseable接口,没有实现close()方法,AutoClosableTest类的try模块就在编译期报错,提示信息如下。123Incompatible types.Required: java.lang.AutoCloseableFound: com.chloneda.jutils.test.Resources 最后,需要说明的是try-with-resources就是一个JVM语法糖!关于JVM语法糖可以查查相关资料,看看Java中有哪些有趣的语法糖! 尾语《Effective Java》在第三版中也推荐使用try-with-resources语句替代try-finally语句。 所以在处理必须关闭的资源时,使用try-with-resources语句替代try-finally语句。生成的代码更简洁,更清晰,并且生成的异常更有用。 try-with-resources语句在编写必须关闭资源的代码时会更容易,也不会出错,而使用try-finally语句实际上是不可能的。 如此,推荐大家使用try-with-resources优雅地关闭资源!]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java ArrayList源码分析]]></title>
<url>%2F%2Fblog%2Fjava-arraylist%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 前言Java系列,尽量采用通俗易懂、循序渐进的方式,让大家真正理解JDK源码的精髓! 关于JDK源码中的ArrayList类,官方文档这样描述: Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.) ArrayList底层实现是数组,自然就具备了数组的基本性质:ArrayList查找、更新操作效率高,速度快;插入、删除操作时需要移动大部分元素,性能较低。 ArrayList是一种相对比较简单的数据结构,它的基本问题是: 是否允许空? 是否允许重复数据? 是否有序? 是否线程安全? 这几个问题上面的官方文档已经说得很清楚了!ArrayList可调整大小的数组实现,本身是有序的,并允许添加任何元素,包括null,同时与Vector大致等效,但不是线程安全的! 值得注意的是,ArrayList的核心问题是: 如何进行自动扩容,实现动态数组功能? 我们循序渐进地来聊聊这个核心问题吧! ArrayList我们先来看看 ArrayList 类有哪些属性!123456789101112// ArrayList默认初始容量为10private static final int DEFAULT_CAPACITY = 10;// 空数组,当传入的容量为0的时候使用,通过new ArrayList(0)创建时用的是这个空数组。private static final Object[] EMPTY_ELEMENTDATA = new Object[0];// 空数组,这种是通过new ArrayList()创建时用的是这个空数组,与EMPTY_ELEMENTDATA的区别是在添加第一个元素时使用这个空数组的会初始化为DEFAULT_CAPACITY(10)个元素。private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];// 存储元素的数组,真正存放元素的地方,使用transient是为了不序列化这个字段。transient Object[] elementData;// ArrayList中真正存储元素的个数private int size;// ArrayList容量的最大值private static final int MAX_ARRAY_SIZE = 2147483639; 从 transient Object[] elementData,这个语句可以看到,ArrayList类是Object类型实现的数组,初始化数组时默认容量为10。接着我们看一段简单的代码:12345ArrayList<String> list = new ArrayList<String>();list.add("月亮1");list.add("地球2");list.add("太阳3");list.remove(0); 在idea中采用Debug模式执行这几条语句时,是这么变化的: 可以看到,add操作直接将数组元素添加在数据末尾,remove操作删除index为0的节点后,会将后面元素移到index为0的地方。 add()函数在ArrayList中,当我们增加元素的时候,会使用 add() 函数,它会将元素放到末尾。具体实现:1234567891011/** * Appends the specified element to the end of this list. * * @param e element to be appended to this list * @return true (as specified by {@link Collection#add}) */public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true;} 这里我们可以看到ArrayList最核心的问题 自动扩容机制 的实现方法 ensureCapacityInternal()。让我们依次来看一下它的具体实现。1234567891011121314151617181920212223242526272829303132private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity;}private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity);}private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // 扩展为原来的1.5倍 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩为1.5倍还不满足需求,直接扩为需求值 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity);} 当增加数据的时候,如果ArrayList的大小已经不满足需求时,那么就将数组变为原长度的1.5倍,之后的操作就是把老的数组拷到新的数组里面。例如,默认的数组大小是10,也就是说当我们add10个元素之后,再进行一次add时,就会发生自动扩容,数组长度由10变为了15。 get()函数接下来get()函数就比较简单了,先做index检查,然后执行访问操作。123456789101112/** * Returns the element at the specified position in this list. * * @param index index of the element to return * @return the element at the specified position in this list * @throws IndexOutOfBoundsException {@inheritDoc} */public E get(int index) { rangeCheck(index); return elementData(index);} set()函数set()函数与get()函数差不多,先做index检查,然后执行赋值操作。12345678910111213141516/** * Replaces the element at the specified position in this list with * the specified element. * * @param index index of the element to replace * @param element element to be stored at the specified position * @return the element previously at the specified position * @throws IndexOutOfBoundsException {@inheritDoc} */public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue;} remove()函数接下来看看 remove() 函数的运用! 123456789101112131415161718192021222324/** * Removes the element at the specified position in this list. * Shifts any subsequent elements to the left (subtracts one from their * indices). * * @param index the index of the element to be removed * @return the element that was removed from the list * @throws IndexOutOfBoundsException {@inheritDoc} */public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) // 把指定元素后面位置的所有元素,利用System.arraycopy方法整体向前移动一个位置 System.arraycopy(elementData, index+1, elementData, index, numMoved); // 最后一个位置的元素指定为null,这样让gc可以去回收它 elementData[--size] = null; // clear to let GC do its work return oldValue;} 上面关键操作就是删除指定元素后,后续所有元素向前移动的实现了! contains()函数contains() 函数就不用多说了,就简单地循环判断一下存不存在某个元素,代码如下。 123456789101112131415161718192021222324/** * Returns true if this list contains the specified element. * More formally, returns true if and only if this list contains * at least one element e such that (o==null;o.equals(e)). * * @param o element whose presence in this list is to be tested * @return true if this list contains the specified element */public boolean contains(Object o) { return indexOf(o) >= 0;}public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1;} 新发现的疑惑在看源码的过程中,发现elementData数组是用transient修饰的!1transient Object[] elementData; 这是为什么?后来发现ArrayList实现了Serializable接口,代码如下。12public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable 这是不是和序列化有关呢?没错! 这意味着ArrayList是可以被序列化的,用transient修饰elementData意味着我不希望elementData数组被序列化。这是为什么?因为序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法: 12345678910111213141516171819202122232425/** * Save the state of the ArrayList instance to a stream (that is, serialize it). * * @serialData The length of the array backing the ArrayList * instance is emitted (int), followed by all of its elements * (each an Object) in the proper order. */private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); }} 每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样的好处是: 加快了序列化的速度 减小了序列化之后的文件大小 不得不承认JDK的源码是经过千锤百炼的,同时也提醒我们,在以后开发过程中,如果遇到类似的情况,不失为一种借鉴的方法! 小结其实Java中的 ArrayList 实现就是 数据结构- 数组 的实现,核心功能是可以进行 自动扩容,我们可以从中了解数组实现的核心思想! 关于JDK中数组的实现ArrayList的源码就分析就到这里了!]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Elasticsearch启动、停止脚本]]></title>
<url>%2F%2Fblog%2Fes-shell%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 Elasticsearch官网 构建Elasticsearch启动脚本 start_es.sh。12345#!/bin/bashexport ES_HOME=xxxsu elastic -c "sh ${ES_HOME}/bin/elasticsearch -d -p ${ES_HOME}/pid" 参数说明: su:登录用户。 elastic:部署Elasticsearch用户,避免root用户而无法启动。 c:c参数后跟具体命令。 d:Elasticsearch作为守护线程后台启动。 p:指定线程ID文件,需要新建。 构建Elasticsearch停止脚本 stop_es.sh。123456#!/bin/bashexport ES_HOME=xxxkill `cat ${ES_HOME}/pid` ``` `cat ${ES_HOME}/pid`]]></content>
<categories>
<category>Elasticsearch</category>
</categories>
<tags>
<tag>Elasticsearch</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Elasticsearch客户端源码剖析]]></title>
<url>%2F%2Fblog%2Fes-clients%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 前言今天我们来聊聊Elasticsearch客户端的类型。我们知道Elasticsearch是一种分布式的海量数据搜索与分析的技术,可以用于电商网站、门户网站、企业IT系统等各种场景下的搜索引擎,也可以用于对海量的数据进行近实时的数据分析。 但Elasticsearch版本迭代更新太快,这就意味着在Elasticsearch升级过程中容易出现兼容性问题。也引出了今天对Elasticsearch客户端种类及使用的问题讨论! ES客户端种类ES官方客户端有TransportClient、Java Low Level REST Client和Java High Level REST Client三种。官方文档对他们的说明是: TransportClient We plan on deprecating the TransportClient in Elasticsearch 7.0 and removing it completely in 8.0. Java Low Level REST Client the official low-level client for Elasticsearch. It allows to communicate with an Elasticsearch cluster through http. Leaves requests marshalling and responses un-marshalling to users. It is compatible with all Elasticsearch versions. Java High Level REST Client the official high-level client for Elasticsearch. Based on the low-level client, it exposes API specific methods and takes care of requests marshalling and responses un-marshalling. 意思就是说,TransportClient将会在将来版本进行废弃移除,官方建议使用Java High Level REST Client。 为什么会这样呢?这里涉及到两个问题: 未来版本为什么会淘汰TransportClient客户端? Java Low/High Level REST Client客户端优点在哪里? 先别急,我们来看看这两个问题! 客户端的使用各客户端使用需要引入相关依赖,这里统一引入相关依赖,后面就不多赘述了! 123456789101112131415161718<!-- elasticsearch core --><dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>${elasticsearch.version}</version></dependency><!-- low level rest client --><dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-client</artifactId> <version>${elasticsearch.version}</version></dependency><!-- high level rest client --><dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>${elasticsearch.version}</version></dependency> 注:TransportClient将会在8.x版本后完全移除! TransportClient初始化TransportClient客户端代码示例: 12345678910public TransportClient initTransportClient(String esClusterName,String host,String port) throws UnknownHostException { Settings settings = Settings.builder() .put("cluster.name", esClusterName) .put("client.transport.sniff", true) .build(); TransportClient client = new PreBuiltTransportClient(settings) .addTransportAddress(new TransportAddress(InetAddress.getByName(host),port); return client;} Java Low Level REST Client初始化 RestClient 客户端代码示例: 12345678public RestClient initRestClient(String host, int port) { RestClientBuilder builder = RestClient.builder(new HttpHost(host, port, "http")); Header[] defaultHeaders = new Header[]{new BasicHeader("header", "value")}; builder.setDefaultHeaders(defaultHeaders); RestClient restClient = builder.build(); return restClient;} Java High Level REST Client初始化 RestHighLevelClient 客户端代码示例: 123456789101112public RestHighLevelClient restHighLevelClient(List<String> hostArray) { //创建HttpHost数组,其中存放es主机和端口的配置信息 HttpHost[] httpHostArray = new HttpHost[hostArray.size()]; for (int i = 0; i < hostArray.size(); i++) { String item = hostArray.get(i); httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http"); } //创建RestHighLevelClient客户端 return new RestHighLevelClient(RestClient.builder(httpHostArray));} 以上就是初始化三种不同客户端的示例代码!下面我们深入客户端代码底层,看看他们之间有什么不一样? 深入客户端的底层TransportClientTransportClient客户端自从Elasticsearch诞生以来,一直是Elasticsearch的一部分。 它是一个特殊的客户端,因为它使用传输层协议(TCP)与Elasticsearch进行通信,如果该客户端与其所使用的Elasticsearch不在同一版本上,则会导致兼容性问题。基于这个原因,官方会在8.x后完全移除! 因此,在这里就不对 TransportClient 客户端底层进行深究了! Java Low Level REST Client2016年,Elasticsearch官方发布了一个低级REST客户端,该客户端基于众所周知的Apache HTTP客户端,它允许使用 HTTP 与任何版本的Elasticsearch集群进行通信。 我们来看看RestClient客户端的代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136package org.elasticsearch.client;public class RestClient implements Closeable { //已省略其他非必要属性代码。。。 // RestClient 类构造器的第一个参数是 CloseableHttpAsyncClient,是Apache HTTP client 中的类,相关请求也是通过该参数 RestClient(CloseableHttpAsyncClient client, long maxRetryTimeoutMillis, Header[] defaultHeaders, HttpHost[] hosts, String pathPrefix, RestClient.FailureListener failureListener) { this.client = client; this.maxRetryTimeoutMillis = maxRetryTimeoutMillis; this.defaultHeaders = Collections.unmodifiableList(Arrays.asList(defaultHeaders)); this.failureListener = failureListener; this.pathPrefix = pathPrefix; this.setHosts(hosts); } //已省略其他非必要代码。。。 public void performRequestAsync(String method, String endpoint, Map<String, String> params, HttpEntity entity, HttpAsyncResponseConsumerFactory httpAsyncResponseConsumerFactory, ResponseListener responseListener, Header... headers) { try { Objects.requireNonNull(params, "params must not be null"); Map<String, String> requestParams = new HashMap(params); String ignoreString = (String)requestParams.remove("ignore"); Object ignoreErrorCodes; if (ignoreString == null) { if ("HEAD".equals(method)) { ignoreErrorCodes = Collections.singleton(404); } else { ignoreErrorCodes = Collections.emptySet(); } } else { String[] ignoresArray = ignoreString.split(","); ignoreErrorCodes = new HashSet(); if ("HEAD".equals(method)) { ((Set)ignoreErrorCodes).add(404); } String[] var12 = ignoresArray; int var13 = ignoresArray.length; for(int var14 = 0; var14 < var13; ++var14) { String ignoreCode = var12[var14]; try { ((Set)ignoreErrorCodes).add(Integer.valueOf(ignoreCode)); } catch (NumberFormatException var17) { throw new IllegalArgumentException("ignore value should be a number, found [" + ignoreString + "] instead", var17); } } } URI uri = buildUri(this.pathPrefix, endpoint, requestParams); HttpRequestBase request = createHttpRequest(method, uri, entity); this.setHeaders(request, headers); RestClient.FailureTrackingResponseListener failureTrackingResponseListener = new RestClient.FailureTrackingResponseListener(responseListener); long startTime = System.nanoTime(); this.performRequestAsync(startTime, this.nextHost(), request, (Set)ignoreErrorCodes, httpAsyncResponseConsumerFactory, failureTrackingResponseListener); } catch (Exception var18) { responseListener.onFailure(var18); } } //已省略其他非必要代码。。。 private static HttpRequestBase createHttpRequest(String method, URI uri, HttpEntity entity) { String var3 = method.toUpperCase(Locale.ROOT); byte var4 = -1; switch(var3.hashCode()) { case -531492226: if (var3.equals("OPTIONS")) { var4 = 3; } break; case 70454: if (var3.equals("GET")) { var4 = 1; } break; case 79599: if (var3.equals("PUT")) { var4 = 6; } break; case 2213344: if (var3.equals("HEAD")) { var4 = 2; } break; case 2461856: if (var3.equals("POST")) { var4 = 5; } break; case 75900968: if (var3.equals("PATCH")) { var4 = 4; } break; case 80083237: if (var3.equals("TRACE")) { var4 = 7; } break; case 2012838315: if (var3.equals("DELETE")) { var4 = 0; } } switch(var4) { case 0: return addRequestBody(new HttpDeleteWithEntity(uri), entity); case 1: return addRequestBody(new HttpGetWithEntity(uri), entity); case 2: return addRequestBody(new HttpHead(uri), entity); case 3: return addRequestBody(new HttpOptions(uri), entity); case 4: return addRequestBody(new HttpPatch(uri), entity); case 5: HttpPost httpPost = new HttpPost(uri); addRequestBody(httpPost, entity); return httpPost; case 6: return addRequestBody(new HttpPut(uri), entity); case 7: return addRequestBody(new HttpTrace(uri), entity); default: throw new UnsupportedOperationException("http method not supported: " + method); } }} 看到上面的代码,RestClient 类构造器的第一个参数是 CloseableHttpAsyncClient,是 Apache HTTP client 中的类,也就是说 RestClient 是基于 Apache HTTP 实现的,这里是 Apache HTTP 的依赖! 12345<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>${http.version}</version></dependency> Java High Level REST Client最重要的是,我们发布了基于低级客户端的高级REST客户端,它负责请求编组和响应解组。 我们来看看 RestHighLevelClient 的底层代码! 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455package org.elasticsearch.client;public class RestHighLevelClient { private final RestClient client; private final NamedXContentRegistry registry; public RestHighLevelClient(RestClient restClient) { this(restClient, Collections.emptyList()); } // 此处省略多处代码! // 该类大部分方法最终会调用以下 performRequestAndParseEntity 方法,我们主要看该方法的调用关系 protected <Req extends ActionRequest, Resp> Resp performRequestAndParseEntity(Req request, CheckedFunction<Req, Request, IOException> requestConverter, CheckedFunction<XContentParser, Resp, IOException> entityParser, Set<Integer> ignores, Header... headers) throws IOException { return this.performRequest(request, requestConverter, (response) -> { return this.parseEntity(response.getEntity(), entityParser); }, ignores, headers); } protected <Req extends ActionRequest, Resp> Resp performRequest(Req request, CheckedFunction<Req, Request, IOException> requestConverter, CheckedFunction<Response, Resp, IOException> responseConverter, Set<Integer> ignores, Header... headers) throws IOException { ActionRequestValidationException validationException = request.validate(); if (validationException != null) { throw validationException; } else { Request req = (Request)requestConverter.apply(request); Response response; try { // 这里的 client 就是RestClient,最终还是调用 RestClient 的方法,也就是说 RestHighLevelClient 是基于 RestClient 的 response = this.client.performRequest(req.getMethod(), req.getEndpoint(), req.getParameters(), req.getEntity(), headers); } catch (ResponseException var13) { ResponseException e = var13; if (ignores.contains(var13.getResponse().getStatusLine().getStatusCode())) { try { return responseConverter.apply(e.getResponse()); } catch (Exception var11) { throw this.parseResponseException(var13); } } throw this.parseResponseException(var13); } try { return responseConverter.apply(response); } catch (Exception var12) { throw new IOException("Unable to parse response body for " + response, var12); } } }} 看上面的代码及注解,我相信你很快就豁然开朗了! 其实上面的问题现在就有答案了!TransportClient废弃的主要原因就是考虑到兼容性的问题,而后续两个客户端在兼容性方面就做的很好! 小结关于Elasticsearch的客户端问题,其实 ES官网 已经说得很明确了,这里也通过代码剖析的方式去认识一下底层的代码,加深理解! 由此可见,HighLevelClient 是基于 RestClient,而 RestClient 又是基于 Apache HTTP 客户端, 这样一来, 在客户端方面, Elasticsearch 将 Java, Python, Php, Javascript 等各种语言的底层接口就都统一起来了; 与此同时, 使用 rest api, 还可以屏蔽各版本之前的差异。 这也提醒我们,在代码的升级过渡期, 处理好新 client 和旧 client 的关系,可以减少代码后期维护的工作量!]]></content>
<categories>
<category>Elasticsearch</category>
</categories>
<tags>
<tag>Elasticsearch</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hello Chloneda]]></title>
<url>%2F%2Fblog%2Fhello-chloneda%2F</url>
<content type="text"><![CDATA[Welcome to chloneda‘s blog!This is my very first post.You can check the Hexo official website and the Markdown website for related commands and syntax. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. Quick StartOther reference website NexT 比特虫 Font Awesome Create a new post1$ hexo new "My New Post" More info: Writing Run server1$ hexo server More info: Server Generate static files1$ hexo generate More info: Generating Deploy to remote sites1$ hexo deploy More info: Deployment]]></content>
<categories>
<category>Test</category>
</categories>
<tags>
<tag>Test</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java基础系列-代码块详解]]></title>
<url>%2F%2Fblog%2Fjava-code-block%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 前言Java系列,尽量采用通俗易懂、循序渐进的方式,让大家真正理解Java基础知识! 代码块分类在Java中,使用{}括起来的代码被称为代码块(Code block),根据其位置和声明的不同,可以分为: 局部代码块。 构造代码块。 同步代码块。 静态代码块。 Java代码块的核心问题是: 代码块初始化是在什么时候? 代码块执行顺序是怎样的? 代码块在继承时,执行顺序是怎样的? 下面将对各种代码块就核心问题展开叙述! 局部代码块在方法中出现,可以限定变量生命周期,及早释放,提高内存利用率。示例代码:12345678910111213141516package com.chloneda.jutils.test;/** * @author chloneda * @description: 局部代码块测试 */public class CodeBlockTest { public static void main(String[] args) { //局部代码块 { int number = 100; System.out.println(number); } //找不到number变量,原因呢??? //System. out.println(number); }} 执行结果:1100 构造代码块在类中方法外出现,每次调用构造方法都会执行,并且在构造方法前执行。示例代码:123456789101112131415161718192021222324252627282930313233package com.chloneda.jutils.test;/** * @author chloneda * @description: 构造代码块类 */class CodeBlock { //构造代码块,在方法外出现 { int number1 = 10; System.out.println("number1: " + number1); } //构造方法 public CodeBlock() { System.out.println("这是构造方法"); } //在构造代码块在构造方法前后出现,但构造代码块先于构造方法执行 { int number2 = 100; System.out.println("number2: " + number2); }}//构造代码块测试类public class CodeBlockTest { public static void main(String[] args) { // 创建对象 CodeBlock codeBlock = new CodeBlock(); // 注意:构造代码块通过构造方法自动调用 }} 执行结果:123number1: 10number2: 100这是构造方法 由上面的执行结果可知: 构造代码块依赖于构造方法,而且优先于构造函数执行,即实例对象建立,才会运行构造代码块,类不能调用构造代码块的。 Tip:构造代码块与构造函数的区别:构造代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。 也就是说,构造代码块中定义的是不同对象共性的初始化内容。 同步代码块同步代码块指的是被Java中Synchronized关键词修饰的代码块,在Java中,Synchronized关键词不仅仅可以用来修饰代码块,与此同时也可以用来修饰方法,是一种线程同步机制,被Synchronized关键词修饰的代码块会被加上内置锁。 需要说明的是Synchronized同步代码块是一种高开销的操作,因此我们应该尽量减少被同步的内容,在很多场景,我们没有必要去同步整个方法,而只需要同步部分代码即可,也就是使用同步代码块(JDK源码中有很多应用)。 同步代码块示例如下。123456789101112131415161718192021package com.chloneda.jutils.test;/** * @author chloneda * @description: 同步代码块测试 */public class CodeBlock implements Runnable { @Override public void run() { synchronized (CodeBlock.class) { System.out.print("同步代码块!"); } } public static void main(String[] args) { CodeBlock a = new CodeBlock(); CodeBlock b = new CodeBlock(); new Thread(a).start(); new Thread(b).start(); }} 此外,静态代码是属于类而不是属于对象的,因此使用Synchronized来修饰静态方法和静态对象的时候,类下的所有对象都会被锁定。 静态代码块在类中方法外出现,并加上static修饰,常用于给类进行初始化,在加载的时候就执行,并且静态代码块执行一次。 代码块执行顺序首先我们验证一下代码块执行顺序,示例代码如下。123456789101112131415161718192021222324252627282930313233343536373839404142434445package com.chloneda.jutils.test;/** * @author chloneda * @description: 验证代码块执行顺序 */class StatisCodeBlock { //静态代码块,在方法外出现 static { int number1 = 20; System.out.println("1、静态代码块变量: " + number1); } //构造代码块,在方法外出现 { int number2 = 100; System.out.println("2、构造代码块变量: " + number2); } public StatisCodeBlock() { System.out.println("这是构造方法 StatisCodeBlock()"); } static { int number3 = 200; System.out.println("3、静态代码块变量: " + number3); } //在构造代码块在构造方法前后,但构造代码块先于构造方法执行 { int number4 = 1000; System.out.println("4、构造代码块变量: " + number4); }}//静态代码块测试类public class CodeBlockTest { public static void main(String[] args) { // 创建对象 StatisCodeBlock codeBlock = new StatisCodeBlock(); // 注意:构造代码块通过构造方法自动调用 System.out.println("======我是分割线======"); StatisCodeBlock codeBlock2 = new StatisCodeBlock(); }} 执行结果:1234567891、静态代码块变量: 203、静态代码块变量: 2002、构造代码块变量: 1004、构造代码块变量: 1000这是构造方法 StatisCodeBlock()======我是分割线======2、构造代码块变量: 1004、构造代码块变量: 1000这是构造方法 StatisCodeBlock() 由上面的执行结果可知: 静态代码块:在类加载JVM时初始化,且只被执行一次;常用来执行类属性的初始化;静态块优先于各种代码块以及构造函数;此外静态代码块不能访问普通变量。 构造代码块:每次调用构造方法,构造代码块都执行一次;构造代码块优先于构造函数执行;同时构造代码块的运行依赖于构造函数。 继承中代码块执行顺序下面再我们验证一下继承中代码块执行顺序,示例代码如下。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071package com.chloneda.jutils.test;/** * @author chloneda * @description: 验证继承中代码块执行顺序 */class Parent { //静态代码块,在方法外出现 static { int number1 = 20; System.out.println("1、父类静态代码块变量: " + number1); } //构造代码块,在方法外出现 { int number2 = 100; System.out.println("2、父类构造代码块变量: " + number2); } public Parent() { System.out.println("父类构造方法 Parent()"); } static { int number3 = 200; System.out.println("3、父类静态代码块变量: " + number3); } //在构造代码块在构造方法前后,但构造代码块先于构造方法执行 { int number4 = 1000; System.out.println("4、父类构造代码块变量: " + number4); }}class Child extends Parent { //静态代码块,在方法外出现 static { int number1 = 2001; System.out.println("11、子类静态代码块变量: " + number1); } //构造代码块,在方法外出现 { int number2 = 10001; System.out.println("22、子类构造代码块变量: " + number2); } public Child() { System.out.println("子类构造方法 Child()"); } static { int number3 = 2002; System.out.println("33、子类静态代码块变量: " + number3); } //在构造代码块在构造方法前后,但构造代码块先于构造方法执行 { int number4 = 100002; System.out.println("44、子类构造代码块变量: " + number4); }}public class CodeBlockTest { public static void main(String[] args) { // 创建对象 Child child = new Child(); // 注意:构造代码块通过构造方法自动调用 }} 执行结果如下。1234567891011121、父类静态代码块变量: 203、父类静态代码块变量: 20011、子类静态代码块变量: 200133、子类静态代码块变量: 20022、父类构造代码块变量: 1004、父类构造代码块变量: 1000父类构造方法 Parent()22、子类构造代码块变量: 1000144、子类构造代码块变量: 100002子类构造方法 Child() 由此可见继承中代码块执行顺序:父类静态块 ==> 子类静态块 ==> 父类代码块 ==> 父类构造器 ==> 子类代码块 ==> 子类构造器 。 小结通过上面的例子讲解,现在可以解答代码块的核心问题了! 静态代码块:在类加载JVM时初始化,且只被执行一次;常用来执行类属性的初始化;静态块优先于各种代码块以及构造函数;此外静态代码块不能访问普通变量。 构造代码块:每次调用构造方法,构造代码块都执行一次;构造代码块优先于构造函数执行;同时构造代码块的运行依赖于构造函数。 代码块初始化时机:构造代码块在实例对象创建时进行初始化;静态代码块在类加载时进行初始化。 代码块执行顺序:静态代码块 ==> main()方法 ==> 构造代码块 ==> 构造方法 ==> 局部代码块 。 继承中代码块执行顺序:父类静态块 ==> 子类静态块 ==> 父类代码块 ==> 父类构造器 ==> 子类代码块 ==> 子类构造器 。 这就是java代码块的全部内容,大家理解了吗?]]></content>
<categories>
<category>Java</category>
</categories>
</entry>
<entry>
<title><![CDATA[深入浅出设计模式系列--单例模式]]></title>
<url>%2F%2Fblog%2Fpattern-singleton%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 前言深入浅出设计模式系列,尽量采用通俗易懂、循序渐进的方式,让大家真正理解设计模式的精髓! 先了解一下单例模式定义:确保一个类只有一个自行实例化的实例,并提供一个全局访问点,向整个系统提供这个实例。 单例模式的本质:控制实例的数目。 单例模式的主要问题我们经常使用的单例模式主要涉及的问题有: 如何保证一个类只有一个实例? 如何保证单例模式在多线程环境下的线程安全? 如何防止反射对单例模式的破坏? 如何保证单例模式在序列化与反序列化过程中实例的唯一性? 下面针对以上的问题进行深入研究! 单例模式的实现单例模式常见的有饿汉式、懒汉式、双重检验锁、静态内部类方式、枚举方式,这里主要讨论多线程环境下的单例模式。 饿汉式这种饿汉式单例是比较常见的,我们看看它是怎么实现的! 12345678910111213141516package com.chloneda.jutils.test;/** * @Created by chloneda * @Description: 饿汉式单例模式 */public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; }} 饿汉式单例本身是线程安全的,但它采用空间换取时间的方式,当类加载时马上就实例化Singleton对象,不管使用者用不用,后续每次调用 getInstance() 方法的时候,就不需要判断它是否实例化,从而节约了时间。但有些情况下需要懒加载实例化对象,针对这种情形,于是有了懒汉式的单例模式。 懒汉式我们看一下懒汉式单例模式的实现。 12345678910111213141516171819package com.chloneda.jutils.test;/** * @Created by chloneda * @Description: 懒汉式单例模式 */public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }} 这就是我们常见的懒汉式单例模式!大家发现没?这种懒汉式单例模式在多线程环境下,存在线程安全的问题!那它是怎么引发多线程安全的问题的呢? 为便于大家理解单例模式在多线程环境下容易出现的问题,下面直接采用图片的方式,请看图片: 从上图可知多线程环境下懒汉式单例模式的问题了吧! 针对上面单例模式存在多线程安全的缺陷,有人说这还不容易解决,直接在 getInstance 方法上加上Java关键词 synchronized, 即。123456public synchronized static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton;} 可是你可别忘了,在Java中每个语句都是执行都需要时间的,加上 synchronized 关键词需要底层执行更多的语句,并且每次都需要通过 getInstance() 方法获取实例,效率非常底,更别说在多线程高并发下的执行情况了,因此必须对此加以改进。 双重检验锁针对上面懒汉式单例的性能问题,我通过修改得到双重检验锁单例。1234567891011121314151617181920212223package com.chloneda.jutils.test;/** * @Created by chloneda * @Description: 双重检验锁单例模式 */public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { // 双重检测机制 synchronized (Singleton.class) { // 同步锁 if (instance == null) { // 双重检测机制 instance = new Singleton(); } } } return instance; }} 上面双重检验锁单例的写法,是不是比懒汉式单例效率高,因为每次需要通过 getInstance 方法获取实例时,只在第一次实例化 instance 加同步锁,后续多线程进入该方法后,直接进入外层 if (instance == null) 判断语句,得知instance实例不为空,就直接返回instance实例了。 虽然上面的双重校验锁机制的单例增加了一定的安全性,提高了效率,但是这个双重检验锁单例也有缺陷,因为它没有考虑到 JVM 编译器的指令重排。 1、什么是指令重排比如 java 中简单的一句 instance = new Singleton(),会被编译器编译成如下 JVM 指令。123memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置instance指向刚分配的内存地址 但是这些指令顺序并非一成不变,有可能会经过 JVM 和 CPU 的优化,指令重排成下面的顺序。123memory = allocate(); //1:分配对象的内存空间 instance = memory; //3:设置instance指向刚分配的内存地址 ctorInstance(memory); //2:初始化对象 2、影响 高并发情况下,线程 A 执行 instance = new Singleton(); 完上面的1、3步骤时,准备走2,即 instance 对象还未完成初始化,但此时 instance 已经不再指向 null 。 此时线程 B 抢占到CPU资源,执行 if(instance == null) 时,返回的结果会是 false, 从而返回一个没有初始化完成的instance对象。 3、解决方法如何去解决这个问题呢?很简单,可以利用关键字 volatile 来修饰 instance 对象,如下优化: 双重检验锁改进 1234567891011121314151617181920212223package com.chloneda.jutils.test;/** * @Created by chloneda * @Description: 单例模式使用双重检验锁方式实现,优点:延迟初始化、性能优化、线程安全 */public class Singleton { private volatile static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }} volatile 修饰符在此处的作用就是阻止变量访问前后的指令重排,从而保证了指令的执行顺序。 即指令的执行顺序是严格按照上文的 1、2、3 步骤来执行,从而保证对象不会出现中间态。 但是上面的双重检验锁改进版单例模式也有问题,因为它无法解决反射对单例模式的破坏性。我将在静态内部类单例模式中加以阐述。 静态内部类在了解静态内部类单例模式之前,让我们先了解一下静态内部类的两个知识。 静态内部类加载一个类时,其内部类不会同时被加载。 一个类被加载,当且仅当其某个静态成员如静态域、构造器、静态方法等被调用时才会被加载。 我们先看一个静态内部类的测试,以验证上面这两个观点。123456789101112131415161718192021222324252627282930313233package com.chloneda.jutils.test;/** * @Created by chloneda * @Description: 静态内部类测试类 */public class OuterClassTest { private OuterClassTest() {} static { System.out.println("1、我是外部类静态模块..."); } // 静态内部类 private static final class StaticInnerTest { private static OuterClassTest oct = new OuterClassTest(); static { System.out.println("2、我是静态内部类的静态模块... " + oct); } static void staticInnerMethod() { System.out.println("3、静态内部类方法模块... " + oct); } } public static void main(String[] args) { OuterClassTest oct = new OuterClassTest(); // 此刻内部类不会被加载 System.out.println("===========分割线==========="); OuterClassTest.StaticInnerTest.staticInnerMethod(); // 调用内部类的静态方法 }} 输出如下。12341、我是外部类静态模块...=========分割线=========2、我是静态内部类的静态模块... com.chloneda.jutils.test.OuterClassTest@b1bc7ed3、静态内部类的方法模块... com.chloneda.jutils.test.OuterClassTest@b1bc7ed 从运行结果来看,验证是正确的! 由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。 由此可得出静态内部类单例模式的写法。 静态内部类12345678910111213141516171819package com.chloneda.jutils.test;/** * @Created by chloneda * @Description: * 单例模式使用静态内部类方式实现,优点:实现代码简洁、延迟初始化、线程安全 */public class Singleton { private static final class SingletonHolder { private static Singleton INSTANCE = new Singleton(); } private Singleton(){} public static Singleton getInstance(){ return SingletonHolder.INSTANCE; }} 这种写法的单例,外部无法访问静态内部类 SingletonHolder,只有当调用 Singleton.getInstance() 方法的时候,才能得到单例对象 INSTANCE。 而且静态内部类单例的 getInstance() 方法中没有使用 synchronized 关键字,提高了执行效率,同时兼顾了懒汉模式的内存优化(使用时才初始化,节约空间,达到懒加载的目的)以及饿汉模式的安全性。 但这种单例也有问题!这种方式需要两个类去做到这一点,也就是说,虽然懒加载静态内部类的对象,但其 外部类及内部静态类的 Class 对象还是会被创建,同时也无法防止反射对单例的破坏性(很多单例的写法都有这个通病),从而无法保证对象的唯一性。 我们通过以下测试类测试反射对静态内部类的破坏性。 12345678910111213141516171819202122232425/** * @Created by chloneda * @Description: 反射破坏静态内部类单例模式的测试类 */public class SingletonReflectTest { public static void main(String[] args) { //创建第一个实例 Singleton instance1 = Singleton.getInstance(); //通过反射创建第二个实例 Singleton instance2 = null; try { Class<Singleton> clazz = Singleton.class; Constructor<Singleton> cons = clazz.getDeclaredConstructor(); cons.setAccessible(true); instance2 = cons.newInstance(); } catch (Exception e) { e.printStackTrace(); } //检查两个实例的hash值 System.out.println("Instance1 hashCode: " + instance1.hashCode()); System.out.println("Instance2 hashCode: " + instance2.hashCode()); }} 输出结果如下。12Instance1 hashCode: 186370029Instance2 hashCode: 2094548358 从输出结果可以看出,通过反射获取构造函数,并调用 setAccessible(true) 就可以调用私有的构造函数,从而得到Instance1和Instance2两个不同的对象。 静态内部类改进 如何防止这种反射对单例的破坏呢?我们可以通过修改构造器,让它在被要求创建第二个实例的时候抛出异常。 静态内部类修改如下。1234567891011121314151617181920212223242526272829package com.chloneda.jutils.test;/** * @Created by chloneda * @Description: 防止反射破坏静态内部类单例模式 */public class Singleton { private static boolean initialized = false; private static final class SingletonHolder { private static Singleton INSTANCE = new Singleton(); } private Singleton() { synchronized (Singleton.class) { if (initialized == false) { initialized = !initialized; } else { throw new RuntimeException("单例模式禁止二次创建,防止反射!"); } } } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } } 我们还用一个 SingletonReflectTest 测试类测试一下,输出结果如下。123456789101112java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.chloneda.jutils.test.SingletonReflectTest.main(Singleton.java:46)Caused by: java.lang.RuntimeException: 单例模式禁止二次创建,防止反射! at com.chloneda.jutils.test.Singleton.<init>(Singleton.java:24) ... 5 moreInstance1 hashCode: 1053782781Exception in thread "main" java.lang.NullPointerException at com.chloneda.jutils.test.SingletonReflectTest.main(Singleton.java:53) 所以我们通过修改构造器防止反射对单例的破坏性。 但是这种方式的单例也存在问题!什么问题呢?即序列化和反序列化之后无法继续保持单例(很多单例的写法也有这个通病)。 我们让上面防止反射破坏静态内部类的单例实现 Serializable 接口。1public class Singleton implements Serializable 并通过以下测试类进行序列化和反序列化测试。123456789101112131415161718192021222324252627/** * @Created by chloneda * @Description: 序列化破坏静态内部类单例模式的测试类 */pubic class SingletonSerializableTest { public static void main(String[] args) { try { Singleton instance1 = Singleton.getInstance(); ObjectOutput out = null; out = new ObjectOutputStream(new FileOutputStream("Singleton.ser")); out.writeObject(instance1); out.close(); //从文件中反序列化一个Singleton对象 ObjectInput in = new ObjectInputStream(new FileInputStream("Singleton.ser")); Singleton instance2 = (Singleton) in.readObject(); in.close(); System.out.println("instance1 hashCode: " + instance1.hashCode()); System.out.println("instance2 hashCode: " + instance2.hashCode()); } catch (Exception e) { e.printStackTrace(); } }} 输出结果如下。12instance1 hashCode: 240650537instance2 hashCode: 1566502717 从结果可以看出,很明显不是同一个单例对象! 那如何解决这个问题呢? 静态内部类再改进 我们可以实现 readResolve() 方法,它代替了从流中读取对象,确保了在序列化和反序列化的过程中没人可以创建新的实例。 可以得到改进版的静态内部类单例,可以有效防止序列化及反射的破坏!12345678910111213141516171819202122232425262728293031323334package com.chloneda.jutils.test;import java.io.*;/** * @Created by chloneda * @Description: 可以防止序列化及反射破坏的静态内部类单例模式 */public class Singleton implements Serializable { private static boolean initialized = false; private static final class SingletonHolder { private static Singleton INSTANCE = new Singleton(); } private Singleton() { synchronized (Singleton.class) { if (initialized == false) { initialized = !initialized; } else { throw new RuntimeException("单例模式禁止二次创建,防止反射!"); } } } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } private Object readResolve() { return getInstance(); }} 我们再用上面的 SingletonSerializableTest 测试类测试一下结果。 输出结果如下。12instance1 hashCode: 240650537instance2 hashCode: 240650537 此时就说明,单例在序列化和反序列化时的对象是一致的了。 其实上面饿汉式、懒汉式、双重校验锁及静态内部类单例所出现的问题,都可以通过枚举型单例进行解决,这也是《Effective Java》中推荐的写法。 枚举型我们知道java中的枚举类是在JDK5中才有的,因此枚举型单例对大部分人来说是比较陌生的,先举一个例子吧! 枚举型单例实现很简单。123public enum Singleton { INSTANCE;} 就三行,很简单吧。那么为了大家更方便理解,我再用以下例子来剖析一下枚举型单例吧! 1234567891011121314151617181920212223package com.chloneda.jutils.test;/** * @Created by chloneda * @Description: 单例类 */public class Singleton{}/** * 枚举型单例 */public enum SingletonEnum { INSTANCE; private Singleton singleton = null; private SingletonEnum() { singleton = new Singleton(); } public Singleton getInstance() { return singleton; }} 上面这个枚举型单例,通过反编译你就会得到以下代码。1234567public final class SingletonEnum extends java.lang.Enum<SingletonEnum> { public static final SingletonEnum INSTANCE; public static SingletonEnum[] values(); public static SingletonEnum valueOf(String); public Singleton getSingleton(); static {};} 上面代码经过处理(只是去掉包名,便于阅读)! 可以发现枚举型单例相关属性都被 static 关键词修饰,仔细一看还是 final 修饰的一个普通类,只不过继承自 java.lang.Enum 枚举类而已。也就是说这个枚举型单例是经过编译器处理的,这是JDK5提供的语法糖(Syntactic Sugar),所谓语法糖是指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,只是在编译器上做了手脚,可以更方便我们程序员使用。 枚举型单例天生就是线程安全的,这是因为 JVM类加载机制 的缘故,这里就不展开论述了,可以参考一下相关资料! 枚举型单例反射问题我们先了解一下枚举型单例为什么可以防止反射的问题? 其实在JDK5中,Java虚拟机对枚举类做了特殊处理,即 JVM 会阻止反射获取枚举类的私有构造方法。 我们用这个枚举型单例作为例子。123456public enum Singleton { INSTANCE; Singleton() { }} 继续使用上文的反射代码 SingletonReflectTest 来进行测试,先把SingletonReflectTest类的创建第一个实例的语句改成这样。 12//创建第一个实例Singleton instance1 = Singleton.INSTANCE; 运行后输出结果如下。1234567java.lang.NoSuchMethodException: com.chloneda.jutils.test.Singleton.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at com.chloneda.jutils.test.SingletonReflectTest.main(SingletonReflectTest.java:18)Instance1 hashCode: 1929600551Exception in thread "main" java.lang.NullPointerException at com.chloneda.jutils.test.SingletonReflectTest.main(SingletonReflectTest.java:27) 直接报异常,也就是说枚举型单例可以完美解决反射的问题。 枚举型单例反序列化问题下面再深入了解一下为什么枚举会满足反序列化的问题 Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。 在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。 以上面的public enum SingletonEnum 枚举为例,序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。 我们通过测试类测试一下,我们首先让枚举型单例 SingletonEnum 实现 Serializable 接口。1public enum SingletonEnum implements Serializable 再使用序列化测试类 SingletonSerializableTest 进行测试,输出结果如下。 12instance1 hashCode: 1674896058instance2 hashCode: 1674896058 也就是说枚举型单例可以解决反序列化带来的问题。 综上所述,枚举型单例可以有效解决线程安全、反射、反序列化带来的问题! 然而你别得意,枚举型单例也有问题,就是它无法进行懒加载,因为枚举型单例的实例对象是在静态代码块即static块进行初始化的,是不是一语惊醒梦中人啊! 中场休息说了这么多单例模式,知道了各个单例模式利弊,所以当我们使用时,我们要根据实际情况做出取舍,因为我们不可能实现一个单例可以满足所有情况。 下面让我们来看看单例模式的实际应用场景吧! 单例模式的实际应用生活中的单例我们计算机上有很多场景应用到单例模式,可能经常看到,但我们并没有认识到,比如以下例子。 Windows的任务管理器(Task Manager)就是很典型的单例模式。你试过能打开两个windows的任务管理器吗? windows的回收站也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。 网站的计数器,一般也是采用单例模式实现,否则难以同步。 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。 Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。 当然还有很多单例模式的应用,希望大家可以发现哦! JDK中的单例java.lang.RuntimeRuntime类封装了Java运行时的环境。每一个java程序实际上都是启动了一个JVM进程,那么每个JVM进程都是对应这一个Runtime实例,此实例是由JVM为其实例化的。每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。 由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个,所以应该使用单例来实现。 我们来看看 java.lang.Runtime 的单例模式实现。123456789public class Runtime { private static Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; } private Runtime() {}} 看见没,这里使用的单例模式是饿汉式单例模式,也就是说,当类第一次被classloader加载的时候,实例就被创建出来了,当调用者每次调用的时候,就不需要再判断这个实例是否已经初始化,典型的空间换时间方案。 这里使用的是饿汉式单例模式,无疑是非常合适的! java.awt.ToolkitToolkit是GUI中的类,与RunTime不同的是Toolkit采用的是懒汉式单例模式,因为它们并不需要事先创建好,只要在第一次真正用到的时候再创建就可以了。 我们来看看Toolkit类的代码。1234567891011121314151617181920212223242526272829303132333435363738394041424344public abstract class Toolkit { private static Toolkit toolkit; public static synchronized Toolkit getDefaultToolkit() { if (toolkit == null) { java.security.AccessController.doPrivileged( new java.security.PrivilegedAction<Void>() { public Void run() { Class<?> cls = null; String nm = System.getProperty("awt.toolkit"); try { cls = Class.forName(nm); } catch (ClassNotFoundException e) { ClassLoader cl = ClassLoader.getSystemClassLoader(); if (cl != null) { try { cls = cl.loadClass(nm); } catch (final ClassNotFoundException ignored) { throw new AWTError("Toolkit not found: " + nm); } } } try { if (cls != null) { toolkit = (Toolkit)cls.newInstance(); if (GraphicsEnvironment.isHeadless()) { toolkit = new HeadlessToolkit(toolkit); } } } catch (final InstantiationException ignored) { throw new AWTError("Could not instantiate Toolkit: " + nm); } catch (final IllegalAccessException ignored) { throw new AWTError("Could not access Toolkit: " + nm); } return null; } }); loadAssistiveTechnologies(); } return toolkit; } } 观察上面的代码你会发现Toolkit是一个抽象类,本身就无法实例化,而是通过反射机制加载类并创建新的实例。 懒汉式单例模式,并不会第一时间创建实例,提高了JVM的启动速度,典型的时间换空间方案,同时也体现了延迟加载的思想。 此外,需要注意的是懒汉式单例模式的线程安全问题,关于网上也有很多版本,都各有优势,大家适当取舍吧! 框架中的单例 Mybatis中的单例模式:如ErrorContext和LogFactory。 Spring框架中的单例模式:采用单例注册表的方式进行实现中的单例模式,如AbstractBeanFactory抽象类的getBeans()方法。 小结这篇文章有点长,不过总算把单例模式说清楚了,哎,我的脑细胞!]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[git rm与git rm -cached的区别]]></title>
<url>%2F%2Fblog%2Fgit-rm%2F</url>
<content type="text"><![CDATA[git rm与git rm –cached的区别 当我们需要删除暂存区或分支上的文件, 同时工作区也不需要这个文件了, 可以使用。123git rm file_pathgit commit -m 'delete somefile'git push 当我们需要删除暂存区或分支上的文件, 但本地又需要使用, 只是不希望这个文件被版本控制, 可以使用。123git rm --cached file_pathgit commit -m 'delete remote somefile'git push]]></content>
<categories>
<category>Git</category>
</categories>
<tags>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[计算机技术类书籍]]></title>
<url>%2F%2Fblog%2Fbook-list-computer%2F</url>
<content type="text"><![CDATA[计算机技术类书籍。 书籍来源于网络,版权归原作者所有,在此不以盈利为目的,禁止用于商业用途,仅限学习使用,如有侵权请联系 chloneda@gmail.com 删除。 语言 Java编程思想 Effective Java中文版 JVM 深入理解Java虚拟机:JVM高级特性与最佳实践 设计模式 研磨设计模式 框架Spring Spring实战 Spring揭秘 HibernateJava Persistence with Hibernate Mybatis Java Persistence with MyBatis 3 数据库 数据库系统概念 MySQL技术内幕:InnoDB存储引擎 高性能MySQL 网络 计算机网络 图解HTTP 操作系统 现代操作系统 深入理解计算机系统 鸟哥的Linux私房菜:基础学习篇 数据结构与算法 算法导论 Java数据结构和算法 代码优化及重构 代码大全 重构-改善既有代码的设计 编程感悟 不止代码 其他 Vim实用技巧]]></content>
<categories>
<category>随笔</category>
</categories>
<tags>
<tag>随笔</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SSH自动断开后重连的解决方案]]></title>
<url>%2F%2Fblog%2Fssh-connect%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 问题场景终端连接远程 SSH 服务,经常会出现长时间无操作后就自动断开,或者无响应,无法再通过键盘输入,只能强行断开重连。 那么有没有办法保持 SSH 连接不断开,或者断开连接后自动重连呢?有的! 解决方法方案一:客户端发送心跳 Linux / Unix 下,编辑 ssh 配置文件:1vim /etc/ssh/ssh_config 在文件中添加以下内容:12ServerAliveInterval 20ServerAliveCountMax 999 ServerAliveInterval:表示每隔多少秒,从客户端向服务器端发送一次心跳(alive 检测)。 ServerAliveCountMax:表示服务端多少次心跳无响应之后,客户端才会认为与服务器的 SSH 连接已经断开,然后断开连接。 上述配置则表示:每隔20秒,向服务器发出一次心跳。若超过999次请求都没有发送成功,则会主动断开与服务器端的连接。 方案二:服务器端发送心跳 在服务器端中,编辑 ssh 配置文件:1sudo vim /etc/ssh/sshd_config 在文件中添加以下内容:12ClientAliveInterval 60ClientAliveCountMax 3 ClientAliveInterval:表示每隔多少秒,从服务器端向客户端发送一次心跳。 ClientAliveInterval:表示客户端多少次心跳无响应之后,服务端才会认为客户端已经断开连接,然后断开连接。 上述配置则表示:每隔60秒,服务器向客户端发出一次心跳。若客户端超过3次请求未响应,则会从服务器端断开与客户端的连接。 所以,总共允许无响应的时间是 60 * 3 = 180 秒以内。 其实,依赖 ssh 客户端定时发送心跳,putty、SecureCRT、XShell 工具也有这个功能。 完!]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[解决Mac无法写入U盘问题]]></title>
<url>%2F%2Fblog%2Fupan-for-mac%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 前言 新手使用MacBook Pro时,会发现Mac系统下只能读取U盘,但不能写入。其实这个问题是因为,Mac OS系统硬盘格式为HFS, Windows 的硬盘格式为 NTFS,二者互不兼容。那么有没有解决的办法呢? 网上的资料一般都安装第三方软件,如 NTFS for MAC 等,但一般都是收费的。或者,格式化U盘,将U盘磁盘格式设定为 FAT 或 exFAT,但个人不提倡。 现在提供一种方法进行开启 Mac OS 读写 NTFS格式 U盘的功能。经过测试! 实现步骤1、打开 终端,输入:1diskutil list 该命令用于列出系统下的各个磁盘信息,找到你要处理的U盘名称,如名称为:Chloneda。 2、在终端中,输入:1sudo vim /etc/fstab 然后输入电脑密码(没有密码的不用输),输入电脑密码后,加入以下内容,进行配置:1LABEL=U盘名称 none ntfs rw,auto,nobrowse 如下图: 注意:如果你的U盘只有一个,只需添加一个即可,不能有空行!其次,如果你的U盘含有空格,如 Chloneda X,U盘名称中的空格用\040代替,即命令应该写成:1LABEL=Chloneda\040X none ntfs rw,auto,nobrowse 参数说明: U盘名称:建议不要有中文。 ntfs rw: 表示把这个分区挂载为可读写的 ntfs格式。 nobrowse:这个代表了在 finder 里不显示这个分区,这个选项非常重要,如果不打开的话挂载会失败。完成后,按 esc 键退出编辑模式,并按 :wq! 命令,然后回车进行保存。 3、创建快捷方式终端中输入以下内容,创建桌面快捷方式。1sudo ln -s /Volumes/U盘名称 ~/Desktop/U盘名称 因为刚刚创建的分区是不会在 Finder 里不显示的,创建桌面快捷方式,方便以后再次访问U盘(可将 快捷方式 拖拽至 Finder 的侧边栏喔)。 4、拔掉U盘,重新插入,可见正常显示,正常读写。 补充1、如果不可以写入U盘,请重启一下电脑。2、如果要恢复之前样子,请输入命令 sudo vim /etc/fstab 重新编辑,把写入的 LABEL 一行删除,重新保存即可。]]></content>
<categories>
<category>采坑日记</category>
</categories>
<tags>
<tag>采坑日记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Docker容器Centos不能使用systemctl命令问题]]></title>
<url>%2F%2Fblog%2Fbug-dock-os%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 最近使用Docker搭建Centos容器时遇到这样的问题:Centos系统的不能使用systemctl命令! 具体场景使用 systemctl 或 service 命令重启服务时。1systemctl restart snmpd.service 会报无权限的错误:1Failed to get D-Bus connection: Operation not permitted; 这是docker中centos7的bug,官网上也提到了这个问题,并给出了 解决办法,但有点复杂。我们可以通过以下方法解决! 首先,使用docker构建centos容器加上 privileged 参数,即在docker run命令是要加上 –privileged=true,该参数在docker容器运行时,让系统拥有真正的root权限。 其次,在启动容器时,在docker run 命令最后,加上/usr/sbin/init,最终命令为:1docker run -v /tmp/:/tmp --privileged --cap-add SYS_ADMIN -e container=docker -it --name=centos -d --restart=always centos /usr/sbin/init 参数说明: -v /tmp/:/tmp:挂载宿主机的一个目录,冒号”:”前面的目录是宿主机目录,后面的目录是容器内目录。 –privileged: 指定容器是否是特权容器。 –cap-add SYS_ADMIN: 添加系统的权限,不然系统很多功能都用不了的。 -e container=docker:设置容器的类型。 -it: 启动互动模式。 /usr/sbin/init:初始容器里的CENTOS,用于启动dbus-daemon。 最后,如果想查看Docker更多内容,请查看Docker官网文档。]]></content>
<categories>
<category>采坑日记</category>
</categories>
<tags>
<tag>采坑日记</tag>
</tags>
</entry>
<entry>
<title><![CDATA[那些优秀的软件工具!]]></title>
<url>%2F%2Fblog%2Ftools%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 本博客用于收录那些优秀的软件工具,并且长期更新! Windowseverythingeverything:是速度最快的文件名搜索软件。其速度之快令人震惊,百G硬盘几十万个文件,可以在几秒钟之内完成索引;文件名搜索瞬间呈现结果。它小巧免费,支持中文,支持正则表达式,可以通过HTTP或FTP分享搜索结果。如果不满意Windows自带的搜索工具,那就用它吧! MacOseZipeZip:是一款 Mac 上完全免费的压缩与解压缩工具软件,主要是提供了文件预览以及加密压缩和解压缩功能,功能足够实用,基本可以替代市面上大部分的 macOS 商业收费压缩工具,个人认为值得力荐。 OmniGraffleOmniGraffle:Windows下使用Visio画框图、流程图非常方便,切换到MAC后,我们可以使用OmniGraffle。 CheatSheetCheatSheet:Mac上的快捷键神器 LinuxRemminaRemmina:是一款在 Linux 和其他类 Unix 系统下的自由开源、功能丰富、强大的远程桌面客户端,它用 GTK+ 3 编写而成。它适用于那些需要远程访问及使用许多计算机的系统管理员和在外出行人员。]]></content>
<categories>
<category>tools</category>
</categories>
<tags>
<tag>tools</tag>
</tags>
</entry>
<entry>
<title><![CDATA[git同步代码至github和gitee(码云)]]></title>
<url>%2F%2Fblog%2Fgit-to-github-gitee%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 我们有时候开发代码需要把代码同步到多个远程库中,如何操作才能做到呢? 我们知道,git是分布式版本控制系统,同步到多个远程库时,需要用不同的名称来标识不同的远程库,而git给远程库起的默认名称是origin。所以我们需要修改、配置名称,以关联不同远程库。有两种方式! 为了方便举例,我以GitHub和Gitee(码云)作为示例! 同步方式命令方式同步先删除已关联的名为origin的远程库:1git remote rm origin 然后,再关联GitHub的远程库:1git remote add github git@github.com:chloneda/demo.git 接着,再关联码云的远程库:1git remote add gitee git@gitee.com:chloneda/demo.git 配置方式同步修改.git文件夹内的config文件:1234567891011[core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true[remote "origin"] url = git@github.com:chloneda/demo.git fetch = +refs/heads/*:refs/remotes/github/*[branch "master"] remote = origin merge = refs/heads/master 将上述文件内容[remote “origin”]内容复制,修改origin名称,内容如下:1234567891011121314[core] repositoryformatversion = 0 filemode = true bare = false logallrefupdates = true[remote "github"] url = git@github.com:chloneda/demo.git fetch = +refs/heads/*:refs/remotes/github/*[remote "gitee"] url = git@gitee.com:chloneda/demo.git fetch = +refs/heads/*:refs/remotes/gitee/*[branch "master"] remote = origin merge = refs/heads/master 查看远程库通过以上两种方式的任一种方式配置完成后,我们用git remote -v查看远程库信息:1234gitee git@gitee.com:chloneda/demo.git (fetch)gitee git@gitee.com:chloneda/demo.git (push)github git@github.com:chloneda/demo.git (fetch)github git@github.com:chloneda/demo.git (push) 可以看到两个远程库,说明配置生效了。 上传代码12git add .git commit -m "update" 提交到github1git push github master 提交到码云1git push gitee master 更新代码12345# 从github拉取更新git pull github# 从gitee拉取更新git pull gitee 踩到的坑上述过程中,更新或提交代码时可能会遇到fatal:refusing to merge unrelated histories (拒绝合并无关的历史) 错误,解决办法: 首先将远程仓库和本地仓库关联起来。 1git branch --set-upstream-to=origin/remote_branch your_branch 其中,origin/remote_branch是你本地分支对应的远程分支,your_branch是你当前的本地分支。 然后使用git pull整合远程仓库和本地仓库。1git pull --allow-unrelated-histories (忽略版本不同造成的影响) 重新更新、提交即可。 注: 如遇到 Git没有共同祖先的两个分支合并 的情形请自行查询!]]></content>
<categories>
<category>Git</category>
</categories>
<tags>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[聊聊SNMP协议]]></title>
<url>%2F%2Fblog%2Fsnmp-protocol%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 SNMP概述SNMP(Simple Network Management Protocol):简单网络管理协议,是基于TCP/IP五层协议中的应用层协议。由于其简单可靠,提供了一种监控和管理网络设备的系统方法,因此受到了众多厂商的欢迎,成为了目前最为广泛的网管协议。 SNMP的基本思想:为不同种类、厂家、型号的设备,定义一个统一的接口和协议,使得管理员可以是使用统一的外观面对这些需要管理的网络设备。通过网络,管理员可以管理位于不同物理空间的设备,从而大大提高网络管理的效率,简化网络管理员的工作。 Snmp版本SNMP目前共有v1,v2,v3这三个版本,三个版本的联系与区别。 SNMP v1:是SNMP协议的最初版本,存在较多安全缺陷,现在这个版本是使用的比较少了。 SNMP v2:也采用团体名认证,在兼容SNMPv1的同时又扩充了SNMPv1的功能,具体是扩展了数据类型、支持分布式网络管理、可以实现大量数据的传输,提高了效率和性能、丰富了故障处理能力及增加了集合处理功能。 SNMP v3:是最新版本的SNMP。它相对于V2版本,在安全性上得到了重要提升,增加了对认证和密文传输的支持。 SNMP核心概念SNMP管理架构SNMP管理架构应包含四个部分进行网络管理:SNMP管理站、SNMP代理、MIB(管理信息库)和SNMP管理协议。 SNMP管理站(management station):通常被称作为网络管理工作站(NMS),负责收集维护各个SNMP元素的信息,通过UDP协议向SNMP代理发送各种命令,当SNMP代理收到命令后,对收集的信息进行处理,并返回SNMP管理站需要的参数。而此时被管理对象中一定要有代理进程,这样才能响应管理站发来的请求。 SNMP代理(Agent):运行在各个被管理的网络节点之上,负责统计该节点的各项信息,并且负责与SNMP管理站交互,接收并执行管理站的命令,上传各种本地的网络信息。 注:协议栈中带有阴影的部分是原主机或路由器所具有的,而没有阴影的部分是为实现网络管理而增加的。 MIB(管理信息库):是对象的集合,它代表网络中可以管理的资源和设备。每个对象基本上是一个数据变量,它代表被管理的对象的一方面的信息。 SNMP管理协议:用于管理站与SNMP代理之间的通信规则,其SNMP报文格式请查看底下章节。 管理站和代理之间利用 SNMP 报文进行通信,而 SNMP 报文又使用 UDP协议来传送,由于采用UDP协议,不需要在代理和管理站之间保持连接。 SNMP报文格式管理站(NMS)和代理(Agent)之间交换的管理信息构成了SNMP报文,报文的基本格式如下图: 下面将对该SNMP报文格式逐个进行说明! SNMP消息报文包含两个部分:SNMP报头和PDU(协议数据单元)。 SNMP报头 版本:版本号字段,规则为版本号减 1,如SNMP V1则应写入 0。如果SNMP代理使用不相同的协议,会直接抛弃与自己协议版本不同的数据报。 共同体(community):即团体名,作为管理进程和代理进程之间的明文口令,相当于密码,默认为public,该字段可读可写,用来限制NMS对Agent的访问。如果团体名没有得到NMS/Agent的认可,该报文将被丢弃。 PDU 类型:填入 0~4 中的一个数字,其对应关系如图。 PDU类型请参考PDU(协议数据单元)章节! Get/Set 首部 请求标识符(request ID),这是由管理进程设置的一个整数值。代理进程在发送 get-response 报文时也要返回此请求标识符。管理进程可同时向许多代理发出 get 报文,这些报文都使用 UDP 传送,先发送的有可能后到达。设置了请求标识符可使管理进程能够识别返回的响应报文对于哪一个请求。 差错状态(error status):由代理进程回答时填入 0~5 中的一个数字,见表 3 的描述。 差错索引(error index):当出现 noSuchName、badValue 或 readOnly 的差错时,由代理进程在回答时设置的一个整数,它指明有差错的变量在变量列表中的偏移。 Trap 首部 企业(enterprise):填入 trap 报文的网络设备的对象标识符。此对象标识符肯定是在图 3 的对象命名树上的 enterprise 结点{1.3.6.1.4.1}下面的一棵子树上。 Trap 类型:此字段正式的名称是 generic-trap,共分为表 4 中的 7 种。 当使用上述类型 2、3、5 时,在报文后面变量部分的第一个变量应标识响应的接口。 特定代码(specific-code):指明代理自定义的时间(若 trap 类型为 6),否则为 0。 时间戳(timestamp):指明自代理进程初始化到 trap 报告的事件发生所经历的时间,单位为 10ms。例如时间戳为 1908 表明在代理初始化后 1908ms 发生了该时间。 变量绑定(variable-bindings):指明一个或多个变量的名和对应的值。在 get 或 get-next 报文中,变量的值应忽略。 PDU(协议数据单元)SNMP v1 版本规定了 5 种核心 PDU(协议数据单元),用来在管理进程和代理之间信息的交换。 get-request 操作:从代理进程处提取一个OID值。 get-next-request 操作:从代理进程(MIB中)处提取紧跟当前参数值的下一个OID值,常被用于检索表数据,也被用于不能指定名称的变量,可以浏览MIB树。 set-request 操作:设置代理进程的一个或多个参数值。 get-response 操作:返回的一个或多个参数值。这个操作是由代理进程发出的,它是前面三种操作的响应操作。 trap 操作:代理进程主动发出的报文,通知管理进程有某些事情发生。 前面的 3 种操作是由管理进程向代理进程发出的,后面的 2 个操作是代理进程发给管理进程的,其中代理进程端是用 161 端口接收 get 或 set 报文,而在管理进程端是用 162 端口来接收 trap 报文。 另外,在SNMP v2版本又增加了 3 种PDU(协议数据单元),它们分别是: inform-request 操作:允许路由器向SNMP管理器发送通知请求。 getBulk-request 操作:从代理进程处提取多个参数值,该操作会根据最大重试值执行一连串的GetNext操作,减少管理站与代理之间的交互,提高效率。 report 操作: SNMP的操作类型其实上述 8 种PDU(协议数据单元)按照功能不同,可以归结为三类操作。 查询、设置SNMP变量,如get-request、get-next-request、set-request、getBulk-request、inform-request。 应答请求,如 get-response。 事件报告,如trap。 实际上,在SNMP中,SNMP管理站对被管理设备的SNMP变量的操作只能有 读 与 写 两种基本动作。 ASN.1(抽象语法标记)ASN.1:高级的数据描述语言。描述数据的类型、结构、组织、及编码方法。包括符号和语法两部分。SNMP使用ASN.1描述PDU和MIB(管理信息库)。 关于ASN.1详细信息请查看这篇博文:SNMP从入门到开发:进阶篇 BER(基本编码规则)BER(Basic Encoding Rule),中文名称:基本编码规则。描述具体的ASN.1对象如何编码为比特流在网络上传输。SNMP使用BER(Basic Encoding Rule)作为编码方案,数据首先先经过BER编码,再经由传输层协议(一边是UDP)发往接收方。接收方在SNMP端口上收到PDU后,经过BER解码后,得到具体的SNMP操作数据。 BER的数据都由三个域构成:标识域(tag)+长度域(length)+值域(value)。 SMI(管理信息结构)SMI(Structure of Managerment Intormation),中文名称:管理信息结构,是SNMP的描述方法。规定了使用ASN.1子类型、符号。ASN.1功能强大,但SNMP只用到了其中很小一部分,对于这一部分内容的描述,限定了范围,即为SMI。SMI规定了使用到的ASN.1类型、宏、符号等。SMI是ASN.1的一个子集和超集。 MIB(管理信息库)MIB(Management Information Base),中文名称:管理信息库,由网络管理协议访问的管理对象数据库。MIB是对象的集合,它代表网络中可以管理的资源和设备。每个对象基本上是一个数据变量,它代表被管理的对象的一方面的信息。 管理信息库 MIB 指明了网络元素所维持的变量(即能够被管理进程查询和设置的信息)。MIB 给出了一个网络中所有可能的被管理对象的集合的数据结构。SNMP 的管理信息库采用和域名系统 DNS 相似的树型结构,它的根在最上面,根没有名字。底下的图 画的是管理信息库的一部分,它又称为对象命名(object naming tree)。 MIB采用分层树形结构,对象命名树的顶级对象有三个,即 ISO、ITU-T 和这两个组织的联合体。在 ISO 的下面有 4 个结点,其中的饿一个(标号 3)是被标识的组织。在其下面有一个美国国防部(Department of Defense)的子树(标号是 6),再下面就是 Internet(标号是 1)。在只讨论 Internet 中的对象时,可只画出 Internet 以下的子树(图中带阴影的虚线方框),并在 Internet 结点旁边标注上{1.3.6.1}即可。 在 Internet 结点下面的第二个结点是 mgmt(管理),标号是 2。再下面是管理信息库,原先的结点名是 mib。1991 年定义了新的版本 MIB-II,故结点名现改为 mib-2,其标识为{1.3.6.1.2.1},或{Internet(1) .2.1}。这种标识为对象标识符。 最初的结点 mib 将其所管理的信息分为 8 个类别,如图,现在的 mib-2 所包含的信息类别已超过 40 个。 应当指出,MIB 的定义与具体的网络管理协议无关,这对于厂商和用户都有利。厂商可以在产品(如路由器)中包含 SNMP 代理软件,并保证在定义新的 MIB 项目后该软件仍遵守标准。用户可以使用同一网络管理客户软件来管理具有不同版本的 MIB 的多个路由器。当然,一个没有新的 MIB 项目的路由器不能提供这些项目的信息。这里要提一下MIB中的对象{1.3.6.1.4.1},即enterprises(企业),其所属结点数已超过 3000。例如IBM为{1.3.6.1.4.1.2},Cisco为{1.3.6.1.4.1.9},Novell为{1.3.6.1.4.1.23}等。世界上任何一个公司、学校只要用电子邮件发往iana-mib@isi.edu进行申请即可获得一个结点名。这样各厂家就可以定义自己的产品的被管理对象名,使它能用SNMP进行管理。 ASN.1、BER、SMI、MIB、PDU的关系关于ASN.1、BER、SMI、MIB、PDU的关系如下图所示。 OID(对象标识符)OID(Object Identifier),中文名称:对象标识符,被管理设备的每个管理资源和对象都有自己的OID(Object Identifier),管理对象通过树状结构进行组织,OID由树上的一系列整数组成,整数之间用点( . )分隔开,树的叶子节点才是真正能够被管理的对象。 常用OID这里总结了一些常用的OID,当需要时可以及时查询。SNMP监控常用OID查询SNMP监控一些常用OID的总结下载不同厂商的MIB包查看不同厂商的OID代号 SNMP协议实现SNMP协议的Java实现是SNMP4J,其jar包可以在SNMP官方网站上下载。开发前请简单了解一下SNMP4J,具体细节请看这篇博文:SNMP4J介绍;更多SNMP4J示例请参考Github。 本文小结SNMP支持Window、Linux、MacOS系统的安装,关于SNMP的安装步骤请自行查询。]]></content>
<categories>
<category>SNMP</category>
</categories>
<tags>
<tag>SNMP</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出Mybatis系列五-TypeHandler简介及配置(mybatis源码篇)]]></title>
<url>%2F%2Fblog%2Fmybatis-5%2F</url>
<content type="text"><![CDATA[注:本文转载自南轲梦 上篇文章《深入浅出Mybatis系列(四)—配置详解之typeAliases别名(mybatis源码篇)》为大家介绍了mybatis中别名的使用,以及其源码。本篇将为大家介绍TypeHandler, 并简单分析其源码。 Mybatis中的TypeHandler是什么? 无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。Mybatis默认为我们实现了许多TypeHandler, 当我们没有配置指定TypeHandler时,Mybatis会根据参数或者返回结果的不同,默认为我们选择合适的TypeHandler处理。 那么,Mybatis为我们实现了哪些TypeHandler呢? 我们怎么自定义实现一个TypeHandler ? 这些都会在接下来的mybatis的源码中看到。 在看源码之前,还是像之前一样,先看看怎么配置吧? 配置TypeHandler123456789101112131415161718192021222324252627282930<configuration> <typeHandlers> <!-- 当配置package的时候,mybatis会去配置的package扫描TypeHandler <package name="com.dy.demo"/> --> <!-- handler属性直接配置我们要指定的TypeHandler --> <typeHandler handler=""/> <!-- javaType 配置java类型,例如String, 如果配上javaType, 那么指定的typeHandler就只作用于指定的类型 --> <typeHandler javaType="" handler=""/> <!-- jdbcType 配置数据库基本数据类型,例如varchar, 如果配上jdbcType, 那么指定的typeHandler就只作用于指定的类型 --> <typeHandler jdbcType="" handler=""/> <!-- 也可两者都配置 --> <typeHandler javaType="" jdbcType="" handler=""/> </typeHandlers> ...... </configuration> 上面简单介绍了一下TypeHandler, 下面就看看mybatis中TypeHandler的源码了。 老规矩,先从对xml的解析讲起: typeHandlers节点源码123456789101112131415161718192021222324252627282930313233343536373839404142/** * 解析typeHandlers节点 */private void typeHandlerElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { //子节点为package时,获取其name属性的值,然后自动扫描package下的自定义typeHandler if ("package".equals(child.getName())) { String typeHandlerPackage = child.getStringAttribute("name"); typeHandlerRegistry.register(typeHandlerPackage); } else { /** * 子节点为typeHandler时, 可以指定javaType属性, * 也可以指定jdbcType, 也可两者都指定 * javaType 是指定java类型 * jdbcType 是指定jdbc类型(数据库类型: 如varchar) */ String javaTypeName = child.getStringAttribute("javaType"); String jdbcTypeName = child.getStringAttribute("jdbcType"); //handler就是我们配置的typeHandler String handlerTypeName = child.getStringAttribute("handler"); //resolveClass方法就是我们上篇文章所讲的TypeAliasRegistry里面处理别名的方法 Class<?> javaTypeClass = resolveClass(javaTypeName); //JdbcType是一个枚举类型,resolveJdbcType方法是在获取枚举类型的值 JdbcType jdbcType = resolveJdbcType(jdbcTypeName); Class<?> typeHandlerClass = resolveClass(handlerTypeName); //注册typeHandler, typeHandler通过TypeHandlerRegistry这个类管理 if (javaTypeClass != null) { if (jdbcType == null) { typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } else { typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); } } else { typeHandlerRegistry.register(typeHandlerClass); } } } }} 接下来看看TypeHandler的管理注册类: TypeHandlerRegistry源码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379/** * typeHandler注册管理类 */public final class TypeHandlerRegistry { //源码一上来,二话不说,几个大大的HashMap就出现,这不又跟上次讲的typeAliases的注册类似么 //基本数据类型与其包装类 private static final Map<Class<?>, Class<?>> reversePrimitiveMap = new HashMap<Class<?>, Class<?>>() { private static final long serialVersionUID = 1L; { put(Byte.class, byte.class); put(Short.class, short.class); put(Integer.class, int.class); put(Long.class, long.class); put(Float.class, float.class); put(Double.class, double.class); put(Boolean.class, boolean.class); put(Character.class, char.class); } }; //这几个MAP不用说就知道存的是什么东西吧,命名的好处 private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler<?>>(JdbcType.class); private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<Type, Map<JdbcType, TypeHandler<?>>>(); private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this); private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<Class<?>, TypeHandler<?>>(); //就像上篇文章讲的typeAliases一样,mybatis也默认给我们注册了不少的typeHandler //具体如下 public TypeHandlerRegistry() { register(Boolean.class, new BooleanTypeHandler()); register(boolean.class, new BooleanTypeHandler()); register(JdbcType.BOOLEAN, new BooleanTypeHandler()); register(JdbcType.BIT, new BooleanTypeHandler()); register(Byte.class, new ByteTypeHandler()); register(byte.class, new ByteTypeHandler()); register(JdbcType.TINYINT, new ByteTypeHandler()); register(Short.class, new ShortTypeHandler()); register(short.class, new ShortTypeHandler()); register(JdbcType.SMALLINT, new ShortTypeHandler()); register(Integer.class, new IntegerTypeHandler()); register(int.class, new IntegerTypeHandler()); register(JdbcType.INTEGER, new IntegerTypeHandler()); register(Long.class, new LongTypeHandler()); register(long.class, new LongTypeHandler()); register(Float.class, new FloatTypeHandler()); register(float.class, new FloatTypeHandler()); register(JdbcType.FLOAT, new FloatTypeHandler()); register(Double.class, new DoubleTypeHandler()); register(double.class, new DoubleTypeHandler()); register(JdbcType.DOUBLE, new DoubleTypeHandler()); register(String.class, new StringTypeHandler()); register(String.class, JdbcType.CHAR, new StringTypeHandler()); register(String.class, JdbcType.CLOB, new ClobTypeHandler()); register(String.class, JdbcType.VARCHAR, new StringTypeHandler()); register(String.class, JdbcType.LONGVARCHAR, new ClobTypeHandler()); register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler()); register(String.class, JdbcType.NCHAR, new NStringTypeHandler()); register(String.class, JdbcType.NCLOB, new NClobTypeHandler()); register(JdbcType.CHAR, new StringTypeHandler()); register(JdbcType.VARCHAR, new StringTypeHandler()); register(JdbcType.CLOB, new ClobTypeHandler()); register(JdbcType.LONGVARCHAR, new ClobTypeHandler()); register(JdbcType.NVARCHAR, new NStringTypeHandler()); register(JdbcType.NCHAR, new NStringTypeHandler()); register(JdbcType.NCLOB, new NClobTypeHandler()); register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler()); register(JdbcType.ARRAY, new ArrayTypeHandler()); register(BigInteger.class, new BigIntegerTypeHandler()); register(JdbcType.BIGINT, new LongTypeHandler()); register(BigDecimal.class, new BigDecimalTypeHandler()); register(JdbcType.REAL, new BigDecimalTypeHandler()); register(JdbcType.DECIMAL, new BigDecimalTypeHandler()); register(JdbcType.NUMERIC, new BigDecimalTypeHandler()); register(Byte[].class, new ByteObjectArrayTypeHandler()); register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler()); register(Byte[].class, JdbcType.LONGVARBINARY,new BlobByteObjectArrayTypeHandler()); register(byte[].class, new ByteArrayTypeHandler()); register(byte[].class, JdbcType.BLOB, new BlobTypeHandler()); register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler()); register(JdbcType.LONGVARBINARY, new BlobTypeHandler()); register(JdbcType.BLOB, new BlobTypeHandler()); register(Object.class, UNKNOWN_TYPE_HANDLER); register(Object.class, JdbcType.OTHER, UNKNOWN_TYPE_HANDLER); register(JdbcType.OTHER, UNKNOWN_TYPE_HANDLER); register(Date.class, new DateTypeHandler()); register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler()); register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler()); register(JdbcType.TIMESTAMP, new DateTypeHandler()); register(JdbcType.DATE, new DateOnlyTypeHandler()); register(JdbcType.TIME, new TimeOnlyTypeHandler()); register(java.sql.Date.class, new SqlDateTypeHandler()); register(java.sql.Time.class, new SqlTimeTypeHandler()); register(java.sql.Timestamp.class, new SqlTimestampTypeHandler()); // issue #273 register(Character.class, new CharacterTypeHandler()); register(char.class, new CharacterTypeHandler()); } public boolean hasTypeHandler(Class<?> javaType) { return hasTypeHandler(javaType, null); } public boolean hasTypeHandler(TypeReference<?> javaTypeReference) { return hasTypeHandler(javaTypeReference, null); } public boolean hasTypeHandler(Class<?> javaType, JdbcType jdbcType) { return javaType != null && getTypeHandler((Type) javaType, jdbcType) != null; } public boolean hasTypeHandler(TypeReference<?> javaTypeReference, JdbcType jdbcType) { return javaTypeReference != null && getTypeHandler(javaTypeReference, jdbcType) != null; } public TypeHandler<?> getMappingTypeHandler( Class<? extends TypeHandler<?>> handlerType) { return ALL_TYPE_HANDLERS_MAP.get(handlerType); } public <T> TypeHandler<T> getTypeHandler(Class<T> type) { return getTypeHandler((Type) type, null); } public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference) { return getTypeHandler(javaTypeReference, null); } public TypeHandler<?> getTypeHandler(JdbcType jdbcType) { return JDBC_TYPE_HANDLER_MAP.get(jdbcType); } public <T> TypeHandler<T> getTypeHandler(Class<T> type, JdbcType jdbcType) { return getTypeHandler((Type) type, jdbcType); } public <T> TypeHandler<T> getTypeHandler( TypeReference<T> javaTypeReference, JdbcType jdbcType) { return getTypeHandler(javaTypeReference.getRawType(), jdbcType); } private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) { Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type); TypeHandler<?> handler = null; if (jdbcHandlerMap != null) { handler = jdbcHandlerMap.get(jdbcType); if (handler == null) { handler = jdbcHandlerMap.get(null); } } if (handler == null && type != null && type instanceof Class && Enum.class.isAssignableFrom((Class<?>) type)) { handler = new EnumTypeHandler((Class<?>) type); } @SuppressWarnings("unchecked") // type drives generics here TypeHandler<T> returned = (TypeHandler<T>) handler; return returned; } public TypeHandler<Object> getUnknownTypeHandler() { return UNKNOWN_TYPE_HANDLER; } public void register(JdbcType jdbcType, TypeHandler<?> handler) { JDBC_TYPE_HANDLER_MAP.put(jdbcType, handler); } // // REGISTER INSTANCE // /** * 只配置了typeHandler, 没有配置jdbcType 或者javaType */ @SuppressWarnings("unchecked") public <T> void register(TypeHandler<T> typeHandler) { boolean mappedTypeFound = false; //在自定义typeHandler的时候,可以加上注解MappedTypes 去指定关联的javaType //因此,此处需要扫描MappedTypes注解 MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class); if (mappedTypes != null) { for (Class<?> handledType : mappedTypes.value()) { register(handledType, typeHandler); mappedTypeFound = true; } } // @since 3.1.0 - try to auto-discover the mapped type if (!mappedTypeFound && typeHandler instanceof TypeReference) { try { TypeReference<T> typeReference = (TypeReference<T>) typeHandler; register(typeReference.getRawType(), typeHandler); mappedTypeFound = true; } catch (Throwable t) { /** * maybe users define the TypeReference with a different * type and are not assignable, so just ignore it */ } } if (!mappedTypeFound) { register((Class<T>) null, typeHandler); } } /** * 配置了typeHandlerhe和javaType */ public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) { register((Type) javaType, typeHandler); } private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) { //扫描注解MappedJdbcTypes MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class); if (mappedJdbcTypes != null) { for (JdbcType handledJdbcType : mappedJdbcTypes.value()) { register(javaType, handledJdbcType, typeHandler); } if (mappedJdbcTypes.includeNullJdbcType()) { register(javaType, null, typeHandler); } } else { register(javaType, null, typeHandler); } } public <T> void register(TypeReference<T> javaTypeReference, TypeHandler<? extends T> handler) { register(javaTypeReference.getRawType(), handler); } /** * typeHandlerhe、javaType、jdbcType都配置了 */ public <T> void register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler) { register((Type) type, jdbcType, handler); } /** * 注册typeHandler的核心方法 * 就是向Map新增数据而已 */ private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) { if (javaType != null) { Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType); if (map == null) { map = new HashMap<JdbcType, TypeHandler<?>>(); TYPE_HANDLER_MAP.put(javaType, map); } map.put(jdbcType, handler); if (reversePrimitiveMap.containsKey(javaType)) { register(reversePrimitiveMap.get(javaType), jdbcType, handler); } } ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler); } // // REGISTER CLASS // // Only handler type public void register(Class<?> typeHandlerClass) { boolean mappedTypeFound = false; MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class); if (mappedTypes != null) { for (Class<?> javaTypeClass : mappedTypes.value()) { register(javaTypeClass, typeHandlerClass); mappedTypeFound = true; } } if (!mappedTypeFound) { register(getInstance(null, typeHandlerClass)); } } // java type + handler type public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) { register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass)); } // java type + jdbc type + handler type public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) { register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass)); } // Construct a handler (used also from Builders) @SuppressWarnings("unchecked") public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) { if (javaTypeClass != null) { try { Constructor<?> c = typeHandlerClass.getConstructor(Class.class); return (TypeHandler<T>) c.newInstance(javaTypeClass); } catch (NoSuchMethodException ignored) { // ignored } catch (Exception e) { throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e); } } try { Constructor<?> c = typeHandlerClass.getConstructor(); return (TypeHandler<T>) c.newInstance(); } catch (Exception e) { throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e); } } /** * 根据指定的pacakge去扫描自定义的typeHander,然后注册 */ public void register(String packageName) { ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>(); resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName); Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses(); for (Class<?> type : handlerSet) { /** * Ignore inner classes and interfaces (including package-info.java) * and abstract classes */ if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) { register(type); } } } // get information /** * 通过configuration对象可以获取已注册的所有typeHandler */ public Collection<TypeHandler<?>> getTypeHandlers() { return Collections.unmodifiableCollection(ALL_TYPE_HANDLERS_MAP.values()); } } 由源码可以看到, mybatis为我们实现了那么多TypeHandler, 随便打开一个TypeHandler,看其源码,都可以看到,它继承自一个抽象类:BaseTypeHandler, 那么我们是不是也能通过继承BaseTypeHandler,从而实现自定义的TypeHandler ? 答案是肯定的, 那么现在下面就为大家演示一下自定义TypeHandler: 自定义TypeHandler1234567891011121314151617181920212223242526272829303132@MappedJdbcTypes(JdbcType.VARCHAR) /*** 此处如果不用注解指定jdbcType, 那么,就可以在配置文件中通过"jdbcType"属性指定,* 同理, javaType 也可通过 @MappedTypes指定*/public class ExampleTypeHandler extends BaseTypeHandler<String> { @Override public void setNonNullParameter(PreparedStatement ps , int i , String parameter , JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); } @Override public String getNullableResult(ResultSet rs , String columnName) throws SQLException { return rs.getString(columnName); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex); }} 然后,就该配置我们的自定义TypeHandler了:1234567891011<configuration> <typeHandlers> <!-- 由于自定义的TypeHandler在定义时已经通过注解指定了jdbcType, 所以此处不用再配置jdbcType --> <typeHandler handler="ExampleTypeHandler"/> </typeHandlers> ...... </configuration> 也就是说,我们在自定义TypeHandler的时候,可以在TypeHandler通过@MappedJdbcTypes指定jdbcType, 通过 @MappedTypes 指定javaType, 如果没有使用注解指定,那么我们就需要在配置文件中配置。 好啦,本篇文章到此结束。]]></content>
<categories>
<category>Mybatis</category>
</categories>
<tags>
<tag>Mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出Mybatis系列九-强大的动态SQL]]></title>
<url>%2F%2Fblog%2Fmybatis-9%2F</url>
<content type="text"><![CDATA[注:本文转载自南轲梦 上篇文章《深入浅出Mybatis系列(八)—mapper映射文件配置之select、resultMap》简单介绍了mybatis的查询,至此,CRUD都已讲完。本文将介绍mybatis强大的动态SQL。 那么,问题来了: 什么是动态SQL? 动态SQL有什么作用? 传统的使用JDBC的方法,相信大家在组合复杂的的SQL语句的时候,需要去拼接,稍不注意哪怕少了个空格,都会导致错误。Mybatis的动态SQL功能正是为了解决这种问题, 其通过 if, choose, when, otherwise, trim, where, set, foreach标签,可组合成非常灵活的SQL语句,从而提高开发人员的效率。下面就去感受Mybatis动态SQL的魅力吧: mybatis中if判断作为程序猿,谁不懂if !在mybatis中也能用 if 啦:1234567<select id="findUserById" resultType="user"> select * from user where <if test="id != null"> id=#{id} </if> and deleteFlag=0;</select> 上面例子: 如果传入的id 不为空, 那么才会SQL才拼接id = #{id}。 这个相信大家看一样就能明白,不多说。 细心的人会发现一个问题:“你这不对啊! 要是你传入的id为null, 那么你这最终的SQL语句不就成了 select * from user where and deleteFlag=0, 这语句有问题!” 是啊,这时候,mybatis的 where 标签就该隆重登场啦: mybatis中where咱们通过where改造一下上面的例子:123456789<select id="findUserById" resultType="user"> select * from user <where> <if test="id != null"> id=#{id} </if> and deleteFlag=0; </where> </select> 有些人就要问了: “你这都是些什么玩意儿! 跟上面的相比, 不就是多了个where标签嘛! 那这个还会不会出现 select * from user where and deleteFlag=0 ?” 的确,从表面上来看,就是多了个where标签而已, 不过实质上, mybatis是对它做了处理,当它遇到AND或者OR这些,它知道怎么处理。其实我们可以通过 trim 标签去自定义这种处理规则。 trim:我的地盘,我做主上面的where标签,其实用trim 可以表示如下:123<trim prefix="WHERE" prefixOverrides="AND |OR "> ... </trim> 它的意思就是: 当WHERE后紧随AND或则OR的时候,就去除AND或者OR。 除了WHERE以外, 其实还有一个比较经典的实现,那就是SET。 set: 信我,不出错123456789101112131415161718<update id="updateUser" parameterType="com.dy.entity.User"> update user set <if test="name != null"> name = #{name}, </if> <if test="password != null"> password = #{password}, </if> <if test="age != null"> age = #{age} </if> <where> <if test="id != null"> id = #{id} </if> and deleteFlag = 0; </where></update> 问题又来了: “如果我只有name不为null, 那么这SQL不就成了 update set name = #{name}, where …….. ? 你那name后面那逗号会导致出错啊!” 是的,这时候,就可以用mybatis为我们提供的set 标签了。下面是通过set标签改造后:1234567891011121314<update id="updateUser" parameterType="com.dy.entity.User"> update user <set> <if test="name != null">name = #{name},</if> <if test="password != null">password = #{password},</if> <if test="age != null">age = #{age},</if> </set> <where> <if test="id != null"> id = #{id} </if> and deleteFlag = 0; </where></update> 这个用trim 可表示为:123<trim prefix="SET" suffixOverrides=","> ...</trim> foreach: 你有for, 我有foreach!java中有for, 可通过for循环, 同样, mybatis中有foreach, 可通过它实现循环,循环的对象当然主要是java容器和数组。123456789<select id="selectPostIn" resultType="domain.blog.Post"> SELECT * FROM POST P WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach></select> 将一个 List 实例或者数组作为参数对象传给 MyBatis,当这么做的时候,MyBatis 会自动将它包装在一个 Map 中并以名称为键。List 实例将会以“list”作为键,而数组实例的键将是“array”。同样, 当循环的对象为map的时候,index其实就是map的key。 choose: 我选择了你,你选择了我Java中有switch, mybatis有choose1234567891011121314<select id="findActiveBlogLike" resultType="Blog"> SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <choose> <when test="title != null"> AND title like #{title} </when> <when test="author != null and author.name != null"> AND author_name like #{author.name} </when> <otherwise> AND featured = 1 </otherwise> </choose></select> 以上例子中: 当title和author都不为null的时候, 那么选择二选一(前者优先), 如果都为null, 那么就选择 otherwise中的, 如果tilte和author只有一个不为null, 那么就选择不为null的那个。 纵观mybatis的动态SQL, 强大而简单, 相信大家简单看一下就能使用了。 好啦,本次就写到这!下篇文章将结合mybatis的源码分析一次sql语句执行的整个过程。]]></content>
<categories>
<category>Mybatis</category>
</categories>
<tags>
<tag>Mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出Mybatis系列八-mapper映射文件配置之select、resultMap]]></title>
<url>%2F%2Fblog%2Fmybatis-8%2F</url>
<content type="text"><![CDATA[注:本文转载自南轲梦 上篇《深入浅出Mybatis系列(七)—mapper映射文件配置之insert、update、delete》介绍了insert、update、delete的用法,本篇将介绍select、resultMap的用法。select无疑是我们最常用,也是最复杂的,mybatis通过resultMap能帮助我们很好地进行高级映射。下面就开始看看select 以及 resultMap的用法: 先看select的配置吧: select配置123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869<select <!-- 1. id (必须配置) id是命名空间中的唯一标识符,可被用来代表这条语句。 一个命名空间(namespace) 对应一个dao接口, 这个id也应该对应dao里面的某个方法(相当于方法的实现),因此id 应该与方法名一致 --> id="selectPerson" <!-- 2. parameterType (可选配置, 默认为mybatis自动选择处理) 将要传入语句的参数的完全限定类名或别名, 如果不配置,mybatis会通过ParameterHandler 根据参数类型默认选择合适的typeHandler进行处理 parameterType 主要指定参数类型,可以是int, short, long, string等类型,也可以是复杂类型(如对象) --> parameterType="int" <!-- 3. resultType (resultType 与 resultMap 二选一配置) resultType用以指定返回类型,指定的类型可以是基本类型,可以是java容器,也可以是javabean --> resultType="hashmap" <!-- 4. resultMap (resultType 与 resultMap 二选一配置) resultMap用于引用我们通过 resultMap标签定义的映射类型, 这也是mybatis组件高级复杂映射的关键 --> resultMap="personResultMap" <!-- 5. flushCache (可选配置) 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false --> flushCache="false" <!-- 6. useCache (可选配置) 将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true --> useCache="true" <!-- 7. timeout (可选配置) 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动) --> timeout="10000" <!-- 8. fetchSize (可选配置) 这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset(依赖驱动) --> fetchSize="256" <!-- 9. statementType (可选配置) STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED --> statementType="PREPARED" <!-- 10. resultSetType (可选配置) FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一个, 默认值为 unset (依赖驱动) --> resultSetType="FORWARD_ONLY"> 配置看起来总是这么多,不过实际常用的配置也就那么几个, 根据自己的需要吧,上面都已注明是否必须配置。 下面还是上个demo及时练练手吧! 数据库:新增两张表(t_course, t_student),以下为相关实体类。 Course.java1234567public class Course { private int id; private String name; private int deleteFlag; //setter和getter方法省略...} Student.java123456789public class Student { private int id; private String idCard; private String name; private List<Course> courseList; private int deleteFlag; //setter和getter方法省略...} CourseDao.java123public interface CourseDao { public Course findCourseById(int courseId); } StudentDao.java123public interface StudentDao { public Student findStudentById(String idCard);} courseDao.xml12345678910111213141516171819202122232425<mapper namespace="com.dy.dao.CourseDao"> <!-- 1.此处直接将resultType 设置为course, 一看就知道我设置了别名吧, 如果没有设置别名,那么resultType = com.dy.entity.Course。 2.可能细心的你会发现:Course.java中的属性名与数据库字段名不一致,下面, 我就在sql语句中用了as, 使之匹配,当然方法不止一种,在学习了resultMap之后, 你能看到一种更直观优雅的方式去将javabean中的属性与数据库字段名保持一致 3.findCourseById 与CourseDao中findCourseById方法对应, 那么传入的参数名称以及类型也应该保持对应关系。 4.可以看到,在sql语句中,通过#{}表达式可以获取参数。 5.下面这条sql语句,实际上的形式是怎么样的?还记得之前说过, mybatis默认为preparedStatement吧,那么,用我们jdbc代码来看,它其实就是: select course_id as id, course_name as name, course_delete_flg as deleteFlag from t_course where course_id=? --> <select id="findCourseById" resultType="course" > select course_id as id , course_name as name , course_delete_flg as deleteFlag from t_course where course_id=#{courseId} </select> </mapper> CourseDaoTest.java123456789101112131415161718192021222324public class CourseDaoTest { @Test public void findCourseById() { SqlSessionFactory sqlSessionFactory = getSessionFactory(); SqlSession sqlSession = sqlSessionFactory.openSession(); CourseDao courseDao = sqlSession.getMapper(CourseDao.class); Course course = courseDao.findCourseById(1); } //Mybatis 通过SqlSessionFactory获取SqlSession, 然后才能通过SqlSession与数据库进行交互 private static SqlSessionFactory getSessionFactory() { SqlSessionFactory sessionFactory = null; String resource = "mybatis-conf.xml"; try { sessionFactory = new SqlSessionFactoryBuilder().build(Resources .getResourceAsReader(resource)); } catch (IOException e) { e.printStackTrace(); } return sessionFactory; } } 上面的示例,我们针对course, 简单演示了 select的用法, 不过有个问题值得思考: 一个student可以对应多个course, 那么,在mybatis中如何处理这种一对多, 甚至于多对多,一对一的关系呢? 这儿,就不得不提到 resultMap 这个东西, mybatis的resultMap功能可谓十分强大,能够处理复杂的关系映射, 那么resultMap 该怎么配置呢? 别急,这就来了: resultMap配置12345678910111213141516171819202122232425262728293031323334353637383940414243<!-- 1.type 对应类型,可以是javabean, 也可以是其它 2.id 必须唯一, 用于标示这个resultMap的唯一性,在使用resultMap的时候,就是通过id指定 --> <resultMap type="" id=""> <!-- id, 唯一性,注意啦,这个id用于标示这个javabean对象的唯一性, 不一定会是数据库的主键(不要把它理解为数据库对应表的主键) property属性对应javabean的属性名,column对应数据库表的列名 (这样,当javabean的属性与数据库对应表的列名不一致的时候,就能通过指定这个保持正常映射了) --> <id property="" column=""/> <!-- result与id相比, 对应普通属性 --> <result property="" column=""/> <!-- constructor对应javabean中的构造方法 --> <constructor> <!-- idArg 对应构造方法中的id参数 --> <idArg column=""/> <!-- arg 对应构造方法中的普通参数 --> <arg column=""/> </constructor> <!-- collection,对应javabean中容器类型, 是实现一对多的关键 property 为javabean中容器对应字段名 column 为体现在数据库中列名 ofType 就是指定javabean中容器指定的类型 --> <collection property="" column="" ofType=""></collection> <!-- association 为关联关系,是实现N对一的关键。 property 为javabean中容器对应字段名 column 为体现在数据库中列名 javaType 指定关联的类型 --> <association property="" column="" javaType=""></association> </resultMap> 好啦,知道resutMap怎么配置后,咱们立即接着上面的demo来练习一下吧: 下面是用resultMap处理一对多关系的映射的示例 一个student对应多个course, 典型的一对多,咱们就来看看mybatis怎么配置这种映射吧: studentDao.xml1234567891011121314151617181920212223242526272829303132333435363738394041<mapper namespace="com.dy.dao.StudentDao"> <!-- 这儿定义一个resultMap --> <resultMap type="student" id="studentMap"> <!-- 数据库中主键是id, 但是我这儿却是指定idCard为主键,为什么? 刚刚讲了,id用来表示唯一性, 我们可以认为只要idCard一样,那么他就是同一个学生。 如果此处用数据库中id, 那么mybatis将会认为数据库中每条记录都是一个student, 这显然不符合逻辑 --> <id property="idCard" column="stu_id_card"/> <result property="id" column="stu_id"/> <result property="name" column="stu_name"/> <result property="deleteFlag" column="stu_delete_flg"/> <!-- 这儿就是实现一对多的关键。 在Student中,courseList为List<Course>, 因此, ofType也应该与之对应(当然,我用了别名,不然要蛋疼的写全名了)。 collection的子标签是在指定Course的映射关系 (由于Course的javabean的属性名与数据库的列名不一致) --> <collection property="courseList" column="stu_course_id" ofType="Course"> <id property="id" column="course_id"/> <result property="name" column="course_name"/> <result property="deleteFlag" column="course_delete_flg"/> </collection> </resultMap> <!-- 这儿将返回类型设置成了上面指定的studentMap --> <select id="findStudentById" resultMap="studentMap"> SELECT s.*, c.* FROM t_student s LEFT JOIN t_course c ON s.stu_course_id=c.course_id WHERE s.stu_id_card=#{idCard} </select> </mapper> StudentDaoTest.java12345678910111213141516171819202122232425262728public class StudentDaoTest { @Test public void findCourseById() { SqlSessionFactory sqlSessionFactory = getSessionFactory(); SqlSession sqlSession = sqlSessionFactory.openSession(); StudentDao studentDao = sqlSession.getMapper(StudentDao.class); Student student = studentDao.findStudentById("20140101"); List<Course> courseList = student.getCourseList(); for (Course course: courseList) { System.out.println(course.getId() + " " + course.getName()); } } //Mybatis 通过SqlSessionFactory获取SqlSession, 然后才能通过SqlSession与数据库进行交互 private static SqlSessionFactory getSessionFactory() { SqlSessionFactory sessionFactory = null; String resource = "mybatis-conf.xml"; try { sessionFactory = new SqlSessionFactoryBuilder().build(Resources .getResourceAsReader(resource)); } catch (IOException e) { e.printStackTrace(); } return sessionFactory; } } 相信通过以上demo, 大家也能够使用mybatis的select 和 resultMap的用法了。上面demo只演示了一对多的映射,其实多对一、多对多也与它类似,所以我就没演示了,有兴趣的可以自己动手再做做。]]></content>
<categories>
<category>Mybatis</category>
</categories>
<tags>
<tag>Mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出Mybatis系列六-objectFactory、plugins、mappers简介与配置]]></title>
<url>%2F%2Fblog%2Fmybatis-6%2F</url>
<content type="text"><![CDATA[注:本文转载自南轲梦 上篇文章《深入浅出Mybatis系列(五)—TypeHandler简介及配置(mybatis源码篇)》简单看了一下TypeHandler, 本次将结束对于mybatis的配置文件的学习, 本次涉及到剩下没提及到的几个节点的配置:objectFactory、databaseIdProvider、plugins、mappers。 那么,接下来,就简单介绍一下这几个配置的作用吧: 1、objectFactory是干什么的? 需要配置吗? MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过参数构造方法来实例化。默认情况下,我们不需要配置,mybatis会调用默认实现的objectFactory。 除非我们要自定义ObjectFactory的实现, 那么我们才需要去手动配置。 那么怎么自定义实现ObjectFactory? 怎么配置呢? 自定义ObjectFactory只需要去继承DefaultObjectFactory(是ObjectFactory接口的实现类),并重写其方法即可。具体的,本处不多说,后面再具体讲解。 写好了ObjectFactory, 仅需做如下配置:1234567<configuration> ...... <objectFactory type="org.mybatis.example.ExampleObjectFactory"> <property name="someProperty" value="100"/> </objectFactory> ...... </configuration plugin有何作用? 需要配置吗? plugins 是一个可选配置。mybatis中的plugin其实就是个interceptor, 它可以拦截Executor 、ParameterHandler 、ResultSetHandler 、StatementHandler 的部分方法,处理我们自己的逻辑。Executor就是真正执行sql语句的东西, ParameterHandler 是处理我们传入参数的,还记得前面讲TypeHandler的时候提到过,mybatis默认帮我们实现了不少的typeHandler, 当我们不显示配置typeHandler的时候,mybatis会根据参数类型自动选择合适的typeHandler执行,其实就是ParameterHandler 在选择。ResultSetHandler 就是处理返回结果的。 怎么自定义plugin? 怎么配置? 要自定义一个plugin, 需要去实现Interceptor接口, 这儿不细说, 后面实战部分会详细讲解。定义好之后,配置如下:123456789<configuration> ...... <plugins> <plugin interceptor="org.mybatis.example.ExamplePlugin"> <property name="someProperty" value="100"/> </plugin> </plugins> ...... </configuration> mappers, 这下引出mybatis的核心之一了,mappers作用 ? 需要配置吗? mappers 节点下,配置我们的mapper映射文件, 所谓的mapper映射文件,就是让mybatis 用来建立数据表和javabean映射的一个桥梁。在我们实际开发中,通常一个mapper文件对应一个dao接口, 这个mapper可以看做是dao的实现。所以,mappers必须配置。 那么怎么配置呢?12345678910111213141516171819202122232425<configuration> ...... <mappers> <!-- 第一种方式:通过resource指定 --> <mapper resource="com/dy/dao/userDao.xml"/> <!-- 第二种方式, 通过class指定接口,进而将接口与对应的xml文件形成映射关系 不过,使用这种方式必须保证 接口与mapper文件同名(不区分大小写), 我这儿接口是UserDao,那么意味着mapper文件为UserDao.xml <mapper class="com.dy.dao.UserDao"/> --> <!-- 第三种方式,直接指定包,自动扫描,与方法二同理 <package name="com.dy.dao"/> --> <!-- 第四种方式:通过url指定mapper文件位置 <mapper url="file://........"/> --> </mappers> ...... </configuration> 本篇仅作简单介绍, 更高级的使用以及其实现原理,会在后面的实战部分进行详细讲解。 这几个节点的解析源码,与之前提到的那些节点的解析类似,源码需要的可以从这里看看。 相关节点源码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283/** * objectFactory 节点解析 */private void objectFactoryElement(XNode context) throws Exception { if (context != null) { //读取type属性的值, 接下来进行实例化ObjectFactory, 并set进 configuration //到此,简单讲一下configuration这个对象,其实它里面主要保存的都是mybatis的配置 String type = context.getStringAttribute("type"); //读取propertie的值, 根据需要可以配置, mybatis默认实现的objectFactory没有使用properties Properties properties = context.getChildrenAsProperties(); ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance(); factory.setProperties(properties); configuration.setObjectFactory(factory); } } /** * plugins 节点解析 */ private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); /** * 由此可见,我们在定义一个interceptor的时候,需要去实现Interceptor, * 这儿先不具体讲,以后会详细讲解 */ Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); interceptorInstance.setProperties(properties); configuration.addInterceptor(interceptorInstance); } } } /** * mappers 节点解析 * 这是mybatis的核心之一,这儿先简单介绍,在接下来的文章会对它进行分析 */ private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { //如果mappers节点的子节点是package, 那么就扫描package下的文件, 注入进configuration String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); //resource, url, class 三选一 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); //mapper映射文件都是通过XMLMapperBuilder解析 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."); } } } } } 本次就简单的到此结束, 从下篇文章开始,将会开始接触到mybatis的核心部分,不过首先还是要讲mapper文件的配置及使用, 并通过源码进一步深入核心。]]></content>
<categories>
<category>Mybatis</category>
</categories>
<tags>
<tag>Mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出Mybatis系列七-mapper映射文件配置之insert、update、delete]]></title>
<url>%2F%2Fblog%2Fmybatis-7%2F</url>
<content type="text"><![CDATA[注:本文转载自南轲梦 上篇文章《深入浅出Mybatis系列(六)—objectFactory、plugins、mappers简介与配置》简单地给mybatis的配置画上了一个句号。那么从本篇文章开始,将会介绍mapper映射文件的配置, 这是mybatis的核心之一,一定要学好。在mapper文件中,以mapper作为根节点,其下面可以配置的元素节点有: select, insert, update, delete, cache, cache-ref, resultMap, sql 。 本篇文章将简单介绍 insert, update, delete 的配置及使用,以后会对mybatis的源码进行深入讲解。 相信,看到insert, update, delete, 我们就知道其作用了,顾名思义嘛,myabtis 作为持久层框架,必须要对CRUD啊。 好啦,咱们就先来看看 insert, update, delete 怎么配置, 能配置哪些元素吧: 相关元素配置12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN" "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd"> <!-- mapper 为根元素节点, 一个namespace对应一个dao --><mapper namespace="com.dy.dao.UserDao"> <insert <!-- 1. id (必须配置) id是命名空间中的唯一标识符,可被用来代表这条语句。 一个命名空间(namespace) 对应一个dao接口, 这个id也应该对应dao里面的某个方法(相当于方法的实现),因此id 应该与方法名一致 --> id="insertUser" <!-- 2. parameterType (可选配置, 默认为mybatis自动选择处理) 将要传入语句的参数的完全限定类名或别名, 如果不配置, mybatis会通过ParameterHandler 根据参数类型默认选择合适的typeHandler进行处理 parameterType 主要指定参数类型,可以是int, short, long, string等类型,也可以是复杂类型(如对象) --> parameterType="com.demo.User" <!-- 3. flushCache (可选配置,默认配置为true) 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空, 默认值:true(对应插入、更新和删除语句) --> flushCache="true" <!-- 4. statementType (可选配置,默认配置为PREPARED) STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement, PreparedStatement 或 CallableStatement,默认值:PREPARED。 --> statementType="PREPARED" <!-- 5. keyProperty (可选配置, 默认为unset) (仅对 insert 和 update 有用)唯一标记一个属性,MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值, 默认:unset。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 --> keyProperty="" <!-- 6. keyColumn (可选配置) (仅对 insert 和 update 有用)通过生成的键值设置表中的列名,这个设置仅在某些数据库 (像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。 如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 --> keyColumn="" <!-- 7. useGeneratedKeys (可选配置, 默认为false) (仅对 insert 和 update 有用)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系数据库管理系统的自动递增字段),默认值:false。 --> useGeneratedKeys="false" <!-- 8. timeout (可选配置, 默认为unset, 依赖驱动) 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。 --> timeout="20"> <update id="updateUser" parameterType="com.demo.User" flushCache="true" statementType="PREPARED" timeout="20"> <delete id="deleteUser" parameterType="com.demo.User" flushCache="true" statementType="PREPARED" timeout="20"></mapper> 以上就是一个模板配置, 哪些是必要配置,哪些是根据自己实际需求,看一眼就知道了。 下面, 还是用第一篇文章《深入浅出Mybatis系列(一)—Mybatis入门》里面的demo来示例吧: User类1234567891011public class User { private int id; private String name; private String password; private int age; private int deleteFlag; //setter和getter方法省略...} UserDao类12345public interface UserDao { public void insertUser (User user); public void updateUser (User user); public void deleteUser (User user);} userDao.xml12345678910111213141516171819202122232425262728293031<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN" "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd"> <mapper namespace="com.dy.dao.UserDao"> <!-- 对应userDao中的insertUser方法, --> <insert id="insertUser" parameterType="com.dy.entity.User"> insert into user(id, name, password, age, deleteFlag) values(#{id} , #{name} , #{password} , #{age} , #{deleteFlag}) </insert> <!-- 对应userDao中的updateUser方法 --> <update id="updateUser" parameterType="com.dy.entity.User"> update user set name = #{name} , password = #{password} , age = #{age} , deleteFlag = #{deleteFlag} where id = #{id}; </update> <!-- 对应userDao中的deleteUser 方法 --> <delete id="deleteUser" parameterType="com.dy.entity.User"> delete from user where id = #{id}; </delete></mapper> 这样,一个简单的映射关系就建立了。仔细观察上面parameterType, “com.dy.entity.User”,包名要是再长点呢,每次都这样写,写得蛋疼了。别忘了之前讲的 typeAliases(别名), 那么这个地方,用上别名,岂不是技能跟蛋疼的长长的包名说拜拜了。好啦,咱们配上别名,在哪儿配? 当然是在mybatis 的全局配置文件(我这儿名字是mybatis-conf.xml), 不要认为是在mapper的配置文件里面配置哈。 mybatis-conf.xml123456789<typeAliases> <!-- 通过package, 可以直接指定package的名字, mybatis会自动扫描你指定包下面的javabean, 并且默认设置一个别名,默认的名字为: javabean 的首字母小写的非限定类名来作为它的别名。 也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user) <package name="com.dy.entity"/> --> <typeAlias alias="user" type="com.dy.entity.User"/> </typeAliases> 这样,一个别名就取好了,咱们可以把上面的 com.dy.entity.User 都直接改为user 了。 这多方便呀! 我这儿数据库用的是mysql, 我把user表的主键id 设置了自动增长, 以上代码运行正常, 那么问题来了(当然,我不是要问学挖掘机哪家强),我要是换成oracle数据库怎么办? oracle 可是不支持id自增长啊? 怎么办?请看下面:123456789101112<!-- 对应userDao中的insertUser方法, --> <insert id="insertUser" parameterType="com.dy.entity.User"> <!-- oracle等不支持id自增长的,可根据其id生成策略,先获取id <selectKey resultType="int" order="BEFORE" keyProperty="id"> select seq_user_id.nextval as id from dual </selectKey> --> insert into user(id, name, password, age, deleteFlag) values(#{id}, #{name}, #{password}, #{age}, #{deleteFlag}) </insert> 同理,如果我们在使用mysql的时候,想在数据插入后返回插入的id, 我们也可以使用 selectKey 这个元素:1234567891011121314151617<!-- 对应userDao中的insertUser方法, --> <insert id="insertUser" parameterType="com.dy.entity.User"> <!-- oracle等不支持id自增长的,可根据其id生成策略,先获取id <selectKey resultType="int" order="BEFORE" keyProperty="id"> select seq_user_id.nextval as id from dual </selectKey> --> <!-- mysql插入数据后,获取id --> <selectKey keyProperty="id" resultType="int" order="AFTER" > SELECT LAST_INSERT_ID() as id </selectKey> insert into user(id, name, password, age, deleteFlag) values(#{id}, #{name}, #{password}, #{age}, #{deleteFlag}) </insert> 这儿,我们就简单提一下 这个元素节点吧:1234567891011121314151617181920212223242526<selectKey <!-- selectKey 语句结果应该被设置的目标属性。如果希望得到多个生成的列, 也可以是逗号分隔的属性名称列表。 --> keyProperty="id" <!-- 结果的类型。MyBatis 通常可以推算出来,但是为了更加确定写上也不会有什么问题。 MyBatis 允许任何简单类型用作主键的类型,包括字符串。如果希望作用于多个生成的列, 则可以使用一个包含期望属性的 Object 或一个 Map。 --> resultType="int" <!-- 这可以被设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它会首先选择主键, 设置 keyProperty 然后执行插入语句。如果设置为 AFTER,那么先执行插入语句, 然后是 selectKey 元素 - 这和像 Oracle 的数据库相似,在插入语句内部可能有嵌入索引调用。 --> order="BEFORE" <!-- 与前面相同,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 语句的映射类型, 分别代表 PreparedStatement 和 CallableStatement 类型。 --> statementType="PREPARED"> 好啦,本篇文章主要介绍了insert, update, delete的配置及用法。 下篇文章将介绍复杂的 select相关的配置及用法, 待这些都讲完后,会先根据源码分析一下mybatis的整个运行流程,然后再深入mybatis的用法。]]></content>
<categories>
<category>Mybatis</category>
</categories>
<tags>
<tag>Mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出Mybatis系列四-配置详解之typeAliases别名(mybatis源码篇)]]></title>
<url>%2F%2Fblog%2Fmybatis-4%2F</url>
<content type="text"><![CDATA[注:本文转载自南轲梦 上篇文章《深入浅出Mybatis系列(三)—配置详解之properties与environments(mybatis源码篇)》 介绍了properties与environments, 本篇继续讲剩下的配置节点之一:typeAliases。 typeAliases节点主要用来设置别名,其实这是挺好用的一个功能, 通过配置别名,我们不用再指定完整的包名,并且还能取别名。 例如: 我们在使用 com.demo.entity. UserEntity 的时候,我们可以直接配置一个别名user, 这样以后在配置文件中要使用到com.demo.entity. UserEntity的时候,直接使用User即可。 就以上例为例,我们来实现一下,看看typeAliases的配置方法: typeAliases配置1234567891011121314<configuration> <typeAliases> <!-- 通过package, 可以直接指定package的名字, mybatis会自动扫描你指定包下面的javabean, 并且默认设置一个别名,默认的名字为: javabean 的首字母小写的非限定类名来作为它的别名。 也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user) <package name="com.dy.entity"/> --> <typeAlias alias="UserEntity" type="com.dy.entity.User"/> </typeAliases> ...... </configuration> 再写一段测试代码,看看有没生效:(我只写一段伪代码)123456Configuration con = sqlSessionFactory.getConfiguration();Map<String, Class<?>> typeMap = con.getTypeAliasRegistry().getTypeAliases();for(Entry<String, Class<?>> entry: typeMap.entrySet()) { System.out.println(entry.getKey() + " ================> " + entry.getValue().getSimpleName());} 上面给大家简单介绍了typeAliases的用法, 接下来就看看Mybatis中的源码了。 老规矩,先从对xml的解析讲起: typeAliases源码123456789101112131415161718192021222324252627282930313233/** * 解析typeAliases节点 */private void typeAliasesElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { //如果子节点是package, 那么就获取package节点的name属性, mybatis会扫描指定的package if ("package".equals(child.getName())) { String typeAliasPackage = child.getStringAttribute("name"); /** * TypeAliasRegistry 负责管理别名, 这儿就是通过TypeAliasRegistry 进行别名注册, * 下面就会看看TypeAliasRegistry源码 */ configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage); } else { //如果子节点是typeAlias节点,那么就获取alias属性和type的属性值 String alias = child.getStringAttribute("alias"); String type = child.getStringAttribute("type"); try { Class<?> clazz = Resources.classForName(type); if (alias == null) { typeAliasRegistry.registerAlias(clazz); } else { typeAliasRegistry.registerAlias(alias, clazz); } } catch (ClassNotFoundException e) { throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e); } } } } } 重要的源码在这儿! TypeAliasRegistry源码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162public class TypeAliasRegistry { /** * 这就是核心所在啊, 原来别名就仅仅通过一个HashMap来实现, * key为别名, value就是别名对应的类型(class对象) */ private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>(); /** * 以下就是mybatis默认为我们注册的别名 */ public TypeAliasRegistry() { registerAlias("string", String.class); registerAlias("byte", Byte.class); registerAlias("long", Long.class); registerAlias("short", Short.class); registerAlias("int", Integer.class); registerAlias("integer", Integer.class); registerAlias("double", Double.class); registerAlias("float", Float.class); registerAlias("boolean", Boolean.class); registerAlias("byte[]", Byte[].class); registerAlias("long[]", Long[].class); registerAlias("short[]", Short[].class); registerAlias("int[]", Integer[].class); registerAlias("integer[]", Integer[].class); registerAlias("double[]", Double[].class); registerAlias("float[]", Float[].class); registerAlias("boolean[]", Boolean[].class); registerAlias("_byte", byte.class); registerAlias("_long", long.class); registerAlias("_short", short.class); registerAlias("_int", int.class); registerAlias("_integer", int.class); registerAlias("_double", double.class); registerAlias("_float", float.class); registerAlias("_boolean", boolean.class); registerAlias("_byte[]", byte[].class); registerAlias("_long[]", long[].class); registerAlias("_short[]", short[].class); registerAlias("_int[]", int[].class); registerAlias("_integer[]", int[].class); registerAlias("_double[]", double[].class); registerAlias("_float[]", float[].class); registerAlias("_boolean[]", boolean[].class); registerAlias("date", Date.class); registerAlias("decimal", BigDecimal.class); registerAlias("bigdecimal", BigDecimal.class); registerAlias("biginteger", BigInteger.class); registerAlias("object", Object.class); registerAlias("date[]", Date[].class); registerAlias("decimal[]", BigDecimal[].class); registerAlias("bigdecimal[]", BigDecimal[].class); registerAlias("biginteger[]", BigInteger[].class); registerAlias("object[]", Object[].class); registerAlias("map", Map.class); registerAlias("hashmap", HashMap.class); registerAlias("list", List.class); registerAlias("arraylist", ArrayList.class); registerAlias("collection", Collection.class); registerAlias("iterator", Iterator.class); registerAlias("ResultSet", ResultSet.class); } /** * 处理别名, 直接从保存有别名的hashMap中取出即可 */ @SuppressWarnings("unchecked") public <T> Class<T> resolveAlias(String string) { try { if (string == null) return null; String key = string.toLowerCase(Locale.ENGLISH); // issue #748 Class<T> value; if (TYPE_ALIASES.containsKey(key)) { value = (Class<T>) TYPE_ALIASES.get(key); } else { value = (Class<T>) Resources.classForName(string); } return value; } catch (ClassNotFoundException e) { throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e); } } /** * 配置文件中配置为package的时候, 会调用此方法,根据配置的报名去扫描javabean , * 然后自动注册别名,默认会使用 Bean 的首字母小写的非限定类名来作为它的别名 * 也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user) */ public void registerAliases(String packageName){ registerAliases(packageName, Object.class); } public void registerAliases(String packageName, Class<?> superType){ ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses(); for(Class<?> type : typeSet){ // Ignore inner classes and interfaces (including package-info.java) // Skip also inner classes. See issue #6 if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { registerAlias(type); } } } public void registerAlias(Class<?> type) { String alias = type.getSimpleName(); Alias aliasAnnotation = type.getAnnotation(Alias.class); if (aliasAnnotation != null) { alias = aliasAnnotation.value(); } registerAlias(alias, type); } /** * 这就是注册别名的本质方法, 其实就是向保存别名的hashMap新增值而已, * 呵呵,别名的实现太简单了,对吧 */ public void registerAlias(String alias, Class<?> value) { if (alias == null) throw new TypeException("The parameter alias cannot be null"); String key = alias.toLowerCase(Locale.ENGLISH); // issue #748 if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) { throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'."); } TYPE_ALIASES.put(key, value); } public void registerAlias(String alias, String value) { try { registerAlias(alias, Resources.classForName(value)); } catch (ClassNotFoundException e) { throw new TypeException("Error registering type alias " +alias+" for "+value+". Cause: " + e, e); } } /** * 获取保存别名的HashMap, Configuration对象持有对TypeAliasRegistry的引用, * 因此,如果需要,我们可以通过Configuration对象获取 */ public Map<String, Class<?>> getTypeAliases() { return Collections.unmodifiableMap(TYPE_ALIASES); }} 由源码可见,设置别名的原理就这么简单,Mybatis默认给我们设置了不少别名,在上面代码中都可以见到。 好啦,本篇内容就是这么简单,到此为止。 下篇将继续讲解还没讲完的配置节点。]]></content>
<categories>
<category>Mybatis</category>
</categories>
<tags>
<tag>Mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出Mybatis系列三-配置详解之properties与environments(mybatis源码篇)]]></title>
<url>%2F%2Fblog%2Fmybatis-3%2F</url>
<content type="text"><![CDATA[注:本文转载自南轲梦 上篇文章《深入浅出Mybatis系列(二)—配置简介(mybatis源码篇)》我们通过对mybatis源码的简单分析,可看出,在mybatis配置文件中,在configuration根节点下面,可配置properties、typeAliases、plugins、objectFactory、objectWrapperFactory、settings、environments、databaseIdProvider、typeHandlers、mappers这些节点。那么本次,就会先介绍properties节点和environments节点。 为了让大家能够更好地阅读mybatis源码,我先简单的给大家示例一下properties的使用方法。 properties节点1234567891011<configuration> <!-- 方法一:从外部指定properties配置文件, 除了使用resource属性指定外,还可通过url属性指定url <properties resource="dbConfig.properties"></properties> --> <!-- 方法二: 直接配置为xml --> <properties> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test1"/> <property name="username" value="root"/> <property name="password" value="root"/> </properties> 那么,我要是 两种方法都同时用了,那么哪种方法优先?当以上两种方法都xml配置优先, 外部指定properties配置其次。至于为什么,接下来的源码分析会提到,请留意一下。 再看一下envirements元素节点的使用方法吧. envirements节点12345678910111213141516171819202122232425262728293031323334<environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <!-- 如果上面没有指定数据库配置的properties文件,那么此处可以这样直接配置 <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test1"/> <property name="username" value="root"/> <property name="password" value="root"/> --> <!-- 上面指定了数据库配置文件, 配置文件里面也是对应的这四个属性 --> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> <!-- 我再指定一个environment --> <environment id="test"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <!-- 与上面的url不一样 --> <property name="url" value="jdbc:mysql://localhost:3306/demo"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> environments元素节点可以配置多个environment子节点, 怎么理解呢? 假如我们系统的开发环境和正式环境所用的数据库不一样(这是肯定的), 那么可以设置两个environment, 两个id分别对应开发环境(dev)和正式环境(final),那么通过配置environments的default属性就能选择对应的environment了, 例如,我将environments的deault属性的值配置为dev, 那么就会选择dev的environment。 至于这个是怎么实现的, 下面源码就会讲。 好啦,上面简单给大家介绍了一下properties 和 environments 的配置, 接下来就正式开始看源码了: 上次我们说过mybatis 是通过XMLConfigBuilder这个类在解析mybatis配置文件的,那么本次就接着看看XMLConfigBuilder对于properties和environments的解析。 XMLConfigBuilder源码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141public class XMLConfigBuilder extends BaseBuilder { private boolean parsed; //xml解析器 private XPathParser parser; private String environment; /** * 上次说到这个方法是在解析mybatis配置文件中能配置的元素节点 * 今天首先要看的就是properties节点和environments节点 */ private void parseConfiguration(XNode root) { try { //解析properties元素 //issue #117 read properties first propertiesElement(root.evalNode("properties")); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); settingsElement(root.evalNode("settings")); //解析environments元素 // read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration.Cause: " + e, e); } } //下面就看看解析properties的具体方法 private void propertiesElement(XNode context) throws Exception { if (context != null) { /** * 将子节点的 name 以及value属性set进properties对象 * 这儿可以注意一下顺序,xml配置优先, 外部指定properties配置其次 */ Properties defaults = context.getChildrenAsProperties(); //获取properties节点上 resource属性的值 String resource = context.getStringAttribute("resource"); //获取properties节点上 url属性的值, resource和url不能同时配置 String url = context.getStringAttribute("url"); if (resource != null && url != null) { throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); } //把解析出的properties文件set进Properties对象 if (resource != null) { defaults.putAll(Resources.getResourceAsProperties(resource)); } else if (url != null) { defaults.putAll(Resources.getUrlAsProperties(url)); } /** * 将configuration对象中已配置的Properties属性与刚刚解析的融合 * configuration这个对象会装载所解析mybatis配置文件的所有节点元素, * 以后也会频频提到这个对象,既然configuration对象用有一系列的get/set方法, * 那是否就标志着我们可以使用java代码直接配置? * 答案是肯定的, 不过使用配置文件进行配置,优势不言而喻 */ Properties vars = configuration.getVariables(); if (vars != null) { defaults.putAll(vars); } //把装有解析配置propertis对象set进解析器, 因为后面可能会用到 parser.setVariables(defaults); //set进configuration对象 configuration.setVariables(defaults); } } //下面再看看解析enviroments元素节点的方法 private void environmentsElement(XNode context) throws Exception { if (context != null) { if (environment == null) { //解析environments节点的default属性的值 //例如: <environments default="development"> environment = context.getStringAttribute("default"); } //递归解析environments子节点 for (XNode child : context.getChildren()) { /** * <environment id="development">, 只有enviroment节点有id属性, * 那么这个属性有何作用?environments节点下可以拥有多个environment子节点 * 类似于这样: * <environments default="development"> * <environment id="development">...</environment> * <environment id="test">...</environment> * </environments> * 意思就是我们可以对应多个环境,比如开发环境,测试环境等, * 由environments的default属性,去选择对应的enviroment */ String id = child.getStringAttribute("id"); //isSpecial就是根据由environments的default属性去选择对应的enviroment if (isSpecifiedEnvironment(id)) { /** * 事务,mybatis有两种:JDBC 和 MANAGED, 配置为JDBC则直接使用JDBC的事务, * 配置为MANAGED则是将事务托管给容器 */ TransactionFactory txFactory =transactionManagerElement(child.evalNode("transactionManager")); /** * enviroment节点下面就是dataSource节点了,解析dataSource节点 * 下面会贴出解析dataSource的具体方法 */ DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); DataSource dataSource = dsFactory.getDataSource(); Environment.Builder environmentBuilder=new Environment.Builder(id) .transactionFactory(txFactory) .dataSource(dataSource); //老规矩,会将dataSource设置进configuration对象 configuration.setEnvironment(environmentBuilder.build()); } } } } //下面看看dataSource的解析方法 private DataSourceFactory dataSourceElement(XNode context) throws Exception { if (context != null) { //dataSource的连接池 String type = context.getStringAttribute("type"); //子节点 name, value属性set进一个properties对象 Properties props = context.getChildrenAsProperties(); //创建dataSourceFactory DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); factory.setProperties(props); return factory; } throw new BuilderException("Environment declaration requires a DataSourceFactory."); } } 通过以上对mybatis源码的解读,相信大家对mybatis的配置又有了一个深入的认识。 还有一个问题, 上面我们看到,在配置dataSource的时候使用了 ${driver} 这种表达式, 这种形式是怎么解析的?其实,是通过PropertyParser这个类解析: PropertyParser源码1234567891011121314151617181920212223242526/** * 这个类解析${}这种形式的表达式 */public class PropertyParser { public static String parse(String string, Properties variables) { VariableTokenHandler handler = new VariableTokenHandler(variables); GenericTokenParser parser = new GenericTokenParser("${", "}", handler); return parser.parse(string); } private static class VariableTokenHandler implements TokenHandler { private Properties variables; public VariableTokenHandler(Properties variables) { this.variables = variables; } public String handleToken(String content) { if (variables != null && variables.containsKey(content)) { return variables.getProperty(content); } return "${" + content + "}"; } }} 好啦,以上就是对于properties 和 environments元素节点的分析,比较重要的都在对于源码的注释中标出。本次文章到此结束,接下来的文章会继续分析其他节点的配置。]]></content>
<categories>
<category>Mybatis</category>
</categories>
<tags>
<tag>Mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出Mybatis系列十-SQL执行流程分析(源码篇)]]></title>
<url>%2F%2Fblog%2Fmybatis-10%2F</url>
<content type="text"><![CDATA[注:本文转载自南轲梦 最近太忙了,一直没时间继续更新博客,今天忙里偷闲继续我的Mybatis学习之旅。在前九篇中,介绍了mybatis的配置以及使用, 那么本篇将走进mybatis的源码,分析mybatis 的执行流程。 SqlSessionFactory 与 SqlSession通过前面的章节对于mybatis 的介绍及使用,大家都能体会到SqlSession的重要性了吧, 没错,从表面上来看,咱们都是通过SqlSession去执行sql语句(注意:是从表面看,实际的待会儿就会讲)。那么咱们就先看看是怎么获取SqlSession的吧: 首先,SqlSessionFactoryBuilder去读取mybatis的配置文件,然后build一个DefaultSqlSessionFactory。源码如下:12345678910111213141516171819202122232425262728/*** 一系列的构造方法最终都会调用本方法(配置文件为Reader时会调用本方法,还有一个InputStream方法与此对应)* @param reader* @param environment* @param properties* @return*/public SqlSessionFactory build(Reader reader,String environment,Properties properties){ try { //通过XMLConfigBuilder解析配置文件,解析的配置相关信息都会封装为一个Configuration对象 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); //这儿创建DefaultSessionFactory对象 return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } }}public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config);} 当我们获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象。源码如下:123456789101112131415161718192021222324252627282930313233343536/*** 通常一系列openSession方法最终都会调用本方法* @param execType * @param level* @param autoCommit* @return*/private SqlSession openSessionFromDataSource(ExecutorType execType , TransactionIsolationLevel level , boolean autoCommit) { Transaction tx = null; try { //通过Confuguration对象获取Mybatis相关配置,Environment对象包含了数据源和事务的配置 final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource() , level , autoCommit); /** * 之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, * 实际呢,其实是通过excutor执行, excutor是对于Statement的封装 */ final Executor executor = configuration.newExecutor(tx, execType); //关键看这儿,创建了一个DefaultSqlSession对象 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { // may have fetched a connection so lets call close() closeTransaction(tx); throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); }} 通过以上步骤,咱们已经得到SqlSession对象了。接下来就是该干嘛干嘛去了(话说还能干嘛,当然是执行sql语句咯)。看了上面,咱们也回想一下之前写的Demo,1234567891011SqlSessionFactory sessionFactory = null; String resource = "mybatis-conf.xml"; try { //SqlSessionFactoryBuilder读取配置文件 sessionFactory = new SqlSessionFactoryBuilder().build(Resources .getResourceAsReader(resource));} catch (IOException e) { e.printStackTrace(); } //通过SqlSessionFactory获取SqlSessionSqlSession sqlSession = sessionFactory.openSession(); 还真这么一回事儿,对吧! SqlSession咱们也拿到了,咱们可以调用SqlSession中一系列的select…, insert…, update…, delete…方法轻松自如的进行CRUD操作了。 就这样? 那咱配置的映射文件去哪儿了? 别急, 咱们接着往下看: 利器之MapperProxy在mybatis中,通过MapperProxy动态代理咱们的dao, 也就是说, 当咱们执行自己写的dao里面的方法的时候,其实是对应的mapperProxy在代理。那么,咱们就看看怎么获取MapperProxy对象吧: 通过SqlSession从Configuration中获取。源码如下:1234567/*** 什么都不做,直接去configuration中找, 哥就是这么任性*/@Overridepublic <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this);} SqlSession把包袱甩给了Configuration, 接下来就看看Configuration。源码如下:123456789/*** 烫手的山芋,俺不要,你找mapperRegistry去要* @param type* @param sqlSession* @return*/public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession);} Configuration不要这烫手的山芋,接着甩给了MapperRegistry, 那咱看看MapperRegistry。 源码如下:1234567891011121314151617181920212223/*** 烂活净让我来做了,没法了,下面没人了,我不做谁来做* @param type* @param sqlSession* @return*/@SuppressWarnings("unchecked")public <T> T getMapper(Class<T> type, SqlSession sqlSession) { //能偷懒的就偷懒,俺把粗活交给MapperProxyFactory去做 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); } } MapperProxyFactory是个苦B的人,粗活最终交给它去做了。咱们看看源码:1234567891011121314151617/*** 别人虐我千百遍,我待别人如初恋* @param mapperProxy* @return*/@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) { //动态代理我们写的dao接口 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader() , new Class[] { mapperInterface }, mapperProxy);} public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy);} 通过以上的动态代理,咱们就可以方便地使用dao接口啦, 就像之前咱们写的demo那样:12UserDao userMapper = sqlSession.getMapper(UserDao.class); User insertUser = new User(); 这下方便多了吧, 呵呵, 貌似mybatis的源码就这么一回事儿啊。 别急,还没完, 咱们还没看具体是怎么执行sql语句的呢。 Excutor 接下来,咱们才要真正去看sql的执行过程了。 上面,咱们拿到了MapperProxy, 每个MapperProxy对应一个dao接口, 那么咱们在使用的时候,MapperProxy是怎么做的呢? 源码奉上: MapperProxy源码12345678910111213141516/*** MapperProxy在执行时会触发此方法*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } final MapperMethod mapperMethod = cachedMapperMethod(method); //二话不说,主要交给MapperMethod自己去管 return mapperMethod.execute(sqlSession, args);} MapperMethod源码12345678910111213141516171819202122232425262728293031323334353637383940414243/*** 看着代码不少,不过其实就是先判断CRUD类型,然后根据类型去选择到底执行sqlSession中的哪个方法,* 绕了一圈,又转回sqlSession了* @param sqlSession* @param args* @return*/public Object execute(SqlSession sqlSession, Object[] args) { Object result; if (SqlCommandType.INSERT == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); } else if (SqlCommandType.UPDATE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); } else if (SqlCommandType.DELETE == command.getType()) { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); } else if (SqlCommandType.SELECT == command.getType()) { 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 { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } } else { 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;} 既然又回到SqlSession了, 那么咱们就看看SqlSession的CRUD方法了,为了省事,还是就选择其中的一个方法来做分析吧。这儿,咱们选择了selectList方法:1234567891011121314151617181920public <E> List<E> selectList(String statement , Object parameter , RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); /** * CRUD实际上是交给Excetor去处理, excutor其实也只是穿了个马甲而已, * 小样,别以为穿个马甲我就不认识你嘞! */ 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(); }} 然后,通过一层一层的调用,最终会来到doQuery方法, 这儿咱们就随便找个Excutor看看doQuery方法的实现吧,我这儿选择了SimpleExecutor: SimpleExecutor12345678910111213141516171819202122public <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()); //StatementHandler封装了Statement, 让 StatementHandler 去处理 return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); }} 接下来,咱们看看StatementHandler 的一个实现类 PreparedStatementHandler(这也是我们最常用的,封装的是PreparedStatement), 看看它使怎么去处理的:12345678public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { //到此,原形毕露, PreparedStatement, 这个大家都已经滚瓜烂熟了吧 PreparedStatement ps = (PreparedStatement) statement; ps.execute(); //结果交给了ResultSetHandler 去处理 return resultSetHandler.<E> handleResultSets(ps);} 到此, 一次sql的执行流程就完了。 我这儿仅抛砖引玉,建议有兴趣的去看看Mybatis3的源码。 好啦,本次就到此结束啦]]></content>
<categories>
<category>Mybatis</category>
</categories>
<tags>
<tag>Mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出Mybatis系列一-Mybatis入门]]></title>
<url>%2F%2Fblog%2Fmybatis-1%2F</url>
<content type="text"><![CDATA[注:本文转载自南轲梦 最近两年 springmvc + mybatis 的在这种搭配还是蛮火的,楼主我呢,也从来没真正去接触过mybatis, 趁近日得闲, 就去学习一下mybatis吧。 本次拟根据自己的学习进度,做一次关于mybatis 的一系列教程, 记录自己的学习历程, 同时也给还没接触过mybatis的朋友探一次道。本系列教程拟 由浅(使用)入深(分析mybatis源码实现),故可能需要好长几天才能更新完。好啦,下面就开始本次的mybatis 学习之旅啦, 本次为第一篇教程, 就先简单地写个demo, 一起来认识一下mybatis吧。 为了方便,我使用了maven,至于maven怎么使用,我就不做介绍了。没用过maven的,也不影响阅读。 Mybatis环境搭建新建web项目, 添加依赖包:mybatis包、数据库驱动包(我使用的是mysql)、日志包(我使用的是log4j), 由于我的是maven项目, 那么添加依赖包就简单了,直接在pom.xml添加依赖即可。 pom.xml 12345678910111213141516171819202122232425262728293031<dependencies> <!-- 添加junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!-- 添加log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.16</version> </dependency> <!-- 添加mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.2.6</version> </dependency> <!-- 添加mysql驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.12</version> </dependency> </dependencies> 配置mybatis在classpath建立一个用于配置log4j的配置文件log4j.properties, 再建立一个用于配置Mybatis的配置文件configuration.xml(文件可随便命名)。log4j的配置,我就不多说,这儿主要说一下configuration.xml: configuration.xml123456789101112131415161718192021222324252627282930313233343536373839404142<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <!-- 指定properties配置文件, 我这里面配置的是数据库相关 --> <properties resource="dbConfig.properties"></properties> <!-- 指定Mybatis使用log4j --> <settings> <setting name="logImpl" value="LOG4J"/> </settings> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <!-- 如果上面没有指定数据库配置的properties文件,那么此处可以这样直接配置 <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test1"/> <property name="username" value="root"/> <property name="password" value="root"/> --> <!-- 上面指定了数据库配置文件, 配置文件里面也是对应的这四个属性 --> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <!-- 映射文件,mybatis精髓, 后面才会细讲 --> <mappers> <mapper resource="com/dy/dao/userDao-mapping.xml"/> </mappers> </configuration> 开始写Demo首先,在mysql数据库test1建立一张表user。 先编写一个实体类User: User类用于与User表相对应。 User12345678910public class User { private int id; private String name; private String password; private int age; private int deleteFlag; //setter和getter方法省略...} 再编写一个UserDao 接口: UserDao12345public interface UserDao { public void insert(User user); public User findUserById (int userId); public List<User> findAllUsers();} 再编写一个userDao-mapping.xml (可随便命名): userDao-mapping.xml123456789<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN" "http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd"> <mapper namespace="com.dy.dao.UserDao"> <select id="findUserById" resultType="com.dy.entity.User" > select * from user where id = #{id} </select></mapper> userDao-mapping.xml相当于是UserDao的实现, 同时也将User实体类与数据表User成功关联起来。 测试代码DemoUserDaoTest12345678910111213141516171819202122public class UserDaoTest { @Test public void findUserById() { SqlSession sqlSession = getSessionFactory().openSession(); UserDao userMapper = sqlSession.getMapper(UserDao.class); User user = userMapper.findUserById(2); Assert.assertNotNull("没找到数据", user); } //Mybatis 通过SqlSessionFactory获取SqlSession, 然后才能通过SqlSession与数据库进行交互 private static SqlSessionFactory getSessionFactory() { SqlSessionFactory sessionFactory = null; String resource = "configuration.xml"; try { sessionFactory = new SqlSessionFactoryBuilder().build(Resources .getResourceAsReader(resource)); } catch (IOException e) { e.printStackTrace(); } return sessionFactory; } } 好啦,这样一个简单的mybatis 的demo就能成功运行啦。通过这个demo, 应该你就也能初步看出mybatis的运行机制,如果不清楚,也没关系。从下一篇文章开始,才开始正式讲解mybatis。]]></content>
<categories>
<category>Mybatis</category>
</categories>
<tags>
<tag>Mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入浅出Mybatis系列二-配置简介(mybatis源码篇)]]></title>
<url>%2F%2Fblog%2Fmybatis-2%2F</url>
<content type="text"><![CDATA[注:本文转载自南轲梦 上篇文章《深入浅出Mybatis系列(一)—Mybatis入门》, 写了一个Demo简单体现了一下Mybatis的流程。本次,将简单介绍一下Mybatis的配置文件: 上次例子中,我们以 SqlSessionFactoryBuilder 去创建 SqlSessionFactory, 那么,我们就先从SqlSessionFactoryBuilder入手, 咱们先看看源码是怎么实现的: SqlSessionFactoryBuilder源码123456789101112131415161718192021222324252627282930313233343536373839404142434445public class SqlSessionFactoryBuilder { /** * Reader读取mybatis配置文件,传入构造方法 * 除了Reader外,其实还有对应的inputStream作为参数的构造方法, * 这也体现了mybatis配置的灵活性 */ public SqlSessionFactory build(Reader reader) { return build(reader, null, null); } public SqlSessionFactory build(Reader reader, String environment) { return build(reader, environment, null); } //mybatis配置文件 + properties, 此时mybatis配置文件中可以不配置properties,也能使用${}形式 public SqlSessionFactory build(Reader reader, Properties properties) { return build(reader, null, properties); } //通过XMLConfigBuilder解析mybatis配置,然后创建SqlSessionFactory对象 public SqlSessionFactory build(Reader reader , String environment , Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); //下面看看这个方法的源码 return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } } public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }} 通过源码,我们可以看到SqlSessionFactoryBuilder 通过XMLConfigBuilder 去解析我们传入的mybatis的配置文件, 下面就接着看看 XMLConfigBuilder 部分源码: XMLConfigBuilder源码12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152/** * mybatis 配置文件解析 */public class XMLConfigBuilder extends BaseBuilder { public XMLConfigBuilder(InputStream inputStream,String environment,Properties props){ this(new XPathParser(inputStream, true, props , new XMLMapperEntityResolver()), environment, props); } 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; } //外部调用此方法对mybatis配置文件进行解析 public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; //从根节点configuration parseConfiguration(parser.evalNode("/configuration")); return configuration; } //此方法就是解析configuration节点下的子节点 //由此也可看出,我们在configuration下面能配置的节点为以下10个节点 private void parseConfiguration(XNode root) { try { //issue #117 read properties first propertiesElement(root.evalNode("properties")); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); settingsElement(root.evalNode("settings")); // read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }} 通过以上源码,我们就能看出,在mybatis的配置文件中: configuration节点为根节点。 在configuration节点之下,我们可以配置10个子节点, 分别为:properties、typeAliases、plugins、objectFactory、objectWrapperFactory、settings、environments、databaseIdProvider、typeHandlers、mappers。 本篇文章就先只介绍这些内容,接下来的文章将依次分析解析这个10个节点中比较重要的几个节点的源码,看看在解析这些节点的时候,到底做了些什么。]]></content>
<categories>
<category>Mybatis</category>
</categories>
<tags>
<tag>Mybatis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Github+Hexo一站式部署个人博客]]></title>
<url>%2F%2Fblog%2Fhexo-blog%2F</url>
<content type="text"><![CDATA[写在前面注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 本文档是Github + Hexo 的搭建个人博客教程,其中Hexo基于Hexo v3.8.0版本,themes主题基于为NexT v7.0.0版本。 搭建博客前置条件可参考 如何搭建个人独立博客? 个人博客地址: Chloneda’s blog 注: 点开侧栏浏览目录可快速定位内容 安装主题在 Hexo 项目源码目录下,有两个重要的配置文件,其名称均为 _config.yml 。 其中,一份位于站点根目录下,主要包含 Hexo本身的配置;另一份位于主题目录下,主要用于主题相关的配置。为了描述方便,在以下说明中,将前者称为站点配置文件, 后者称为主题配置文件。 下载NexT主题12cd hexogit clone https://github.com/theme-next/hexo-theme-next themes/next 启用NexT主题修改站点配置文件,查找关键词theme,并修改为主题名字next:1234# Extensions #(注意冒号间的空格)## Plugins: https://hexo.io/plugins/## Themes: https://hexo.io/themes/theme: next 在切换主题之后、验证之前, 我们最好使用 hexo clean 来清除 Hexo 的缓存。 主题设置设置Scheme在Hexo主题中,有四种不同的模式!进入主题配置文件,搜索关键词找到scheme属性,选择自己喜欢的模式:12345678# ---------------------------------------------------------------# Scheme Settings# ---------------------------------------------------------------# Schemes# scheme: Muse # 默认 Scheme,这是 NexT 最初的版本,黑白主调,大量留白scheme: Mist # Muse 的紧凑版本,整洁有序的单栏外观# scheme: Pisces # 默认 Scheme,这是 NexT 最初的版本,黑白主调,大量留白# scheme: Gemini # 类似 Pisces 设置语言编辑站点配置文件,搜索关键词language,并设置成你所需要的语言:1language: zh-CN 设置菜单进入主题配置文件,找到menu字段,菜单内容的设置格式是:item name: link || menu photo,其中item name 是一个名称,link时具体菜单分类,菜单的||后面是菜单的图标,具体菜单图标可参考Font Awesome网站。12345678# 菜单示例配置menu: home: / || home reading: /reading/ || book archives: /archives/ || archive categories: /categories/ || th #tags: /tags/ || tags about: /about/ || user 头像设置在主题配置文件,搜索字段avatar,值设置成头像的链接地址。1234567# 将头像放置主题目录下的 source/uploads/ (新建uploads目录若不存在) 配置为:avatar: /uploads/avatar.png# 放置在 source/images/ 目录下, 配置为:avatar: /images/avatar.png# 完整的互联网 URIavatar: url: http://example.com/avatar.png 设置侧边栏在主题配置文件,搜索sidebar关键词,设置为hide模式,如下图所示:12345sidebar: #display: post // 默认显示方式 #display: always // 一直显示 display: hide // 初始隐藏 #display: remove // 移除侧边栏 各位可根据个人喜好进行设置。 设置站点描述在站点配置文件中,搜索关键词Site,如下:123456# Sitetitle: Chloneda #你的站点标题subtitle: Less is moredescription: Less is more #你的站点描述keywords: chlonedaauthor: chloneda #站点作者 进阶设定添加标签页面hexo根目录下,执行以下命令,新建标签页面。1hexo new page tags 修改站点目录source/tags的 index.md 文件:123456---title: 添加标签页面测试tags: Test #添加标签categories: Test #添加分类comments: false--- 修改主题配置文件,搜索关键词menu,取消 #tags: /tags/ || tags注释,内容如下:12345678# 菜单示例配置menu: home: / || home reading: /reading/ || book archives: /archives/ || archive categories: /categories/ || th tags: /tags/ || tags about: /about/ || user 新添菜单翻译对应的中文打开 hexo>theme>next>languages>zh-CN.yml 文件,在menu下添加 tags: 标签:1234567891011menu: home: 首页 archives: 归档 categories: 分类 tags: 标签 about: 关于 search: 搜索 schedule: 日程表 sitemap: 站点地图 commonweal: 公益404 resources: 资源 注:添加其他页面也类似。 首页显示预览首页显示文章列表,列表里的每一篇文章只显示预览,不显示全文。 进入主题配置文件,搜索关键词auto_excerpt,把enable对应的false改为true。12345# Automatically Excerpt. Not recommand.# Please use <!-- more --> in the post to control excerpt accurately.auto_excerpt: enable: false length: 150 友情链接打开主题配置文件,搜索关键字 Blog rolls,添加自己需要的链接:123links: #连接 baidu: https://www.baidu.com/ google: https://www.google.com/ 本地搜索在Hexo的根目录下执行以下命令。1$ npm install hexo-generator-searchdb --save 打开主题配置文件,搜索关键字local_search,将enable的值设置为 true:1234# Local search# Dependencies: https://github.com/theme-next/hexo-generator-searchdblocal_search: enable: true 打开站点配置文件,搜索关键词search,修改为如下内容:123456# 本地搜索search: path: search.xml field: post format: html limit: 10000 添加RSS在Hexo根目录执行安装指令,安装 hexo-generator-feed 插件:1npm install hexo-generator-feed --save 打开站点配置文件,追加feed信息:1234567# 设置RSSfeed: type: rss2 path: rss2.html limit: 5 hub: content: 'true' 打开主题配置文件,找到rss,设置为:1rss: /atom.xml 添加社交链接在主题配置文件中,找到social属性,添加社交链接,步骤如下:12345social: E-Mail: mailto:yourname@gmail.com || envelope Google: https://plus.google.com/yourname || google Twitter: https://twitter.com/yourname || twitter Facebook: https://www.facebook.com/yourname || facebook 格式为: 社交平台名称:链接 设置代码高亮首先需要改动的地方有: 站点配置文件_config.yml。 主题配置文件_config.yml。 在站点配置文件中,搜索highlight关键词:12345highlight: enable: true line_number: true auto_detect: true tab_replace: 文字自动检测默认不启动,改成true使其起作用。 再到主题配置文件,搜索highlight_theme关键词,修改代码主题样式:1234# Code Highlight theme# Available values: normal | night | night eighties | night blue | night bright# https://github.com/chriskempson/tomorrow-themehighlight_theme: night 添加复制按钮在主题配置文件中,搜索关键词codeblock,将copy_button的enable值修改为true。1234567codeblock: # Manual define the border radius in codeblock # Leave it empty for the default 1 border_radius: # Add copy button on codeblock copy_button: enable: true 添加阅读次数统计主题配置文件中,搜索关键词busuanzi_count,设置文章阅读次数统计及网站访客量:12345678910# Show Views/Visitors of the website/page with busuanzi.# Get more information on http://ibruce.info/2015/04/04/busuanzibusuanzi_count: enable: true total_visitors: true total_visitors_icon: user total_views: true total_views_icon: eye post_views: true post_views_icon: eye 添加 README.md每个项目README.md文件可以简单说明这个项目的用途。在Hexo目录下的 source 根目录下添加一个 README.md 文件,修改站点配置文件,将 skip_render 参数的值设置为:1skip_render: README.md 再次使用hexo d命令部署博客的时候就不会在渲染 README.md 这个文件。 进阶配置自定义网站头像自定义头像可以使用 比特虫 网站制作! 在主题配置文件中,按以下修改:1234567favicon: small: /images/favicon-16x16-next.png #你的头像名称 medium: /images/favicon-32x32-next.png #你的头像名称 apple_touch_icon: /images/apple-touch-icon-next.png safari_pinned_tab: /images/logo.svg #android_manifest: /images/manifest.json #ms_browserconfig: /images/browserconfig.xml 添加自定义页面[友链]设置菜单项的显示中文文本,打开themes/next/languages/zh-CN.yml文件,搜索 menu 关键字,修改对应中文或者新增。123456789101112menu: home: 首页 archives: 归档 categories: 分类 tags: 标签 about: 关于 search: 搜索 # schedule: 日程表 # sitemap: 站点地图 # commonweal: 公益404 # 新增menu links: 友链 # 新增该选项表示新增“友链”菜单 在主题配置文件,搜索menu,新增links: /links/ || link:123456789# 菜单示例配置menu: home: / || home reading: /reading/ || book archives: /archives/ || archive categories: /categories/ || th #tags: /tags/ || tags about: /about/ || user links: /links/ || link hexo根目录下,执行以下命令,新建友链页面。1hexo new page links 修改站点目录下source/links的 index.md 文件:123456---title: 友链tags: linkscategories: linkscomments: false--- 注:其它自定义菜单也是类似步骤 增加背景音乐在本博客的侧边栏增加网易云音乐,生成音乐外链可参考链接 ,复制链接,将外链插入到Hexo根路径的侧边栏文件中:/themes/next/layout/_macro/sidebar.swig,即侧边栏友情链接theme.links这一项之后。12345678910111213141516{% if theme.links %} <div> <div class="links-of-blogroll-title"> ....省略部分代码 </div> <ul class="links-of-blogroll-list"> ....省略部分代码 </ul> </div>{% endif %} <div id="music163player"> <iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=5239700&auto=0&height=66"> </iframe> </div> 添加打赏功能如今已进入知识付费时代,打赏是读者对笔者创造的最大支持,更是对劳动者的尊重。打赏功能具体步骤为:获取二维码 微信二维码的获取(可百度)。 获取支付宝收款二维码(可百度)。 添加二维码图片资源得到二维码图片资源后,读者们可将二维码图片放到NexT根目录/source/images/文件夹下。 开启打赏功能打开主题配置文件,搜索reward关键词,添加打赏的配置信息。12345678# Reward# If true, reward would be displayed in every article by default.# And you can show or hide one article specially through add page variable `reward: true/false`.reward: enable: true //默认是false,改为true comment: 您的支持是对我最大的鼓励 wechatpay: /images/wechatpay.jpg #图片链接或图片相对路径 alipay: /images/alipay.jpg #图片链接或图片相对路径 开启版权声明打开主题配置文件,搜索关键字 creative_commons , post 改为 true:1234creative_commons: license: by-nc-sa sidebar: false post: true 优化及设置优化urlseo搜索引擎优化认为,网站的最佳结构是三层,但是默认hexo编译的站点打开文章的url是:sitename/year/mounth/day/title四层的结构,url不利于搜索引擎搜索。 因此,我们可以将url直接改成sitename/blog/title的形式,同时title最好是用英文,在站点配置文件搜索permalink关键词,并修改如下。1234url: https://chloneda.github.io/root: /permalink: /blog/:title.htmlpermalink_defaults: Hexo博客备份利用github分支功能进行博客备份,思路说明: master分支:存放博客的静态网页(默认分支)。 hexo分支:存放Hexo博客的源码文件。 master分支部署进入站点配置文件编辑,搜索deploy关键词:1234deploy: type: git repo: https://github.com/你的github用户名/你的github用户名.github.io.git branch: master 修改更新博客内容并保存。 执行hexo clean清除本地旧代码。 执行hexo g -d生成静态网站并部署到GitHub的master分支上。 hexo分支配置 hexo分支,该分支为博客源码分支。 使用git clone -b hexo 你的github仓库路径, 拷贝源码仓库。 修改hexo主配置_config.xml的deploy部分配置,设置静态页面的发布分支为master。 添加.gitignore文件,将静态网页的目录及其他无需提交的源文件及目录排除掉。 博客源码更新在本地对博客进行修改后,提交hexo源代码:1234git checkout hexogit add .git commit -m 'Code update'git push origin hexo 发布hexo静态文件hexo根目录依次执行以下命令:123hexo cleanhexo generate 或者 hexo ghexo deploy 或者 hexo d 本地资料丢失或其他主机搭建博客步骤: 拷贝hexo分支源码到本地:git clone -b hexo github项目地址.git。 安装hexo及各类插件。 本地安装调试。 Hexo部署脚本Hexo修改后利用deploy.sh脚本一键部署,提高部署效率。123456789101112131415161718192021#!/bin/bashDIR=`dirname $0`# Generate bloghexo cleanhexo generatesleep 5# Deployhexo deploysleep 5# Push hexo codegit add .current_date=`date "+%Y-%m-%d %H:%M:%S"`git commit -m "Blog updated: $current_date"sleep 2git push origin hexoecho "=====>Finish!<=====" 把该脚本存放至 hexo根目录中,并附加脚本执行权限:1chmod 775 deploy.sh 在hexo目录根执行脚本:1./deploy.sh 可一键部署博客及备份博客源码至github的分支hexoCode上。 提升你的博客更多提升NexT主题的方法请参考以下网页。 Hexo高阶教程:打造定制化博客 让你的hexo博客在搜索引擎中排第一 结束小语本文采用Github + Hexo搭建的个人博客,在搭建过程优化总结,及时记录,希望对各位有所帮助!本文如有错误,欢迎在 Github 的issue中提出,非常感谢!!! 更多详情请参考: Hexo官网 NexT主题 Hexo插件 Markdown]]></content>
<categories>
<category>Hexo</category>
</categories>
<tags>
<tag>Hexo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ubuntu中FTP安装配置及基本概念]]></title>
<url>%2F%2Fblog%2Fftp%2F</url>
<content type="text"><![CDATA[注:本文出自博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 安装用apt-get工具安装vsftpd1$ sudo apt-get install vsftpd 检查FTP端口是否已经打开1$ netstat -tnl 或 ps -ef | grep ftp 检查FTP服务是否开启1$ service vsftpd status 如果FTP服务已经开启,则会显示如下信息,由Active关键词可知FTP服务正在运行FTP启动、停止、重启、查看状态的三种方式。123$ service vsftpd start|stop|restart|status$ systemctl start|stop|restart|status vsftpd$ /etc/init.d/vsftpd start|stop|restart|status|reload reload为重新加载配置文件 配置修改FTP配置文件1$ sudo vi /etc/vsftpd.conf FTP主要配置主要配置说明:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354# 设置登录FTP欢迎信息ftpd_banner=Welcome to CHL FTP service. # 基本配置1listen=YES # 服务器监听local_enable=YES # 是否允许本地用户访问write_enable=YES # 是否允许上传文件,不开启会报 550 permission deniedanonymous_enable=NO # 匿名访问允许,默认不要开启anon_upload_enable=YES # 匿名上传允许,默认是NOanon_mkdir_write_enable=YES # 匿名创建文件夹允许 # 基本配置2local_umask=022 # FTP上本地的文件权限,默认是077。此时umask为022,则目录为777-022=755,文件为666-022=644。dirmessage_enable=YES # 进入文件夹允许 connect_from_port_20=YES # 启用20号端口作为数据传送的端口 data_connection_timeout=120 # 设置数据连接超时时间# 日志配置utf8_filesystem=YES # vsftpd使用utf8文件系统use_localtime=YESxferlog_enable=YES # 激活上传和下传的日志 xferlog_file=/var/log/vsftpd.log # 设定系统维护记录FTP服务器上传和下载情况的日志文件xferlog_std_format=YES # 使用标准的日志格式 # 自定义local_root=/share/vsftpd # 设置自定义的ftp根目录的位置# 读写权限allow_writeable_chroot=YES # 解决"500 OOPS: vsftpd: refusing to run with writable root inside chroot()" 问题write_enable=YES # 允许向FTP服务器写入权限chown_uploads=YES # 设定是否允许改变上传文件的属主,与下面一个设定项配合使用chown_username=whoever # 设置想要改变的上传文件的属主,可设为ftpascii_upload_enable=YES # 允许服务器以ASCII方式传输数据,但引起"SIZE /big/file"方式的DoS攻击ascii_download_enable=YESdeny_email_enable=YES # 黑名单设置。如果很讨厌某些email address,可以取消他的登录权限banned_email_file=/etc/vsftpd.banned_emails# FTP限制最大连接数和传输速率,进行资源控制,避免负担过大而运行异常max_client=50 # FTP服务器的所有客户端最大连接数不超过50个max_per_ip=5 # 同一IP地址的FTP客户机与FTP服务器建立的最大连接数不超过5个local_max_rate=100000 # FTP服务器的本地用户最大传输速率设置为100KB/s.anon_max_rate=50000 # FTP服务器的匿名用户最大传输速率设置为50KB/s.# 权限设置#是否启动userlist为禁止模式,YES表示在userlist中的用户禁止登录ftp(黑名单),NO表示黑名单失效userlist_deny=NOuserlist_enable=NO # 是否启动限制用户的名单为允许模式,上面的YES限制了所有用户,可以用这个名单作为白名单,作为例外允许访问ftp根目录以外userlist_file=/etc/vsftpd.user_list# 在默认配置下,本地用户登入FTP后可以使用cd命令切换到其他目录,这样会对系统带来安全隐患,可配置如下chroot_list_enable=YES # 设置是否启用chroot_list_file配置项指定的用户列表文件。默认值为NO。chroot_local_user=YES # 用于指定用户列表文件中的用户是否允许切换到上级目录。默认值为NO。chroot_list_file=/etc/vsftpd.chroot_list # 禁用名单,用于指定用户列表,该文件用于控制哪些用户可以切换到home目录的上级目录。 通过搭配能实现以下几种效果 当chroot_list_enable=YES,chroot_local_user=YES时,在/etc/vsftpd.chroot_list文件中列出的用户,可以切换到其他目录;未在文件中列出的用户,不能切换到其他目录。 当chroot_list_enable=YES,chroot_local_user=NO时,在/etc/vsftpd.chroot_list文件中列出的用户,不能切换到其他目录;未在文件中列出的用户,可以切换到其他目录。 当chroot_list_enable=NO,chroot_local_user=YES时,所有的用户均不能切换到其他目录。 当chroot_list_enable=NO,chroot_local_user=NO时,所有的用户均可以切换到其他目录。 配置正确后可浏览器或终端输入以下信息访问123ftp://服务器IP # 浏览器方式访问$ ftp 服务器IP # 终端方式访问 也可以通过浏览器这样访问。1ftp://用户名:密码@IP/具体FTP路径 # 如:ftp://vsftpd:vsftpd@192.167.2.20/chl 修改默认端口默认FTP服务器端口号是21,出于安全目的,有时需修改默认端口号,编辑/etc/vsftpd.conf文件。1listen_port=6666 重新指定了FTP服务器的端口号,需重启服务使配置生效,并利用终端访问。12$ /etc/init.d/vsftpd restart$ ftp 服务器IP 6666 注:端口号需正确,否则连接失败。 设置FTP目录创建FTP根目录,需与配置文件一致。1$ mkdir -p /share/vsftpd 创建FTP用户1$ sudo useradd -g vsftpd -d /share/vsftpd -m test 命令参数说明: g:用户所在的组 d:指定FTP目录 m:不建立默认家目录 设置FTP用户密码1$ sudo passwd vsftpd 编辑/etc/vsftpd.chroot_list文件,将vsftpd的帐户名添加进去,保存退出,并重启FTP服务。 卸载当我们不需要FTP时,可以卸载FTP并删除FTP用户。1$ sudo apt-get remove --purge vsftpd # purge 选项表示彻底删除改软件和相关文件 删除FTP用户1$ sudo userdel vsftpd 常用命令路径切换FTP可以定位服务器与本地硬盘的路径。其中使用 lcd 命令切换宿主机本地路径,命令如下:1$ lcd 目录名 # 进入宿主机目录 而用 cd 命令切换远程服务器的路径,命令如下:1$ cd 目录名 # 进入FTP服务器目录 说到这里,得说说 ! 命令的作用,在FTP中!会执行宿主机shell命令,如:12$ !cmd [args] # 在宿主机中执行交互shell,exit回到FTP环境,例:$ !dir 或 !ls 或如果不加!,显示FTP服务器当前目录内容,如:1$ dir 或 ls 此外,ftp命令支持”含有空格”的文件夹/文件名,即在引用时加上双引号””。 下载文件 get:一次只下载一个文件。 mget:一次可以下载多个文件,而且支持通配符。 上传文件 send: 上传一个文件。 put:上传一个文件。 mput: 上传多个文件。 其他命令其实FTP命令的核心就是善用 help 或 ? 查看具体命令的含义,例如:1ftp> help 或 ? 可利用 ? [cmd] 或 help [cmd] 查看具体命令含义,如图。 有时侯我们会对多个文件进行操作,此时需要对每一个文件都选择y/n,挺麻烦的!可用prompt命令关掉交互方式。12prompt off # 关闭prompt on # 打开 其他信息FTP 数字代码的意义 参考资料vsftpd最详细的配置文件]]></content>
<categories>
<category>FTP</category>
</categories>
<tags>
<tag>FTP</tag>
</tags>
</entry>
</search>