-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.xml
More file actions
526 lines (526 loc) · 435 KB
/
search.xml
File metadata and controls
526 lines (526 loc) · 435 KB
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
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[Go 语法入门]]></title>
<url>%2F2019%2F01%2F30%2FGo-%E8%AF%AD%E6%B3%95%E5%85%A5%E9%97%A8%2F</url>
<content type="text"><![CDATA[Go 简介 Go语言是谷歌推出的一种全新的开源的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性,让构造简单、可靠且高效的软件变得容易 。 Go的目标是希望提升现有编程语言对程序库等依赖性(dependency)的管理,这些软件元素会被应用程序反复调用。由于存在并行编程模式,因此这一语言也被设计用来解决多处理器的任务。 特色简洁 快速 安全 并行 有趣 开源 内存管理 数组安全 编译迅速 基础语法123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264package main // 包声明// 引入包import "fmt"// 这种因式分解关键字的写法一般用于声明全局变量var ( a int b bool)// main 函数:程序的入口func main() { // 注:{ 不能在单独的行上 // 注:一行代表一个语句结束,推荐不使用 ; 结尾。只有多个语句在一行的时候使用分号分隔 // 变量声明 /* var a int = 10 var b = 10 // 自行判断变量类型 c := 10 // 省略var关键字,但只能在函数体类使用(推荐) // 支持多变量声明 var c, d int = 1, 2 var e, f = 123, "hello" */ // 注:如果一个局部变量声明之后并未使用,会报错 // 定义常量 const LENGTH int = 10 // 枚举常量 const ( Unknown = 0 Female = 1 Male = 2 ) // iota 特殊常量,在 const关键字出现时将被重置为 0(const 内部的第一行之前), // const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。 const ( a = 0 //0,iota += 1 b = iota //1 c //2 d = "ha" //独立值,iota += 1 e //"ha" iota += 1 f = 100 //iota +=1 g //100 iota +=1 h = iota //7,恢复计数 i1 //8 ) fmt.Println(a, b, c, d, e, f, g, h, i1) println() /* switch 用法: 1. 支持多条件匹配 2. 不同的 case 之间不使用 break 分隔,默认只会执行一个 case。 3. 如果想要执行多个 case,需要使用 fallthrough 关键字,也可用 break 终止。 */ var grade = "B" var marks = 90 switch marks { case 90: grade = "A" case 80: grade = "B" case 50, 60, 70: grade = "C" default: grade = "D" } fmt.Printf("你的等级是 %s\n", grade) // switch 语句还可以被用于 type-switch 来判断某个interface变量中实际存储的变量类型。 var x interface{} switch i := x.(type) { case nil: fmt.Printf("x 的类型 :%T", i) case int: fmt.Printf("x 是 int 型") case float64: fmt.Printf("x 是 float64 型") case func(int) float64: fmt.Printf("x 是 func(int) 型") case bool, string: fmt.Printf("x 是 bool 或 string 型") default: fmt.Printf("未知型") } fmt.Println() /* 循环控制 1.支持 break,continue,goto 2.for 可实现类似while的语法 */ var i, j int // 打印100以内的素数 for i = 2; i < 100; i++ { for j = 2; j <= (i / j); j++ { if i%j == 0 { break // 如果发现因子,则不是素数 } } if j > (i / j) { fmt.Printf("%d 是素数\n", i) } } i = 1; j = 5 for i <= j { if i == 4 { i++ continue } fmt.Print(i) i++ } fmt.Println() fmt.Println(max(2, 5)) fmt.Println(swap("World", "Hello")) var c1 Circle c1.radius = 10.00 fmt.Println("圆的面积 = ", c1.getArea()) // nextNumber 为一个函数,函数 i 为 0 nextNumber := getSequence() /* 调用 nextNumber 函数,i 变量自增 1 并返回 */ fmt.Println(nextNumber()) fmt.Println(nextNumber()) /* 创建新的函数 nextNumber1,并查看结果 */ nextNumber1 := getSequence() fmt.Println(nextNumber1()) fmt.Println(nextNumber1()) // 数组 var variable_name [SIZE] variable_type var balance = []float32{1000.0, 2.0, 3.4, 7.0, 50.0} fmt.Println(balance[1]) // 多维数组 var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type var arr = [3][4]int{ {0, 1, 2, 3}, /* 第一行索引为 0 */ {4, 5, 6, 7}, /* 第二行索引为 1 */ {8, 9, 10, 11}, /* 第三行索引为 2 */ } fmt.Println(arr[2][1]) // 9 var array = []int{1, 2, 3, 4} // 未定义长度的数组只能传给不限制数组长度的函数 setArray(array) // 定义了长度的数组只能传给限制了相同数组长度的函数 var array2 = [5]int{1, 2, 3, 4, 5} setArray2(array2) // 指针 i = 20 // 重新赋值 var ptr *int // 声明指针变量 if ptr == nil { // nil 表示空指针 fmt.Println("当前ptr为空指针") } ptr = &i // ptr 指针指向变量i fmt.Println("变量地址:", ptr, "\n变量的值:", *ptr) // 指针数组 此处的max必须为常量 const MAX = 4 var ptr2 [MAX]*int for i = 0; i < MAX; i++ { ptr2[i] = &array[i] // 将数组中每个元素的地址赋值给指针数组 } for i = 0; i < MAX; i++ { fmt.Printf("a[%d] = %d\n", i, *ptr2[i]) } // 指针的指针 var pptr **int pptr = &ptr fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr) // 结构体 circle := Circle{12} // 省略key,以默认的顺序初始化字段 circle = Circle{radius: 5} // 结构体初始化 key => value 格式、字段可以省略 fmt.Println("修改前:", circle.radius) alert(&circle) fmt.Println("修改后:", circle.radius) // 类型转换 type_name(expression) type_name为类型,expression为表达式。 i = 3 j = 4 f1 := float32(i)/float32(j) // i和j都需要转型,否则Invalid operation: float32(i)/j (mismatched types float32 and int) fmt.Println( f1 ) // 0.75 fmt.Printf("%f",f1) // 0.750000}/* 函数定义func function_name( [parameter list] ) [return_types] { 函数体}*/func max(a, b int) int { if a > b { return a } else { return b }}// 可返回多个值func swap(x, y string) (string, string) { return y, x}/* 定义结构体 */type Circle struct { radius float64}/*Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。func (variable_name variable_data_type) function_name() [return_type]{ // 函数体}*///该 method 属于 Circle 类型对象中的方法func (c Circle) getArea() float64 { //c.radius 即为 Circle 类型对象中的属性 return 3.14 * c.radius * c.radius}/* 函数闭包Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。以下实例中,我们创建了函数 getSequence() ,返回另外一个函数。该函数的目的是在闭包中递增 i 变量*/func getSequence() func() int { i := 0 return func() int { i += 1 return i }}// 未定义长度的数组只能传给 不限制数组长度 的函数func setArray(params []int) { fmt.Println("params array length of setArray is : ", len(params))}// 定义了长度的数组只能传给限制了相同数组长度的函数func setArray2(params [5]int) { fmt.Println("params array length of setArray2 is : ", len(params))}// 指针版变量交换,不支持函数重载func swap1(x *int, y *int) { *x, *y = *y, *x}// 结构体指针,引用传递,此时的形参和实参指向的是同一个对象func alert(circle *Circle) { circle.radius = 20} 进阶语法切片 Slice 对数组功能的增强,可以理解为动态数组,弥补了数组长度固定的限制,同时可以向切片中追加元素。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849package mainimport "fmt"func main() { arr := []int{1,2,3,4,5} // 切片的定义 var identifier []type var s1 []int if s1 == nil { printSlice(s1) // len=0 cap=0 slice=[] fmt.Println("当前切片s1为空切片") } // make() 函数来创建切片 参数:类型,长度,容量(可选) var s2 []int = make([]int, 5) s3 := make([]int, 5, 10) // 直接初始化 此时len=cap=3 s1 =[] int {1,2,3} // 初始化为数组arr的引用 s1 = arr[:] // 将arr中从下标startIndex到endIndex-1下的元素创建为一个新的切片(startIndex、endIndex可缺省) s1 = arr[1:5] // 注:通过数组创建的只是数组的引用,修改数组时,切片的值也会修改 arr[4] = 6 printSlice(s1) // len=4 cap=4 slice=[2 3 4 6] printSlice(s2) // len=5 cap=5 slice=[0 0 0 0 0] printSlice(s3) // len=5 cap=10 slice=[0 0 0 0 0] // 切片截取(通过[low_index:upper_index],可缺省,low ~ (upper-1)下标) printSlice(s1[1:4]) // len=3 cap=3 slice=[3 4 6] var s4 []int // 切片的追加 s4 = append(s4, 0) s4 = append(s4, 1) printSlice(s4) // len=2 cap=2 slice=[0 1] // 切片的拷贝 s5 := make([]int, len(s4), (cap(s4))*2) copy(s5, s4) // 拷贝s4的内容到s5 printSlice(s5)}func printSlice(x []int) { // len 获取切片长度,cap 切片容量 fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)} 范围 Range Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在map集合中返回 key-value 对的 key 值。 123456789101112131415161718192021222324252627282930313233343536373839package mainimport "fmt"func main() { arr := []int{1,2,3,4,5} sum := 0 // range 可以返回数组or切片中元素的下标和元素的值,此时如果不需要使用下标时,可以用_表示省略 for index, num := range arr { fmt.Println(index, num) sum += num } fmt.Println("sum:", sum) // range 也可以用在map的键值对上。 kvs := map[string]string{"a": "apple", "b": "banana"} for k, v := range kvs { fmt.Printf("%s -> %s\n", k, v) } // range 也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。 for i, c := range "go" { fmt.Println(i, c) }}/*0 11 22 33 44 5sum: 15a -> appleb -> banana0 1031 111*/ 集合 Map 存储的 key-value 键值对,使用 hash 表实现。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546package mainimport "fmt"func main() { // 声明map,默认为nil map[key_type]value_type var countryCapitalMap map[string]string if countryCapitalMap == nil { fmt.Println("当前集合为 nil") } // 使用make函数初始化map countryCapitalMap = make(map[string]string) if countryCapitalMap != nil { fmt.Println("当前集合不为 nil") } // map插入key - value对,各个国家对应的首都 countryCapitalMap[ "France" ] = "Paris" countryCapitalMap[ "Italy" ] = "罗马" countryCapitalMap[ "Japan" ] = "东京" countryCapitalMap[ "India " ] = "新德里" // map集合中range返回key for country := range countryCapitalMap { fmt.Println(country, "首都是", countryCapitalMap[country]) } // 查看元素key在集合中是否存在 captial, ok := countryCapitalMap[ "美国" ] if ok { fmt.Println("美国的首都是", captial) } else { fmt.Println("美国的首都不存在") }}/*当前集合为 nil当前集合不为 nilFrance 首都是 ParisItaly 首都是 罗马Japan 首都是 东京India 首都是 新德里美国的首都不存在*/ 接口 interface 接口把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。 12345678910111213141516171819202122232425262728293031323334353637package mainimport "fmt"// 定义接口type Phone interface { call() // 接口定义的方法}type NokiaPhone struct {}/* 接口的实现方法func (struct_name_variable struct_name) method_name() [return_type] { // 实现}*/func (nokiaPhone NokiaPhone) call() { fmt.Println("I am Nokia, I can call you!")}type IPhone struct {}func (iPhone IPhone) call() { fmt.Println("I am iPhone, I can call you!")}func main() { var phone Phone phone = new(NokiaPhone) phone.call() phone = new(IPhone) phone.call()} 错误处理 通过内置的错误接口提供错误处理机制。 error 内置接口: 123type error interface { Error() string} 使用 errors.New 可返回一个错误信息。 123456789101112131415161718192021222324252627282930313233343536package mainimport ( "errors" "fmt" "math")func main() { result, err := sqrt(-2) if err == nil { fmt.Println(result) } else { fmt.Println(err.Error()) // 输出错误信息 } result, err = sqrt(8) if err == nil { fmt.Println(result) } else { fmt.Println(err.Error()) }}func sqrt(f float64) (float64, error) { if f < 0 { return 0, errors.New("math: square root of negative number") } return math.Sqrt(f), nil}/*math: square root of negative number2.8284271247461903*/ 通过实现上述的 Error 接口方法,来生成发生错误时返回的信息。 1234567891011121314151617181920212223242526272829303132333435363738394041package mainimport "fmt"type Man struct { age int}// 实现 Error 接口方法 func (man Man) Error() string { strFmt := "Man.age 为%d, age 不能为负数" return fmt.Sprintf(strFmt, man.age)}// 初始化 Man 对象func initMan(age int) (res Man, errorMassage string) { man := Man{age} if age < 0 { errorMassage = man.Error() return } return man, ""}func main() { man, errorMassage := initMan(10) if errorMassage == "" { fmt.Println(man.age) } else { fmt.Println(errorMassage) } if _, errorMassage := initMan(-10); errorMassage != "" { fmt.Println(errorMassage) }}/*10Man.age 为-10, age 不能为负数*/ Go 并发goroutine 通过 go 关键字来开启 goroutine 即可。以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。 goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。 12345678910111213141516171819package mainimport ( "fmt" "time")func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) }}func main() { // 此时的 hello和world 输出时,没有固定的顺序 go say("world") say("hello")} 通道 通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。 12# 通道的声明,默认情况下通道是不带缓冲区的,发送端发送数据,必须等到接收端接受数据。ch := make(chan int) 两个线程分别计算数组的前后部分,然后通过通道将数据返回给主线程,再求和 123456789101112131415161718192021222324package mainimport "fmt"func sum(s []int, c chan int) { sum := 0 for _, v := range s { // range 返回索引和值 sum += v } c <- sum // 把 sum 发送到通道 c}func main() { s := []int{7, 2, 8, -9, 4, 0} // 声明通道 c := make(chan int) go sum(s[:len(s)/2], c) // s[:len(s)/2] 表示数组的前半部分 go sum(s[len(s)/2:], c) x, y := <-c, <-c // 从通道 c 中接收 fmt.Println(x, y, x+y)} 带缓冲区的通道 带缓冲区的通道允许发送端的数据的发送和接收端的数据的获取处于异步状态,即发送端可以一直发送数据到缓冲区里,除非缓冲区满了,则会阻塞。此时就必须有接收端进行数据的接受。 注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方如果没有值可以接受则会一直阻塞。 123456789101112131415161718192021222324252627282930package mainimport "fmt"func main() { // 缓冲区大小为3的通道 ch := make(chan int, 3) // 因为 ch 是带缓冲的通道,我们可以同时发送多个数据 // 而不用立刻需要去同步读取数据 ch <- 1 ch <- 2 ch <- 3 // 关闭通道 close(ch) fmt.Println("第一个数据:", <-ch) // 通过 range 遍历通道内的数据 for i := range ch { fmt.Println(i) }}/*第一个数据: 123*/ 菜鸟教程 http://www.runoob.com/go/go-tutorial.html]]></content>
<categories>
<category>Go 学习</category>
</categories>
<tags>
<tag>golang</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux 学习笔记]]></title>
<url>%2F2019%2F01%2F15%2FLinux%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[Centos虚拟机安装下载镜像 下载地址:https://www.centos.org/download/ 点击 Minimal ISO ,选择其中一个可用的源,下载 Minimal 版本即可(最小化安装版本,只包含必须的软件包) 安装 使用 VMware 安装Centos 选择语言进入安装目录 选择安装目录 设置用户信息 设置root用户密码(提示密码等级弱,双击完成按钮即可) 等待完成安装,重启 准备工作查看IPip addr 命令 此时IP地址就是网卡的 inet 的值,而上图第一个是本地服务地址,不是我们想要的。第二个没有 inet 这个属性值。 接下来使用vi编辑 /etc/sysconfig/network-scripts/ifcfg-XXX 配置网卡(XXX对应上图的ens33) 修改ONBOOT值为 yes 。表示默认启动网卡。然后重启网络服务service network restart 接下来就可以查看到IP地址了! ifconfig 命令 使用该命令时,会提示command not found。我们需要使用yum install net-tools命令安装相应的服务。然后就可以使用 ifconfig 命令了! 替换默认源 使用163源的帮助文档 http://mirrors.163.com/.help/centos.html 首先备份/etc/yum.repos.d/CentOS-Base.repo 1mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 下载对应版本repo文件, 放入/etc/yum.repos.d/(操作前请做好相应备份) CentOS7: http://mirrors.163.com/.help/CentOS7-Base-163.repo 运行以下命令生成缓存 12yum clean allyum makecache 安装 Vim1yum install vim SSH 工具SSH服务端安装 对于服务器版本的系统默认是已经安装了ssh服务的。 123456# 安装SSHyum install openssh-server# 启动SSHservice sshd start# 设置开机运行chkconfig sshd on SSH客户端安装 yum install openssh-clients 连接ssh服务端 ssh root@192.168.156.188 然后输入密码,即可连接到远程的SSH服务端。 存在的问题 Windows平台的Xshell连接服务器后提示WARNING! The remote SSH server rejected X11 forwarding request.警告 解决办法: 编辑 /etc/ssh/sshd_config。设置 X11Forwarding yes。如果依然无法解决则修改Xshell中当前连接的SSH->隧道(Tunneling),然后关闭 X11转发 。 SSH config用法详解 config 用于方便我们管理多个SSH,存放路径为 ~/.ssh/config 。 配置语法:host 别名、 HostName 主机名、 User 用户名、Port 端口号、IdentityFile 秘钥文件(私钥) 配置完成之后,我们就可以使用ssh 别名的方式访问服务器。 免密码登录方案之SSH Key 使用ssh工具生成公钥和私钥,然后在服务端进行注册,将生成的公钥复制到服务器中。然后就可以实现免密登录了。 Windows平台 通过 Xshell -> 工具 -> 用户秘钥管理者 -> 生成 -> 设置秘钥类型和秘钥长度 -> 设置秘钥名称和秘钥加密的密码 -> 点击完成(另保存公钥) Linux平台 进入.ssh目录 -> 使用 ssh-keygen -t rsa 命令 -> 设置秘钥名称和密码 然后将(mindyu.pub)文件中公钥的复制到服务器端的 ~/.ssh/authorized_keys 文件中去。 如果客户端是Linux平台,还需要将私钥进行加载 ssh-add ~/.ssh/mindyu。然后就可以免密访问。而Windows平台只需要设置Xshell用户身份认证方式为Public Key即可。 SSH 端口安全 修改SSH默认的端口号 12345# 修改配置文件中 Port (可以监听多个端口)vim /etc/ssh/sshd_config# 重启SSH服务service sshd restart Linux 常用命令软件操作命令 软件包管理器:yum 安装软件:yum install xxx 卸载软件:yum remove xxx 搜索软件:yum search xxx 清理缓存:yum clean packages 列出已安装:yum list 软件包信息:yum info xxx 服务器硬件资源和磁盘操作 内存:free -m 硬盘:df -h 负载:w 、top (Load Average 就是一段时间 (1 分钟、5分钟、15分钟) 内平均 Load ) CPU:cat /proc/cpuinfo 文件和文件夹操作Linux 文件目录结构 文件基本操作 命令 解释 命令 解释 ls / ll 查看目录下文件 touch 新建文件 mkdir 创建文件夹(-p 逐层创建) cd 进入目录 rm 删除文件和目录(-r 循环) cp 复制 mv 移动 pwd 显示路径 文件编辑神器 Vim 快捷键键盘图: 工作模式: Linux vi/vim | 菜鸟教程 文件权限421 rwx (读4、写2、可执行1) 1drwxr-xr-x // 表示当前为文件夹,创建者权限为rwx,用户组权限为r-x,其他用户权限为r-x 文件搜索、查找、读取 命令 解释 命令 解释 tail 从文件尾开始读 head 从文件头读 cat 读取整个文件 more 分页读取 less 可控分页 grep 搜索关键字 find 查找文件 wc 统计个数 文件的压缩和解压tar 命令 以下5个独立命令,解压缩时只能用到其中一个。12345-c: 建立压缩档案 -x:解压 -t:查看内容 -r:向压缩归档文件末尾追加文件 -u:更新原压缩包中的文件 下面的参数是根据需要在压缩或解压档案时可选的。12345-z:有gzip属性的 -j:有bz2属性的 -Z:有compress属性的 -v:显示所有过程 -O:将文件解开到标准输出 -f 命令为必选的命令,后面添加档案名 1-f: 使用档案名字,切记,这个参数是最后一个参数,后面只能接档案名。 Linux tar.gz、tar、bz2、zip 等解压缩、压缩命令详解 系统用户操作命令 命令 解释 useradd 添加用户 adduser 添加用户 userdel 删除用户(-r 表示删除用户目录) passwd 设置密码 在 CentOS 中,useradd和adduser是一样的。都是在创建了用户之后,会在/etc/passwd文件中加一条新建用户的记录,然后在/home目录下创建新用户的主目录,并把/etc/skel目录中的文件复制到这个主目录下面。 注意:这种方法创建的新用户,在设置密码之前是不能登陆到系统上的,需要在root权限下使用“passwd 用户名”的方法为指定的用户设置密码。下次才能以该用户名和密码登陆到系统中。 在 Ubuntu 中,推荐使用adduser命令,adduser命令会创建用户,同时创建同名的组,添加用户到对应的组中,创建对应的home文件夹,拷贝/etc/skel文件,最后输入用户密码。而useradd还需要添加其他参数。 防火墙相关设置安装及使用 安装:yum install firewalld 启动:service firewalld start 检查状态:service firewalld status 关闭或禁用防火墙:service firewalld stop/disable 配置 firewalld-cmd 查看版本: firewall-cmd –version 查看帮助: firewall-cmd –help 显示状态: firewall-cmd –state 列出所有的区域:firewall-cmd –get-zones 列出默认区域:firewall-cmd –get-default-zone 列出所有区域配置: firewall-cmd –list-all-zone 查看所有打开的端口: firewall-cmd –zone=public –list-ports 查看规则:iptables -L -n 更新防火墙规则: firewall-cmd –reload 查看区域信息: firewall-cmd –get-active-zones 查看指定接口所属区域: firewall-cmd –get-zone-of-interface=eth0 添加服务 firewall-cmd –add-service=ssh firewall-cmd –query-service=ssh firewall-cmd –remove-service=ssh 添加端口 添加:firewall-cmd –zone=public –add-port=80/tcp 重新载入:firewall-cmd –reload 查看:firewall-cmd –zone=public –query-port=80/tcp 删除:firewall-cmd –zone=public –remove-port=80/tcp 提权和文件上传和下载 使用普通用户时,当我们安装软件等操作时,就会提示权限不够,此时就需要提权操作。但是当我们使用 sudo 进行提权时,会提示 xxx用户 is not in the sudoers file. This incident will be reported.即当前用户不在 sudoers 文件中。 那么就需要使用 root 用户登录然后通过 visudo 命令添加。 123## Allows people in group wheel to run all commands%wheel ALL=(ALL) ALL%mindyu ALL=(ALL) ALL # 允许mindyu用户使用提权操作 服务器端下载命令 123wget http://www.baidu.comcurl -o baidu.html http://www.baidu.com // -o 指定文件名 本地上传命令 对于 Linux 平台: 12345# 上传本地 test.txt 文件到服务器的 tmp 目录下scp test.txt root@192.168.156.188:/tmp/# 下载服务器的 test.txt 文件到本地当前目录scp root@192.168.156.188:/tmp/test.txt ./ 对于 Windows 平台: 使用 Xshell 软件即可。首先需要在服务器端安装 yum install lrzsz,然后就可以使用一下命令: 12345# 上传,回车之后选择需上传的文件rz # 下载,xxx表示文件名,回车之后选择下载的路径sz xxx 另外 WinSCP 软件可以实现可视化的文件上传下载功能。EditPlus 软件可以实现修改远程服务器配置文件。 WebServer 安装和配置Apache基本操作 解释 命令 安装 yum install httpd 启动 service httpd start 停止 service httpd stop 虚拟主机配置 配置 /etc/httpd/conf/httpd.conf 文件 123456789<VirtualHost *:80> ServerName www.mindyu.test DocumentRoot /data/www <Directory "/data/www"> Options Indexes FollowSymLinks AllowOverride None Require all granted </Directory></VirtualHost> 在本地需要修改 host 文件,添加 www.mindyu.test 的映射。Windows 平台在 C:\WINDOWS\system32\drivers\etc 路径下。 如果配置完成之后,访问该网址时,依然进入的是 Apache的默认页,则 sudo setenforce 0即可! 同时可以通过修改默认配置 /etc/selinux/config文件中的 SELINUX 值为 disabled 。 伪静态 通过 rewrite 模块实现 12345678910111213<VirtualHost *:80> ServerName www.mindyu.test DocumentRoot /data/www <Directory "/data/www"> Options Indexes FollowSymLinks AllowOverride None Require all granted <IfModule mod_rewrite.c> RewriteEngine On RewriteRule ^(.*).htmp$ index.html # 将所有 .htmp 请求重写到 index.html </IfModule> </Directory></VirtualHost> Nginx安装 添加源 1sudo rpm -Uvh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm 安装 1sudo yum install -y nginx 使用 12345678# 启动sudo service nginx start# 重载sudo service nginx reload# 停止sudo service nginx stop# 开机启动sudo systemctl enable nginx.service 配置一个虚拟主机 在 /etc/nginx/conf.d/ 目录下,新建一个 test.conf 的文件进行以下配置。 123456server { listen 80; server_name www.mindyu.test; root /data/www; index index.html index.htm;} 配置完成之后,进行重载即可访问。 注:当 Apache 服务启动时,就无法在启动 nginx 服务了,因为它们默认都是 80 端口。 伪静态 123456789server { listen 80; server_name www.mindyu.test; root /data/www; index index.html index.htm; location / { rewrite ^(.*)\.htmp$ /index.html; }} 日志记录 nginx 可进行日志记录,通过配置 nginx.conf 文件 1234567# 格式化样式log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; # 日志路径和采用的格式化方式 access_log /var/log/nginx/access.log main; 另外也可以在每个不同的虚拟主机中进行单独配置,实现不同的应用生成不同的日志文件。 反向代理和负载均衡 123456789101112131415upstream test_hosts{ server 185.199.111.153:80 weight=5; server 192.168.156.188:80 weight=1;}server { listen 80; server_name www.mindyu.test; root /data/www; index index.html index.htm; location / { # rewrite ^(.*)\.htmp$ /index.html; proxy_pass http://test_hosts; } } MySQL 数据库服务安装 CentOS 7 默认安装 mariadb。在安装 MySQL 之前需要先卸载该软件。通过搜索命令 yum search mysql,即可查找到 mariadb 的安装情况。然后卸载 sudo yum remove mariadb-libs.x86_64。接下来就是安装 MySQL 的过程了。 首先在 MySQL 官网下载源 1wget https://dev.mysql.com/get/mysql80-community-release-el7-2.noarch.rpm 安装源(安装完成之后就可以通过 yum search mysql 查找到 mysql 了) 1sudo yum localinstall mysql80-community-release-el7-2.noarch.rpm 安装 MySQL 1sudo yum install mysql-community-server.x86_64 基本操作 启动:sudo service mysqld start 停止:sudo service mysqld stop 重启:sudo service mysqld restart 查看mysql进程:ps -ef | grep mysql 服务启动之后可以使用 cat /var/log/mysqld.log | grep password 命令查看临时密码,拿到临时密码之后进行登录 mysql -uroot -pXXXXXXXXXX 。登录之后可以看到提示 mysql: [Warning] Using a password on the command line interface can be insecure.表示该密码不安全需要修改密码。 修改密码命令:ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';。但是由于密码设置的过于简单会提示 ERROR 1819 (HY000): Your password does not satisfy the current policy requirements。此时需要进一步修改密码策略和密码长度限制。 MySQL 5.7: 12set global validate_password_policy=0;set global validate_password_length=1; MySQL 8.0: 12set global validate_password.policy=0;set global validate_password.length=1; 设置远程访问123456789show databases;user mysql;select Host,User from user \G;# % 表示所有主机都可以访问update user set host='%' where Host='localhost' and User='root';# 刷新权限flush privileges; 修改后: 但是使用 MySQL 8.0 时,当我们使用本地的可视化软件 SQLyog 连接时,提示 1251 错误。 解决方案 12345# 修改mysql_native_passwd密码ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';# 然后刷新权限flush privileges; 开启 genolog 可以记录所有执行的 sql 语句。 12345# 设置general_log的路径set global general_log_file="/tmp/general.log";# 开启general_logset global general_log=on; 创建用户12345678# 创建新用户 mindyu。任何主机都可访问,密码为 123456create user 'mindyu'@'%' identified by '123456';# 赋予权限 *.* 表示所有库所有表 grant all privileges on *.* to 'mindyu'@'%'; # 刷新权限flush privileges; 赋予权限: grant to 收回权限:revoke from 忘记密码时如何找回 在 /etc/my.cnf 配置文件中加入 skip-grant-tables。然后重启 MySQL 服务,即可无密码登录服务。 12345678910111213# 首先刷新权限flush privileges;# 修改密码use mysql;ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';# 如果提示密码不安全则设置密码策略,再修改密码set global validate_password.policy=0;set global validate_password.length=1;# 再刷新权限即可flush privileges; 缓存服务memcached12345678安装sudo yum install memcached 启动 -d:后台启动 -l:监听IP地址 -m:分配内存大小 -p:端口号memcached -d -l -m -p 停止kill pid telnet 命令 12345# 安装yum install telnet.*# 检测端口是否连通(quit退出)telnet 127.0.0.1 11211 Redis1234567891011121314# 获取源码(官网的下载链接)wget http://download.redis.io/releases/redis-5.0.3.tar.gz# 解压tar zxvf redis-5.0.3.tar.gz# 此时编译时会提示 没有 gcc 命令,所以先安装 gccyum install gcc# 再次 make 时,仍然报错 fatal error: jemalloc/jemalloc.h: No such file or directorymake MALLOC=libc# 执行安装sudo make install Java Web环境配置Jdk 安装12# 安装 openjdk -y 表示自动选择确认 sudo yum -y install java-1.8.0-openjdk Tomcat 安装1234567891011121314# 下载(官网)wget http://mirror.bit.edu.cn/apache/tomcat/tomcat-8/v8.5.37/bin/apache-tomcat-8.5.37.tar.gz# 解压tar zxvf apache-tomcat-8.5.37.tar.gz# 重命名mv apache-tomcat-8.5.37 tomcat# 启动./tomcat/bin/startup.sh# 关闭./tomcat/bin/shutdown.sh 此时就可以使用 ip:8080 访问tomcat服务器了,我们可以将Tomcat和nginx结合起来,通过域名的方式直接访问8080端口tomcat服务器。 进入 /etc/nginx/conf.d 目录下,新建一个配置文件,将80端口的请求转发给8080端口的Tomcat服务器。 1234567server { listen 80; server_name www.java.test; location / { proxy_pass http://127.0.0.1:8080; } } Maven 安装1234567891011# 下载wget http://mirrors.shu.edu.cn/apache/maven/maven-3/3.6.0/binaries/apache-maven-3.6.0-bin.tar.gz# 解压tar zxvf apache-maven-3.6.0-bin.tar.gz# 重命名mv apache-maven-3.6.0 maven# 添加软链(这样就可以在命令行使用 mvn 命令)sudo ln -s ~/maven/bin/mvn /usr/bin/mvn Python 环境配置pip 安装 python2.7.5版本中,python目录下没有Scripts这个文件夹 ,导致一些指令使用不了,yum install python-pip 无法安装。 解决方案: 安装 setuptools ,进入官网下载最新版本的 setuptools ,然后解压,进入目录进行安装python setup.py install。 安装 pip , 下载最新版本的 pip,然后解压,进入目录进行安装python setup.py install。 pip 替换豆瓣源 创建文件夹 mkdir ~/.pip 编辑配置文件 vim ~/.pip/pip.conf 123[global]timeout=60index-url=http://mirrors.aliyun.com/pypi/simple/ python 虚拟环境 virtualenv 用来为一个应用创建一套“隔离”的Python运行环境 1234567891011# 安装sudo pip install virtualenv# 创建一个隔离的 python 环境(--no-site-packages 表示不携带系统已安装的第三方包)virtualenv pyvenv# 进入环境source pyvenv/bin/activate# 退出环境deactivate 服务管理crontab 定时任务 linux 系统中的计划性任务就是由 cron (crond) 这个系统服务来控制的。该系统服务是默认启动的。另外, 由于使用者自己也可以设置计划任务,所以 Linux 系统也提供了使用者控制计划任务的命令:crontab 命令。 crond 是linux下用来周期性的执行某种任务或等待处理某些事件的一个守护进程,该进程每分钟会定期检查是否有要执行的任务,如果有要执行的任务,则自动执行该任务。 Linux下的任务调度分为两类,系统任务调度和用户任务调度。 系统任务调度:系统周期性所要执行的工作,比如写缓存数据到硬盘、日志清理等。 在/etc目录下有一个crontab文件,这个就是系统任务调度的配置文件。 123456789101112131415# crontab 文件SHELL=/bin/bashPATH=/sbin:/bin:/usr/sbin:/usr/binMAILTO=root# For details see man 4 crontabs# Example of job definition:# .---------------- minute (0 - 59)# | .------------- hour (0 - 23)# | | .---------- day of month (1 - 31)# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat# | | | | |# * * * * * user-name command to be executed 用户任务调度:用户定期要执行的工作,比如用户数据备份、定时邮件提醒等。用户可以使用 crontab 工具来定制自己的计划任务。所有用户定义的crontab 文件都被保存在 /var/spool/cron 目录中。 Ntpdata 日期同步 Linux 系统的时间经常和当前时间有区别。可以使用 ntpdate 命令来设置系统时间。 1234567891011121314151617# 安装sudo yum install ntpdate# 同步时间ntpdate cn.pool.ntp.org# 查看系统时间date# 查看软链(对应时区信息)ll /etc/localtime# 删除软链rm /etc/localtime # 新建上海时区的软链即可ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 定时任务 12345678# 新建定时任务crontab -e # 添加更新任务ntpdate cn.pool.ntp.org# 查看定时任务crontab -l Logratate 日志切割 Logratate 是Linux系统自带的日志切割管理工具,可以自动进行日志的分割、压缩、覆盖等操作,同时可以设置按时间(天、周、月)、日志大小来执行分割操作。 系统的配置文件在 /etc/logrotate.d 目录下和/etc/logrotate.conf。 比如 /etc/logrotate.d/nginx 文件: 123456789101112131415/var/log/nginx/*.log { daily # 按天分割 missingok # 在日志轮循期间,任何错误将被忽略,例如“文件无法找到”之类的错误 rotate 52 # 一次最多存储52个归档日志 compress # 在轮循任务完成后,已轮循的归档将使用gzip进行压缩 delaycompress # 最近的日志不归档 notifempty # 如果日志文件为空,轮循不会进行 create 640 nginx adm # 以指定的权限创建全新的日志文件 sharedscripts postrotate # 脚本命令 if [ -f /var/run/nginx.pid ]; then kill -USR1 `cat /var/run/nginx.pid` fi endscript} sharedscripts:运行postrotate脚本,作用是在所有日志都轮转后统一执行一次脚本。如果没有配置这个,那么每个日志轮转后都会执行一次脚本 dateext:使用当期日期作为命名格式 dateformat .%s:配合dateext使用,紧跟在下一行出现,定义文件切割后的文件名,必须配合dateext使用,只支持 %Y %m %d %s 这四个参数 运维中的日志切割操作梳理 supervisor 进程管理 supervisor 是一个 Linux 上用来管理程序后台运行的工具,支持程序的自启动,挂掉重启,日志,查看服务状态等功能。可配置程序随系统启动,并支持挂掉重启,增强程序稳定性。 安装及配置 12345678910111213141516171819202122232425262728293031# 安装yum install supervisor # 创建配置文件夹mkdir /etc/supervisor# 生成配置文件到 supervisor 目录echo_supervisord_conf > /etc/supervisor/supervisord.conf# 修改配置文件,在文件尾添加[include]files = /etc/supervisor/conf.d/*.ini# 创建 conf.d 文件夹存放各个服务的配置mkdir conf.d# 创建 redis 服务配置touch redis.ini# 编辑服务配置[program:redis]command=/usr/local/bin/redis-serverautostart=trueautorestart=truestartsecs=3# 启动supervisord -c /etc/supervisor/supervisord.conf # 客户端工具查看服务启动状态supervisorctl -c /etc/supervisor/supervisord.conf supervisor 开机自启 12345# 编辑启动文件vim /etc/rc.local# 在新行添加要执行的命令supervisord -c /etc/supervisor/supervisord.conf 常用命令 123456789101112# 查看所有 actionsupervisorctl help# 控制所有进程supervisorctl start allsupervisorctl stop allsupervisorctl restart all# 控制目标进程supervisorctl stop redissupervisorctl start redissupervisorctl restart redis 监控系统 Zabbix https://www.zabbix.com/download 安装完成之后提示403错误 ——————————————-未完待续—————————————————]]></content>
<categories>
<category>Linux 学习</category>
</categories>
<tags>
<tag>CentOS</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring 框架笔记]]></title>
<url>%2F2019%2F01%2F05%2FSpring%20%E6%A1%86%E6%9E%B6%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[七大模块 Spring Core:Core 封装包是框架的最基础部分,提供 IOC 和依赖注入特性。这里的基础概念是BeanFactory,它提供对 Factory 模式的经典实现来消除对程序性单例模式的需要,并真正地允许你从程序逻辑中分离出依赖关系和配置。 Spring Context: 构建于Core封装包基础上的 Context 封装包,提供了一种框架式的对象访问方法,有些像 JNDI 注册器。Context 封装包的特性得自于Beans封装包,并添加了对国际化(I18N)的支持(例如资源绑定),事件传播,资源装载的方式和 Context 的透明创建,比如说通过Servlet容器。 Spring DAO: DAO (Data Access Object)提供了 JDBC 的抽象层,它可消除冗长的 JDBC 编码和解析数据库厂商特有的错误代码。 并且,JDBC封装包还提供了一种比编程性更好的声明性事务管理方法,不仅仅是实现了特定接口,而且对所有的 POJOs(plain old Java objects)都适用。 Spring ORM:ORM 封装包提供了常用的“对象/关系”映射APIs的集成层。 其中包括JPA、JDO、Hibernate 和 iBatis 。利用ORM封装包,可以混合使用所有 Spring 提供的特性进行“对象/关系”映射,如前边提到的简单声明性事务管理。 Spring AOP:Spring的 AOP 封装包提供了符合AOP Alliance规范的面向方面的编程实现,让你可以定义,例如方法拦截器(method-interceptors)和切点(pointcuts),从逻辑上讲,从而减弱代码的功能耦合,清晰的被分离开。而且,利用source-level的元数据功能,还可以将各种行为信息合并到你的代码中。 Spring Web:Spring 中的 Web 包提供了基础的针对Web开发的集成特性,例如多方文件上传,利用Servlet listeners进行IOC容器初始化和针对Web的 ApplicationContext。当与WebWork或Struts一起使用Spring时,这个包使Spring可与其他框架结合。 Spring Web MVC:Spring中的MVC封装包提供了Web应用的Model-View-Controller(MVC)实现。Spring的MVC框架并不是仅仅提供一种传统的实现,它提供了一种清晰的分离模型,在领域模型代码和Web Form之间。并且,还可以借助Spring框架的其他特性。 Ioc (Inverse of Control)控制反转将依赖对象的创建和管理交由Spring容器,而依赖注入(Dependency Injection)则是在控制反转的基础上将 Spring 容器管理的依赖对象注入到应用之中。 将对象的创建从编译时延期到运行时,即通过配置进行加载,是纯粹的针对接口编程。解除了程序对具体实现的依赖,可以灵活的组装各种实现,因此可以轻松的将各种成熟的工具继承到Spring中来。 Spring IoC 容器主要是基于 BeanFactory 和 ApplicationContext 两个接口,其中 ApplicationContext 又是 BeanFactory 的子接口。所以,换句话说,BeanFactory 是 Spring IoC 容器的顶层接口,而 ApplicationContext 是最常用接口。 Spring IoC 容器利用 Java 的 POJO 类和配置元数据(XML,Java 注释或 Java 代码)来生成完全配置和可执行的系统或应用程序。 AOP面向方面(切面)编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存、权限认证、性能分析等等。将辅助性逻辑与主逻辑进行分离。 AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。 AspectJ 是静态代理的增强,所谓的静态代理就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。在编译阶段将 Aspect 织入 Java 字节码中, 运行的时候就是经过增强之后的AOP对象。 Spring AOP 使用的动态代理,所谓的动态代理就是说 AOP 框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。包括JDK动态代理和CGLIB动态代理。 JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是 InvocationHandler 接口和 Proxy 类。 CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类。注意:CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。 Spring Bean 的生命周期 实例化 Bean 对于 BeanFactory 容器,当客户向容器请求一个尚未初始化的 bean 时,或初始化 bean 的时候需要注入另一个尚未初始化的依赖时,容器就会调用 createBean 进行实例化。 对于 ApplicationContext 容器,当容器启动结束后,便实例化所有的 bean。 设置对象属性(依赖注入) 注入Aware接口 Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给bean。BeanNameAware、BeanFactoryAware、ApplicationContextAware。 BeanPostProcessor 接口方法(前置处理) 执行 postProcessBeforeInitialzation( Object bean, String beanName ) 若 Bean 实现了 InitializingBean 接口,并重写了 afterPropertiesSet 方法,可在该方法中添加我们自定义的逻辑,则 Spring 会在前置处理完成后执行 afterPropertiesSet 函数 Spring为了降低对客户代码的侵入性,给 bean 的配置提供了 init-method 属性,该属性指定了在这一阶段需要执行的函数名。Spring便会在初始化阶段执行我们设置的函数 BeanPostProcessor 接口方法(后置处理) 执行 postProcessAfterInitialzation( Object bean, String beanName ) DisposableBean和destroy-method 通过给 destroy-method 指定函数,就可以在 bean 销毁前执行指定的逻辑。 BeanFactory 和 ApplicationContext 有什么区别BeanFactorySpring IoC 容器的顶层接口。BeanFactory 接口定义了 Spring IoC 中最重要的方法(簇): getBean() 方法。这个方法用于从 Spring IoC 容器中获得 Bean 。 ApplicationContext最常用接口,由 BeanFactory 接口派生而来。拥有 BeanFactory 接口的所有功能同时还具有以下功能: 提供国际化的消息访问 (扩展了MessageSource接口) 资源访问,如 URL 和文件(扩展了ResourceLoader(资源加载器)接口) 事件传播 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层 Spring 框架中用到了哪些设计模式单例模式Spring下默认的 bean 均为 singleton,可以通过 singleton=“true|false” 或者 scope=“?” 来指定 Spring 的单例模式为可继承的单例类,使用的是单例注册表的方式 使用 map 实现注册表 使用 protect 修饰构造方法(其它单例方法构造方法都是私有的,就无法实现继承) 工厂模式将对象的创建和使用相分离,将对象的创建及初始化职责交给工厂对象。 BeanFactory 代理模式(1、JDK动态代理。2、CGLib字节码生成技术代理。)对类进行方法级别的切面增强,即生成被代理类的代理类, 并在代理类的方法前,设置拦截器,通过执行拦截器中的内容增强了代理方法的功能,实现的面向切面编程。 观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 ApplicationListener 策略模式定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。可使得算法可独立于使用它的客户而变化 模板方法定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤 Spring 事务实现方式编程式事务管理将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在编程式管理事务时, 必须在每个事务操作中包含额外的事务管理代码。 声明式事务管理将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。事务管理作为一种横切关注点,可以通过 AOP 方法模块化。Spring 通过 Spring AOP 框架支持声明式事务管理。 Spring 从不同的事务管理 API 中抽象了一整套的事务机制。开发人员不必了解底层的事务 API,就可以利用这些事务机制。有了这些事务机制。事务管理代码就能独立于特定的事务技术了。 用事务通知声明式地管理事务 用 @Transactional 注解声明式地管理事务 为了将方法定义为支持事务处理的,可以为方法添加 @Transactional 注解。根据 Spring AOP 基于代理机制,只能标注公有方法。 可以在方法或者类级别上添加 @Transactional 注解。当把这个注解应用到类上时,这个类中的所有公共方法都会被定义成支持事务处理的。 在 Bean 配置文件中只需要启用<tx:annotation-driven> 元素,并为之指定事务管理器就可以了。 如果事务处理器的名称是 transactionManager,就可以在<tx:annotation-driven> 元素中省略 transaction-manager 属性。这个元素会自动检测该名称的事务处理器。 Spring 事务的传播机制 https://www.cnblogs.com/softidea/p/5962612.html 事务传播行为规定了事务方法和事务方法发生嵌套调用时事务是如何传播的。 事务传播类型(七种) Spring 自定义注解 https://www.jianshu.com/p/7c2948f64b1c Spring MVC 启动流程web.xml 配置1234567891011121314151617181920212223242526272829303132333435363738394041<web-app> <display-name>Web Application</display-name> <!--全局变量配置--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext-*.xml</param-value> </context-param> <!--监听器--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!--解决乱码问题的filter--> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--Restful前端控制器--> <servlet> <servlet-name>springMVC_rest</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>springMVC_rest</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping></web-app> 启动流程1、解析里的键值对。 2、创建一个application内置对象即ServletContext,servlet上下文,用于全局共享。 3、将的键值对放入ServletContext即application中,Web应用内全局共享。 4、读取标签创建监听器,一般会使用ContextLoaderListener类,如果使用了ContextLoaderListener类,Spring就会创建一个WebApplicationContext类的对象,WebApplicationContext类就是IoC容器,ContextLoaderListener类创建的IoC容器是根IoC容器为全局性的,并将其放置在appication中,作为应用内全局共享,键名为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,可以通过以下两种方法获取。这个全局的根IoC容器只能获取到在该容器中创建的Bean不能访问到其他容器创建的Bean,也就是读取web.xml配置的contextConfigLocation参数的xml文件来创建对应的Bean。 12WebApplicationContext applicationContext = (WebApplicationContext) application.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);WebApplicationContext applicationContext1 = WebApplicationContextUtils.getWebApplicationContext(application); 5、listener创建完成后如果有则会去创建filter。 6、初始化创建,一般使用DispatchServlet类。 7、DispatchServlet的父类FrameworkServlet会重写其父类的initServletBean方法,并调用initWebApplicationContext()以及onRefresh()方法。 8、initWebApplicationContext()方法会创建一个当前servlet的一个IoC子容器,如果存在上述的全局WebApplicationContext则将其设置为父容器,如果不存在上述全局的则父容器为null。 9、读取标签的配置的xml文件并加载相关Bean。 10、onRefresh()方法创建Web应用相关组件。 Spring MVC 运行流程流程示意图 Spring工作流程描述 用户向服务器发送请求,请求被 Spring 前端控制器 DispatcherServlet 捕获 DispatcherServlet 对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以 HandlerExecutionChain 对象的形式返回 DispatcherServlet 根据获得的 Handler,选择一个合适的 HandlerAdapter。(附注:如果成功获得 HandlerAdapter 后,此时将开始执行拦截器的 preHandler(…)方法) 提取 Request 中的模型数据,填充 Handler入参,开始执行Handler(Controller) Handler 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象然后调用拦截器的 postHandle 方法 根据返回的 ModelAndView,选择一个适合的 ViewResolver(必须是已经注册到 Spring 容器中的ViewResolver )返回给 DispatcherServlet ViewResolver 结合Model和View,来渲染视图 将渲染结果返回给客户端 执行拦截器的afterCompletion方法 SpringMVC与Struts2区别框架机制Struts2 采用 Filter(StrutsPrepareAndExecuteFilter)实现,SpringMVC(DispatcherServlet)则采用 Servlet 实现。 Filter 在容器启动之后即初始化;服务停止以后销毁,晚于 Servlet。 Servlet 在是在调用时初始化,先于 Filter 调用;服务停止后销毁。 拦截机制 Struts2 Struts2 框架是类级别的拦截,每次请求就会创建一个 Action,和 Spring 整合时Struts2的 ActionBean 注入作用域是原型模式 prototype(否则会出现线程并发问题),然后通过 setter , getter 把 request 数据注入到属性。 Struts2 中,一个 Action 对应一个 request,response 上下文,在接收参数时,可以通过属性接收,这说明属性参数是让多个方法共享的。 Struts2 中 Action 的一个方法可以对应一个url,而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了 SpringMVC SpringMVC是方法级别的拦截,一个方法对应一个 Request 上下文,所以方法基本上是独立的,独享 request,response 数据。而每个方法同时又和一个 url 对应,参数的传递是直接注入到方法中的,是方法所独有的。处理结果通过 ModelMap 返回给框架。 在 Spring 整合时,SpringMVC 的 Controller Bean 默认单例模式Singleton,所以默认对所有的请求,只会创建一个 Controller,因为没有共享的属性,所以是线程安全的,如果要改变默认的作用域,需要添加 @Scope 注解修改。 性能方面SpringMVC实现了零配置,由于 SpringMVC 基于方法的拦截,只要加载一次单例模式bean注入。 Struts2是类级别的拦截,每次请求对应实例一个新的Action,需要加载所有的属性值注入。 SpringMVC开发效率和性能高于Struts2。 设计思想Struts2 更加符合 OOP 的编程思想, SpringMVC 就比较谨慎,在 servlet 上扩展。 集成方面SpringMVC 集成了 Ajax,使用非常方便,只需一个注解 @ResponseBody 就可以实现,然后直接返回响应文本即可 而 Struts2 拦截器集成了 Ajax,在 Action 中处理时一般必须安装插件或者自己写代码集成进去,使用起来也相对不方便。 Spring Boot什么是Spring BootSpring Boot 简化了基于 Spring 的应用开发,通过少量的代码就能创建一个独立的、产品级别的 Spring 应用。Spring Boot 依赖 Spring 框架来管理对象的依赖。 主要目标 为所有Spring开发提供一个更快更广泛的入门体验; 开箱即用,不合适时也可以快速抛弃; 提供一系列大型项目常用的非功能性特征,比如:嵌入式服务器、安全性、度量、运行状况检查、外部化配置等; 零配置(无冗余代码生成和XML强制配置 ,遵循“约定大于配置”);]]></content>
<categories>
<category>Spring 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>Spring</tag>
<tag>Spring MVC</tag>
</tags>
</entry>
<entry>
<title><![CDATA[品优购项目笔记(下)]]></title>
<url>%2F2018%2F12%2F22%2F%E5%93%81%E4%BC%98%E8%B4%AD%E9%A1%B9%E7%9B%AE%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%8B%EF%BC%89%2F</url>
<content type="text"><![CDATA[购物车解决方案 用户在商品详细页点击加入购物车,提交商品 SKU 编号和购买数量,添加到购物车。当用户在未登录的情况下,将此购物车存入 cookies , 在用户登陆的情况下,将购物车数据存入 redis 。如果用户登陆时,cookies 中存在购物车,需要将 cookies 的购物车合并到 redis 中存储. 购物车数据存储结构: 购物车实体类: 123456public class Cart implements Serializable{ // 对每一个商家购物车的封装 private String sellerId; // 商家ID private String sellerName; // 商家名称 private List<TbOrderItem> orderItemList; // 购物车明细 // setter getter方法 } Cookie 存储购物车 服务实现层:向购物车列表中添加物品 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091@Overridepublic List<Cart> addGoodsToCartList(List<Cart> cartList, Long itemId, Integer num) { // 1. 根据skuID (itemId)查询商品明细sku的对象 TbItem item = itemMapper.selectByPrimaryKey(itemId); if (item == null) throw new RuntimeException("商品不存在"); // 时间差,比如在添加提交订单时,商品下架了 if (!"1".equals(item.getStatus())) throw new RuntimeException("商品不存在"); // 2. 根据sku对象获取商家ID String sellerId = item.getSellerId(); // 3. 根据商家ID在购物车列表中查询购物车对象 Cart cart = searchCartBySellerId(cartList, sellerId); // 4.如果购物车列表中不存在该商家ID对应的购物车对象 if (cart == null) { // 4.1创建该商家的购物车对象 cart = new Cart(); cart.setSellerId(sellerId); cart.setSellerName(item.getSeller()); // 创建购物车明细对象 List<TbOrderItem> orderItemList = new ArrayList<>(); TbOrderItem orderItem = createOrderItem(item, num); orderItemList.add(orderItem); cart.setOrderItemList(orderItemList); // 4.2将该购物车对象添加到购物车列表中 cartList.add(cart); }else { // 5. 如果购物车列表中存在该商家ID对应的购物车对象 // 然后判断购物车对象中是否存在该商品的明细对象 TbOrderItem orderItem = searchOrderItemByItemId(cart.getOrderItemList(), itemId); if (orderItem == null) { // 5.1 如果明细列表中不存在,创建明细对象添加到购物车对象中 // 创建购物车明细对象 orderItem = createOrderItem(item, num); cart.getOrderItemList().add(orderItem); }else { // 5.2 如果明细列表中存在,则增加对应的数量 orderItem.setNum(orderItem.getNum()+num); // 更改数量 orderItem.setTotalFee( new BigDecimal(orderItem.getPrice().doubleValue()*orderItem.getNum()) ); // 更改价格 if(orderItem.getNum()<1) cart.getOrderItemList().remove(orderItem); // 当明细的数量小于1时移除 if (cart.getOrderItemList().size()<1) cartList.remove(cart); // 当购物车的明细项数为0时,移除购物车列表该对象 } } return cartList;}/** * 根据商家ID在购物车列表中查询该商家的购物车 * @param cartList * @param sellerId * @return */private Cart searchCartBySellerId(List<Cart> cartList, String sellerId) { for(Cart cart :cartList) { if (sellerId.equals(cart.getSellerId())) { return cart; } } return null;}// 创建新的购物明细对象private TbOrderItem createOrderItem(TbItem item, Integer num) { if(num<1) throw new RuntimeException("非法数量"); TbOrderItem order = new TbOrderItem(); order.setGoodsId(item.getGoodsId()); order.setItemId(item.getId()); order.setNum(num); order.setPicPath(item.getImage()); order.setPrice(item.getPrice()); order.setSellerId(item.getSellerId()); order.setTitle(item.getTitle()); order.setTotalFee(new BigDecimal( item.getPrice().doubleValue()*num )); return order;}/** * 在购物车明细列表中,根据SKUID查询购物车明细对象 * @param orderItemList * @param itemId * @return */private TbOrderItem searchOrderItemByItemId(List<TbOrderItem> orderItemList, Long itemId) { for(TbOrderItem orderItem : orderItemList) { if (orderItem.getItemId().longValue()==itemId.longValue()) { return orderItem; } } return null;} 控制层: 12345678910111213141516171819202122232425262728293031323334353637/** * 购物车列表 * * @param request * @return */@RequestMapping("/findCartList")public List<Cart> findCartList() { String cartListString = util.CookieUtil.getCookieValue(request, "cartList", "UTF-8"); if (cartListString == null || cartListString.equals("")) { cartListString = "[]"; } List<Cart> cartList_cookie = JSON.parseArray(cartListString, Cart.class); return cartList_cookie;}/** * 添加商品到购物车 * * @param request * @param response * @param itemId * @param num * @return */@RequestMapping("/addGoodsToCartList")public Result addGoodsToCartList(Long itemId, Integer num) { try { List<Cart> cartList = findCartList();// 获取购物车列表 cartList = cartService.addGoodsToCartList(cartList, itemId, num); util.CookieUtil.setCookie(request, response, "cartList", JSON.toJSONString(cartList), 3600 * 24, "UTF-8"); return new Result(true, "添加成功"); } catch (Exception e) { e.printStackTrace(); return new Result(false, "添加失败"); }} 前端服务层:将逻辑实现放在服务层,以便重用。 1234567891011121314// 购物车明细求和this.sum=function(cartList){ var total = {totalNum:0, totalMoney:0}; for(var i=0;i<cartList.length;i++){ var cart = cartList[i]; for(var j=0;j<cart.orderItemList.length;j++){ var item=cart.orderItemList[j]; // 购物车明细 total.totalNum +=item.num; total.totalMoney += item.totalFee; } } return total;} Redis 存储购物车 判断当前用户是否登陆,如果未登录采用 Cookie 存储,如果登录则采用 Redis 存储。登录后要进行 Cookie 购物车与 Redis 购物车的合并操作,并清除 Cookie 购物车。 首先修改 spring-security 配置文件 123456789101112<!-- <http pattern="/cart/*.do" security="none"></http> --> <!-- 这种方式会导致不会经过登录认证操作,无法获取登录用户名 --><!-- entry-point-ref 入口点引用 --><http use-expressions="false" entry-point-ref="casProcessingFilterEntryPoint"> <intercept-url pattern="/cart/*.do" access="IS_AUTHENTICATED_ANONYMOUSLY"/> <intercept-url pattern="/**" access="ROLE_USER"/> <csrf disabled="true"/> <!-- custom-filter为过滤器, position 表示将过滤器放在指定的位置上,before表示放在指定位置之前 ,after表示放在指定的位置之后 --> <custom-filter ref="casAuthenticationFilter" position="CAS_FILTER" /> <custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/> <custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/> </http> 之前配置的过滤购物车拦截的方式,会导致该逻辑不会经过spring security的生命周期。那么在 CartController 中就无法通过 SecurityContextHolder.getContext().getAuthentication().getName() 的方式来获取当前登录用户名。报空指针异常。 access=”IS_AUTHENTICATED_ANONYMOUSLY” 用于设置资源可以在不登陆时可以访问。此 配 置 与 security=”none” 的 区 别 在 于 当 用 户 未 登 陆 时 获 取 登 陆 人 账 号 的 值 为 anonymousUser ,而 security=”none”的话,无论是否登陆都不能获取登录人账号的值。 服务层实现 12345678910111213@Overridepublic List<Cart> findCartListFromRedis(String username) { // System.out.println("从redis中获取购物车数据"); List<Cart> cartList = (List<Cart>) redisTemplate.boundHashOps("cartList").get(username); if(cartList == null) cartList = new ArrayList<>(); return cartList;}@Overridepublic void addCartListToRedis(String username, List<Cart> cartList) { // System.out.println("向Redis中存储购物车数据"); redisTemplate.boundHashOps("cartList").put(username, cartList);} 控制层实现,通过判断用户是否登录来选择从cookie还是redis中读取数据 123456789101112131415161718192021222324252627@RequestMapping("/findCartList")public List<Cart> findCartList() { // 获取当前登录用户名 String username = SecurityContextHolder.getContext().getAuthentication().getName(); // 从cookie中读取购物车数据 String cookieValue = util.CookieUtil.getCookieValue(request, "cartList", "UTF-8"); if (cookieValue == null || "".equals(cookieValue)) cookieValue = "[]"; List<Cart> cartList_cookie = JSON.parseArray(cookieValue, Cart.class); if (username.equals("anonymousUser")) { // 如果未登录 从cookie中读取 // System.out.println("从cookie中读取"); return cartList_cookie; }else { // 用户已登录 从redis中读取 List<Cart> cartList_redis = cartService.findCartListFromRedis(username); if (cartList_cookie.size()>0) { // 本地购物车未合并 // System.out.println("合并本地和redis购物车数据"); // 合并本地和redis购物车数据 cartList_redis = cartService.mergeCartList(cartList_cookie, cartList_redis); // 在存储到redis中 cartService.addCartListToRedis( username, cartList_redis); // 清空本地缓存购物车 util.CookieUtil.deleteCookie(request, response, "cartList"); } return cartList_redis; }} 合并购物车 当用户登录时,需要将存储在cookie中的购物车数据添加到redis服务器中。同时清空本地cookie中的数据。已达到合并的效果。 12345678910@Overridepublic List<Cart> mergeCartList(List<Cart> cartList1, List<Cart> cartList2) { if(cartList1==null && cartList2 == null) return new ArrayList<Cart>(); for(Cart cart : cartList2) { for(TbOrderItem orderItem : cart.getOrderItemList()) { cartList1 = addGoodsToCartList(cartList1, orderItem.getItemId(), orderItem.getNum()); } } return cartList1;} 循环一个 cartList 中的 orderItem 数据,然后逐个添加到另一个 cartList 中。然后控制层在 findCartList 中调用,因为用户登录之后,进入购物车页面时必定经过查询购物车数据的方法。在前面的控制层实现中已经给出代码实现。 跳板页的思想 用户添加购物车完成之后,如果需要购买那么必须先完成登录。该系统使用cas实现的单点登录。如果我们通过直接跳转到 http://localhost:9100/cas/login cas服务器地址来完成登录,这样会出现登录完成之后页面会跳转到cas服务器的登录成功的提示页面,而不是我们想要的购物车页面。 我们可以采用一种跳板页的方法来实现。点击登录然后跳转到跳板页 login.html。但是由于当前状态为未登录,该页面会被Spring Security拦截然后重定向到cas服务器的登录页,登录成功后会重新回到 login.html 页面。然后在 login.html 页面中执行 js 回跳到购物车页面即可。 1234<!-- login.html中添加跳转页面 --><script type="text/javascript"> location.href="cart.html";</script> 跨域解决方案与提交订单JS 跨域请求 通过 js 在不同的域之间进行数据传输或通信,比如用 ajax 向一个不同的域请求数据,或者通过 js 获取页面中不同域的框架中(iframe)的数据。只要协议、域名、端口有任何一个不同,都被当作是不同的域。这也是面试中经常会问到的一个问题。 商品详情页面点击提交订单,就会异步调用购物车模块的添加商品到购物车的逻辑。该过程就会用到跨域操作。如果不考虑跨域问题会出现如下问题。点击添加购物车没有响应。 跨域解决方案 CORS CORS 是一个 W3C 标准,全称是”跨域资源共享”(Cross-origin resource sharing)。CORS 需要浏览器和服务器同时支持。除了 IE10 以前的浏览器之外其它浏览器都支持。(IE并未实现W3C标准) CORS 可以解决跨越问题,允许浏览器向跨源服务器发出 XMLHttpRequest 请求。当存在跨域请求时,浏览器会自动添加附加的头信息,有时候会多一次附加请求,但是用户不会察觉。实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。 请求过程:首先浏览器向服务器发送一个预请求,服务器返回一个 Preflight Response,如果服务器同意跨域请求,那么浏览器才能继续发送跨域请求。 123// 服务器只需要配置 response 响应头信息即可response.setHeader("Access-Control-Allow-Origin", "http://localhost:9105"); // 允许跨域请求response.setHeader("Access-Control-Allow-Credentials", "true"); // 允许携带cookie (方法中如果会操作cookie的话,必须添加该配置) Access-Control-Allow-Origin 的配置表示服务器支持跨域请求的地址。此时也就是允许 http://localhost:9105 该地址的跨域请求。 另外 Spring 4.2 版本之后支持注解式跨域请求@CrossOrigin(origins="http://localhost:9105",allowCredentials="true") // spring 4.2版本以上支持注解的方式,allowCredentials="true"可以缺省 CORS 请求默认不发送 Cookie 和 HTTP 认证信息。如果要把 Cookie 发到服务器,一方面要服务器同意,指定 Access-Control-Allow-Credentials 字段。另一方面,开发者必须在 AJAX 请求中打开 withCredentials 属性。否则,即使服务器同意发送 Cookie,浏览器也不会发送(点击添加购物车,登录之后,cookie中的购物车数据不会合并到用户的购物车中)。或者,服务器要求设置 Cookie,浏览器也不会处理。 123456789101112131415// 添加到购物车$scope.addToCart=function(){ // alert('sku_id:'+ $scope.sku.id); // 执行跨域请求 $http.get("http://localhost:9107/cart/addGoodsToCartList.do?itemId="+ $scope.sku.id +"&num=" + $scope.num, {'withCredentials':true}).success( function(response){ if (response.success) { location.href="http://localhost:9107/cart.html"; // 跳转到购物车页面 }else{ alert(response.message); } } );} 结算页信息显示 收件人的的选择 点击购物车进入结算页,首先会让用户选择收货地址以及收货人信息。而收获人信息是个用户进行关联的,所以将该模块放置在user模块中,在 AddressService 中新增一个通过用户ID查询收货人的信息的方法。然后在controller层,通过SpringSecurityu获取当前登录用户的ID,然后通过服务方法从数据库中取出用户的收获信息列表。前端以列表的信息显示出来。 123456789101112131415161718192021222324// 查询用户的收货地址信息$scope.findAddress=function(){ cartService.findAddress().success( function(response){ $scope.addressList = response; // 查找默认地址 for(var i=0;i<$scope.addressList.length;i++){ if($scope.addressList[i].isDefault=='1'){ $scope.address=$scope.addressList[i];break; } } } );}// 选择地址$scope.selectAddress=function(address){ $scope.address = address;}// 该地址是否被选$scope.isSelectedAddress=function(address){ return ($scope.address == address);} 支付方式 通过 $scope.order={paymentType:'1'};中的paymentType字段来绑定页面的支付方式,1表示微信支付,2表示货到付款 。 商品清单与金额显示 cartController.js 中之前实现了获取购物车的信息。此处的商品清单也可以通过该方法,从redis中获取用户购买的信息。然后通过 ng-repeat 循环遍历显示即可。合计金额也可以通过 sum 方法计算求和。 分布式 ID 生成器 snowflake 算法。由 Twitter 推出的一款开源的分布式自增ID生成算法。 结构: 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 第一位为未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年),然后是5位datacenterId和5位workerId(10位的长度最多支持部署1024个节点),最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号) 一共加起来刚好64位,为一个Long型。(转换成字符串后长度最多19) snowflake生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和workerId作区分不同机器之间也就不会出现重复ID的情况),并且效率较高。经测试snowflake每秒能够产生26万个ID。 注:UUID 存在的问题:128位(16字节)较占内存,无法生成有序的ID。 IdWorker 生成器工具类位于 common 工程。创建一个 IdWorker 对象,然后调用 nextId() 即可生成一个全局唯一ID。在Spring工程中,我们可以通过配置的方式来构造bean。 123456<bean id="idWorker" class="util.IdWorker"> <!-- 进程 ID --> <constructor-arg index="0" value="0"></constructor-arg> <!-- 数据中心 ID --> <constructor-arg index="1" value="0"></constructor-arg></bean> 保存订单 取出 redis 中存储的用户购物车数据,生成对应的订单项,采用 snowflake 算法生成唯一的订单ID,然后根据商品的SKU信息生成多个订单项。 微信二维码支付模块二维码生成插件 qrious qrious 是一款基于 HTML5 Canvas 的纯 JS 二维码生成插件。通过 qrious.js 可以快速生成各种二维码,你可以控制二维码的尺寸颜色,还可以将生成的二维码进行 Base64 编码。 配置参数: 微信扫码支付 商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于 PC 网站支付、实体店单品或订单支付、媒体广告支付等场景。 具体开发文档:https://pay.weixin.qq.com/wiki/doc/api/index.html 主要了解Native支付的统一下单和查询订单API。 实现原理:首先引入微信支付的sdk依赖。使用 HttpClient 工具类,来模拟浏览器行为,去调用微信支付的 api 接口,向该地址提交相应的数据,然后获取结果。 由于使用微信扫码支付的申请条件比较苛刻,所以这一模块无法完成测试。提供的公共号、商户号、秘钥信息等,无法完成签名。返回的信息为签名错误。 服务接口层 pay-interface 服务实现层 pay-service 依赖 pay 接口,和common模块(将 httpclient 工具类放在公共层、以及微信支付的基本信息配置),spring dubbo 依赖以及微信的 SDK。实现生成二维码和查询订单状态的方法。 控制层 cart-web 模块依赖支付服务,生成订单之后进入支付页面,调用服务层的生成二维码的方法,得到返回的 code_url ,然后让前端 qrious 插件根据该 url 去生成二维码即可。然后后端定时调用查询订单状态的方法,每隔3秒钟去查询订单支付状态,同时每隔五分钟之后就提示二维码支付超时,然后前端就收到超时信息之后,可以将其显示出来或者重新生成二维码信息。同时如果前端页面被关闭,后端会在五分钟之后返回超时的提示。 支付日志 (1)在用户下订单时,判断如果为微信支付,就向支付日志表添加一条记录,信息包括支付总金额、订单 ID(一个支付日志对应多个订单)、用户 ID 、下单时间等信息,支付状态为 0(未支付) (2)生成的支付日志对象放入 redis 中,以用户 ID 作为 key,这样在生成支付二维码时就可以从 redis 中提取支付日志对象中的金额和订单号。 (3)当用户支付成功后,修改支付日志的支付状态为 1(已支付),并记录微信传递给我们的交易流水号。根据订单 ID(多个)修改订单的状态为 2(已付款)。同时删除缓存中的支付日志。 秒杀解决方案 秒杀的特点就是在特定的时间对限量的商品进行抢购。在该时间可能存在很高的并发请求,而造成对后端数据库的巨大压力。此时可以采用缓存机制,来避免用户直接与数据库的交互。秒杀时说有数据都存储在缓存中,只有当商品抢购完或者时间到期时才将缓存中的数据一次性存入数据库中。 模块需求: (1)商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息 (2)运营商审核秒杀申请 (3)秒杀频道首页列出正在秒杀的商品,用户点击秒杀商品图片跳转到秒杀商品详细页。 (4)商品详细页显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存。当库存为 0 或不在活动期范围内时无法秒杀。 (5)秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。 (6)当用户秒杀下单 5 分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。 秒杀频道首页 seckill 模块服务层 12345678910111213141516171819202122232425262728293031/** * 返回当前正在参与秒杀的商品 */@Overridepublic List<TbSeckillGoods> findList() { List<TbSeckillGoods> seckillGoodsList = redisTemplate.boundHashOps("seckillGoods").values(); if (seckillGoodsList==null || seckillGoodsList.size()==0) { // 当前缓存还没有数据 TbSeckillGoodsExample example = new TbSeckillGoodsExample(); Criteria criteria = example.createCriteria(); criteria.andStatusEqualTo("1"); // 已审核状态 criteria.andStockCountGreaterThan(0); // 库存量>0 criteria.andStartTimeLessThanOrEqualTo(new Date()); // 当前时间大于等于开始时间 criteria.andEndTimeGreaterThanOrEqualTo(new Date());// 当前时间晚于结束时间 seckillGoodsList = seckillGoodsMapper.selectByExample(example ); // 从数据库中读取数据 for(TbSeckillGoods seckillGood : seckillGoodsList) { // 将当前时间的秒杀商品按商品ID存入缓存 redisTemplate.boundHashOps("seckillGoods").put(seckillGood.getId(), seckillGood); } System.out.println("从数据库中读取秒杀商品并放入缓存"); }else { System.out.println("从缓存中读取秒杀商品数据"); } return seckillGoodsList;}@Overridepublic TbSeckillGoods findOneFromRedis(Long id) { return (TbSeckillGoods) redisTemplate.boundHashOps("seckillGoods").get(id);} 控制层 12345678910111213/** * 从数据库中查询当前秒杀的商品 * @return */@RequestMapping("/findList")public List<TbSeckillGoods> findList() { return seckillGoodsService.findList();}@RequestMapping("/findOneFromRedis")public TbSeckillGoods findOneFromRedis(Long id) { return seckillGoodsService.findOneFromRedis(id);} 前端服务层 123456789101112131415app.service('seckillGoodsService',function($http){ //读取列表数据绑定到表单中 this.findList=function(){ return $http.get('seckillGoods/findList.do'); } this.findOne=function(id){ return $http.get('seckillGoods/findOneFromRedis.do?id='+id); } this.submitOrder=function(seckillId){ return $http.get('seckillOrder/submitOrder.do?seckillId='+seckillId); }}); 前端控制层 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455app.controller('seckillGoodsController', function($scope, $location, $interval, seckillGoodsService){ // 读取列表数据绑定到表单中 $scope.findList=function(){ seckillGoodsService.findList().success( function(response){ $scope.list=response; } ); } //查询实体 点击秒杀首页商品的详情页时,跳转到详情页,传递商品的ID信息 $scope.findOne=function(){ seckillGoodsService.findOne($location.search()['id']).success( function(response){ $scope.entity = response; totalSecond = Math.floor((new Date($scope.entity.endTime).getTime() - (new Date().getTime()))/1000); time = $interval(function(){ // 倒计时的实现 if (totalSecond>0) { $scope.timeString = convertSecondToTime(totalSecond); --totalSecond; }else{ alert("秒杀已结束"); $interval.cancel(time); } }, 1000); } ); } convertSecondToTime=function(totalSecond){ var sec = totalSecond%60; totalSecond = Math.floor(totalSecond/60); var min = totalSecond%60; totalSecond = Math.floor(totalSecond/60); var hour = totalSecond%24; totalSecond = Math.floor(totalSecond/24); var day = totalSecond; return day==0? hour+":"+min+":"+sec : day+"天 "+hour+":"+min+":"+sec; } $scope.submitOrder=function(){ seckillGoodsService.submitOrder($scope.entity.id).success( function(response){ if (response.success) { alert("抢购成功,请在五分钟内付款"); location.href="pay.html"; }else{ alert(response.message); } } ); } }); 秒杀倒计时效果 $interval 服务用来间歇性处理某事务 格式:$interval(执行的函数,间隔的毫秒数,运行次数); 123456789time = $interval(function(){ // 倒计时的实现 if (totalSecond>0) { $scope.timeString = convertSecondToTime(totalSecond);// 格式转换 --totalSecond; }else{ alert("秒杀已结束"); $interval.cancel(time); // 退出定时任务 }}, 1000); 秒杀提交订单 服务层逻辑 1234567891011121314151617181920212223242526272829303132@Overridepublic void submitOrder(Long seckillId, String userId) { // 从缓存中查询商品 TbSeckillGoods seckillGoods = (TbSeckillGoods) redisTemplate.boundHashOps("seckillGoods").get(seckillId); // 判断商品状态 if (seckillGoods == null) { throw new RuntimeException("商品不存在"); }else if(seckillGoods.getStockCount()<=0){ throw new RuntimeException("商品已抢购一空"); } // 商品库存 -1 seckillGoods.setStockCount(seckillGoods.getStockCount()-1); if (seckillGoods.getStockCount() == 0) { // 商品被抢空 redisTemplate.boundHashOps("seckillGoods").delete(seckillId); // 删除缓存中该商品 seckillGoodsMapper.updateByPrimaryKey(seckillGoods); // 同步到数据库 }else { // 更新秒杀商品中数据 redisTemplate.boundHashOps("seckillGoods").put(seckillId, seckillGoods); } // 生成订单信息 TbSeckillOrder order = new TbSeckillOrder(); order.setId(idWorker.nextId()); // 生成订单ID order.setSeckillId(seckillId); // 秒杀商品ID order.setMoney(seckillGoods.getCostPrice()); // 秒杀价格 order.setUserId(userId); order.setSellerId(seckillGoods.getSellerId()); // 商家ID order.setCreateTime(new Date()); order.setStatus("0"); // 状态 redisTemplate.boundHashOps("seckillOrder").put(userId, order);} 控制层 123456789101112131415161718@RequestMapping("/submitOrder")public Result submitOrder(Long seckillId) { String username = SecurityContextHolder.getContext().getAuthentication().getName(); if ("anonymousUser".equals(username)) { // 如果未登录 return new Result(false, "用户未登录"); } try { seckillOrderService.submitOrder(seckillId, username); return new Result(true, "订单提交成功"); } catch (RuntimeException e) { e.printStackTrace(); return new Result(false, e.getMessage()); } catch (Exception e) { e.printStackTrace(); return new Result(false, "订单提交失败"); }} 秒杀支付 用户抢购成功之后跳转到支付页面。调用微信二维码支付接口,生成二维码,用户支付完成之后跳转到收获人地址信息填写页面。超过五分钟未付款就取消订单,恢复redis中的库存量,同时关闭微信订单,防止用户支付无效的订单。由于微信支付接口无法测试该功能预留。 SpringTask 任务调度 实现上述秒杀功能之后,我想你一定也有和我一样的疑惑,那就是秒杀的商品什么时候从数据库中实时更新了,先前实现的逻辑只有在第一次访问秒杀系统时,即判断 redis 缓存中是否存在对应的商品信息,如果不存在就从数据库中取出数据同时还需要全量添加到缓存中。但是之后就一直从缓存中取数据。 此时就可以使用 SpringTask 任务调度来实现计划任务,即在某个时间点执行某件事。实现每秒钟去增量更新redis数据库中的秒杀商品信息。 Cron 表达式格式 Cron 表达式是一个字符串,字符串以 5 或 6 个空格隔开,分为 6 或 7 个域,每一个域代表一个含义,Cron 有如下两种语法格式:(1)Seconds Minutes Hours DayofMonth Month DayofWeek Year(2)Seconds Minutes Hours DayofMonth Month DayofWeek 每一个域可出现的字符如下: Seconds: 可出现”, - * /“四个字符,有效范围为 0-59 的整数 Minutes: 可出现”, - * /“四个字符,有效范围为 0-59 的整数 Hours: 可出现”, - * /“四个字符,有效范围为 0-23 的整数 DayofMonth: 可出现”, - * / ? L W C”八个字符,有效范围为 1-31 的整数 Month: 可出现”, - * /“四个字符,有效范围为 1-12 的整数或 JAN-DEC(英文单词的前三个字母) DayofWeek: 可出现”, - * / ? L C #”四个字符,有效范围为 1-7 的整数或 SUN-SAT 两个范围。1表示星期天,2 表示星期一, 依次类推 Year: 可出现”, - * /“四个字符,有效范围为 1970-2099 年 每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是: 1234567891011121314151617181920212223242526272829303132333435363738(1)*: 表示匹配该域的任意值,假如在 Minutes 域使用, 即表示每分钟都会触发事件。(2)?: 只能用在 DayofMonth 和 DayofWeek 两个域。它也匹配域的任意值,但实际不会。因为DayofMonth 和 DayofWeek 会相互影响。例如想在每月的 20 日触发调度,不管 20 日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用,如果使用表示不管星期几都会触发,实际上并不是这样。(3)-: 表示范围,例如在 Minutes 域使用 5-20,表示从 5 分到 20 分钟每分钟触发一次(4)/: 表示起始时间开始触发,然后每隔固定时间触发一次,例如在 Minutes 域使用 5/20,则意味着 5 分钟触发一次,而 25,45 等分别触发一次.(5),: 表示列出枚举值值。例如:在 Minutes 域使用 5,20,则意味着在 5 和 20 分每分钟触发一次。(6)L: 表示最后,只能出现在 DayofWeek 和 DayofMonth 域,如果在 DayofWeek 域使用 5L,意味着在最后的一个星期四触发。(7)W: 表示有效工作日(周一到周五),只能出现在 DayofMonth 域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth 使用 5W,如果 5 号是星期六,则将在最近的工作日:星期五,即 4 号触发。如果 5 号是星期天,则在 6 号(周一)触发;如果 5 号在星期一到星期五中的一天,则就在 5 号触发。另外一点,W 的最近寻找不会跨过月份。(8)LW: 这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。(9)#: 用于确定每个月第几个星期几,只能出现在 DayofMonth 域。例如在 4#2,表示某月的第二个星期三。Cron 表达式例子:0 0 10,14,16 * * ? 每天上午 10 点,下午 2 点,4 点0 0/30 9-17 * * ? 每天上午九点到下午五点每半小时0 0 12 ? * WED 表示每个星期三中午 12 点"0 0 12 * * ?" 每天中午 12 点触发"0 15 10 ? * *" 每天上午 10:15 触发"0 15 10 * * ?" 每天上午 10:15 触发"0 15 10 * * ? *" 每天上午 10:15 触发"0 15 10 * * ? 2005" 2005 年的每天上午 10:15 触发"0 * 14 * * ?" 在每天下午 2 点到下午 2:59 期间的每 1 分钟触发"0 0/5 14 * * ?" 在每天下午 2 点到下午 2:55 期间的每 5 分钟触发"0 0/5 14,18 * * ?" 在每天下午 2 点到 2:55 期间和下午 6 点到 6:55 期间的每 5 分钟触发"0 0-5 14 * * ?" 在每天下午 2 点到下午 2:05 期间的每 1 分钟触发"0 10,44 14 ? 3 WED" 每年三月的星期三的下午 2:10 和 2:44 触发"0 15 10 ? * MON-FRI" 周一至周五的上午 10:15 触发"0 15 10 15 * ?" 每月 15 日上午 10:15 触发"0 15 10 L * ?" 每月最后一日的上午 10:15 触发"0 15 10 ? * 6L" 每月的最后一个星期五上午 10:15 触发"0 15 10 ? * 6L 2002-2005" 2002 年至 2005 年的每月的最后一个星期五上午 10:15 触发"0 15 10 ? * 6#3" 每月的第三个星期五上午 10:15 触发 表达式 表示含义 0 0 10,14,16 ? 每天上午 10 点,下午 2 点,4 点 0 0/30 9-17 ? 每天上午九点到下午五点每半小时 0 0 12 ? * WED 每个星期三中午 12 点 0 0 12 ? 每天中午 12 点触发 0 15 10 ? 每天上午 10:15 触发 0 15 10 ? 每天上午 10:15 触发 0 15 10 ? * 每天上午 10:15 触发 0 15 10 ? 2018 2018 年的每天上午 10:15 触发 0 14 * ? 在每天下午 2 点到下午 2:59 期间的每 1 分钟触发 0 0/5 14 ? 在每天下午 2 点到下午 2:55 期间的每 5 分钟触发 0 0-5 14 ? 每天下午 2 点到下午 2:05 期间的每 1 分钟触发 0 10,44 14 ? 3 WED 每年三月的星期三的下午 2:10 和 2:44 触发 0 15 10 ? * MON-FRI 周一至周五的上午 10:15 触发 0 15 10 ? * 6L 每月的最后一个星期五上午 10:15 触发 0 15 10 ? * 6L 2002-2005 2002 年至 2005 年的每月的最后一个星期五上午 10:15 触发 0 15 10 ? * 6#3 每月的第三个星期五上午 10:15 触发 秒杀商品的增量更新和过期删除 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354@Componentpublic class SeckillTask { @Autowired private RedisTemplate redisTemplate; @Autowired private TbSeckillGoodsMapper seckillGoodsMapper; /** * 定时刷新秒杀商品 */ @Scheduled(cron="0/5 * * * * ?") // 每分钟执行一次 public void refreshSeckillGoods(){ System.out.println("执行了增量更新任务调度"+new Date()); // 查询 Redis 中所有商品键集合 List ids = new ArrayList<>(redisTemplate.boundHashOps("seckillGoods").keys()); // 第一次执行为 [] // 查询正在秒杀的商品列表 TbSeckillGoodsExample example = new TbSeckillGoodsExample(); Criteria criteria = example.createCriteria(); criteria.andStatusEqualTo("1"); // 已审核状态 criteria.andStockCountGreaterThan(0); // 库存量>0 criteria.andStartTimeLessThanOrEqualTo(new Date()); // 当前时间大于等于开始时间 criteria.andEndTimeGreaterThanOrEqualTo(new Date());// 当前时间晚于结束时间 if(ids.size()>0) criteria.andIdNotIn(ids); // 排除已存在的商品,实现增量更新 List<TbSeckillGoods> seckillGoodsList = seckillGoodsMapper.selectByExample(example ); // 从数据库中读取数据 // 装入缓存 for( TbSeckillGoods seckill:seckillGoodsList ){ redisTemplate.boundHashOps("seckillGoods").put(seckill.getId(), seckill); System.out.println("添加商品:" + seckill.getId()); } System.out.println("将"+seckillGoodsList.size()+"条商品装入缓存"); } // 每秒钟在缓存中查询已过期的商品,发现过期的秒杀商品后同步到数据库,并在缓存中移除该秒杀商品 @Scheduled(cron="* * * * * ?") // 每秒钟执行一次 public void removeSeckillGoods() { System.out.println("执行了删除过期商品任务调度"+new Date()); List<TbSeckillGoods> seckillGoodsList = redisTemplate.boundHashOps("seckillGoods").values(); for( TbSeckillGoods seckillGood : seckillGoodsList) { if (seckillGood.getEndTime().getTime()<new Date().getTime()) { redisTemplate.boundHashOps("seckillGoods").delete(seckillGood.getId()); // 删除缓存数据 seckillGoodsMapper.updateByPrimaryKey(seckillGood); // 向数据库保存记录 System.out.println("移除秒杀商品:"+seckillGood.getId()); } } }} Maven Profile 项目开发中会用到很多配置文件,比如 mysql 、redis 以及其他很多的 properties 配置文件。而在我们开发和部署的时候(开发环境、测试环境、生产环境),这些配置文件往往是不同的。而如果需要我们每次去切换也是非常麻烦的。而 Maven Profile 就可以帮我们完成动态选择配置文件的工作。profile 可以让我们定义一系列的配置信息,然后指定其激活条件。 动态切换模块的端口号,比如对于 page-web 模块来说,默认端口号为9105. 123456789101112131415161718192021222324252627282930313233343536373839<!-- 配置默认属性 方式一 <properties> <port>9105</port> </properties> --> <profiles> <profile> <id>dev</id> <properties> <port>9105</port> </properties> <activation> <!-- 配置默认属性 方式二 --> <activeByDefault>true</activeByDefault> </activation> </profile> <profile> <id>pro</id> <properties> <port>9205</port> </properties> </profile> </profiles> <build> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <!-- 指定端口 --> <port>${port}</port> <!-- 请求路径 --> <path>/</path> </configuration> </plugin> </plugins> </build> 当我们执行命令 tomcat7:run -P pro 发现以 9205 端口启动,执行命令 tomcat7:run -P dev 发现以 9105 端口启动。 (-P 后边为 profile 的 id)而当我们不指定环境时,默认环境为开发环境。上述代码提供两种配置方式。 切换数据库连接配置 对于数据库操作模块(dao)在 src/main/resources/properties/目录下有 db.properties 配置文件。其中配置了连接数据库的相关配置,数据库驱动、url、用户名、密码等。 (1) 首先编写不同的环境配置文件,在资源文件夹目录下创建 filter 文件夹,添加多个不同环境下配置文件。 12345# 生成环境下的配置文件env.jdbc.driver=com.mysql.jdbc.Driverenv.jdbc.url=jdbc:mysql://localhost:3306/pinyougoudb?characterEncoding=utf-8env.jdbc.username=rootenv.jdbc.password=123456 将原来的 db.properties 文件修改为: 1234jdbc.driver=${env.jdbc.driver}jdbc.url=${env.jdbc.url}jdbc.username=${env.jdbc.username}jdbc.password=${env.jdbc.password} (2) 定义 Profile 1234567891011121314151617<profiles> <profile> <id>dev</id> <properties> <env>dev</env> </properties> <activation> <activeByDefault>true</activeByDefault> </activation> </profile> <profile> <id>pro</id> <properties> <env>pro</env> </properties> </profile></profiles> (3) 资源过滤与变量替换 123456789101112<build> <!-- 资源过滤与变量替换 --> <filters> <filter>src/main/resources/filters/db_${env}.properties</filter> </filters> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources></build> maven filter可利用指定的xxx.properties文件中对应的key=value对资源文件中的${key}进行替换,最终把你的资源文件中的 username=${key}替换成 username=value 即可完成对配置文件的动态选择。打包:对 dao 工程执行 package -P pro打包为jar文件。解压 jar 文件后可以发现 db.properties 配置中被替换为生产环境。测试运行时通过 install -P pro 执令,将生产环境添加到本地仓库中。 切换注册中心连接配置 在每一个项目中都存在对注册中心的配置,而这个 IP 地址信息在生产环境下肯定是需要进行修改的。 首先集中配置注册中心地址。 (1) 在 common 模块下的src/main/resources/properties目录中创建 dubbox.properties 配置文件并配置全局dubbox的地址:address=192.168.25.130:2181 (2) Spring 目录下创建 spring 配置文件 applicationContext-dubbox.xml 配置如下: 1234567891011121314<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath*:properties/*.properties" /> <dubbo:registry protocol="zookeeper" address="${address}"/></beans> (3) 所有的服务工程与 web 工程都要依赖 pinyougou-common 。 并删除每个工程中关于注册中心地址的配置(4) 安装 pinyougou-common 到本地仓库,然后测试运行。 MavenProfile 配置 (1) 创建 filters 文件夹,建立多个不同环境的 dubbox 配置。env.address=192.168.25.130:2181 (2) 修改 properties/dubbox.properties 文件。address=${env.address} (3) 定义 profile 1234567891011121314151617<profiles> <profile> <id>dev</id> <properties> <env>dev</env> </properties> <activation> <activeByDefault>true</activeByDefault> </activation> </profile> <profile> <id>pro</id> <properties> <env>pro</env> </properties> </profile></profiles> (4) 资源配置与变量替换 1234567891011<build> <filters> <filter>src/main/resources/filters/dubbox_${env}.properties</filter> </filters> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources></build> 遇到的问题: 所有的 web 工程在依赖 common 工程的情况下,如果不配置 <dubbo:registry > 的话会报错。 原因是 web 工程中无法依赖的 common 公共模块中的 applicationContext-dubbox.xml文件中的配置。存在两种问题: 1.该web工程为依赖 common 模块 2. 加载 spring 容器时,未添加 common 中applicationContext-dubbox.xml文件地址 1234567<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-security.xml,classpath*:spring/applicationContext*.xml</param-value></context-param><listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener> param-value 标签中如果有配置,则用逗号分隔即可。classpath*:不仅包含class路径,还包括jar文件中(class路径)进行查找。有时候会用模糊匹配的方式去配置多个配置文件。 但是如果配置文件是在jar包里,模糊匹配就找不到了。可以用逗号隔开的方式配置多个配置文件。 MongoDB 一个跨平台的,面向文档的数据库。它介于关系数据库和非关系数据库之间,是非关系数据库当中功能最丰富,最像关系数据库的产品。它支持的数据结构非常松散,是类似 JSON 的 BSON 格式,因此可以存储比较复杂的数据类型。 特性: 面向集合存储、易于存储对象类型的数据 模式自由,对象的字段没有固定的限制 支持动态查询,查询语言强大 支持完全索引 支持复制和故障恢复 使用高效的二进制数据存储,可存放图片、视频等大文件 支持多种语言 结构层次: 使用场景: 数据量较大时,数据价值不高时。比如评论系统、商品足迹信息。 其他业务功能分析 用户中心 订单中心:订单信息、商品收获、商品评价、物流信息跟踪 秒杀订单中心:同用户中心 我的收藏:商品收藏信息的管理 我的足迹:足迹信息管理 个人信息管理:信息完善、收货地址信息、密码管理、手机认证 商家后台 订单管理: 查询、发货、退货 秒杀订单管理:秒杀中的商品在redis中、已完成的秒杀商品在数据库中 运营商后台 订单管理:所有商家订单的信息管理 秒杀订单管理:查询 评价系统 数据访问层:对 mongodb 数据库的操作 服务层:逻辑实现 评价系统在工程中的调用:(1)在商品详细页显示该商品的所有评论信息(CORS 跨域)(2)用户中心 web 工程引用评价服务 可以对已收货的订单追加评价。(3)商家后台 web 工程引用评价服务 可以查看订单的评价(4)运营商后台 web 工程引用评价服务 可以查看订单的评价(5)任务服务 pinyougou-task-service 引用评价服务和搜索服务,统计每个商品的评价更新到 solr 索引库中。 商家首页 商家商品信息的显示、用户也可以直接进入商家的首页选择该商家的商品 资金结算 平台作为第三方,用户付款到平台,然后平台定时给商家结算。存在佣金的问题,平台可能按照不同商品类型收取不同比例的销售提成。]]></content>
<categories>
<category>项目学习</category>
</categories>
<tags>
<tag>品优购</tag>
<tag>电商系统</tag>
<tag>项目实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[品优购项目笔记(中)]]></title>
<url>%2F2018%2F12%2F20%2F%E5%93%81%E4%BC%98%E8%B4%AD%E9%A1%B9%E7%9B%AE%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%AD%EF%BC%89%2F</url>
<content type="text"><![CDATA[网页静态化技术 网页静态化技术和缓存技术的共同点都是为了减轻数据库的访问压力,但是具体的应用场景不同,缓存比较适合小规模的数据,而网页静态化比较适合大规模且相对变化不太频繁的数据。另外网页静态化还有利于 SEO(搜索引擎优化)。静态界面通过 Nginx 服务器部署可以达到5万的并发,而Tomcat只有几百。 Freemarker* 模板引擎,基于模板来生成文本输出。与web容器无关。 模板文件的元素 文本,直接输出的部分 注释,<#– 该内容不会输出 –> 插值,${…} 将使用数据模型中的部分来替代输出 FTL 指令,实现逻辑 生成文件 12345678910111213141516171819202122public static void main(String[] args) throws IOException, TemplateException { // 1. 创建一个配置对象 Configuration configuration = new Configuration(Configuration.getVersion()); // 2. 设置模板所在的目录 configuration.setDirectoryForTemplateLoading(new File("E:\\eclipse-workspace\\freemarkerDemo\\src\\main\\resources\\")); // 3. 设置默认字符编码 configuration.setDefaultEncoding("utf-8"); // 4. 加载模板,创建一个模板对象 Template template = configuration.getTemplate("test.ftl"); // 5. 模板的数据集模型 Map<String, String> map = new HashMap<String, String>(); map.put("name", "Mindyu"); map.put("message", "this is a freemarker demo!"); // 6. 模板输出流对象 Writer out = new FileWriter("d:\\src\\test.html"); // 7. 输出文件 template.process(map, out); // 8. 关闭输出流对象 out.close(); } FTL 指令 assgin 用于在页面上定义一个变量:<#assign info={“mobile”:”aa”,’address’:’11’} > include 用于模板文件的嵌套:<#include “head.ftl”> if 指令 条件判断语句 list 指令 对集合的遍历 (goods_index 获得索引) 123<#list goodsList as goods> ${goods_index+1} 商品名称: ${goods.name} 价格:${goods.price}<br></#list> 内建函数 (语法格式:变量+?+函数名称) ${goodsList?size} 获取集合的大小 <#assign object=text?eval> 转换 JSON 字符串为对象 ${today?date} 当前日期 (dataModel.put(“today”, new Date());) ${today?time} 当前时间 ${today?datetime} 当前日期+时间 ${today?string(“yyyy年MM月”)} 日期格式化 ${number} 数字会以每三位一个分隔符显示 123,456,789 ${number?c} 将数字转换为字符串 空值处理运算符 variable?? 判断变量是否存在,存在则返回true ${aaa!’-‘} 缺失变脸默认值,若aaa为空值则使用默认值‘-’ 运算符 算数运算符 +、-、*、/ 逻辑运算符 && || ! 比较运算符 = 、==、!=、>(gt)、<(lt)、>=(gte)、<=(lte) 商品详情页的数据显示 创建 pinyougou-page-interface 工程,创建 com.pinyougou.page.service 包,包下创建接口 ItemPageService。然后再创建服务层,来实现接口方法。pom 文件中添加 freemarker 依赖。Spring 配置文件中添加 freemarker 的bean. 1234<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer"> <property name="templateLoaderPath" value="/WEB-INF/ftl/" /> <property name="defaultEncoding" value="UTF-8" /></bean> 服务层生成静态页面的方法: 123456789101112131415161718192021222324252627282930313233343536373839404142@Overridepublic boolean genItemHtml(Long goodsId) { try { Configuration configuration = freeMarkerConfig.getConfiguration(); Template template = configuration.getTemplate("item.ftl"); // 创建数据模型 Map<Object, Object> dataModel = new HashMap<>(); // 1.商品主表信息 TbGoods goods = goodsMapper.selectByPrimaryKey(goodsId); dataModel.put("goods", goods); // 2.商品详细信息 TbGoodsDesc goodsDesc = goodsDescMapper.selectByPrimaryKey(goodsId); dataModel.put("goodsDesc", goodsDesc); // 3.读取商品分类 String itemCat1 = itemCatMapper.selectByPrimaryKey(goods.getCategory1Id()).getName(); String itemCat2 = itemCatMapper.selectByPrimaryKey(goods.getCategory2Id()).getName(); String itemCat3 = itemCatMapper.selectByPrimaryKey(goods.getCategory3Id()).getName(); dataModel.put("itemCat1", itemCat1); dataModel.put("itemCat2", itemCat2); dataModel.put("itemCat3", itemCat3); // 4.读取SKU列表信息 TbItemExample example = new TbItemExample(); Criteria criteria = example.createCriteria(); criteria.andGoodsIdEqualTo(goodsId); // 设置SPU信息 criteria.andStatusEqualTo("1"); // 存在状态 example.setOrderByClause("is_default desc"); // 按是否默认降序排序,目的是为了方便前端可以直接取出默认选项 List<TbItem> itemList = itemMapper.selectByExample(example); dataModel.put("itemList", itemList); Writer out = new FileWriter("D:\\src\\item\\"+goodsId+".html"); template.process(dataModel, out); out.close(); return true; } catch (IOException e) { e.printStackTrace(); } catch (TemplateException e) { e.printStackTrace(); } return false;} 在运营商管理后台引入依赖,因为需要在运营商审核之后生成静态页面。 freemarker 图片列表的生成(扩展属性、规格列表类似) 通过 assign指令,将字符串转换为对象格式<#assign imageList=goodsDesc.itemImages?eval />,然后在图片显示区遍历图片对象。 12345678910111213141516171819<!--默认第一个预览--><div id="preview" class="spec-preview"> <#if (imageList?size>0)> <span class="jqzoom"><img jqimg="${imageList[0].url}" src="${imageList[0].url}" width="400px" height="400px"/></span> </#if></div><!--下方的缩略图--><div class="spec-scroll"> <a class="prev">&lt;</a> <!--左右按钮--> <div class="items"> <ul> <#list imageList as item> <li><img src="${item.url}" bimg="${item.url}" onmousemove="preview(this)" /></li> </#list> </ul> </div> <a class="next">&gt;</a></div> 商品详情页-前端逻辑 静态页面的动态效果,就需要 angularjs 来实现。比如商品购买数量的点击事件对应到angularjs的变量中、规格的选择。都已变量的形式与页面进行绑定。 不同规格的标题、价格等信息都不相同(SKU信息),为了实现静态页面的效果可以在将SKU信息生成到静态页面。以变量的形式保存在前端。然后用户点击不同规格时,去匹配对应的SKU列表中的某一条数据。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162 //控制层 app.controller('itemController' ,function($scope){ $scope.specificationItems={}; // 存储用户选择的规格 // 数量加减 $scope.addNum=function(x){ $scope.num+=x; if ($scope.num<1) $scope.num=1; } // 选择规格 $scope.selectSpecification=function(key,value){ $scope.specificationItems[key]=value; searchSku(); // 查询sku } // 判断规格是否被选中 $scope.isSelected=function(key,value){ if($scope.specificationItems[key]==value){ return true; }return false; } $scope.sku={}; // 加载默认的sku信息 $scope.loadSku=function(){ $scope.sku=skuList[0]; $scope.specificationItems=JSON.parse(JSON.stringify($scope.sku.spec)); // 深克隆 } // 判断两个对象是否匹配 isEqual=function(map1,map2){ for(var k in map1){ if(map1[k]!=map2[k]){ return false; } } for(var k in map2){ if(map2[k]!=map1[k]){ return false; } } return true; } // 根据规格查询sku信息 searchSku=function(){ for(var i=0;i<skuList.length;i++){ if( isEqual($scope.specificationItems, skuList[i].spec) ){ $scope.sku=skuList[i]; return; } } $scope.sku={id:0,title:'--------',price:0}; } // 添加到购物车 $scope.addToCart=function(){ alert('sku_id:'+ $scope.sku.id); } }); 系统模块的对接 运营商管理后台在审核之后进行静态页面的生成。创建 page-web 工程,用于存储生成页面。实现前端 angular 动态逻辑和静态模板的实现。 修改搜索系统模块中的search.html。点击搜索页面的图片跳转到静态页面。 消息中间件解决方案 JMS消息中间件 消息中间件利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展进程间的通信。对于消息中间件,常见的角色大致也就有 Producer(生产者)、Consumer(消费者)。 常见产品: ActiveMQ Apache 出品,最流行的,能力强劲的开源消息总线。 RabbitMQ AMQP 协议的领导实现,支持多种场景。 ZeroMQ 史上最快的消息队列系统 Kafka 高吞吐,在一台普通的服务器上就可以达到 10W/s的吞吐速率;完全的分布式系统。适合处理海量数据。 JMS(Java 消息服务) Java 平台上有关面向消息中间件的技术规范,它便于消息系统中的 Java 应用程序进行消息交换,并且通过提供标准的产生、发送、接收消息的接口简化企业应用的开发。是一系列接口规范。 消息是 JMS 中的一种类型对象,由两部分组成:报头和消息主体。报头由路由信息以及有关该消息的元数据组成。消息主体则携带着应用程序的数据或有效负载。消息正文格式: TextMessage–一个字符串对象 MapMessage–一套名称-值对 ObjectMessage–一个序列化的 Java 对象 BytesMessage–一个字节的数据流 StreamMessage – Java 原始值的数据流 JMS 消息传递类型 点对点模式:一个生产者一个消费者,存在多个消费者时,只有一个消费者可以获取消息。(未消费的消息会存储在队列中直到被消费) 发布订阅模式:一个生产者产生消息并进行发送后,可以由多个消费者进行接收。(如果消息发送时没有消费者,那么这个消息无效,不会再被消费) 安装 下载、解压、赋权、启动服务(./activemq start)。ActiveMQ 管理页面端口8161。(用户:admin 密码:admin) 点对点模式案例 引入依赖 12345<dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-client</artifactId> <version>5.13.4</version></dependency> 消息生产者: 12345678910111213141516171819202122public static void main(String[] args) throws JMSException { // 1. 创建连接工厂 ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.25.130:61616"); // 2. 创建连接对象 Connection connection = connectionFactory.createConnection(); // 3. 启动连接 connection.start(); // 4. 获取session(会话对象) 参数1:是否启动事务 参数2:消息确认方式 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 5. 创建队列对象 Queue queue = session.createQueue("test-queue"); // 6. 创建消息生产者对象 MessageProducer producer = session.createProducer(queue); // 7. 创建消息对象(TextMessage) TextMessage message = session.createTextMessage("这是一条text消息"); // 8. 发送消息 producer.send(message); // 9. 关闭资源 producer.close(); session.close(); connection.close();} 注:创建session的第二个参数为消息确认模式:AUTO_ACKNOWLEDGE = 1 自动确认、CLIENT_ACKNOWLEDGE = 2 客户端手动确认、DUPS_OK_ACKNOWLEDGE = 3 自动批量确认、SESSION_TRANSACTED = 0 事务提交并确认。 消息消费者: 1234567891011121314151617181920212223242526272829303132public static void main(String[] args) throws JMSException, IOException { // 1. 创建连接工厂 ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.25.130:61616"); // 2. 创建连接对象 Connection connection = connectionFactory.createConnection(); // 3. 启动连接 connection.start(); // 4. 获取session(会话对象) 参数1:是否启动事务 参数2:消息确认方式 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 5. 创建队列对象 Queue queue = session.createQueue("test-queue"); // 6. 创建消息的消费者对象 MessageConsumer consumer = session.createConsumer(queue); // 7. 设置监听 consumer.setMessageListener(new MessageListener() { @Override public void onMessage(Message message) { TextMessage textMessage = (TextMessage) message; try { System.out.println(""+ textMessage.getText()); } catch (JMSException e) { e.printStackTrace(); } } }); // 8. 等待键盘输入 System.in.read(); // 9. 关闭资源 consumer.close(); session.close(); connection.close();} 发布订阅模式 只需要修改上述第五步中,创建对应的主题对象即可Topic topic = session.createTopic("test-topic"); JMS 应用 运营商后台管理模块中,商品审核之后需要导入 solr 索引库和生成静态页面。对于这种同步调用的情况存在耦合度高、后期不易维护、同步执行、导致审核过程缓慢、用户体验性不好等多种问题。我们可以采用消息中间件来进行解耦,实现运营商后端与搜索服务的零耦合。运营商执行审核后,向activeMQ 发送消息(SKU列表),搜索服务从activeMQ接收到消息执行导入操作。 然后搜索模块采用 solr 系统实现,那么我们可以采用点对点的方式实现消息服务,而静态页面生成服务,由于静态页面存储于多个服务器,并且各个服务器数据相同,需要实现服务器之间同步更新的效果,所以需要采用发布订阅的方式实现。 导入搜索系统的消息生产者实现: 解除耦合(移除itemService服务依赖) 引入activeMQ客户端依赖、spring-jms依赖。 创建jms生产者配置文件 123456789101112131415161718192021222324252627282930313233<!-- 真正可以产生Connection的ConnectionFactory,由对应的 JMS服务厂商提供--> <bean id="targetConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://192.168.25.130:61616"/> </bean> <!-- Spring用于管理真正的ConnectionFactory的ConnectionFactory --> <bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory"> <!-- 目标ConnectionFactory对应真实的可以产生JMS Connection的ConnectionFactory --> <property name="targetConnectionFactory" ref="targetConnectionFactory"/> </bean> <!-- Spring提供的JMS工具类,它可以进行消息发送、接收等 --> <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate"> <!-- 这个connectionFactory对应的是我们定义的Spring提供的那个ConnectionFactory对象 --> <property name="connectionFactory" ref="connectionFactory"/> </bean> <!--这个是队列目的地,点对点的 文本信息--> <bean id="queueSolrDestination" class="org.apache.activemq.command.ActiveMQQueue"> <constructor-arg value="pinyougou_queue_solr"/> </bean> <!--这个是队列目的地,点对点的 文本信息,删除操作--> <bean id="queueSolrDeleteDestination" class="org.apache.activemq.command.ActiveMQQueue"> <constructor-arg value="pinyougou_queue_solr_delete"/> </bean> <!--这个是订阅模式 生成页面--> <bean id="topicPageDestination" class="org.apache.activemq.command.ActiveMQTopic"> <constructor-arg value="pinyougou_topic_page"/> </bean> <!--这个是订阅模式 删除页面--> <bean id="topicPageDeleteDestination" class="org.apache.activemq.command.ActiveMQTopic"> <constructor-arg value="pinyougou_topic_page_delete"/> </bean> web.xml文件中引入该配置文件(contextConfigLocation) 代码实现,注入所用的对象服务(jmsTemplate、queueSolrDestination、queueSolrDeleteDestination) 123456789101112131415161718192021222324/********导入到索引库**********/// 得到需要的SKU列表List<TbItem> itemList = goodsService.findItemListByGoodsIdAndStatus(ids, status);// 导入到solr // itemSearchService.importItemList(itemList);final String jsonString = JSON.toJSONString(itemList); // 转换为json字符串jmsTemplate.send(queueSolrDestination, new MessageCreator() { @Override public Message createMessage(Session session) throws JMSException { return session.createTextMessage(jsonString); }});/********生成静态页面**********//*for (final Long id : ids) { itemPageService.genItemHtml(id);}*/jmsTemplate.send(topicPageDestination, new MessageCreator() { @Override public Message createMessage(Session session) throws JMSException { return session.createObjectMessage(ids); }}); 消息消费者(搜索服务) 添加 activeMQ 依赖 添加spring配置文件 applicationContext-jms-consumer.xml 12345678910111213141516171819202122232425262728293031323334<!-- 真正可以产生Connection的ConnectionFactory,由对应的 JMS服务厂商提供--> <bean id="targetConnectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://192.168.25.130:61616"/> </bean><!-- Spring用于管理真正的ConnectionFactory的ConnectionFactory --> <bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory"> <!-- 目标ConnectionFactory对应真实的可以产生JMS Connection的ConnectionFactory --> <property name="targetConnectionFactory" ref="targetConnectionFactory"/> </bean> <!--这个是队列目的地,导入到索引库--> <bean id="queueSolrDestination" class="org.apache.activemq.command.ActiveMQQueue"> <constructor-arg value="pinyougou_queue_solr"/> </bean> <!-- 消息监听容器 --><bean class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <property name="connectionFactory" ref="connectionFactory" /> <property name="destination" ref="queueSolrDestination" /> <property name="messageListener" ref="itemSearchListener" /></bean><!--这个是队列目的地,删除索引库--> <bean id="queueSolrDeleteDestination" class="org.apache.activemq.command.ActiveMQQueue"> <constructor-arg value="pinyougou_queue_solr_delete"/> </bean> <!-- 消息监听容器 --><bean class="org.springframework.jms.listener.DefaultMessageListenerContainer"> <property name="connectionFactory" ref="connectionFactory" /> <property name="destination" ref="queueSolrDeleteDestination" /> <property name="messageListener" ref="itemDeleteListener" /></bean> 消息监听类: 12345678910111213141516171819202122@Componentpublic class ItemSearchListener implements MessageListener { @Autowired private ItemSearchService itemSearchService; @Override public void onMessage(Message message) { TextMessage textMessage = (javax.jms.TextMessage) message; try { String text = textMessage.getText(); System.out.println("监听到消息:"+text); List<TbItem> itemlist = JSON.parseArray(text,TbItem.class); itemSearchService.importItemList(itemlist); System.out.println("导入到solr索引库"); } catch (JMSException e) { e.printStackTrace(); } }} 商品删除(移除solr索引库记录)类似。以及网页静态化,主要是消息模式为发布订阅模式。运营商执行商品审核后,向 activeMQ 发送消息(商品 ID集合),网页生成服务从 activeMQ 接收到消息后执行网页生成操作。 系统模块依赖关系图 存在的问题 1Exception sending context initialized event to listener instance of class org.springframework.web.context.ContextLoaderListener org.springframework.beans.factory.BeanDefinitionStoreException: Invalid bean definition with name 'dataSource' defined in URL [jar:file:/D:/Program%20Files/Maven/repository/com/pinyougou/pinyougou-dao/0.0.1-SNAPSHOT/pinyougou-dao-0.0.1-SNAPSHOT.jar!/spring/applicationContext-dao.xml]: Could not resolve placeholder 'jdbc.url' in string value "{jdbc.url}"; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'jdbc.url' in string value "{jdbc.url}" 提示找不到配置文件中的jdbc.url配置。是因为在page-service中,在生成静态页面时会用到一个页面生成路径的配置信息。然后在spring中的配置文件中设置<context:property-placeholder location="classpath:config/page.properties" /> 。但是该服务依赖dao模块,这个模块中的数据库连接池的配置信息存放在 properties/db.properties 中,然后在 dao 模块中配置了 <context:property-placeholder location="classpath*:properties/*.properties" /> 。此时 page-service 模块中的配置会覆盖该配置,就导致了无法访问 properties/db.properties 中数据库连接池的配置信息。解决方法就是使 <context:property-placeholder location="classpath*:*/*.properties" /> 包含 dao 模块中的加载配置即可。 SpringBoot 框架与短信解决方案Spring Boot入门 Spring 为企业级 Java 开发提供了一种相对简单的方法,通过依赖注入和面向切面编程,用简单的 Java 对象(Plain Old Java Object,POJO)实现了 EJB 的功能。 虽然 Spring 的组件代码是轻量级的,但它的配置却是重量级的。开始的基于XML配置,Spring2.5引入基于注解的组件扫描,3.0引入基于java的配置。主要是希望简化繁琐的配置。另外项目依赖管理也是一个难题,依赖的版本库会不会起冲突。 而Spring Boot解决了上述问题,它致力于帮助开发者更容易的创建基于 Spring 的应用程序和服务,让更多人的人更快的对 Spring 进行入门体验,为 Spring生态系统提供了一种固定的、约定优于配置风格的框架。 Spring Boot 具有的特性: 提供更快的入门体验 开箱即用,没有代码生成,也无需XML配置。也可以实现修改默认值。 提供大型项目中常见的非功能特性,如嵌入式服务器、安全、指标。 并不是Spring功能的增强,而是提供一种快速使用Spring的方式。 Spring Boot Demo 添加依赖 123456789101112<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.0.RELEASE</version></parent><dependencies> <!-- web的启动器, 通过依赖传递引入web项目所需的jar包 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies> 定义引导类 Application 1234567891011121314/*@SpringBootApplication 其实就是以下三个注解的总和@Configuration: 用于定义一个配置类@EnableAutoConfiguration :Spring Boot 会自动根据你 jar包的依赖来自动配置项目。@ComponentScan: 告诉 Spring 哪个 packages 的用注解标识的类会被 spring 自动扫描并且装入 bean容器。*/@SpringBootApplicationpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } Spring MVC 实现 hello world输出 123456789101112@RestControllerpublic class HelloWorldController { @Autowired private Environment env; // 用于获取 application.properties 配置中的属性 @RequestMapping("/info") public String info() { return "hello world. url:"+env.getProperty("url"); } } 启动引导类即可, http://localhost:8080/info 常用配置: 端口号修改(只需要在 application.properties 文件中配置 server.port) 读取配置文件信息(注入 Environment 对象,使用getProperty方法) 热部署(pom文件中添加 spring-boot-devtools 依赖即可) Spring Boot与ActiveMQ整合 使用内嵌服务 spring-boot-starter-activemq 创建消息生产者 123456789101112@RestControllerpublic class QueueController { @Autowired private JmsMessagingTemplate jmsMessagingTemplate; @RequestMapping("/send") public void sendMessage(String text) { jmsMessagingTemplate.convertAndSend("spring_boot_text", text); } } 创建消息消费者 123456789@Componentpublic class Consumer { @JmsListener(destination="spring_boot_text") // destination和消息生产者相同 public void readMessage(String text) { System.out.println("接收到消息:"+text); } } 启动服务即可。http://localhost:8088/send.do?text=aaaaaa Spring Boot内置了ActiveMQ服务。 常用配置: 注:引入外部的ActiveMQ服务spring.activemq.broker-url=tcp://192.168.25.130:61616 短信解决方案项目需求 构建一个通用的短信发送服务(独立于品优购的单独工程),接收 activeMQ 的消息(MAP类型) 消息包括手机号(mobile)、短信模板号(template_code)、签名(sign_name)、参数字符串(param )。该微服务通过短信验证码平台的API,实现验证码的发送功能。 验证码发送平台 由于阿里大于注册需要认证,比较繁琐,所以此处先不实现验证码发送模块。腾讯云的短信服务可以个人认证,但是需要域名备案,这个功能先预留,后期继续完成。 用户注册模块工程搭建 用户服务接口层 user-interface 用户服务实现层 user-service 用户中心控制层 user-web 添加web.xml 引入依赖 user接口、spring依赖 添加 Spring 配置文件 静态原型页面 注册判断短信验证码 输入手机号,用户点击“获取验证码”,向后端传递手机号。后端随机生成六位数字作为验证码,同时将其保存在redis中(手机号作为key、验证码作为value), 同时向 ActiveMQ 发送消息。然后短信监听服务接受消息然后向验证码平台发送消息。 用户点击完成注册时,后端根据手机号查询用户输入的验证码与redis中的验证码是否匹配,如果匹配那么就执行注册,向数据库添加一条用户记录,否则提示不能完成注册。 服务层: 123456789101112131415161718192021@Overridepublic void createSmsCode(String phone) { // 1.生成六位随机码 String smsCode = (long)(Math.random()*1000000)+""; System.out.println("验证码:"+smsCode); // 2.将验证码存入redis redisTemplate.boundHashOps("smscode").put(phone, smsCode); // 3.发送相应的消息给ActiveMQ // 待完成..... 将消息发送给ActiveMQ即可}@Overridepublic boolean checkSmsCode(String phone, String smsCode) { // 获取redis中的验证码 String systemCode = (String) redisTemplate.boundHashOps("smscode").get(phone); if (systemCode == null || !systemCode.equals(smsCode)) { return false; } return true;} 控制层: 12345678910111213141516171819202122232425262728293031/** * 注册用户 * @param user * @return */@RequestMapping("/add")public Result add(@RequestBody TbUser user,String smsCode){ // 用户注册前进行校验(用户输入的验证码和redis中的验证码进行比较) if (!userService.checkSmsCode(user.getPhone(), smsCode)) { return new Result(false, "验证码有误"); } try { userService.add(user); return new Result(true, "注册成功"); } catch (Exception e) { e.printStackTrace(); return new Result(false, "注册失败"); }} /** * 生成验证码 * @param phone */@RequestMapping("/createSmsCode")public Result createSmsCode(String phone) { if (PhoneFormatCheckUtils.isPhoneLegal(phone)) { userService.createSmsCode(phone); return new Result(true, "验证码发送成功"); } return new Result(false, "验证码发送失败");} 前端控制层: 1234567891011121314151617181920212223242526272829 //控制层 app.controller('userController' ,function($scope,$controller,userService){ // 注册 $scope.register=function(){ // 判断两次输入密码是否一致 if ($scope.entity.password!=$scope.password) { alert("两次输入的密码不一致,请重新输入"); $scope.entity.password = ""; $scope.password = ""; return ; } // 新增 userService.add($scope.entity,$scope.smsCode).success( function(response){ alert(response.message); } ); } // 生成验证码 $scope.createSmsCode=function(){ userService.createSmsCode($scope.entity.phone).success( function(response){ alert(response.message); } ); }}); 单点登录解决方案 单点登录(Single Sign On),是目前比较流行的企业业务整合的解决方案之一。SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。对于分布式的项目,多个子系统分别部署在不同的服务器中,此时采用传统的 session 来记录用户信息是无法实现的。 CAS CAS 为 Web 应用系统提供一种可靠的单点登录方法。CAS 包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。 原理图 访问流程 访问服务:用户发送请求访问应用系统提供的服务资源(也是cas client) 定向认证:cas client 会重定向(浏览器url会变化)用户请求到 cas server 用户认证:和用户进行身份认证 发送票据:cas server生成一个ticket ,先给浏览器用户,然后浏览器将其带入到cas client端 验证票据:cas client 向 cas server 请求验证 ticket 的合法性 传输用户信息:验证通过,cas server 会将用户的信息传输给cas client cas 服务端部署 cas 服务端就是一个 war 包,解压对应的压缩包,将cas-server-webapp-4.0.0.war放入tomcat的webapps下,启动tomcat完成解压。 常用配置修改 默认用户和密码为 casuser、Mellon。可以在 cas 的 WEB-INF->deployerConfigContext.xml 12345678<bean id="primaryAuthenticationHandler" class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler"> <property name="users"> <map> <entry key="casuser" value="Mellon"/> <entry key="admin" value="admin"/> </map> </property> </bean> 端口号修改:修改tomcat的默认端口(conf/server.xml),然后 cas 的 WEB-INF/cas.properties 修改server.name=http://localhost:9100 单点退出然后跳转到目标页面 cas 的 WEB-INF/cas-servlet.xml 123<bean id="logoutAction" class="org.jasig.cas.web.flow.LogoutAction"p:servicesManager-ref="servicesManager"p:followServiceRedirects="${cas.logout.followServiceRedirects:true}"/> 去除https认证,cas 默认使用的是 https 协议,该协议需要申请 SSL 证书。一般在开发测试阶段可以使用http协议即可。 修改 cas 的 WEB-INF/deployerConfigContext.xml,增加 p:requireSecure=”false” 123<!-- Required for proxy ticket mechanism. --><bean id="proxyAuthenticationHandler" class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" p:requireSecure="false"/> 修改 cas 的/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml 12345<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" p:cookieSecure="false" p:cookieMaxAge="3600" p:cookieName="CASTGC" p:cookiePath="/cas" /> 修改 cas 的 WEB-INF/spring-configuration/warnCookieGenerator.xml 12345<bean id="warnCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" p:cookieSecure="false" p:cookieMaxAge="3600" p:cookieName="CASPRIVACY" p:cookiePath="/cas" /> 注:参数 p:cookieSecure=”true”,TRUE 为采用 HTTPS 验证,FALSE 为不采用 https 验证。参数 p:cookieMaxAge=”-1”,是 COOKIE 的最大生命周期,-1 为无生命周期,即只在当前打开的窗口有效,关闭或重新打开其它窗口,仍会要求验证。可以根据需要修改为大于 0 的数字,比如 3600 等,意思是在 3600 秒内,打开任意窗口,都不需要验证。 CAS 客户端Demo 创建 casclient_demo1 工程(war) 引入cas client依赖。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051<dependencies> <!-- cas --> <dependency> <groupId>org.jasig.cas.client</groupId> <artifactId>cas-client-core</artifactId> <version>3.3.3</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency></dependencies><build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.6</version> <configuration> <webResources> <resource> <directory>src/main/webapp/WEB-INF</directory> <filtering>true</filtering> <targetPath>WEB-INF</targetPath> </resource> </webResources> </configuration> </plugin> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <configuration> <!-- 指定端口 --> <port>9001</port> <!-- 请求路径 --> <path>/</path> </configuration> </plugin> </plugins></build> 添加 web.xml 配置 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374<!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置 --><listener> <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class></listener><!-- 该过滤器用于实现单点登出功能,可选配置。 --><filter> <filter-name>CAS Single Sign Out Filter</filter-name> <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class></filter><filter-mapping> <filter-name>CAS Single Sign Out Filter</filter-name> <url-pattern>/*</url-pattern></filter-mapping><!-- 该过滤器负责用户的认证工作,必须启用它 --><filter> <filter-name>CASFilter</filter-name> <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class> <init-param> <param-name>casServerLoginUrl</param-name> <param-value>http://localhost:9100/cas/login</param-value> <!--这里的 server 是服务端的 IP --> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://localhost:9001</param-value> </init-param></filter><filter-mapping> <filter-name>CASFilter</filter-name> <url-pattern>/*</url-pattern></filter-mapping><!-- 该过滤器负责对 Ticket 的校验工作,必须启用它 --><filter> <filter-name>CAS Validation Filter</filter-name> <filter-class> org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter </filter-class> <init-param> <param-name>casServerUrlPrefix</param-name> <param-value>http://localhost:9100/cas</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://localhost:9001</param-value> </init-param></filter><filter-mapping> <filter-name>CAS Validation Filter</filter-name> <url-pattern>/*</url-pattern></filter-mapping><!-- 该过滤器负责实现 HttpServletRequest 请求的包裹, 比如允许开发者通过 HttpServletRequest 的 getRemoteUser() 方法获得 SSO 登录用户的登录名,可选配置。 --><filter> <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name> <filter-class> org.jasig.cas.client.util.HttpServletRequestWrapperFilter </filter-class></filter><filter-mapping> <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name> <url-pattern>/*</url-pattern></filter-mapping><!-- 该过滤器使得开发者可以通过 org.jasig.cas.client.util.AssertionHolder 来获取用户的登录名。 比如 AssertionHolder.getAssertion().getPrincipal().getName()。 --><filter> <filter-name>CAS Assertion Thread Local Filter</filter-name> <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class></filter><filter-mapping> <filter-name>CAS Assertion Thread Local Filter</filter-name> <url-pattern>/*</url-pattern></filter-mapping> 主页面 index.jsp <%=request.getRemoteUser()%> 获取远程登录用户名 然后再创建客户端工程2。启动cas服务端和cas客户端,然后http://localhost:9001 和9002端口,都会跳转到cas的登录页面。实现单点登录。单点退出只需访问 http://localhost:9100/cas/logout即可。 CAS 服务端数据源设置 使用项目中 user 表中的用户信息来实现登录验证。 修改 cas 服务端的 WEB-INF/deployerConfigContext.xml 123456789101112131415161718192021222324<!-- 数据源 --><bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" p:driverClass="com.mysql.jdbc.Driver" p:jdbcUrl="jdbc:mysql://127.0.0.1:3306/pinyougoudb?characterEncoding=utf8" p:user="root" p:password="123456" /><!-- 默认密码解码方式 --><bean id="passwordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" c:encodingAlgorithm="MD5" p:characterEncoding="UTF-8" /><bean id="dbAuthHandler" class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler" p:dataSource-ref="dataSource" p:sql="select password from tb_user where username = ?" p:passwordEncoder-ref="passwordEncoder"/><!----------------------另外配置认证管理器-------------------><constructor-arg> <map> <entry key-ref="proxyAuthenticationHandler" value-ref="proxyPrincipalResolver" /> <!-- 默认的认证处理方式 <entry key-ref="primaryAuthenticationHandler" value-ref="primaryPrincipalResolver" /> --> <entry key-ref="dbAuthHandler" value-ref="primaryPrincipalResolver" /> </map> </constructor-arg> 配置了数据库连接池相关信息,那么就需要把数据库相应的jar包引入 CAS 服务端界面改造 cas server 服务端提供了默认的登录界面,那我们如何修改为我们自己需要的登录页面了。步骤如下: 将 login.html 拷贝到 cas 系统下的 WEB-INF\view\jsp\default\ui 目录下 将 css、js、img 等静态资源文件夹拷贝到 cas 目录下。web 工程的根目录 将原来的 casLoginView.jsp 改名(以做参照模板),将 login.html 改名为 casLoginView.jsp 添加 jsp 指令 修改 form 标签,保留原页面样式 修改用户名输入框,保留原页面样式 修改密码框,保留源页面样式 修改登录按钮,保留原页面的样式 错误提示<form:errors path="*" id="msg" cssClass="errors" element="div" htmlEscape="false" /> 注:错误提示信息默认为英文,使用了国际化标准。在 cas 的 WEB-INF\classes 中的 messages_zh_CN.properties 文件中添加配置。 12authenticationFailure.AccountNotFoundException=用户名或密码错误authenticationFailure.FailedLoginException=用户名或密码错误 第一个是用户名不存在时的错误提示 第二个是密码错误的提示 修改 cas-servlet.xml,设置国际化为 zn_CN(默认为 en ) 。 12<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"p:defaultLocale="zh_CN" /> 国际化:i18n。英文为:internationalization 。18表示中间的字符数。 用户中心实现单点登录(cas client与Spring Security集成) 引入 springSecurity、cas 客户端和 springSecurity Cas 整合包依赖 123456789101112131415161718192021222324<!-- spring-security配置 --><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId></dependency><!-- spring-security-cas --><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId></dependency><dependency> <groupId>org.jasig.cas.client</groupId> <artifactId>cas-client-core</artifactId> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>log4j-over-slf4j</artifactId> </exclusion> </exclusions></dependency> web.xml 添加 spring-security 过滤器,设置首页 1234567891011121314151617181920<welcome-file-list> <welcome-file>home-index.html</welcome-file></welcome-file-list><!-- 省略post乱码过滤器 --><context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-security.xml</param-value></context-param><listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class></filter><filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern></filter-mapping> 构建 UserDetailsServiceImpl 认证类,实现UserDetailsService接口 123456789101112public class UserDetailServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { System.out.println("经过认证类:"+username); Collection<GrantedAuthority> authorities = new ArrayList<>(); // 角色固定了,如果存在多种角色的话,那么此处可能会去数据库中查找来实现动态设置用户角色 authorities.add(new SimpleGrantedAuthority("ROLE_USER")); return new User(username, "", authorities); }} 添加 spring-security.xml 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081<?xml version="1.0" encoding="UTF-8"?><beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <!-- 匿名访问资源 --> <http pattern="/css/**" security="none"></http> <http pattern="/js/**" security="none"></http> <http pattern="/img/**" security="none"></http> <http pattern="/plugins/**" security="none"></http> <!-- 注册登陆 --> <http pattern="/register.html" security="none"></http> <http pattern="/user/add.do" security="none"></http> <http pattern="/user/createSmsCode.do" security="none"></http> <!-- entry-point-ref 入口点引用 --> <http use-expressions="false" entry-point-ref="casProcessingFilterEntryPoint"> <intercept-url pattern="/**" access="ROLE_USER"/> <csrf disabled="true"/> <!-- custom-filter为过滤器, position 表示将过滤器放在指定的位置上,before表示放在指定位置之前 ,after表示放在指定的位置之后 --> <custom-filter ref="casAuthenticationFilter" position="CAS_FILTER" /> <custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/> <custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/> </http> <!-- CAS入口点 开始 --> <beans:bean id="casProcessingFilterEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint"> <!-- 单点登录服务器登录URL --> <beans:property name="loginUrl" value="http://localhost:9100/cas/login"/> <beans:property name="serviceProperties" ref="serviceProperties"/> </beans:bean> <beans:bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties"> <!--service 配置自身工程的根地址+/login/cas --> <beans:property name="service" value="http://localhost:9106/login/cas"/> </beans:bean> <!-- CAS入口点 结束 --> <!-- 认证过滤器 开始 --> <beans:bean id="casAuthenticationFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter"> <beans:property name="authenticationManager" ref="authenticationManager"/> </beans:bean> <!-- 认证管理器 --> <authentication-manager alias="authenticationManager"> <authentication-provider ref="casAuthenticationProvider"> </authentication-provider> </authentication-manager> <!-- 认证提供者 --> <beans:bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider"> <beans:property name="authenticationUserDetailsService"> <beans:bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"> <beans:constructor-arg ref="userDetailsService" /> </beans:bean> </beans:property> <beans:property name="serviceProperties" ref="serviceProperties"/> <!-- ticketValidator 为票据验证器 --> <beans:property name="ticketValidator"> <beans:bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator"> <beans:constructor-arg index="0" value="http://localhost:9100/cas"/> </beans:bean> </beans:property> <beans:property name="key" value="an_id_for_this_auth_provider_only"/> </beans:bean> <!-- 认证类 --> <beans:bean id="userDetailsService" class="com.pinyougou.user.service.UserDetailServiceImpl"/> <!-- 认证过滤器 结束 --> <!-- 单点登出 开始 --> <beans:bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/> <beans:bean id="requestSingleLogoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter"> <beans:constructor-arg value="http://localhost:9100/cas/logout?service=http://localhost:9103"/> <!-- 退出登陆并跳转到首页 --> <beans:constructor-arg> <beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/> </beans:constructor-arg> <beans:property name="filterProcessesUrl" value="/logout/cas"/> <!-- 此时直接请求 logout/cas 即可实现单点退出,相当于上面链接的一个别名 --> </beans:bean> <!-- 单点登出 结束 --> </beans:beans> 获取当前登录用户名,借助Spring Security的方法。SecurityContextHolder.getContext().getAuthentication().getName();即可得到用户名信息。]]></content>
<categories>
<category>项目学习</category>
</categories>
<tags>
<tag>品优购</tag>
<tag>电商系统</tag>
<tag>项目实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[品优购项目笔记(上)]]></title>
<url>%2F2018%2F12%2F19%2F%E5%93%81%E4%BC%98%E8%B4%AD%E9%A1%B9%E7%9B%AE%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%8A%EF%BC%89%2F</url>
<content type="text"><![CDATA[品牌管理模块功能实现 运用AngularJS前端框架的常用指令 完成品牌管理的列表功能 完成品牌管理的分页列表功能 完成品牌管理的增加功能 完成品牌管理的修改功能 完成品牌管理的删除功能 完成品牌管理的条件查询功能 前端框架 AngularJS四大特征 MVC 模式 Model: 数据,其实就是angular变量($scope.XX); View: 数据的呈现,Html+Directive(指令); Controller: 操作数据,就是function,数据的增删改查; 双向绑定框架采用并扩展了传统HTML,通过双向的数据绑定来适应动态内容,双向的数据绑定允许模型和视图之间的自动同步。遵循声明式编程应该用于构建用户界面以及编写软件构建,而指令式编程非常适合来表示业务逻辑的理念。 依赖注入对象在创建的时候,其依赖对象由框架来自动创建并注入进来。即最少知道法则。 模块化设计 高内聚低耦合法则1)官方提供的模块 ng、ngRoute、ngAnimate2)用户自定义的模块 angular.module(‘模块名’,[ ]) 常见指令 ng-app 定义 AngularJS 应用程序的根元素,表示以下的指令 angularJS 都会识别,且在页面加载完时会自动初始化。 ng-model 指令用于绑定变量,将用户在文本框输入的内容绑定到变量上,而表达式可以实时地输出变量。 ng-init 对变量初始化或调用某方法。 ng-controller 用于指定所使用的控制器,在控制器中定义函数和变量,通过scope 对象来访问。 ng-click 单击事件指令,点击时触发控制器的某个方法。 ng-if 判断语句,条件不存在就不执行。 ng-repeat 指令用于循环集合变量。 $index 用于获取 ng-repeat 指令循环中的索引。 $http 内置服务,用于访问后端数据。 $location 服务,用于获取链接地址中的参数值。$location.search()['id']id对应的值。(注:地址中 ? 前需要添加 # ) eg: http://localhost:9102/admin/goods_edit.html#?id=149187842867969 ng-bind-html 指令用于显示 html 内容 app.filter 过滤器,通过 | 来调用过滤器 $sce 服务 严格控制上下文访问,为防止 跨站XSS。该服务可以实现安全控制,比如允许html标签的插入转换。 复选框的使用 定义一个用于存储选中 ID 的数组,当我们点击复选框后判断是选择还是取消选择,如果是选择就加到数组中,如果是取消选择就从数组中移除。在后续点击删除按钮时需要用到这个存储了 ID 的数组。 12345678910// 存储当前选中复选框的id集合$scope.selectIds = [];$scope.updateSelection = function($event, id){ if ($event.target.checked) { // 当前为勾选状态 $scope.selectIds.push(id); // 向selectIds集合中添加元素 } else { var index = $scope.selectIds.indexOf(id); $scope.selectIds.splice(index, 1); // 参数1:移除的下标位置,参数2:需要移除的元素个数 }} 规格及模板管理前端分层开发 运用 MVC 的思想,将 js 和 html 代码分离,提高程序的可维护性。 实现方式:自定义服务,同后端的 service 层,封装一些操作,比如请求后端数据。在不同控制器通过依赖注入相关服务,即可调用服务的方法。将代码分为前端页面、前端服务层、前端控制层。 主键回填 修改 Mapper.xml 文件 123<selectKey resultType="java.lang.Long" order="AFTER" keyProperty="id"> SELECT LAST_INSERT_ID() AS id</selectKey> 对于规格与具体规格选项,可以创建一个组合实体类,包括 规格 和 规格选项的集合。在插入规格之后,通过主键回填,获取规格 ID ,然后将 ID 作为外键添加到规格选项中去。 select2 组件-多选下拉列表 引入 select 2 相关的 js 和 css。 设置数据源 1$scope.brandList={data:[{id:1,text:'联想'},{id:2,text:'华为'},{id:3,text:'小米'}]}; // 品牌列表 实现多选下拉框 1<input select2 select2-model="entity.brandIds" config="brandList" multiple placeholder=" 选择品牌(可多选)" class="form-control" type="text"/> multiple 表示可多选 Config 用于配置数据来源 select2-model 用于指定用户选择后提交的变量 模板列表显示 将从后台获取的 json 字符串中的某个属性的值提取出来,用逗号分隔,更直观的显示。 12345678910// 提取 json 字符串数据中某个属性,返回拼接字符串逗号分隔$scope.jsonToString = function(jsonString,key){ var json=JSON.parse(jsonString); // 将 json 字符串转换为 json 对象 var value=""; for(var i=0;i<json.length;i++){ if(i>0) value += ","; value += json[i][key]; } return value;} Spring Security 安全框架 为基于 Spring 的企业应用系统提供声明式的安全访问控制的解决方案。提供一组可以在 Spring 应用上下文中配置的 Bean。 使用步骤 引入 jar 包 12345678910<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>4.1.0.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>4.1.0.RELEASE</version></dependency> web.xml 文件中引入 spring-security.xml 配置文件 123456789101112131415161718<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-security.xml</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> spring-security.xml 配置文件设置页面拦截规则、认证管理器以及不拦截的资源(静态资源、登陆页面) 1234567891011121314151617181920212223242526272829<!-- 设置页面不登陆也可以访问 --><http pattern="/*.html" security="none"></http><http pattern="/css/**" security="none"></http><http pattern="/img/**" security="none"></http><http pattern="/js/**" security="none"></http><http pattern="/plugins/**" security="none"></http><!-- 页面的拦截规则 use-expressions:是否启动SPEL表达式 默认是true --><http use-expressions="false"> <!-- 当前用户必须有ROLE_USER的角色 才可以访问根目录及所属子目录的资源 --> <intercept-url pattern="/**" access="ROLE_ADMIN"/> <!-- 开启表单登陆功能 --> <form-login login-page="/login.html" default-target-url="/admin/index.html" authentication-failure-url="/login.html" always-use-default-target="true"/> <csrf disabled="true"/> <headers> <frame-options policy="SAMEORIGIN"/> </headers> <logout/> <!-- 退出登录 --></http><!-- 认证管理器 --><authentication-manager> <authentication-provider> <user-service> <user name="admin" password="123456" authorities="ROLE_ADMIN"/> <user name="yang" password="123456" authorities="ROLE_ADMIN"/> </user-service> </authentication-provider> </authentication-manager> CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。 XSS(跨站脚本攻击)利用站点内的信任用户,往Web页面里插入恶意Script代码 。 CSRF通过伪装来自受信任用户的请求来利用受信任的网站。 商家系统登录安全控制安全控制 自定义认证类,创建类 UserDetailsServiceImpl.java 实现 UserDetailsService 接口 实现类中添加 SellerService 属性、和 setter 注入方法,修改 loadUserByUserName 方法。 配置 spring-security.xml。认证管理器中 authentication-provider 引用userDetailService 的bean,同时通过 dobbo 去依赖一个 sellerService 对象。 BCrypt 加密算法 用户表的密码通常使用 MD5 等不可逆算法加密后存储,为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的 salt(盐值)加密。 特定字符串是程序代码中固定的,salt 是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。 BCrypt 算法将 salt 随机并混入最终加密后的密码,验证时也无需单独提供之前的 salt,从而无需单独处理 salt 问题。 123456789101112131415161718192021222324252627282930/** * 认证类 * @author YCQ * */public class UserDetailsServiceImpl implements UserDetailsService{ private SellerService sellerService; public void setSellerService(SellerService sellerService) { // 通过配置的方式添加 this.sellerService = sellerService; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// System.out.println("执行 UserDetailsServiceImpl 认证"); // 构建角色列表 List<GrantedAuthority> grantAuths = new ArrayList<>(); grantAuths.add(new SimpleGrantedAuthority("ROLE_SELLER")); TbSeller seller = sellerService.findOne(username); if (seller!=null && "1".equals(seller.getStatus())) { return new User(username, seller.getPassword(), grantAuths); }else { return null; } }} spring-security 配置 123456789101112131415161718<!-- 认证管理器 --><authentication-manager> <authentication-provider user-service-ref="userDetailService"> <password-encoder ref="bcryptEncoder"></password-encoder> </authentication-provider> </authentication-manager><!-- 认证类 --><beans:bean id="userDetailService" class="com.pinyougou.service.UserDetailsServiceImpl"> <beans:property name="sellerService" ref="mSellerService"></beans:property></beans:bean><!-- 引用dubbo 服务 --><dubbo:application name="pinyougou-shop-web" /><dubbo:registry address="zookeeper://107.191.52.91:2181"/><dubbo:reference id="mSellerService" interface="com.pinyougou.sellergoods.service.SellerService"></dubbo:reference><beans:bean id="bcryptEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"></beans:bean> 注:浏览器控制台提示 [DOM] Input elements should have autocomplete attributes (suggested: “current-password”) 为浏览器表单默认的记忆功能,可以在 input 标签中添加 autocomplete=”off|on” 即可。 商品分类管理多级分类列表 将商品分类分为三级,进入页面首先显示所有一级分类(主分类),点击查询下级,可查看当前主分类下的次分类,再次点击进入三级分类。三级分类为最后一级,列表中不显示查询下级按钮,同时更新面包屑导航。直接点击面包屑导航,可以实现直接层级跳转。 面包屑导航 123456789101112131415161718// 当前面包屑等级$scope.grade = 1;$scope.setGrade=function(value){ $scope.grade = value;}$scope.selectList=function(p_entity){ if ($scope.grade == 1) { $scope.entity_1 = null; $scope.entity_2 = null; } else if ($scope.grade == 2){ $scope.entity_1 = p_entity; $scope.entity_2 = null; } else { $scope.entity_2 = p_entity; } $scope.findByParentId(p_entity.id);} 页面配置 123<li><a href="#" ng-click="grade=1;selectList({id:0})">顶级分类列表</a></li><li><a href="#" ng-click="grade=2;selectList(entity_1)">{{entity_1.name}}</a></li><li ng-if="entity_2!=null"><a href="#" ng-click="grade=3;selectList(entity_2)">{{entity_2.name}}</a></li> 修改商品分类 实现类型模板的下拉框,采用 select2 组件实现。 123<td>类型模板</td><td><input select2 ng-model="entity.typeId" config="itemList" placeholder="商品类型模板" class="form-control" type="text"/></td> config 为数据来源 ng-model 绑定类型对象数据 itemList 的来源:itemCatController 中 findItemList() 方法 -> typeTemplateService 的 selectOptionList() 方法 -> 请求后端 /typeTemplate/selectOptionList -> TypeTemplateService 服务层 -> TypeTemplateMapper 层方法 删除商品分类 判断所选分类下是否存在子分类,存在则不能删除。 123456789101112131415161718192021222324/** * 批量删除 * @param ids * @return */@RequestMapping("/delete")public Result delete(Long[] ids){ try { // 判断当前所有分类是否存在子分类 boolean flag = false; // 不存在 for (Long id : ids) { if(itemCatService.findByParentId(id)!=null && itemCatService.findByParentId(id).size()!=0){ flag = true;break; } } if (flag) return new Result(false, "当前所选分类存在子分类,切勿删除"); itemCatService.delete(ids); return new Result(true, "删除成功"); } catch (Exception e) { e.printStackTrace(); return new Result(false, "删除失败"); }} SPU 与 SKU SPU (标准产品单位)为商品信息聚合的最小单位是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。属性相同、特性相同的商品为一个SPU。 SKU (库存量单位) 为物理上不可分割的最小存货单元。不同的规格、颜色、款式为不同的SKU。 分布式文件服务器 FastDFS FastDFS 是用 c 语言编写的一款开源的分布式文件系统。FastDFS 为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS 很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。 FastDFS 架构包括 Tracker server 和 Storage server。 Tracker server (追踪服务器、调度服务器)作用为负载均衡和调度。 Storage server (存储服务器)作用为文件存储。 客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。 服务端角色: Tracker : 管理集群,tracker也可以实现集群,每一个节点地位平等,一种备份的机制。tracker负责收集 storage 集群的存储状态。 Stroage :实际保存文件。分为多个组,组内文件相同,起到备份作用。组间文件不同,起到分布式存储。 商品分类级联刷新 通过 Angular JS 变量监控方法,实现选择一级分类之后,初始化二级分类的列表信息。 123456789101112// angularjs变量监控方法,查询二级分类信息$scope.$watch('entity.goods.category1Id',function(newValue, oldValue){ if (newValue != undefined && newValue != "") { // alert("category1Id"+newValue); itemCatService.findByParentId(newValue).success( function(response){ $scope.itemCat2List = response; $scope.entity.goods.category2Id = ""; } ); }}); 商品录入【SKU商品信息】 对于同一个产品分为多种不同的规格组合。根据选择的规格录入商品的 SKU 信息,当用户选择相应的规格,下面的 SKU 列表就会自动生成。 实现思路:(1)我们先定义一个初始的不带规格名称的集合,只有一条记录。(2)循环用选择的规格,根据规格名称和已选择的规格选项对原集合进行扩充,添加规格名称和值,新增的记录数与选择的规格选项个数相同 12345678910111213141516171819202122232425262728293031323334// 创建SKU列表$scope.creatItemList=function(){ // 列表初始化,规格对象、价格、库存量、状态、是否默认 $scope.entity.itemList = [ {spec:{},price:0,num:9999,status:'0',isDefault:'0'} ]; var items = $scope.entity.goodsDesc.specificationItems; for (var i = 0; i < items.length; i++) { $scope.entity.itemList = addColumn($scope.entity.itemList, items[i].attributeName, items[i].attributeValue); }}/** * $scope.entity.itemList: * [{"spec":{"网络":"移动3G","机身内存":"16G"},"price":0,"num":9999,"status":"0","isDefault":"0"}, * {"spec":{"网络":"移动3G","机身内存":"32G"},"price":0,"num":9999,"status":"0","isDefault":"0"}, * {"spec":{"网络":"联通3G","机身内存":"16G"},"price":0,"num":9999,"status":"0","isDefault":"0"}, * {"spec":{"网络":"联通3G","机身内存":"32G"},"price":0,"num":9999,"status":"0","isDefault":"0"}] */// 深克隆方法 原集合、列名、列值addColumn=function(list, columnName, columnValues){ var newList = []; for (var i = 0; i < list.length; i++) { var oldRow = list[i]; for (var j = 0; j < columnValues.length; j++) { var newRow = JSON.parse( JSON.stringify(oldRow) ); newRow.spec[columnName] = columnValues[j]; newList.push(newRow); } } return newList;} 商家后台列表显示 状态显示: 商品信息表(goods)中状态子段为 audit_status 。存储的为数字,0表示未审核、1表示已审核、2表示审核未通过、3为已关闭。从后台获取的状态值,直接在前端进行修改。通过一个status数组存储: $scope.status=[‘未审核’,’已审核’,’审核未通过’,’关闭’];//商品状态 然后列表中显示为 。 分类信息显示: 商品分为三级分类。存储于 tb_item_cat 表中。包括 id、父级id、分类名称、对应绑定的类型id。但是为了避免商品查询时重复的关联查询,可以采用现将所有分类信息读取到本地,然后在前端进行分类id到分类名称的转换操作。 1234567891011$scope.itemCatList = [];// 全部商品分类查询,存储在itemList数组中,然后再前端页面通过数组下标直接将商品分类ID转换为商品分类名称,避免后端连接查询。$scope.findItemList = function(){ itemCatService.findAll().success( function(response){ for (var i = 0; i < response.length; i++) { $scope.itemCatList[response[i].id] = response[i].name; } } );} 将分类结果 response 对象封装为数组类型,数组下标为商品分类id,数组值为商品分类的名称。然后在列表项中通过 将id转换为名称。 存在的问题 pinyougou-shop-web 模块中分页插件提示 ClassNotFoundException。但是页面可以访问。 12345<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>${pagehelper.version}</version></dependency> 如上配置之后,又出现下图错误,导致商品列表无法显示。(但是 manager-web 模块中也没有引入pagehelper,但是没有出现问题) 商品删除 逻辑删除,通过修改数据库表中的 is_delete 字段为1,然后过滤掉商品。然后查询时,在 findPage() 方法中添加 criteria.andIsDeleteIsNull() 条件。 注解式事务配置 创建 applicationContext-tx.xml 配置文件 1234567<!-- 事务管理器 --><bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /></bean><!-- 开启事务控制的注解支持 --><tx:annotation-driven transaction-manager="transactionManager"/> 然后在方法或服务实现类上添加 @Transactional 注解。 网站前台广告服务 设计为广告分类表(id、name)与广告内容表(id、categoryId、title、url、pic、status、order)。广告有首页轮播广告、今日推荐、各品类楼层广告等分类。 Redis 缓存数据库用于解决高访问量对后端数据库造成的很大的访问压力。(另一种解决方案为网页静态化) Spring Data Redis 提供了在 srping 应用中通过简单的配置访问 redis 服务,对 reids 底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate 提供了 redis 各种操作、异常处理及序列化,支持发布订阅,并对 spring 3.1 cache 进行了实现。 spring-data-redis 针对 jedis 提供了如下功能: 1.连接池自动管理,提供了一个高度封装的“RedisTemplate”类。 2.针对 jedis 客户端中大量 api 进行了归类封装,将同一类型操作封装为 operation 接口 操作样例: key-value 键值对操作 插入:redisTemplate.boundValueOps(“name”).set(“mindyu”); 读取:redisTemplate.boundValueOps(“name”).get(); 删除:redisTemplate.delete(“name”); Set 类型操作(无序集合) 插入:redisTemplate.boundSetOps(“nameset”).add(“曹操”); 读取:redisTemplate.boundSetOps(“nameset”).members(); 删除:redisTemplate.boundSetOps(“nameset”).remove(“曹操”); // 单一元素 redisTemplate.delete(“name”); // 整个集合 List 集合 (有序) rightPush() 、leftPush()、读取:range(0,10)、index(1)、remove(1, “value”) // 1 表示删除数据的个数 Hash 类型 put(“key”,”value”)、读取所有键:keys()、读取所有值:values()、get(“key”)、delete(“key”) 使用 Redis 缓存时,需要注意,当数据修改时需要清除缓存数据,使其达到一致性约束。必须修改广告时,如果修改了该广告所属的分类,那么需要同时清除原分类以及新分类的缓存信息。 出现的问题: 首页在加载广告模块时,出现 “Failed to load resource: net::ERR_BLOCKED_BY_CLIENT” 错误,是因为谷歌浏览器的广告插件,导致无法加载该图片。 搜索解决方案简介 Solr 是一个开源搜索平台,用于构建搜索应用程序。 它建立在 Lucene(全文搜索引擎)之上。 Solr 是企业级的,快速的和高度可扩展的。 使用 Solr 构建的应用程序非常复杂,可提供高性能。Solr 是一个可扩展的,可部署,搜索/存储引擎,优化搜索大量以文本为中心的数据。 安装及配置 安装 Tomcat,解压缩。 解压 solr。 把 solr 下的 dist 目录 solr-4.10.3.war 部署到 webapps 下(去掉版本号,方便访问)。 启动 Tomcat 解压缩 war 包 把solr下 example/lib/ext 目录下的所有的扩展 jar 包,添加到 solr 的工程中(\WEB-INF\lib目录下)。 创建一个 solrhome 。solr 下的 /example/solr 目录就是一个 solrhome。复制此目录到 D 盘改名为 solrhome 关联 solr 及 solrhome。需要修改 solr 工程的 web.xml 文件。 12345<env-entry> <env-entry-name>solr/home</env-entry-name> <env-entry-value>d:\solrhome</env-entry-value> <env-entry-type>java.lang.String</env-entry-type></env-entry> 启动 Tomcat 。访问 http://localhost:8080/solr 即可 中文分析器 IK Analyzer IK Analyzer 是一个开源的,基于 java 语言开发的轻量级的中文分词工具包。 配置 把 IKAnalyzer2012FF_u1.jar 添加到 solr 工程的 lib 目录下 solr 工程下创建 WEB-INF/classes 文件夹,用于存放扩展词典、停用词词典、配置文件。 修改 Solrhome 中的 schema.xml 文件,配置一个 FieldType,使用 IKAnalyzer 123<fieldType name="text_ik" class="solr.TextField"> <analyzer class="org.wltea.analyzer.lucene.IKAnalyzer"/></fieldType> 配置域 域相当于数据库的表字段,用户存放数据,用户可根据业务需要去定义相关的 Field(域),一般来说,每一种域对应着一种数据,用户对同一种数据进行相同的操作。域的常用属性: name 域的名称 type 域的类型 indexed 是否索引 stored 是否存储 required 是否必须 multiValued 是否多值 复制域: 将某一个域中的数据复制到另一个域中。比如商品查询时,同样一个关键字可能是品牌、商品标题、商品分类、商家名称等多种可能。此时就需要复制域。 动态域: 对于字段名称不固定的情况下,用于动态扩充字段。比如商品的规格的值不是固定的(不同商品可能存在不同的规格项)。 出现的错误 前端可以从后台获取数据( itemsearch/search.do正常获取数据 ),但是控制台显示” TypeError: Cannot read property ‘success’ of undefined “错误。 原因是因为: 12345app.service('searchService', function($http){ this.search=function(searchMap){ return $http.post('itemsearch/search.do',searchMap); }}); angularjs 服务层的search方法并未 return。 12. 服务启动超时:com.alibaba.dubbo.remoting.TimeoutException: Waiting server-side response timeout. start time: 2018-12-02 08:35:41.093, end time: 2018-12-02 08:35:46.094, client elapsed: 0 ms, server elapsed: 5001 ms, timeout: 5000 ms。 网站前台 portal-web 模块出现的原因是因为没有启动 redis 服务器。然后前台广告数据获取不到。 搜索模块 search-web :就很奇怪,dubbox 服务正常、solr 服务正常。昨天晚上还是正常的,上午纠结了半天,然后不知道为啥突然又好了。。。 烦躁 批量数据导入 solr 系统 将商品数据导入到 solr 系统。 创建 solr-util (jar),引入 dao 模块以及 spring 相关依赖。 创建spring 的配置文件,添加包扫描。 <context:component-scan base-package=”com.pinyougou.solrutil”></context:component-scan> 依赖 pojo 模块,为实体类添加 @Field 注解。 pojo 中引入 spring-data-solr 依赖(会自动引入其所依赖solr包)动态域中@Dynamic 注解是该包提供的 添加 solr.xml 配置文件与 spring 目录中。 123456<!-- solr 服务器地址 --><solr:solr-server id="solrServer" url="http://127.0.0.1:8080/solr" /><!-- solr 模板,使用 solr 模板可对索引库进行 CRUD 的操作 --><bean id="solrTemplate" class="org.springframework.data.solr.core.SolrTemplate"> <constructor-arg ref="solrServer" /><bean> 通过 spring 注入 SolrTemplate 模板类对象。 使用 SolrTemplate 对象执行相应的方法。 关键字搜索模块 通过注入 SolrTemplate 对象,使用该对象实现关键字搜索。 123456789101112131415161718192021@Service(timeout=5000) // 超时5S,默认是1Spublic class ItemSearchServiceImpl implements ItemSearchService{ @Autowired private SolrTemplate solrTemplate; @Override public Map search(Map searchMap) { Map map = new HashMap(); Query query = new SimpleQuery("*:*"); Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords")); query.addCriteria(criteria); ScoredPage<TbItem> page = solrTemplate.queryForPage(query, TbItem.class); map.put("rows", page.getContent()); // page.getContent() 返回一个 List 集合 return map; }} 搜索结果高亮显示 将搜索关键字在搜索结果中,高亮显示出来。实现原理也就是在关键字前后添加html标签:关键字 后端实现代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748@Overridepublic Map search(Map searchMap) { Map map = new HashMap(); /* Query query = new SimpleQuery("*:*"); Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords")); query.addCriteria(criteria); ScoredPage<TbItem> page = solrTemplate.queryForPage(query, TbItem.class); map.put("rows", page.getContent()); // page.getContent() 返回一个 List 集合 */ // 高亮显示 HighlightQuery query = new SimpleHighlightQuery(); // 构建高亮选项 HighlightOptions highlightOptions = new HighlightOptions().addField("item_title"); // 高亮域(可以为多个) highlightOptions.setSimplePrefix("<em style='color:red'>"); // 前缀 highlightOptions.setSimplePostfix("</em>"); // 后缀 query.setHighlightOptions(highlightOptions); // 为查询设置高亮查询 Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords")); query.addCriteria(criteria); HighlightPage<TbItem> page = solrTemplate.queryForHighlightPage(query, TbItem.class); // 高亮入口集合(每条高亮结果的入口) List<HighlightEntry<TbItem>> entryList = page.getHighlighted(); for (HighlightEntry<TbItem> entry : entryList) { // 获取高亮列表(高亮域的个数) List<Highlight> hightLightList = entry.getHighlights(); /* for (Highlight highLight : hightLightList) { // 每个域可能存在多值(复制域) List<String> sns = highLight.getSnipplets(); System.out.println(sns); }*/ if (entry.getHighlights().size()>0 && entry.getHighlights().get(0).getSnipplets().size()>0) { TbItem item = entry.getEntity(); item.setTitle(entry.getHighlights().get(0).getSnipplets().get(0)); // 用高亮标签结果替换 } } map.put("rows", page.getContent()); return map;} 前端实现: angularJS 会将后端插入的html标签原样输出,而不会去解析。这是防止html攻击采取的一种安全策略。可以使用 $sce 服务的 trustAsHtml 方法来实现转换。 12345678// 定义过滤器app.filter('trustHtml', ['$sce', function($sce){ return function(data){ // 传入参数时,被过滤的内容 return $sce.trustAsHtml(data); // 返回的是过滤后的内容(信任html的转换) } } ]); 然后在页面通过 <div class="attr" ng-bind-html="item.title | trustHtml"></div>来调用转换方法。 搜索业务规则搜索模块 用户输入搜索关键字,显示列表结果和商品分类信息。因为一个关键字可能对应多种商品分类 根据第一个商品分类,默认查询该分类的模板ID,然后根据模板ID查询品牌列表和规格列表 当用户点击某一个商品分类时,则显示该分类对应商品结果,同时根据该分类的模板ID查询对应的品牌列表和规格列表 当用户点击商品品牌列表时,筛选出当前所选的品牌商品信息 当用户点击商品规格列表时,筛选出当前所选的规格所对应的商品信息 用户点击价格区间时,商品信息根据价格进行过滤 用户点击搜索面板上的条件时,隐藏该条件 系统搜索量很大,所以需要将搜索信息放置到 Redis 缓存数据库中。 缓存商品分类信息 12345678private void saveToRedis() { // 将模板ID放入缓存 分类名称作为key,模板ID作为value List<TbItemCat> itemCatList = findAll(); for (TbItemCat itemCat : itemCatList) { redisTemplate.boundHashOps("itemCat").put(itemCat.getName(), itemCat.getTypeId()); } System.out.println("将模板ID放入缓存");} 缓存所有的品牌信息和规格信息 12345678910111213141516private void saveToRedis() { List<TbTypeTemplate> typeTempList = findAll(); for(TbTypeTemplate template : typeTempList) { Long id = template.getId(); // 将模板ID作为key 品牌列表作为value List brandList = JSON.parseArray(template.getBrandIds(), Map.class); // {id:1,text:联想} redisTemplate.boundHashOps("brandList").put(id, brandList); // 将模板ID作为key 规格列表作为value List<Map> specList = findSpecList(id); redisTemplate.boundHashOps("specList").put(id, specList); } System.out.println("完成品牌列表、规格列表缓存");} 分类列表查询(spring data solr 条件查询) 12345678910111213141516171819202122232425private List<String> searchCategoryList(Map searchMap) { List<String> list = new ArrayList<>(); Query query = new SimpleQuery("*:*"); // 根据关键字查询 Criteria criteria = new Criteria("item_keywords").is(searchMap.get("keywords")); // where ... query.addCriteria(criteria); // 设置分组选项 GroupOptions groupOptions = new GroupOptions().addGroupByField("item_category"); // group by ....(可以有多个分组域) query.setGroupOptions(groupOptions); // 获取分组页 GroupPage<TbItem> queryForGroupPage = solrTemplate.queryForGroupPage(query, TbItem.class); // 获取分组结果对象 GroupResult<TbItem> groupResult = queryForGroupPage.getGroupResult("item_category"); // 获取分组入口页 Page<GroupEntry<TbItem>> groupEntries = groupResult.getGroupEntries(); // 遍历获取每个对象的值 for(GroupEntry<TbItem> entry : groupEntries) { list.add(entry.getGroupValue()); } return list;} 过滤条件的构建 当点击搜索面板的分类、品牌和规格时,实现查询条件的构建。查询 条件以面包屑的形式显示。当面包屑显示分类、品牌和规格时,要同时隐藏搜索面板对应的区域。点击面包屑查询条件的撤销链接时,重新显示搜索面板相应的区域。 面包屑其实就是显示搜索对象。可将搜索对象定义为$scope.searchMap={'keywords':'','category':'','brand':'',spec:{}};。然后实现添加查询条件和取消查询条件。 1234567891011121314151617181920212223242526// 搜索$scope.search = function() { searchService.search($scope.searchMap).success(function(response) { $scope.resultMap = response; // 搜索返回的结果 });}// 添加查询搜索项$scope.addSearchItem=function(key,value){ if (key == 'brand' || key == 'category') { // 如果点击品牌和分类 $scope.searchMap[key] = value; }else{ $scope.searchMap.spec[key]=value; } $scope.search();}// 取消查询条件$scope.removeSearchItem=function(key){ if (key == 'brand' || key == 'category') { // 如果点击品牌和分类 $scope.searchMap[key] = ""; }else{ delete $scope.searchMap.spec[key]; } $scope.search();} 价格区间筛选 点击搜索面板的价格区间,实现按价格筛选相应的商品。和上述的过滤条件类似,前端依然将价格区间以字符串的形式放入到 searchMap 集合中(如 ‘price’:’500-1000’)。然后后端通过字符串的截取获得相应的价格区间,然后进而筛选。 自定义搜索结果分页 前端将当前页数和页大小通过 searchMap 传给后端,然后后端通过构建 solr 的 query 对象实现分页效果。然后返回当前页数据和总页数以及总记录数。$scope.searchMap={'keywords':'','category':'','brand':'','spec':{},'price':'','pageNo':1,'pageSize':40 };//搜索条件封装对象 通过当前页数、总页数然后构建分页标签。 12345678910111213141516171819202122232425262728// 构建分页标签buildPageLable=function(){ $scope.pageLable=[]; var firstPage = 1; // 开始页码 var lastPage = $scope.resultMap.totalPages; // 截止页码 $scope.firstDot = true; $scope.lastDot = true; if (lastPage > 5){ if ($scope.searchMap.pageNo<=3){ // 当前页码小于3,显示前五页 lastPage = 5; $scope.firstDot = false; }else if ($scope.searchMap.pageNo>=lastPage-2) { // 当前页码大于总页数-2,则显示后5页 firstPage = lastPage - 4; $scope.lastDot = false; }else { firstPage = $scope.searchMap.pageNo - 2; lastPage = $scope.searchMap.pageNo + 2; } }else{ $scope.firstDot = false; $scope.lastDot = false; } for (var i = firstPage; i <= lastPage; i++) { $scope.pageLable.push(i); }} 多关键字搜索 在搜索时,分词器首先会将我们输入的关键字进行分词,然后对每个分词都会去搜索对应的结果,然后求得并集。比如搜索“三星手机”时,会将“三星”的搜索集合和“手机”搜索结构都返回给我们。这样做可以显示更多数据,让用户有更多的选择。同时会根据搜索的关键字匹配度进行排序。 此时注意:当搜索关键字有空格时,中文分词无法进行分词,那么就会导致搜索出来的结果较少或者没有。然后可以采用在后端去掉关键字中所有的空格。原来如此,我平时搜索的时候经常喜欢敲空格,以为这样多个条件就能更精准的搜索我想要的额,套路套路。 搜索数据排序 根据综合、价格升降序、新品的来实现排序。前端传递两个参数,分别为待排序的字段名称和排序方式(升序or降序)。 123456789101112// 1.7 排序String sortValue = (String) searchMap.get("sort"); // 升序 or 降序String sortFiled = (String) searchMap.get("sortFiled"); // 升序字段if (!"".equals(sortValue) && !"".equals(sortFiled)) { if (sortValue.equals("ASC")) { // 升序 Sort sort = new Sort(Sort.Direction.ASC, "item_"+sortFiled); query.addSort(sort); }else if(sortValue.equals("DESC")) { Sort sort = new Sort(Sort.Direction.DESC, "item_"+sortFiled); query.addSort(sort); }} 销量和评价的排序(待完成): 增加域 item_salecount 用于存储每一个 SKU 的销量信息,然后定时更新每一个 SKU 的销量数据(固定时间,比如一个月,否则会导致新上架的商品无法排在前列),同时每天定时更新一次销量数据。 隐藏品牌列表 当用户搜索的关键字包含品牌时隐藏品牌列表。也就是判断搜索关键字中是否存在返回的品牌列表中的信息。这个过程中发现,搜索关键字 searchMap.keywords 和输入框进行了绑定。那么当我们修改输入框的时候,可能就会影响品牌列表的显示。 此处将 search 重载,添加一个带 keywords的方法。然后搜索框就不和搜索关键字进行绑定,而是以传递参数的形式赋值给 searchMap。 首页和搜索页对接 在首页输入框中输入关键字,然后跳转到搜索页面,查询对应关键字的数据。 首页通过 链接的形式传递参数location.href="http://localhost:9104/search.html#?keywords="+$scope.keywords; 然后搜索模块使用 $location 服务接受参数。 123456// 引入 $location 服务// 接受首页跳转$scope.loadKeywords=function(){ $scope.searchMap.keywords = $location.search()['keywords']; $scope.search();} 索引库的增量更新 实现在商品审核之后将数据更新到 solr 索引库,在商品删除的时候删除 solr 索引库中相应的记录。(增量更新) 商品审核是对商品表(SPU信息)进行操作,但是索引库中存储的是SKU信息,所以首先需要通过商品的 SPU 信息查询该商品对应的 SKU 信息,然后将查询到的集合提交给 solrTemplate。删除可以直接根据 goodsId 集合进行条件删除。]]></content>
<categories>
<category>项目学习</category>
</categories>
<tags>
<tag>品优购</tag>
<tag>电商系统</tag>
<tag>项目实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[品优购项目学习笔记]]></title>
<url>%2F2018%2F12%2F18%2F%E5%93%81%E4%BC%98%E8%B4%AD%E9%A1%B9%E7%9B%AE%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[ 一个综合性的 B2B2C 的电商网站系统。网站采用商家入驻的模式,商家入驻平台提交申请,有平台进行资质审核,审核通过后,商家拥有独立的管理后台录入商品信息。商品经过平台审核后即可发布。 Github地址 项目简介系统模块 网站前台 运营商平台 商家管理平台 框架组合 前端 angularJS + Bootstrap 后端 Spring + SpringMVC + mybatis + Dubbox 系统架构 面向服务的架构(SOA架构)。控制层与服务层分离,通过网络调用。 模块关联关系图 项目环境搭建Dubbox框架 致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。远程服务调用的分布式框架。 原理图 节点角色说明: Provider: 暴露服务的服务提供方。 Consumer: 调用远程服务的服务消费方。 Registry: 服务注册与发现的注册中心。 Monitor: 统计服务的调用次调和调用时间的监控中心。 Container: 服务运行容器。 Dubbox 本地 jar 包部署与安装 : Dubbox 并不在 maven 中央仓库,需安装到本地仓库。将 dubbo-2.8.4.jar 包放到 d:\setup, 然后输入命令 mvn install:install-file -Dfile=d:\setup\dubbo-2.8.4.jar -DgroupId=com.alibaba -DartifactId=dubbo -Dversion=2.8.4 -Dpackaging=jar 即可。 管理中心的部署 开发过程中需要知道注册了哪些服务以便测试与管理。通过部署一个管理中心来实现。其实管理中心就是一个web应用,部署到tomcat即可。 编译 dubbox 源码,dubbox-master.zip 文件中的 dubbox-master 目录下,执行 mvn package -Dmaven.skip.test=true 。即可在 target 目录下看到 dubbo-admin-2.8.4.war 。将 war 包放置到服务器的 webapps 下。 如果你部署在zookeeper同一台主机并且端口是默认的2181,则无需修改任何配置。如果不是在一台主机上或端口被修改,需要修改 WEB-INF 下的 dubbo.properties ,修改如下配置:dubbo.registry.address=<zookeeper://127.0.0.1:2181> 修改后重新启动tomcat。 http://虚拟机ip:8080/dubbo-admin 用户名 root 密码 注册中心 Zookeeper 上传 zookeeper 安装包,解压缩,创建 data 目录,修改 zoo.cfg 配置文件的 dataDir 配置 启动命令 安装目录 /root/zookeeper-3.4.6/ 依赖配置 克隆项目之后,使用 Eclipse 打开,导入Maven项目,此时会报找不到 dubbox 和 fastDFS 依赖包。而这两个依赖包不在 Maven 中央仓库中,所以需要我们手动将这两个包导入本地依赖库。 1234567mvn install:install-file -Dfile=D:\src\dubbo-2.8.4.jar -DgroupId=com.alibaba -DartifactId=dubbo -Dversion=2.8.4 -Dpackaging=jar// 然后配置离线约束:XML Cataloghttp://code.alibabatech.com/schema/dubbo/dubbo.xsdmvn install:install-file -DgroupId=org.csource.fastdfs -DartifactId=fastdfs -Dversion=1.2 -Dpackaging=jar -Dfile=d:\src\fastdfs_client_v1.20.jar 以上配置包存放在 https://github.com/Mindyu/pinyougou 下的 src 目录中。另外如果本地仓库安装依赖包之后依然报错,那么可能是 Eclipse 的 Maven 配置路径没有修改为本地的仓库路径。 项目技术要点索引 前端框架 AngularJS select2 组件使用 Spring Security BCrypt 加密算法 分布式文件服务器 FastDFS Spring Data Solr 搜索解决方案 网页静态化技术 Freemarker 消息中间件解决方案 JMS Spring Boot 框架 Spring Boot 与 ActiveMQ 整合 短信解决方案 单点登录解决方案 Cookie + Redis 购物车 跨域解决方案 分布式 ID 生成器 二维码生成插件 qrious 秒杀解决方案 SpringTask 任务调度、Cron表达式、Maven Profile]]></content>
<categories>
<category>项目学习</category>
</categories>
<tags>
<tag>品优购</tag>
<tag>电商系统</tag>
<tag>项目实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redis 缓存数据库笔记]]></title>
<url>%2F2018%2F12%2F08%2FRedis%20%E7%BC%93%E5%AD%98%E6%95%B0%E6%8D%AE%E5%BA%93%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[Redis 数据结构 string(字符串) list(双向链表) dict(hash表) set(集合) zset(排序set) 事务实现原理事务阶段 事务开始 命令入队 事务执行 原理 将客户端的请求命令放入到事务队列中,然后向客户端响应一个 QUEUE ,服务器并不会立即执行这些命令。只有命令:EXEC,DISCARD,WATCH,MULTI命令会被立即执行,其它命令服务器不会立即执行 命令 MULTI 启动事务的标识 EXEC 执行事务 WATCH 在 EXEC 命令执行之前,监视任意数量的数据库键,并在执行 EXEC 命令时判断是否至少有一个被 watch 的键值被修改 DISCARD 清空客户端的事务队列里的所有命令,并取消客户端的事务标记,取消所有 WATCH。 Redis 持久化RDB持久化 将Reids在内存中的数据库记录定时dump到磁盘。指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是 fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。 优势: 整个 Redis 数据库将只包含一个文件,可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。 性能最大化,使用子线程去执行持久化工作。 数据集很大,RDB的启动效率会更高。 缺陷: 无法保证数据的高可用性,即最大限度的避免数据丢失 由于 RDB 是通过 fork 子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。 AOF(append only file)持久化 将Reids的操作日志以追加的方式写入文件。以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。 优势: 可以带来更高的数据安全性,即数据持久性。拥有3种同步策略,即每秒同步、每修改同步和不同步。 该机制对日志文件的写入操作采用的是 append 模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。 AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。该日志文件可用于重建。 缺点: 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。 Redis 热点数据 redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。 数据淘汰策略 volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰 volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰 volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰 allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 no-enviction(驱逐):禁止驱逐数据 热点数据的应用 MySQL 中存在2000万条数据,redis 中只存20万数据。限定 Redis 占用的内存(20W 数据大约占用的内存),Redis 会根据自身数据淘汰策略(LRU),加载热数据到内存。 缓存系统面临的问题缓存一致性问题 缓存系统与底层数据的一致性。这点在底层系统是“可读可写”时,写得尤为重要 有继承关系的缓存之间的一致性。为了尽量提高缓存命中率,缓存也是分层:全局缓存,二级缓存。他们是存在继承关系的。全局缓存可以有二级缓存来组成。 多个缓存副本之间的一致性。为了保证系统的高可用性,缓存系统背后往往会接两套存储系统(如memcache,redis等) 缓存穿透 一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的 value ,就应该去后端系统查找(比如DB)。如果 key 对应的 value 是一定不存在的,并且对该 key 并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。 如何避免 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 了之后就清理缓存。 对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该 bitmap 过滤。 缓存雪崩 当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。 如何避免 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。 不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀。 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。 缓存数据的淘汰 缓存淘汰的策略有两种: (1) 定时去清理过期的缓存。 (2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂。 预估失效时间 版本号(必须单调递增,时间戳是最好的选择) 提供手动清理缓存的接口。 一致性 Hash 算法定义 一致性 Hash 算法将整个哈希值空间组织成一个虚拟的圆环,好处就是提高容错性和可扩展性。对于节点的增减都只需重定位环空间中的一小部分数据。 原理 一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希环如下: 整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32-1,也就是说0点左侧的第一个点代表2^32-1, 0和2^32在零点中方向重合,我们把这个由2^32个点组成的圆环称为Hash环。 各个服务器使用Hash进行一次哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置 将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器! 优势 解决分布式系统每个节点都有可能失效,并且新的节点很可能动态的增加进来的情况,保证当系统的节点数目发生变化的时候,只有小部分数据受到影响,我们的系统仍然能够对外提供良好的服务。 缺陷 Hash 环的数据倾斜问题,即:一致性 Hash 算法在服务节点很少时,容易因为节点分布不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题 可以采用虚拟节点机制。具体做法可以在服务器IP或主机名的后面增加编号来实现。同时数据定位算法不变,只是多了一步虚拟节点到实际节点的映射 Redis、Memcache和MongoDB的区别Redis 支持多种数据结构 支持持久化操作 RDB、AOF 支持简单的事务需求,不支持回滚 单线程请求 Memcached 只支持简单的key/value数据结构 无法进行持久化,数据不能备份,只能用于缓存使用,且重启后数据全部丢失 应用场景:用于在动态系统中减少数据库负载,提升性能;做缓存,提高性能(适合读多写少,对于数据量比较大,可以采用sharding) mongoDB 文档性的数据库(存放xml、json、bson类型系那个的数据) 存放 json 格式数据 持久化:延时批量(groupcommit)提交写入 数据分析的功能(mapreduce) 适合场景:事件记录、内容管理或者博客平台,比如评论系统 NoSQL NoSQL是非关系型数据库,NoSQL = Not Only SQL。 关系型数据库采用的结构化的数据,NoSQL 采用的是键值对的方式存储数据。 在处理非结构化/半结构化的大数据时;在水平方向上进行扩展时;随时应对动态增加的数据项时可以优先考虑使用NoSQL数据库。 在考虑数据库的成熟度、支持、分析和商业智能、管理及专业性等问题时,应优先考虑关系型数据库。 参考博客: Redis 事务 https://blog.csdn.net/bugall/article/details/52386698 Redis、Memcache和MongoDB的区别 https://www.cnblogs.com/tuyile006/p/6382062.html]]></content>
<categories>
<category>Redis 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>Redis</tag>
<tag>NoSQL</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HTTP 笔记]]></title>
<url>%2F2018%2F12%2F05%2FHTTP%20%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[Forward 和 Redirect 的区别直接转发方式(Forward、转发) 服务器内部的重定向,服务器直接访问目标地址的 url 网址,把里面的东西读取出来,但是客户端并不知道,因此用 forward 的话,客户端浏览器的网址是不会发生变化的。 关于 request:由于在整个定向的过程中用的是同一个 request,因此 forward 会将 request 的信息带到被重定向的 jsp 或者 servlet 中使用。 间接转发方式(Redirect、重定向) 客户端的重定向,是完全的跳转。即服务器返回的一个 url 给客户端浏览器,然后客户端浏览器会重新发送一次请求,到新的 url 里面,因此浏览器中显示的url 网址会发生变化。 因为这种方式比 forward 多了一次网络请求,因此效率会低于 forward。 HTTP 与 HTTPsHTTP HTTP是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。 HTTP 存在的问题 使用明文进行通信,内容容易被监听 不验证通信方的身份,通信方可能遭伪装 无法证明报文的完整性,报文可能被篡改 HTTPS HTTPS是以安全为目标的HTTP通道,简单讲是HTTP的安全版。HTTP 先和SSL(Secure Sockets Layer安全套接层) 通信,再由SSL 和TCP 通信。使用隧道进行通信,使其具备加密、认证和完整性保护。 加密 对称密钥加密 加密和解密使用同一个密钥 优点:运算速度快 缺点:密钥容易被获取 非对称密钥加密(公开密钥加密) 使用一对密钥进行加密和解密,分别为公开密钥和私有密钥。 优点:更为安全 缺点:运算速度慢 混合加密方式 (HTTPS采用的加密方式) 使用公开密钥进行加密用于传输对称密钥,之后使用对称密钥加密通信。 认证 通过证书来对通信方进行认证 完整性 SSL 提供报文摘要功能来验证完整性 HTTPS 协议的主要作用可以分为两种 一种是建立一个信息安全通道,来保证数据传输的安全 一种就是确认网站的真实性。 区别 https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。 http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。 http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。 http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。 HTTP 方法种类(8种) GET:请求指定的页面信息,并返回实体主体。 HEAD:类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头 POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。 PUT:从客户端向服务器传送的数据取代指定的文档的内容。 DELETE:请求服务器删除指定的页面。 CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。 OPTIONS:允许客户端查看服务器的性能。 TRACE:回显服务器收到的请求,主要用于测试或诊断。 GET 与 POST 区别 GET执行一次与执行多次效果是一样的。服务器状态是一样的。幂等性 GET在浏览器回退时是无害的,而POST会再次提交请求。 GET产生的URL地址可以被收藏为书签,而POST不可以。 GET请求会被浏览器主动cache,而POST不会,除非手动设置。 GET请求只能进行url编码,而POST支持多种编码方式。 GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。 GET请求在URL中传送的参数是有长度限制的,而POST没有。 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。 GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。 GET参数通过URL传递,POST放在Request body中。 GET产生一个TCP数据包;POST产生两个TCP数据包。 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据); 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。 HTTP 状态码 1 信息,服务器收到请求,需要请求者继续执行操作 2 成功,操作被成功接收并处理 201 Created 已创建。成功请求并创建了新的资源 202 Accepted 已接受。已经接受请求,但未处理完成 204 No Content 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 205 Reset Content 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域 206 Partial Content 部分内容。服务器成功处理了部分GET请求 3 重定向,需要进一步的操作以完成请求 4 客户端错误,请求包含语法错误或无法完成请求 401 Unauthorized 请求要求用户的身份认证 403 Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求 404 Not Found 服务器无法根据客户端的请求找到资源(网页) 5** 服务器内部错误,服务器在处理请求的过程中发生了错误 501 Not Implemented 服务器不支持请求的功能,无法完成请求 502 Bad Gateway 充当网关或代理的服务器,从远端服务器接收到了一个无效的请求 HTTP 头部 通常HTTP消息包括客户机向服务器的请求消息和服务器向客户机的响应消息。这两种类型的消息由一个起始行,一个或者多个头域,一个只是头域结束的空行和可选的消息体组成。HTTP的头域包括通用头,请求头,响应头和实体头四个部分。 通用头域 Cache-Control 指定请求和响应遵循的缓存机制 Connection 连接状态 (长连接 or 短连接) Date 消息发送的时间 Pragma 包含实现特定的指令 Transfer-Encoding、Upgrade、Via 请求头 Method Request-URI HTTP-Version 请求方法 OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE 请求 URL HTTP 版本 eg: HTTP/1.1 响应头 HTTP-Version Status-Code Reason-Phrase HTTP -Version 表示支持的HTTP版本,例如为HTTP/1.1。 Status- Code 是一个三个数字的状态代码。 Reason-Phrase 给Status-Code提供一个简单的文本描述。 实体消息 Content-Type 实体头用于向接收方指示实体的介质类型 Content-Length 表示实际传送的字节数 Allow 实体头至服务器支持哪些请求方法(如GET、POST等) Content-Range 表示传送的范围 Content-Encoding 指文档的编码(Encode)方法 Cookie与Session区别 Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中。 Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息。 流程 每次HTTP请求的时候,客户端都会发送相应的 Cookie 信息到服务端。实际上大多数的应用都是用 Cookie 来实现 Session 跟踪的,第一次创建 Session 的时候,服务端会在 HTTP 协议中告诉客户端,需要在 Cookie 里面记录一个 Session ID,以后每次请求把这个会话 ID 发送到服务器,我就知道你是谁了。 Cookie的内容主要包括:名字,值,过期时间,路径和域 Cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session。 常见问题 如果客户端浏览器禁用了Cookie怎么办? URL重写的技术来进行会话跟踪,客户端每次请求时,在URL中添加一个诸如 sid=xxxxx 这样的参数 表单隐藏字段。服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器 HTTP状态码 http://www.runoob.com/http/http-status-codes.html]]></content>
<categories>
<category>HTTP 学习</category>
</categories>
<tags>
<tag>HTTP</tag>
<tag>笔记整理</tag>
<tag>面经</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 异常与反射]]></title>
<url>%2F2018%2F11%2F30%2FJava%20%E5%BC%82%E5%B8%B8%E4%B8%8E%E5%8F%8D%E5%B0%84%2F</url>
<content type="text"><![CDATA[Throwable Java异常的基类 Error: 用于标记严重错误。合理的应用程序不应该去 try/catch 这种错误。绝大多数的错误都是非正常的,就根本不该出现的。常见的有OOM、StackOverflowError。 Exception: 用于指示一种合理的程序想去 catch 的条件。即它仅仅是一种程序运行条件,而非严重错误,并且鼓励用户程序去 catch 它。 Error和RuntimeException 及其子类都是未检查的异常(unchecked exceptions),而所有其他的Exception类都是检查异常(checked exceptions) checked exceptions 通常是从一个可以恢复的程序中抛出来的,并且最好能够从这种异常中使程序恢复。比如 ParseException(DateFormat日期转换时)、IOException、SQLException、InterruptedException(Thread.sleep方法)、ClassNotFoundException、FileNotFoundException等。检查异常发生在编译阶段,必须要使用 try…catch(或者throws)否则编译不通过。 unchecked exceptions 通常是如果一切正常的话本不该发生的异常,但是的确发生了。发生在运行期,具有不确定性,主要是由于程序的逻辑问题所引起的。比如IndexOutOfBoundException、ClassCastException(强制类型转换)、NullPointerException、NumberFormatException(Integer.valueOf(“123a”);)、ArithmeticException(15/0)、ConcurrentModificationException(迭代时快速失败)、NoSuchMethodException等。这类错误本身就是bug,应该被修复,出现此类错误时程序就应该立即停止执行。因此,面对 Errors 和 unchecked exceptions 应该让程序自动终止执行,程序员不该做诸如 try/catch 这样的事情,而是应该查明原因,修改代码逻辑。 一个线程运行时发生异常会怎样?存在两种情形(属于运行时异常): ① 如果该异常被捕获或抛出,则程序继续运行。 ② 如果异常没有被捕获该线程将会停止执行。 Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候JVM会使用Thread.getUncaughtExceptionHandler() 来查询线程的 UncaughtExceptionHandler ,并将线程和异常作为参数传递给 handler 的 uncaughtException() 方法进行处理。 Java 反射概述在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象的方法的功能就是反射。 使用的前提条件 必须先得到代表字节码的Class对象,Class类用于表示.class文件(字节码)。 在运行期间,一个类,只有一个Class对象产生。 获取Class对象的三种方式 Object ——> getClass(); 任何数据类型(包括基本数据类型)都有一个“静态”的class属性 Student.class 通过Class类的静态方法:Class.forName(String className) (常用) JDBC加载驱动 比较: 源自Java 编程思想 Student.class 只是加载类,不会进行初始化操作。 Class.forName(String className) 会加载类,同时链接、初始化(类加载流程) 使用规范获取构造方法 批量的方法: public Constructor[] getConstructors():所有”公有的”构造方法 public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有) 获取单个的方法,并调用: public Constructor getConstructor(Class… parameterTypes): 获取单个的”公有的”构造方法 public Constructor getDeclaredConstructor(Class… parameterTypes):获取”某个构造方法”可以是私有的,或受保护、默认、公有 调用构造方法: Constructor–>newInstance(Object… initargs) 获取成员变量并调用 批量的 Field[] getFields():获取所有的”公有字段” Field[] getDeclaredFields():获取所有字段,包括:私有、受保护、默认、公有 获取单个的 public Field getField(String fieldName):获取某个”公有的”字段 public Field getDeclaredField(String fieldName):获取某个字段(可以是私有的) 设置字段的值: Field –> public void set(Object obj,Object value): 参数说明:obj:要设置的字段所在的对象 value:要为字段设置的值; 获取成员方法并调用 批量的: public Method[] getMethods():获取所有”公有方法”;(包含了父类的方法也包含Object类) public Method[] getDeclaredMethods():获取所有的成员方法,包括私有的(不包括继承的) 获取单个的: public Method getMethod(String name,Class<?>… parameterTypes): 参数: name : 方法名;Class … : 形参的Class类型对象 public Method getDeclaredMethod(String name,Class<?>… parameterTypes) 调用方法: Method –> public Object invoke(Object obj,Object… args) 参数说明:obj : 要调用方法的对象; args:调用方式时所传递的实参; 反射 main 方法,对于静态方法 getMethod 方法的第一个参数为null 通过反射运行配置文件内容,通过修改配置文件就可以实现对不同的类进行反射 通过反射越过泛型检查 通过反射获得方法的参数名称 JDK 1.8 通过设置编译时是否保留参数名称,否则参数名称将会变成 arg1、arg2等。勾选最下面的一个选项即可。 案例: 1234567891011121314// 获取某方法的参数名称public static List<String> getParameterNameJava8(Class clazz, String methodName) { List<String> lists = new ArrayList<>(); Method[] methods = clazz.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { if (methods[i].getName().equals(methodName)) { Parameter[] p = methods[i].getParameters(); for (Parameter parameter : p) { lists.add(parameter.getName()); } } } return lists;} Java 新特性https://www.cnblogs.com/tony-yang-flutter/p/3503935.html jdk 1.5 枚举 for-each 泛型 自动拆箱和装箱功能 可变参数 静态导入 Lock 并发包 jdk 1.6 synchronized 的升级 (自旋、锁消除、锁粗化) jdk 1.7 泛型推断Map<String, List> map = new HashMap<>(); 二进制面值int i = 0b1010; // 10 数字变量对下滑线的支持int i = 12_3; switch对String的支持 Try-with-resources 利用Try-Catch-Finally管理资源(旧的代码风格) 即使try语句块中抛出的异常与异常传播更相关,最终还是finally语句块中抛出的异常会根据调用栈向外传播。会抛出finally语句块中资源关闭的异常。 try-with-resources管理资源 当try-with-resources结构中抛出一个异常,同时FileInputStream被关闭时(调用了其close方法)也抛出一个异常,try-with-resources结构中抛出的异常会向外传播,而FileInputStream关闭时抛出的异常被抑制了。 AutoCloseable和Closeable的关系 java.lang.AutoCloseable Java 7引入,为所有可以关闭的对象提供资源释放实现,成为java.io.Closeable的父接口 使用Java 7以及更高版本实现接口后可以使用try-with-resource语法实现自动释放资源 close()方法声明抛出java.lang.Exception,意味着接口实现类的close()方法可以声明抛出Exception或者Exception的子类 close()方法不需要保证多次调用不产生副作用 java.io.Closeable Java 5引入,为流对象提供资源释放实现 使用Java 7及更高版本实现接口后可以使用try-with-resource语法实现自动释放资源 close()方法声明抛出java.io.IOException,意味着接口实现类的close()方法只能声明抛出IOException或者IOException的子类 close()方法需要保证多次调用不产生副作用 jdk 1.8 Lambda 表达式 https://www.cnblogs.com/franson-2016/p/5593080.html 编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能。 基本语法: (parameters) -> expression (parameters) ->{ statements; } 接口的默认方法 https://blog.csdn.net/qq_40369829/article/details/79356133 默认方法 default : 可以通过实现接口的类实例化的对象来调用,也可以被重写。 是 public 的。 静态方法 static: 只能在本接口中调用,实现类中也不能调用。职责上是工具方法 12345678910public interface A { int get(); static int getStatic() { return 1; } default int getDefault() { getStatic(); return 1; }} 注解(Annotation) https://www.cnblogs.com/xdp-gacl/p/3622275.html Fork/Join 框架 在必要的情况下,将一个大任务,进行拆分(fork)成若干个子任务(拆到不能再拆,这里就是指我们制定的拆分的临界值),再将一个个小任务的结果进行join汇总。 Fork/Join与传统线程池的区别 Fork/Join采用“工作窃取模式”,当执行新的任务时他可以将其拆分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随即线程中偷一个并把它加入自己的队列中。 就比如两个CPU上有不同的任务,这时候A已经执行完,B还有任务等待执行,这时候A就会将B队尾的任务偷过来,加入自己的队列中,对于传统的线程,ForkJoin更有效的利用的CPU资源! optional https://blog.csdn.net/aitangyong/article/details/54564100 使用 Optional 避免 null 导致的 NullPointerException Optional.of() 或者 Optional.ofNullable():创建 Optional 对象,差别在于 of 不允许参数是null,而 ofNullable 则无限制。 Optional.empty():所有null包装成的Optional对象 isPresent():判断值是否存在 ifPresent(Consumer consumer):如果option对象保存的值不是null,则调用consumer对象,否则不调用 orElse(value):如果optional对象保存的值不是null,则返回原来的值,否则返回value参数 jvm 元空间代替永久代 HashMap 链表长度>=8时,转换为红黑树;取消 Segment 使用 CAS 和 Synchronized 进行锁操作。]]></content>
<categories>
<category>Java 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>Java异常</tag>
<tag>Java反射</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 类加载和双亲委派模型]]></title>
<url>%2F2018%2F11%2F28%2FJava%20%E7%B1%BB%E5%8A%A0%E8%BD%BD%E4%B8%8E%E5%8F%8C%E4%BA%B2%E5%A7%94%E6%B4%BE%E6%A8%A1%E5%9E%8B%2F</url>
<content type="text"><![CDATA[类加载过程加载,连接(验证,准备,解析),初始化 类加载阶段总共分为五个阶段:加载 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 class 文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类) 验证 为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 包含四种验证: 文件格式验证 验证字节流是否符合 class 文件格式的规范,并且能被当前版本的虚拟机处理 元数据验证 对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求 字节码验证 进行数据流和控制流分析 符号引用验证 准备 为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java 堆中。静态常量会直接赋值。 默认值 解析 虚拟机将常量池中的符号引用替换为直接引用的过程。 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。 直接引用:指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。 初始化 类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始化阶段,才开始真正执行类中定义的Java程序代码。 初始化阶段是执行类构造器 clinit() 方法的过程。clinit() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。 类加载器分类启动类加载器(Bootstrap ClassLoader)负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。使用C++实现,属于虚拟机的自身的一部分。而其他的类加载器使用Java语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader。 扩展类加载器(Extension ClassLoader)负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。 应用程序类加载器(Application ClassLoader)负责加载用户路径(classpath)上的类库。可以通过ClassLoader.getSystemClassLoader()来获取它。 用户自定义类加载器通过继承 java.lang.ClassLoader 类的方式实现。 自定义加载器 继承 ClassLoader 类并重写 findClass 方法。否则会报运行时异常。 123protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } 如果想打破双亲委派模型则需要重写 loadClass 方法。默认的 loadClass 方法是实现了双亲委派机制的逻辑,即会先让父类加载器加载,当无法加载时才由自己加载。 12345678910111213141516171819202122232425262728293031323334353637protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先检查请求的类是否已经被加载过 Class<?> c = findLoadedClass(name); if (c == null) { // 未被加载 long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); // 调用父加载器的 loadclass } else { c = findBootstrapClassOrNull(name); // 父加载器为空时,使用启动类加载器 } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } 双亲委派模型定义当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。 注:启动类加载器无法被 java 程序直接引用,用户在编写自定义类加载器时,如果需要把请求加载委派给引导类加载器,那么字节使用 null 即可。 好处:比如加载位于rt.jar包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。保证类的唯一性、 因类加载器的分层使得类也分层次加载(带有优先级)。 打破双亲委派模型的三种情况 双亲委派模型与JDK1.2时引入的,为了向前兼容,新增一个 protected 的 findClass() 方法。在此之前用户继承 ClassLoader 类就是为了重写 loadClass() 方法。而之后则不推荐覆盖 loadClass() 方法,而是把类加载逻辑写到 findClass() 方法中。 双亲委派很好的解决了各个类加载器的基础类的统一问题。但是如果基础类需要回调用户代码则无法实现。比如 JNDI(Java 命名和目录接口),由启动类加载器加载,实现对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的 ClassPath 下的 JNDI 接口提供者(SPI),但是启动类加载器不可能“认识”这些代码。JDBC、JCE、JAXB、JBI也是如此。 解决方案为:线程上下文类加载器。该类加载器通过 Thread 类的 setContextClassLoader() 方法进行设置。 如果创建线程时没有还未设置,则从父线程中继承一个,如果应用程序的全局范围内都没有设置过的话,那么该类加载器就是应用程序类启动器。 然后上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作。 由于用户对程序动态性的追求而导致。比如代码热替换、模块热部署等。 OSGI(Open Service Gateway Initiative)开放服务网关协议为 Java 动态化模块化系统的一系列规范。而 OSGI 实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(OSGI中称为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。 在OSGI环境下,类加载器不再是双亲委派模型的树状结构,而是进一步发展为更加复杂的网状结构。加载流程为: 将以 java.* 开头的类委派给父类加载器加载 否则,将委派列表名单内的类委派给父类加载器加载 否则,将 Import 列表中的类委派给 Export 这个类的 Bundle 的类加载器加载 否则,查找当前 Bundle 的 ClassPath ,使用自己的类加载器加载 否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载 否则,查找 Dynamic Import 列表的 Bundle,委派给对应的 Bundle 的类加载器加载 否则,类查找失败。 常见问题何时开始类的初始化(主动引用) 创建类的实例 访问类的静态变量(除常量【被final修辞的静态变量】) 访问类的静态方法 反射如(Class.forName(“my.xyz.Test”)) 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化(对于接口,使用时才会去初始化父接口) 虚拟机启动时,定义了Main()方法的那个类先初始化 不会初始化类的情况 (被动引用) 子类调用父类的静态变量,子类不会被初始化。只有父类被初始化。对于静态字段,只有直接定义这个字段的类才会被初始化 通过数组定义来引用类,不会触发类的初始化 SubClass[] arr= new SubClass[10]; 访问类的常量,不会初始化类 反射通过 .class 获取 class 对象时 反射通过 Class.forName(“my.xyz.Test”,false,ClassLoader.getSystemClassLoader()) 第二个参数代表是否初始化类 测试12345678910111213141516171819202122232425package com.java;public class Main { public static void main(String[] args) { System.out.println(Father.FAN); // 访问常量 Class clazz = Father.class; // .class 获取 class 对象 System.out.println(clazz.getName()); // 输出全限定名 System.out.println(clazz.getSimpleName()); // 输出类名(不包含包名) Father[] f = new Father[10]; // 数组定义来引用类 try { Class.forName("Father",false,ClassLoader.getSystemClassLoader()); // 也不会初始化 System.out.println("----------"); Class.forName("Father"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }class Father{ public static final int FAN = 1; static { System.out.println("Father类初始化"); }} 运行结果图]]></content>
<categories>
<category>Java 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>Java类加载机制</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java Object类与内部类]]></title>
<url>%2F2018%2F11%2F24%2FJava%20Object%E7%B1%BB%E4%B8%8E%E5%86%85%E9%83%A8%E7%B1%BB%2F</url>
<content type="text"><![CDATA[Object 类 Java Object类为所有类的基类,所有类都是该类的子类。有时候面试官也会问到Object类中存在哪些方法等问题。 包含的所有方法 9个 public 方法(4+5) getClass()、hashCode()、equals(Object obj)、toString() notify()、notifyAll()、wait()、wait(long timeout)、wait(long timeout, int nanos) 2个 protect 方法 clone()、finalize() JDK1.8 Object类源码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051public class Object { private static native void registerNatives(); static { registerNatives(); } public final native Class<?> getClass(); public native int hashCode(); public boolean equals(Object obj) { return (this == obj); } public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException; public final void wait(long timeout, int nanos) throws InterruptedException { if (timeout < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos > 0) { timeout++; } wait(timeout); } public final void wait() throws InterruptedException { wait(0); } protected void finalize() throws Throwable { } protected native Object clone() throws CloneNotSupportedException;} wait() 与 notify()/notifyAll() wait(): 释放占有的对象锁,线程进入等待池,释放cpu,而其他正在等待的线程即可抢占此锁,获得锁的线程即可运行程序。 Thread.sleep():此方法为线程方法,线程调用此方法后,会休眠一段时间,休眠期间,会暂时释放cpu,但并不释放对象锁。 notify(): 该方法会唤醒因为调用对象的wait()而等待的线程,其实就是对对象锁的唤醒,从而使得wait()的线程可以有机会获取对象锁。调用notify()后,并不会立即释放锁,而是继续执行当前代码,直到 synchronized 中的代码全部执行完毕,才会释放对象锁。JVM则会在等待的线程中调度一个线程去获得对象锁,执行代码。注意:wait() 方法需要 try/catch 包裹(会出现 InterruptedException 检查异常),另外wait()和notify()必须在 synchronized 代码块中(否则出现 IllegalMonitorStateException 运行时异常)。 notifyAll()则是唤醒所有等待的线程。 内部类 https://www.cnblogs.com/chenssy/p/3388487.html为什么要使用内部类 每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多重继承的解决方案变得更加完整。 特性 1、内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。 2、在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。 3、创建内部类对象的时刻并不依赖于外围类对象的创建。 4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。 5、内部类提供了更好的封装,除了该外围类,其他类都不能访问。 成员内部类 成员内部类中不能存在任何 static 的变量和方法 成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。 推荐使用 getxxx() 来获取成员内部类,尤其是该内部类的构造函数无参数时 。 12345678910111213141516171819202122232425262728public class OuterClass { private String str; public void outerDisplay(){ System.out.println("outerClass..."); } public class InnerClass{ public void innerDisplay(){ //使用外围内的属性 str = "chenssy..."; System.out.println(str); //使用外围内的方法 outerDisplay(); } } /*推荐使用getxxx()来获取成员内部类,尤其是该内部类的构造函数无参数时 */ public InnerClass getInnerClass(){ return new InnerClass(); } public static void main(String[] args) { OuterClass outer = new OuterClass(); OuterClass.InnerClass inner = outer.getInnerClass(); inner.innerDisplay(); }} 局部内部类 嵌套在方法和作用于内 对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。 匿名内部类 匿名内部类是没有访问修饰符的 new 匿名内部类,这个类首先是要存在的。如果我们将那个InnerClass接口注释掉,就会出现编译出错。 注意getInnerClass()方法的形参,第一个形参是用final修饰的,而第二个却没有。同时我们也发现第二个形参在匿名内部类中没有使用过,所以当所在方法的形参需要被匿名内部类使用,那么这个形参就必须为final。 匿名内部类是没有构造方法的。 12345678910111213141516171819public class OuterClass { public InnerClass getInnerClass(final int num,String str2){ return new InnerClass(){ int number = num + 3; public int getNumber(){ return number; } }; /* 注意:分号不能省 */ } public static void main(String[] args) { OuterClass out = new OuterClass(); InnerClass inner = out.getInnerClass(2, "chenssy"); System.out.println(inner.getNumber()); }}interface InnerClass { int getNumber();} 静态内部类 它的创建是不需要依赖于外围类的 它不能使用任何外围类的非static成员变量和方法]]></content>
<categories>
<category>Java 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>Java内部类</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 并发编程]]></title>
<url>%2F2018%2F11%2F18%2FJava%20%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%2F</url>
<content type="text"><![CDATA[Java 并发基础 在并发编程中,如果想要一个程序正确的执行,必须保证原子性、可见性以及有序性,只要有一个没有被保证,就有可能会导致程序运行不正确。 需要考虑的问题 原子问题 一个操作或多个操作要么全部执行并且执行过程中不能被任何操作打断,要么都不执行。 可见性问题 多个线程访问同一个变量时,一个线程修改了这个变量的值,其它线程能够立即看到修改的值。 有序性问题 程序执行的顺序按照代码的先后顺序执行。在 java 内存模型中,允许编译器和处理器对执行进行重排,但是重排过程不会影响单线程程序的执行,却会影响到多线程并发执行的正确性。 指令重排 一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。如果两个语句之间没有数据依赖,那么可能会被重排。指令重排不会影响单个线程的执行,但是会影响到线程并发执行的正确性。 CyclicBarrier 和 CountDownLatch 的区别 CountDownLatch 简单的说就是一个或多个线程等待,直到它所等待的其他线程都执行完成并且调用countDown() 方法发出通知后,当前线程才可以继续执行。 CyclicBarrier 是所有线程都进行等待,直到所有线程都准备好进入 await() 方法之后,所有线程同时开始执行! CountDownLatch 的计数器只能使用一次。而 CyclicBarrier 的计数器可以使用 reset() 方法重置。所以 CyclicBarrier 能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。 CyclicBarrier 还提供其他有用的方法,比如 getNumberWaiting 方法可以获得 CyclicBarrier 阻塞的线程数量。isBroken 方法用来知道阻塞的线程是否被中断。如果被中断返回true,否则返回false。 BIO、NIO、AIO同步、异步、阻塞、非阻塞 https://blog.csdn.net/u013851082/article/details/53942947 同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪 异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知) 阻塞方式是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到有东西可读或者可写为止 非阻塞状态下, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待 Java对 BIO、NIO、AIO 的支持 Java BIO :同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 Java NIO :同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。 Java AIO :异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理数据即可 BIO、NIO、AIO适用场景分析: BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。 NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。 AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。 ThreadLocal (线程本地存储) http://www.jasongj.com/java/threadlocal/ 线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问,可以用来解决对某一个变量的访问冲突问题。当使用 ThreadLocal 维护变量的时候,为每一个使用该变量的线程提供一个独立的变量副本。(会导致内存资源占用增加) 方法详解 1234public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本public void set(T value) { } //set()用来设置当前线程中变量的副本public void remove() { } //remove()用来移除当前线程中变量的副本protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的 原理 Thread 在内部是通过 ThreadLocalMap 来维护 ThreadLocal 变量表ThreadLocal.ThreadLocalMap<ThreadLocal, Object>;初始化时,在 Thread 里面,threadLocals 为空,当通过 ThreadLocal 变量调用 get() 方法或者 set() 方法,就会对 Thread 类中的 threadLocals 进行初始化,并且以当前 ThreadLocal 变量为键值,以 ThreadLocal 要保存的副本变量为 value,存到 threadLocals。然后在当前线程里面,如果要使用副本变量,就可以通过 get 方法在 threadLocals 里面查找调用ThreadLocal.get方法时,实际上是从当前线程中获取ThreadLocalMap<ThreadLocal, Object>,然后根据当前 ThreadLocal 获取当前线程共享变量Object。 应用场景 解决 数据库连接、Session管理 等。ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。 存储结构的优势 ThreadLocalMap 依赖于 Thread ,线程死去的时候,线程本地变量 ThreadLocalMap 则销毁。 ThreadLocalMap<ThreadLocal,Object>键值对数量为 ThreadLocal 的数量,一般来说 ThreadLocal 数量很少,相比在 ThreadLocal 中用 Map<Thread, Object> 键值对存储线程本地变量(Thread 数量一般来说比 ThreadLocal 数量多),性能提高很多。 ThreadLocalMap 与内存泄漏 ThreadLocalMap 的每个 Entry 都是一个对 键 的弱引用(下一次GC时会被回收),每个 Entry 都包含了一个对 值 的强引用。 使用弱引用的原因在于,当没有强引用指向 ThreadLocal 变量时(线程未结束,但是引用 ThreadLocal 的对象被回收),它可被回收,从而避免上文所述 ThreadLocal 不能被回收而造成的内存泄漏的问题。但是会导致 ThreadLocal 被回收但是 值 未被回收,另外 Entry 无法被移除。从而使得实例被该 Entry 引用而无法被回收造成内存泄漏。 针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。 为什么要使用 ThreadLocal https://blog.csdn.net/zhangzeyuaaa/article/details/43564471 如果我们在方法中要使用方法外的变量(不包括当前类或者父类的成员变量),有两种方式可以引用到方法外的变量: 1.方法传参。 2.将需要被引用的变量定义为类的静态变量。 两种方式都有弊端:方法传参的弊端是可能需要在很多地方传递这个参数,耦合度高(因为变量的存储和获取可能在不同的模块中);定义为类的静态变量则会引发线程安全问题。 关于ThreadLocalMap内部类的简单介绍 初始容量16,负载因子2/3,解决冲突的方法是线性探测再散列(取下一个可用的地址),也就是:在当前hash的基础上再自增一个常量。 AQS 抽象队列同步器 一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。 原理 使用一个 volatile int 类型的成员变量 state 来表示状态信息。 当 state>0 时表示已经获取了锁,当 state = 0 时表示释放了锁。 它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态 state 进行操作,当然 AQS 可以确保对 state 的操作是安全的。 比如:Semaphore 用它来表现剩余的许可数,ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消) 阻塞线程节点队列 CLH Node queue 内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。(阻塞和唤醒使用的是 LockSupport.park() 和 LockSupport.unpark() 阻塞原语) CLH 队列是一个非阻塞的 FIFO 队列,也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和 CAS 保证节点插入和移除的原子性。实现无锁且快速的插入。 使用方法 https://blog.csdn.net/vernonzheng/article/details/8275624 使用 AQS 来实现一个同步器需要覆盖实现如下几个方法 tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态; tryRelease(int arg):独占式释放同步状态; tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败; tryReleaseShared(int arg):共享式释放同步状态; isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占; AQS 实现类继承关系图 ReentrantLock 需要记录当前线程获取原子状态的次数,如果次数为零,那么就说明这个线程放弃了锁(也有可能其他线程占据着锁从而需要等待),如果次数大于1,也就是获得了重进入的效果,而其他线程只能被park住,直到这个线程重进入锁次数变成0而释放原子状态。 内部Sync类实现的是tryAcquire,tryRelease, isHeldExclusively三个方法(因为获取锁的公平性问题,tryAcquire由继承该Sync类的内部类FairSync和NonfairSync实现) Semaphore 记录当前还有多少次许可可以使用,到0,就需要等待,也就实现并发量的控制,可作为流量控制。Semaphore一开始设置许可数为1,就相当于一把互斥锁。 Semaphore内部类Sync则实现了 tryAcquireShared 和 tryReleasedShared (与 CountDownLatch 相似,因为公平性问题,tryAcquireShared 由其内部类FairSync和NonfairSync实现)。 CountDownLatch 闭锁则要保持其状态,在这个状态到达终止态之前,所有线程都会被park住,闭锁可以设定初始值,这个值的含义就是这个闭锁需要被countDown()几次,因为每次CountDown是sync.releaseShared(1),而一开始初始值为10的话,那么这个闭锁需要被countDown()十次,才能够将这个初始值减到0,从而释放原子状态,让等待的所有线程通过。 CountDownLatch 内部类Sync实现了 tryAcquireShared 和 tryReleasedShared。 FutureTask 需要记录任务的执行状态,当调用其实例的 get 方法时,内部类 Sync 会去调用AQS的 acquireSharedInterruptibly() 方法,而这个方法会反向调用 Sync 实现的 tryAcquireShared() 方法,即让具体实现类决定是否让当前线程继续还是 park ,而 FutureTask 的 tryAcquireShared 方法所做的唯一事情就是检查状态,如果是 RUNNING 状态那么让当前线程 park 。而跑任务的线程会在任务结束时调用FutureTask 实例的 set 方法(与等待线程持相同的实例),设定执行结果,并且通过 unpark 唤醒正在等待的线程,返回结果。 FutureTask内部类 Sync 也实现了 tryAcquireShared 和 tryReleasedShared。 java 并发包(J.U.C) 同步控制工具 https://blog.csdn.net/heting717/article/details/76768971ReentrantLock特性 可重入(获取锁的线程可以重复进入,但要重复退出) 可中断 lockInterruptibly() 可限时 tryLock()(规定的时间内未能获取锁,则返回 false) 公平锁与非公平锁(默认) 一般意义上的锁是不公平的,不一定先来的线程能先得到锁,后来的线程就后得到锁。不公平的锁可能会产生饥饿现象。公平锁的意思就是,这个锁能保证线程是先来的先得到锁。虽然公平锁不会产生饥饿现象,但是公平锁的性能会比非公平锁差很多。 ReentrantLock 的实现 实现主要由3部分组成: CAS状态 等待队列 park() ReentrantLock 的父类中会有一个 state 变量来表示同步的状态 通过 CAS 操作来设置 state 来获取锁,如果设置成了1,则将锁的持有者给当前线程 如果拿锁不成功,则会做一个申请,tryAcquire,因为此时可能另一个线程已经释放了锁。 如果还是没有申请到锁,就 addWaiter,意思是把自己加到等待队列中去 Condition Condition与ReentrantLock的关系就类似于 Object.wait()/notify() 与synchronized await()方法会使当前线程等待,同时释放当前锁,当其他线程中使用signal()时或者signalAll()方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和 Object.wait() 方法很相似。 awaitUninterruptibly() 方法与 await() 方法基本相同,但是它并不会在等待过程中响应中断。 singal() 方法用于唤醒一个在等待中的线程。相对的 singalAll() 方法会唤醒所有在等待中的线程。这和 Obejct.notify() 方法很类似。 Semaphore 信号量,控制线程同时进入临界区的数量,可用作流量控制 常用方法: acquire() : 获得许可从这个信号量,会阻塞,直到一个可用,或 interrupted线程。 acquireUninterruptibly() : 获得许可从这个信号量,阻塞,直到一个是可用的。 tryAcquire() : 获得许可从这个信号量,只有一个可用的时候调用。 tryAcquire(long timeout, TimeUnit unit) : 获得许可从这个信号量,如果没有一个可用在给定的等待时间和当前线程则返回false,同时可以响应中断。 release() : 发布许可证,返回信号量。 ReadWriteLock 读和写是两种不同的功能,读-读不互斥,读-写互斥,写-写互斥。这样的设计是并发量提高了,又保证了数据安全。 CountDownLatch 倒数计时器、闭锁 一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检查。只有等所有检查完毕后,引擎才能点火。这种场景就非常适合使用CountDownLatch。它可以使得点火线程,等待所有检查线程全部完工后,再执行。 CyclicBarrier 屏障 CyclicBarrier 是所有线程都进行等待,直到所有线程都准备好进入 await() 方法之后,所有线程同时开始执行!另外 CyclicBarrier 可以反复使用。 LockSupport 提供线程阻塞原语,和 suspend 类似。与 suspend 相比 不容易引起线程冻结,使用 LockSupport 则不会发生死锁。park() 能够响应中断,但不抛出异常。中断响应的结果是,park() 函数的返回,可以从Thread.interrupted() 得到中断标志。LockSupport 提供 park() 和 unpark() 方法实现阻塞线程和解除线程阻塞,实现的阻塞和解除阻塞是基于”许可(permit)”作为关联,permit 相当于一个信号量(0,1),默认是0。线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态。]]></content>
<categories>
<category>Java 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>Java多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 锁详解]]></title>
<url>%2F2018%2F11%2F16%2FJava%20%E9%94%81%E8%AF%A6%E8%A7%A3%2F</url>
<content type="text"><![CDATA[synchronized 与 Lock 的区别synchronized 存在层次:Java 的关键字,在jvm层面上 锁的释放:1、获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁 锁的获取:假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 锁状态:无法判断 锁类型:可重入 不可中断 非公平 性能:适用于少量同步 Lock 存在层次:一个类 锁的释放:在 finally 语句块中必须释放锁,不然容易造成线程死锁 锁的获取:分情况而定,Lock 有多个锁获取的方式,可以尝试获得锁,线程可以不会一直等待 锁状态:可以判断 锁类型:可重入 可中断 可公平(默认为非公平) 性能:适用于大量同步 常见问题:Synchronized 同步静态方法 和 非静态方法 Synchronized 同步静态方法 是对整个类加锁 Synchronized 同步非静态方法 是对类的某个对象加锁 Synchronized(this) 对当前对象进行加锁,Synchronized(Test.class)对类对象进行加锁 静态方法同步与对象方法同步不互斥 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071/**测试结果:Sun Aug 12 11:22:30 CST 2018同步块完成:Sun Aug 12 11:22:33 CST 2018同步方法完成:Sun Aug 12 11:22:36 CST 2018静态方法同步完成:Sun Aug 12 11:22:40 CST 2018*/import java.util.Date;public class Main { public static void main(String[] args) { Test t = new Test(); Thread t1 = new Thread(new Runnable() { @Override public void run() { Test.fun1(); // 静态方法同步,对类进行加锁 } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { t.fun2(); // 对同步块,对Test的对象进行加锁 } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { t.fun3(); // 对非静态方法同步,对Test的对象进行加锁 } }); System.out.println(new Date()); t1.start(); t2.start(); t3.start(); try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } }}class Test { public synchronized static void fun1() { try { Thread.sleep(10000); System.out.println("静态方法同步完成:"+new Date()); } catch (InterruptedException e) { e.printStackTrace(); } } public void fun2() { synchronized (this) { try { Thread.sleep(3000); System.out.println("同步块完成:"+new Date()); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void fun3() { try { Thread.sleep(3000); System.out.println("同步方法完成:"+new Date()); } catch (InterruptedException e) { e.printStackTrace(); } }} synchronized 锁优化 https://blog.csdn.net/championhengyi/article/details/80105718自旋锁 互斥同步的进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。 自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。 在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。 锁消除 锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。 锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。 1234567public String concatString(String s1, String s2, String s3) { StringBuffer sb = new StringBuffer(); // StringBuffer类是一个线程安全类 sb.append(s1); sb.append(s2); sb.append(s3); return sb.toString();} 锁粗化 如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。 轻量级锁 相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。 偏向锁 偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。 当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。 Java 对象头信息Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间。 一个字节 32 位。 锁机制存在的问题 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。 一个线程持有锁会导致其它所有需要此锁的线程挂起。 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。 乐观锁与悲观锁 独占锁是一种悲观锁,synchronized 就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。 另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因冲突而失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。 CAS机制(Compare and Swap) CAS 操作包含三个操作数 —— 内存位值(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。 无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。) 目的: 通过 CAS 机制实现非阻塞算法,较与 synchronized 阻塞算法(避免线程间上下文的切换),性能上有很大的提升。 存在的问题: ABA问题。因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。解决办法有:使用锁或者将多个变量封装为一个对象 。 concurrent (J.U.C)包的源代码实现原理 声明共享变量为 volatile 使用 CAS 的原子条件更新来实现线程之间的同步 配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。 线程同步的多种实现 Synchronized Lock Semaphore(1) volatile 共享变量 生产者与消费者问题https://zhuanlan.zhihu.com/p/20300609]]></content>
<categories>
<category>Java 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>synchronized优化</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 线程与线程池]]></title>
<url>%2F2018%2F11%2F14%2FJava%20%E7%BA%BF%E7%A8%8B%E6%B1%A0%2F</url>
<content type="text"><![CDATA[Java线程Java线程状态 初始(NEW):新创建线程对象,但还没有调用start()方法。 运行(RUNNABLE):Java 虚拟机中线程的可运行状态包括操作系统中线程的就绪(ready)和运行中(running)两种状态。即处于该状态时,线程可能正在等待来自操作系统的其他资源(如CPU)。 阻塞(BLOCKED):表示线程阻塞于锁。 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。 终止(TERMINATED):表示该线程已经执行完毕。 线程的状态变化图 JDK1.8 线程状态源码 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364public enum State { /** * Thread state for a thread which has not yet started. */ NEW, /** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */ RUNNABLE, /** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {@link Object#wait() Object.wait}. */ BLOCKED, /** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: * <ul> * <li>{@link Object#wait() Object.wait} with no timeout</li> * <li>{@link #join() Thread.join} with no timeout</li> * <li>{@link LockSupport#park() LockSupport.park}</li> * </ul> * * <p>A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called <tt>Object.wait()</tt> * on an object is waiting for another thread to call * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on * that object. A thread that has called <tt>Thread.join()</tt> * is waiting for a specified thread to terminate. */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * <ul> * <li>{@link #sleep Thread.sleep}</li> * <li>{@link Object#wait(long) Object.wait} with timeout</li> * <li>{@link #join(long) Thread.join} with timeout</li> * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> * </ul> */ TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ TERMINATED; } 线程常用方法 thread.join() https://www.cnblogs.com/huangzejun/p/7908898.html 作用 当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。 可以用来实现多个线程需要按一定的顺序执行的需求。 Thread.yield() 线程让步(静态本地方法) 作用 让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行! Thread.sleep() 线程睡眠 (静态本地方法) 作用 让当前线程由“运行状态”进入到“阻塞状态”,并不会释放锁。 线程中断方法 interrupted() 返回当前的线程的中断状态,并清除中断标识位。静态方法public static boolean interrupted() { return currentThread().isInterrupted(true); } isInterrupted() 返回调用该方法的对象所表示的线程的中断状态,且不会清除中断标识位。实例方法public boolean isInterrupted() { return isInterrupted(false); } 代码测试: 线程池详细参数 corePoolSize:核心池的大小。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中; maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程; keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0; unit:参数keepAliveTime的时间单位 workQueue:一个阻塞队列,用来存储等待执行的任务 threadFactory:线程工厂,主要用来创建线程 handler:表示当拒绝处理任务时的策略 线程池状态 runState 表示当前线程池的状态,它是一个 volatile 变量用来保证线程之间的可见性,高3位表示状态。 12345volatile int runState;static final int RUNNING = 0;static final int SHUTDOWN = 1;static final int STOP = 2;static final int TERMINATED = 3; 当创建线程池后,初始时,线程池处于RUNNING状态; 如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不接受新任务,但处理排队任务 如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不接受新任务,不处理排队任务,并中断正在进行的任务 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。 1.8 使用的原子类 线程池的种类 fixThreadPool 固定线程有指定的线程数的线程池,有核心的线程,里面有固定的线程数量,响应的速度快。正规的并发线程,多用于服务器。固定的线程数由系统资源设置。 cacheThreadPool 缓存线程池只有非核心线程,最大线程数很大(Int.Max(values)),它会为每一个任务添加一个新的线程,这边有一个超时机制,当空闲的线程超过60s内没有用到的话,就会被回收。缺点就是没有考虑到系统的实际内存大小。 singleThreadPool 单线程线程池只有一个核心线程,通过指定的顺序将任务一个个丢到线程,都乖乖的排队等待执行,不处理并发的操作,不会被回收。效率慢。 ScheduledThreadPool唯一一个有延迟执行和周期重复执行的线程池。它的核心线程池固定,非核心线程的数量没有限制,但是闲置时会立即被回收。 线程池的实现 https://www.cnblogs.com/wxwall/p/7050698.html原理图 实现要点 线程池里的核心线程数与最大线程数 线程池里真正工作的线程 worker 线程池里用来存取任务的队列 BlockingQueue 线程中的任务 task 阻塞队列 BlockingQueue http://ifeve.com/java-blocking-queue/ 定义 一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。 作用 常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。 四种处理方法 put、take、offer、poll、add、remove 的区别 区别详解 抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。实际上调用的还是返回特殊值的方法。 返回特殊值:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。 超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。 Java常用阻塞队列 ArrayBlockingQueue ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列。通常情况下为了保证公平性会降低吞吐量。底层以数组的形式保存数据(实际上可看作一个循环数组)。在添加元素取出元素时会加锁。 LinkedBlockingQueue 一个用链表实现的有界阻塞队列。此队列的默认的最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。 PriorityBlockingQueue 一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器 comparator 来指定元素的排序规则。元素按照升序排列。 SynchronousQueue 一个不存储元素的阻塞队列。每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。SynchronousQueue 可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue。 任务拒绝策略 ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 线程池的主要处理流程 线程池的好处 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。]]></content>
<categories>
<category>Java 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>线程池</tag>
<tag>线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 内存模型与内存划分]]></title>
<url>%2F2018%2F11%2F13%2FJava%20%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E4%B8%8E%E5%86%85%E5%AD%98%E5%88%92%E5%88%86%2F</url>
<content type="text"><![CDATA[JVM 内存模型主要目标定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。 规则所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。 内存间交互操作关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成 lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。 unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用 load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。 use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。 assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。 write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。 重排序 种类 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 如何避免重排序 为了保证内存的可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。 内存屏障类型 volatile 原理 https://www.jianshu.com/p/2ab5e3d7e510 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障; 在每个volatile读操作前插入 LoadLoad屏障,在读操作后插入LoadStore屏障; happens-before 原则happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据 八条原则 程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。 锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。 volatile 规则: volatile 变量的写先发生于读,这保证了 volatile 变量的可见性,简单的理解就是,volatile 变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。 线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的 start 方法之前修改了共享变量的值,那么当线程B执行 start 方法时,线程A对共享变量的修改对线程B可见 线程终止规则:线程的所有操作先于线程的终结,Thread.join() 方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。 对象终结规则:对象的构造函数执行,结束先于finalize()方法 传递性:A先于B ,B先于C 那么A必然先于C JVM 参数列表 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0 -Xmx3550m:最大堆内存为3550M。 -Xms3550m:初始堆内存为3550M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。空闲空间小于40%时会扩大堆,空闲空间大于70%时会缩小堆。 -Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 永久代大小。永久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。 -Xss128k:设置每个线程的堆栈大小。 -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6 -XX:MaxPermSize=16m:设置永久代大小为16m。 -XX:MaxTenuringThreshold=15:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概率。 JDK1.8 永久代 1.8之前32位机器默认永久代大小为64M,64位机器默认永久代大小为85M。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域的被占满,两个区都需要进行垃圾回收。存在的问题:1. 一旦类的元数据(描述数据的数据,对数据及信息资源的描述性信息。)超过设定的永久代大小,程序就会耗尽内存,并出现OOM。2. 元数据信息会随着每一次 Full GC 发生移动。3. 永久代的空间大小很难确定,类的总数、常量池的大小、方法的总数。 1.8取消永久代,将类的元数据信息移到了一个与堆不相连的本地内存区域(元空间)。 影响: 将类的元数据分配在本地内存中,元空间的最大可分配空间就是系统的可用内存空间。 JVM可以自动根据类的元数据大小动态增加元空间的容量。 Java 内存划分运行时数据区程序计数器(线程私有)在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。 由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。 Java 虚拟机栈(线程私有)Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。 当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。 生命周期与线程相同。 栈帧 局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。局部变量所需的内存空间在编译期间确定的。 操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。动态链接就是将常量池中的符号引用在运行期转化为直接引用。 本地方法栈(线程私有)本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。 堆(线程共享)用来存储对象本身的以及数组。 堆也是Java垃圾收集器管理的主要区域。 方法区(线程共享)存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。 有时候也称为永久代,在该区内很少发生垃圾回收,但是并不代表不发生GC,在这里进行的GC主要是对方法区里的常量池和对类的卸载。 在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到 JVM 后,对应的运行时常量池就被创建出来。当然并非 Class 文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如 String 的 intern() 方法。 内存溢出与内存泄漏内存溢出 out of memory程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储 int 类型数据的存储空间,但是你却存储 long 类型的数据,那么结果就是内存不够用。 内存泄漏 memory leak程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。 存在情况 静态集合类 如静态的HashMap、LinkedList等。长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。 各种连接 如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。若没有显式关闭,则会造成内存泄漏。 变量不合理的作用域 一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。 内部类持有外部类 当内部类被长期引用时,由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。 改变哈希值 当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了(需重写 hashCode 方法),否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前对象,造成内存泄露。 参考博客: https://www.cnblogs.com/dingyingsi/p/3760447.html]]></content>
<categories>
<category>Java 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>Java内存模型</tag>
<tag>Java内存划分</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java GC笔记]]></title>
<url>%2F2018%2F11%2F13%2FJava%20GC%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[GC 算法标记清除算法 Mark-Sweep原理 标记出所有需要回收的对象 标记完成后统一回收所有被标记的对象 缺点 效率不高,标记和清除效率都不高 空间碎片问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作 复制算法 Copying原理 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。 当这一块的内存用完了,就将还存活着的对象依次复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉。 优点 实现简单,运行高效 不用考虑内存碎片问题 缺点 可用内存缩小为原来的一半 在对象存活率较高的情况下,会进行较多的复制操作导致效率降低 标记整理算法 Mark-Compact原理 标记 将所有存活的对象向一端移动,然后清理掉边界以外的内存 分代收集算法 Generational Collection原理 根据对象存活周期的不同将内存划分为几块。 一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。 垃圾回收器Serial 收集器 Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器 特点 针对新生代; 采用复制算法; 单线程收集; 进行垃圾收集时,必须暂停所有工作线程,直到完成,即会”Stop The World”; Serial/Serial Old组合收集器运行示意图 ParNew 收集器 ParNew 垃圾收集器是 Serial 收集器的多线程版本。 特点 新生代收集器; 采用复制算法; 多线程收集; ParNew/Serial Old组合收集器运行示意图 Parallel Scavenge 收集器 与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) 高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间 特点 新生代收集器; 采用复制算法; 多线程收集; 它的关注点与其他收集器不同 CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间; Parallel Scavenge 收集器的目标则是达一个可控制的吞吐量(Throughput) GC自适应的调节策略(GC Ergonomiscs) 只需设置好内存数据大小(如”-Xmx”设置最大堆); 然后使用”-XX:MaxGCPauseMillis”或”-XX:GCTimeRatio”给JVM设置一个优化目标; 那些具体细节参数的调节就由JVM自适应完成; Serial Old 收集器 Serial Old 是 Serial 收集器的老年代版本; 特点 针对老年代; 采用”标记-整理”算法(还有压缩,Mark-Sweep-Compact); 单线程收集; Serial/Serial Old收集器运行示意图 应用场景 主要用于 Client 模式; 在 Server 模式有两大用途 在JDK1.5及之前,与 Parallel Scavenge 收集器搭配使用(JDK1.6有Parallel Old收集器可搭配) 作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用 Parallel Old 收集器 Parallel Old 垃圾收集器是 Parallel Scavenge 收集器的老年代版本;JDK1.6中才开始提供 特点 针对老年代; 采用”标记-整理”算法; 多线程收集; Parallel Scavenge/Parallel Old 收集器运行示意图 使用场景 JDK1.6及之后用来代替老年代的 Serial Old 收集器; 特别是在Server模式,多CPU的情况下; 这样在注重吞吐量以及CPU资源敏感的场景,就有了 Parallel Scavenge 加 Parallel Old收集器的”给力”应用组合 CMS收集器 并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器 特点 针对老年代; 基于”标记-清除”算法(不进行压缩操作,产生内存碎片); 以获取最短回收停顿时间为目标; 并发收集、低停顿; 需要更多的内存(缺点) 应用场景 与用户交互较多的场景; 希望系统停顿时间最短,注重服务的响应速度;以给用户带来较好的体验; 如常见WEB、B/S系统的服务器上的应用; 运行过程 初始标记(CMS initial mark) 仅标记一下GC Roots能直接关联到的对象; 速度很快; 但需要”Stop The World”; 并发标记(CMS concurrent mark) 进行GC Roots Tracing的过程; 刚才产生的集合中标记出存活对象; 应用程序也在运行; 并不能保证可以标记出所有的存活对象; 重新标记(CMS remark) 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录; 需要”Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率; 并发清除(CMS concurrent sweep) 回收所有的垃圾对象; 缺点 对CPU资源非常敏感 并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。 CMS 的默认收集线程数量是=(CPU数量+3)/4; 无法处理浮动垃圾 在并发清除时,用户线程新产生的垃圾,称为浮动垃圾; 产生大量内存碎片 CMS 基于”标记-清除”算法,清除后不进行压缩操作;可通过配置实现内存碎片整理 promotion failed – concurrent mode failure Minor GC后, surviver 空间容纳不了剩余对象,将要放入老年代,老年代有碎片或者不能容纳这些对象,即 CMS 预留内存空间无法满足程序需要,就会出现一次”Concurrent Mode Failure”失败。 解决方案 让 CMS 在进行一定次数的 Full GC(标记清除)的时候进行一次标记整理算法,在一定程度上控制碎片的数量。 使用标记整理清除碎片-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 增大新生代或者surviver区空间大小 concurrent mode failure 将大对象放入老年代空间时,老年代空间无法满足程序需要,就会出现一次”Concurrent Mode Failure”失败。这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次 Full GC 的产生,此时代价较大(会Stop the world),所以CMS预留内存空间不能设置得太小。 解决方案 调低触发 CMS GC 执行的阀值。CMS GC 触发主要由 CMSInitiatingOccupancyFraction 值决定,默认情况是当老年代已用空间为68%时,即触发 CMS GC。尽早进行CMS操作,使其预留空间变大 增大老年代空间。 CMS 收集器运行示意图 G1 收集器 G1(Garbage-First)是JDK7-u4才推出商用的收集器; 特点 并行与并发 分代收集,收集范围包括新生代和老年代 结合多种垃圾收集算法,空间整合,不产生碎片 可预测的停顿:低停顿的同时实现高吞吐量 应用场景 面向服务端应用,针对具有大内存、多处理器的机器; 最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案; 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒; 运作过程 初始标记(Initial Marking) 仅标记一下GC Roots能直接关联到的对象; 且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的 Region 中创建新对象; 需要”Stop The World”,但速度很快; 并发标记(Concurrent Marking) 进行 GC Roots Tracing 的过程; 刚才产生的集合中标记出存活对象; 耗时较长,但应用程序也在运行; 并不能保证可以标记出所有的存活对象; 最终标记(Final Marking) 为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录; 上一阶段对象的变化记录在线程的Remembered Set Log; 这里把Remembered Set Log合并到Remembered Set中; 需要”Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率; 筛选回收(Live Data Counting and Evacuation) 首先排序各个 Region 的回收价值和成本; 然后根据用户期望的GC停顿时间来制定回收计划; 最后按计划回收一些价值高的 Region 中垃圾对象; 回收时采用”复制”算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存; 可以并发进行,降低停顿时间,并增加吞吐量; G1收集器运行示意图 参数配置收集器设置 -XX:+UseSerialGC:设置串行收集器 -XX:+UseParallelGC:设置并行收集器 -XX:+UseParalledlOldGC:设置并行年老代收集器 -XX:+UseConcMarkSweepGC:设置并发收集器 垃圾回收统计信息 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename 并行收集器设置 -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。 -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) 并发收集器设置 -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。 -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。 垃圾收集器期望的目标 停顿时间 停顿时间越短就适合需要与用户交互的程序; 良好的响应速度能提升用户体验; 吞吐量 高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务; 主要适合在后台计算而不需要太多交互的任务; 覆盖区 在达到前面两个目标的情况下,尽量减少堆的内存空间; 可以获得更好的空间局部性; 面试常见的问题对象提升原则 对象优先分配在 Eden 区,空间不够时,执行 Minor GC 。 大对象直接进入老年代(需要大量连续内存空间的对象)。目的:避免在 Eden 区和两个 Survivor 区之间发生大量的内存拷贝。 长期存活的对象进入老年代。Survivor 区中的对象每经过一次 Minor GC ,其对象年龄就+1。 动态判断对象的年龄。如果 Survivor 区中的相同年龄的所有对象的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。 空间分配担保。每次 Minor GC 时,计算 Survivor 区移至老年区的对象平均大小,是否大于老年区剩余空间大小。是否需要 Full GC。 触发 Full GC 的情况 老年代空间不足 永久代被占满 CMS GC时出现 Promotion Failed 和 Concurrent Mode Failure(产生的浮动垃圾) 空间分配担保失败(晋升到老年代的平均大小大于老年代的剩余空间) Stop the World GC 事件/过程发生过程当中停止所有应用程序线程的执行。 停顿保证系统状态在某一瞬间的一致性,并且不会产生新的垃圾,有利于垃圾回收器更好的标记垃圾对象。 判断对象是否存活 引用计数法 问题: 循环引用问题 GC ROOTS 可达行分析法 Java 虚拟机栈引用的对象 本地方法栈引用的对象 方法区静态变量引用的对象 方法区常量引用的对象 GC是在什么时候,对什么东西,做了什么事情 堆中年轻代的 eden 区满执行 minor gc,提升到老年代的对象大于老年代剩余空间大小则执行 full gc。我们无法控制 GC 发生时间,另外 System.gc 只是建议 JVM 进行 GC 。 从 gc root 开始搜索找不到的对象,而且经过一次标记、清理,仍然没有复活的对象。 删除不使用的对象,回收内存空间;运行默认的 finalize,JVM 对年轻代执行复制算法,对老年代执行标记清理/标记整理算法。]]></content>
<categories>
<category>Java 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>垃圾回收器</tag>
<tag>GC算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 集合笔记]]></title>
<url>%2F2018%2F11%2F12%2FJava%20%E9%9B%86%E5%90%88%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[Java 集合关系图 Iterator 和 ListIterator 的区别Iterator 接口 可以应用于所有的集合,Set、List和Map和这些集合的子类型 只能顺序向后遍历. 只能遍历,使用迭代器的方法可以删除元素。 ListIterator 只能用于List及其子类型。 有add方法,可以向List中添加对象 有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历 可以定位当前索引的位置,nextIndex()和previousIndex()可以实现。 可以实现对象的修改,set()方法可以实现。 HashSet与TreeSet的区别HashSet 内部使用 HashMap 存储对象,将对象存储在 map 的 key 处,而 map 的 value 默认为一个静态的 object 对象。 12// Dummy value to associate with an Object in the backing Mapprivate static final Object PRESENT = new Object(); HashSet 实现了 Set 接口,它不允许集合中出现重复元素。当我们提到HashSet时,第一件事就是在将对象存储在HashSet之前,要确保重写hashCode() 方法和equals() 方法,这样才能比较对象的值是否相等,确保集合中没有储存相同的对象。 TreeSet 通过 TreeMap 实现,而 TreeMap 基于红黑树实现排序的效果。 特点 TreeSet是用来排序的, 可以指定一个顺序, 对象存入之后会按照指定的顺序排列 使用方式 a.自然顺序(Comparable) TreeSet类的add()方法中会把存入的对象提升为Comparable类型 调用对象的compareTo()方法和集合中的对象比较 根据compareTo()方法返回的结果进行存储 b.比较器顺序(Comparator) 创建TreeSet的时候可以制定一个Comparator 如果传入了Comparator的子类对象, 那么TreeSet就会按照比较器中的顺序排序 add()方法内部会自动调用Comparator接口中compare()方法排序 调用的对象是compare方法的第一个参数,集合中的对象是compare方法的第二个参数 c.两种方式的区别 TreeSet构造函数什么都不传, 默认按照类中 Comparable 的顺序(没有就报错ClassCastException) TreeSet如果传入Comparator, 就优先按照Comparator ArrayList与LinkedList的区别ArrayList 基于动态数组实现 支持随机访问 不利于插入与删除 LinkedList 基于链表实现 新增与删除操作只需移动指针 访问时需要遍历 ArrayList与Vector的区别ArrayList 默认长度为 10 扩容时*1.5,更节省空间 Vector 方法都是同步的,线程安全 默认长度也是 10 扩容时*2 CopyOnWrite容器(写时复制的容器)原理: 当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。 优势: 可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。 缺点: 内存占用问题 因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象。注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存。 数据一致性问题 CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。 使用场景: 适用于读多写少的并发场景 CopyOnWriteArrayList 添加的时候是需要加锁的 读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。 ConcurrentHashMap、Collections.synchronizedMap、HashMap与HashTable的区别equals 方法和 hashCode 方法 重写 equals 方法时必须要重写 hashCode 方法 在一个应用程序执行期间,如果一个对象的 equals 方法做比较所用到的信息没有被修改的话,那么对该对象调用 hashCode 方法多次,它必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,这个整数可以不同,即这个应用程序这次执行返回的整数与下一次执行返回的整数可以不一致。 如果两个对象根据 equals(Object) 方法是相等的,那么调用这两个对象中任一个对象的 hashCode 方法必须产生同样的整数结果。 如果两个对象根据 equals(Object) 方法是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,不要求必须产生不同的整数结果。 静态类部类 Node<K,V> 重写了 hashCode 方法 1234public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); // 会去调用key和value重写的hashCode 方法,然后异或} String 的 hashCode h = 31 * h + val[i]; 为什么乘 31 31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终的出来的结果只能被素数本身和被乘数还有1来整除!。(减少冲突) 31可以 由 i*31== (i<<5)-i 来表示,现在很多虚拟机里面都有做相关优化.(提高算法效率) 选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。(减少冲突) 31只占用5bits,相乘造成数据溢出的概率较小。 Hash 冲突的解决方法 开放定址法 线性探测再散列 di = 1 , 2 , 3 , … , m-1 平方探测再散列 di = 1^2 , -1^2 , 2^2 , -2^2 , 3^2 , -3^2 , … , k^2 , -k^2 (避免聚集) 随机探测再散列 di 是一组伪随机数列 链地址法 再哈希 (有多个 hashCode 方法) 建立公共溢出区 HashMap capacity :当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。loadFactor:负载因子,默认为 0.75。threshold:扩容的阈值,等于 capacity * loadFactor 继承自 AbstractMap,实现了 Map 接口 线程不安全 只有containsValue和containsKey方法 允许有一个 null 键和多个 null 值 重新计算 hash 值return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 高16位与低16位异或运算 默认容量为 16 ,扩容时 *2(总是 2 的次方,原因是可以使用位运算取代取模运算) 1.7与1.8的区别 1.7 采用头插法,问题是多线程下扩容可能导致死锁。 1.8 采用尾插法;当链表长度>=8时,采用红黑树,提高检索效率。 HashTable 继承自 Dictionary,实现 Map 、Enumeration接口 线程安全 有containsValue、contains(同containsValue)和containsKey方法 键和值都不允许有 null。put(null, null)时,编译可以通过,但是运行时会抛出空指针异常 直接使用对象的 hash 值 默认容量为 11 ,扩容时 *2+1(保证为奇数)为什么保证为奇数?在应用数据分布在等差数据集合(如偶数)上时,如果公差与桶容量有公约数n,则至少有 (n-1)/n 数量的桶是利用不到的。 实际上 HashMap 也会有此问题,并且不能指定桶容量。所以 HashMap 会在取模哈希前先做一次哈希。 ConcurrentHashMap http://www.importnew.com/22007.html JDK 1.7实现 一个ConcurrentHashMap由多个segment(分段锁)(默认为16个)组成,每个segment包含一个 Entry 的数组。这里比HashMap多了一个segment类。该类继承了ReentrantLock类,所以本身是一个锁。当多线程对ConcurrentHashMap操作时,不是完全锁住map,而是锁住相应的segment。这样提高了并发效率。 Segment 数组不可以扩容,每个segment中的Entry的数组初始大小为2,可以扩容。 ConcurrentHashMap 提供并发的检索和更新操作,功能规范和 hashtable 一样。 更新操作的并发级别是可以配置的,由 concurrencyLevel 决定,作为hint。这个参数将整个hash表分割成相互独立的子表,无竞争操作。这个 concurrencyLevel 的设置根据使用场景的并发修改量来确定,过大则浪费内存空间,过小则影响并发性能。最好是在构造的时候就确定。 当需要遍历、size()、empty()时,需要获取所有的 segment 的锁,效率降低。 执行 size() 时,先尝试不加锁,如果连续两次不加锁得到的结果一致,则认为这个值是正确的。如果连续执行三次,依然不满足,则对每一个 segment 进行加锁求 size。 不允许空键、空值。同hashtable,异hashmap。 迭代元素不会抛出并发修改异常,迭代元素只是迭代器创建时数据结构的快照。迭代同时只能被一个线程使用。 JDK 1.8实现 当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。 put 的详细过程 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); // 得到 hash 值 int hash = spread(key.hashCode()); // 用于记录相应链表的长度 int binCount = 0; for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; // 如果数组"空",进行数组初始化 if (tab == null || (n = tab.length) == 0) // 初始化数组,后面会详细介绍 tab = initTable(); // 找该 hash 值对应的数组下标,得到第一个节点 f else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 如果数组该位置为空, // 用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了 // 如果 CAS 失败,那就是有并发操作,进到下一个循环就好了 if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } // hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容 else if ((fh = f.hash) == MOVED) // 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了 tab = helpTransfer(tab, f); else { // 到这里就是说,f 是该位置的头结点,而且不为空 V oldVal = null; // 获取数组该位置的头结点的监视器锁 synchronized (f) { if (tabAt(tab, i) == f) { if (fh >= 0) { // 头结点的 hash 值大于 0,说明是链表 // 用于累加,记录链表的长度 binCount = 1; // 遍历链表 for (Node<K,V> e = f;; ++binCount) { K ek; // 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } // 到了链表的最末端,将这个新值放到链表的最后面 Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { // 红黑树 Node<K,V> p; binCount = 2; // 调用红黑树的插值方法插入新节点 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } // binCount != 0 说明上面在做链表操作 if (binCount != 0) { // 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8 if (binCount >= TREEIFY_THRESHOLD) // 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换, // 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树 // 具体源码我们就不看了,扩容部分后面说 treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // addCount(1L, binCount); return null;} 详细步骤: 获取table数组 若为null,则初始化initTable 通过hash值找到对应的hash桶,若该桶中没有元素,则先采用CAS机制put新节点,成功则跳出循环 如果当前节点hash值为-1,则说明正在扩容,去帮助扩容helpTransfer(tab, f) 否则对该头节点进行加锁。 如果当前结点hash值>=0,则说明是链表形式,遍历链表,若找到hash值相同且key.equals(ek),则更新该节点中的value,若未找到则在链表尾添加新节点。 如果当前结点hash值小于0,判断如果 f instanceof TreeBin ,那么就是红黑树的结构。 如果再链表中添加节点,判断链表长度是否达到了8,是否需要转换为红黑树。 检索操作get不阻塞,很可能和更新操作重叠,总能得到最近更新完成的值。 1234567static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; // next 也具有 volatile 的特性,可以保证总能得到最近更新完成的值。 // ....} size() 方法可以直接获取 ConcurrentHashMap 中元素的个数。CounterCell 静态内部类包含一个 volatile long value,此 value 表示每一个 hash 桶中元素的数量。然后使用一个 CounterCell 的数组保存每个 hash 中的元素个数。求 size 时只需要统计对数组累加即可。当添加或者删除节点时,采用 CAS 机制修改 CounterCell 数组中的元素的值。 1234@sun.misc.Contended static final class CounterCell { volatile long value; CounterCell(long x) { value = x; }} 扩容:tryPresize sizeCtl 字段用于初始化和扩容时,为负数表示正在初始化或者扩容,-1表示初始化,该数+1表示正在扩容的线程数。 数据迁移:transfer transferIndex 字段表示扩容时,下一个任务的位置。 Collections.synchronizedMap类似与 HashTable 的实现,为每一个方法都加上 synchronized 关键字。 SynchronizedMap 为 Collections 类中的一个内部类,保存一个map对象和一个Object对象的互斥量。 123456private final Map<K,V> m; // Backing Mapfinal Object mutex; // Object on which to synchronizeSynchronizedMap(Map<K,V> m) { this.m = Objects.requireNonNull(m); mutex = this; // mutex默认为当前对象,也可以传递一个对象} 然后调用 map 对象时,先对 mutex 加锁,然后再调用原来 map 的自身的方法。 123public int size() { synchronized (mutex) {return m.size();}} LinkedHashMap为 HashMap 的每个元素添加一个前驱和后继指针形成双链表结构,通过 accessOrder 字段设置链表顺序,为 true ,则按 LRU 顺序(每次访问将元素移到链尾);为 false 则按插入顺序。]]></content>
<categories>
<category>Java 学习</category>
</categories>
<tags>
<tag>笔记整理</tag>
<tag>面经</tag>
<tag>Java集合</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ubuntu16.04 常用命令集合]]></title>
<url>%2F2018%2F04%2F28%2FUbuntu16-04-%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4%E9%9B%86%E5%90%88%2F</url>
<content type="text"><![CDATA[软件安装方法一: 1234sudo apt-get install 软件名 //最常用方法sudo apt-get remove 软件名 //卸载软件sudo apt-get update //更新软件列表,会访问源列表里的每个网址,并读取软件列表,然后保存在本地电脑sudo apt-get upgrade //更新软件,把本地已安装的软件,与刚update软件进行对比,如果发现已安装的软件版本太低,就会提示你更新。 方法二: deb软件包可直接安装 1sudo dpkg -i package.deb //deb包安装方法 deb详解: 123456789dpkg -r package 删除包 dpkg -P package 删除包(包括配置文件)dpkg -L package 列出与该包关联的文件 dpkg -l packag 显示该包的版本edpkg –unpack package.deb 解开 deb 包的内容 dpkg -S keyword 搜索所属的包内容 dpkg -l 列出当前已安装的包dpkg -c package.deb 列出 deb 包的内容 dpkg –configure package 配置包 方法三: rpm软件包安装Ubuntu的软件包格式是deb,如果要安装rpm的包,则要先用alien把rpm转换成deb。 12345sudo apt-get install alien //安装alien工具alien -d *.rpm //转换rpm包为deb包,然后再按方法二安装alien -i *.rpm //直接对rpm包进行安装 文件管理 常用命令: 123456789101112~ //当前用户主目录 cd ~ //home目录(/开头是绝对路径;.开头是相对路径)pwd //获取当前路径mkdir mydir //新建目录mydircp test(此处可为路径) /opt/test //将当前目录下的test复制到test中rm //删除文件mv oldname newname //重命名remove 'y/a-z/A-Z/' *.c //删文件cat test //查看文件test;-n显示行号file test //查看文件类型ls //查看当前目录下的文件ls -l 文件名称 //查看详细信息(文件夹将-l改为-ld) 文件权限说明:12// 文件属性 连接数 文件拥有者 所属群组 文件大小 文件修改时间 文件名drwxrwxr-x 9 mindyu mindyu 4096 4月 29 01:15 Blog r可读,w可写,x 可执行,-不可读/写/执行 文件属性占10个位置例如:d rwx rwx r-x 第一个字符指定了文件类型:‘- ’ 代表非目录的文件‘d’ 代表一个目录。第二段是文件拥有者User的属性,第三段是文件所属群组Group的属性,第四段是对于其它用户Other的属性。 权限修改方法一: 12//chmod [-R] 模式 文件chmod abc file 其中a,b,c各为一个数字,分别表示User、Group、及Other的权限。r=4,w=2,x=1 例子: 若要rwx属性则4+2+1=7;若要rw-属性则4+2=6;若要r-x属性则4+1=5。 123sudo chmod 600 ××× (只有所有者有读和写的权限)sudo chmod 644 ××× (所有者有读和写的权限,组用户和其他用户只有读的权限)sudo chmod 777 ××× (每个人都有读和写以及执行的权限) 方法二: 1chmod [ugoa] {+|-|=} [rwxst] 文件 用户参数:u 文件主 g 同组用户 o 其他用户 a 所有用户操作方法:+ 增加后列权限 - 取消后列权限 = 置成后列权限操作参数:r 可读 w 可写 x 可执行 s 运行时可置UID t 运行时可置GID 123chmod u+rw abc.txt //给用户增加读写权限chmod o-rwx abc.txt //不允许其他用户读写执行chmod g=rx abc.txt //只允许群组读取和执行 apt-get常用命令 update - 取回更新的软件包列表信息 upgrade - 进行一次升级 install - 安装新的软件包(注:软件包名称是 libc6 而非 libc6.deb) remove - 卸载软件包 purge - 卸载并清除软件包的配置 autoremove - 卸载所有自动安装且不再使用的软件包 dist-upgrade - 发布版升级,见 apt-get(8) dselect-upgrade - 根据 dselect 的选择来进行升级 build-dep - 为源码包配置所需的编译依赖关系 clean - 删除所有已下载的包文件 autoclean - 删除已下载的旧包文件 check - 核对以确认系统的依赖关系的完整性 source - 下载源码包文件 download - 下载指定的二进制包到当前目录 changelog - 下载指定软件包,并显示其changelog 解压缩命令.tar解包:tar xvf FileName.tar打包:tar cvf FileName.tar DirName 将DirName文件夹打包成一个FileName.tar文件打包:将一大堆文件或目录变成一个总的文件,压缩:将一个大的文件通过一些压缩算法变成一个小文件。 .gz解压1:gunzip FileName.gz解压2:gzip -d FileName.gz压缩:gzip FileName .tar.gz 和 .tgz解压:tar zxvf FileName.tar.gz压缩:tar zcvf FileName.tar.gz DirName .bz2解压1:bzip2 -d FileName.bz2解压2:bunzip2 FileName.bz2压缩: bzip2 -z FileName .tar.bz2解压:tar jxvf FileName.tar.bz2压缩:tar jcvf FileName.tar.bz2 DirName .bz解压1:bzip2 -d FileName.bz解压2:bunzip2 FileName.bz压缩:未知 .tar.bz解压:tar jxvf FileName.tar.bz压缩:未知 .Z解压:uncompress FileName.Z压缩:compress FileName .tar.Z解压:tar Zxvf FileName.tar.Z压缩:tar Zcvf FileName.tar.Z DirName .zip解压:unzip FileName.zip压缩:zip FileName.zip DirName .rar解压:rar x FileName.rar压缩:rar a -r FileName.rar DirName解压需要安装:sudo apt-get install unrar压缩需要安装:sudo apt-get install rar]]></content>
<categories>
<category>Ubuntu学习</category>
</categories>
<tags>
<tag>Ubuntu16.04</tag>
<tag>Linux命令</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ubuntu16.04LTS安装及配置Hadoop]]></title>
<url>%2F2018%2F04%2F27%2FUbuntu16-04%E5%AE%89%E8%A3%85%E5%8F%8A%E9%85%8D%E7%BD%AEHadoop%E8%AF%A6%E7%BB%86%E8%BF%87%E7%A8%8B%2F</url>
<content type="text">< Jdk版本:1.8.0 MyEclipse版本:MyEclipse2017 CI 10 安装路径信息hadoop安装路径:/usr/local/hadoop/hadoop-2.7.3 jdk路径:/usr/lib/jvm/java-8-oracle MyEclipse路径:/opt/MyEclipse 2017 一、Java 环境安装此处引用我朋友的CSDN上一篇关于java web环境配置的博客 添加ppa 12sudo add-apt-repository ppa:webupd8team/javasudo apt-get update 安装oracle-java-installerJDK8:sudo apt-get install oracle-java8-installerJDK7:sudo apt-get install oracle-java7-installer 安装时会提示你同意Oracle的服务条款,选择ok,然后选择yes这种方式安装后jdk路径为/usr/lib/jvm/java-8-oracle。 二、安装ssh server 实现免密码登录Hadoop需要使用ssh进行通信,首先我们需要在我们的操作系统上安装ssh。在安装之前,我们需要查看系统是否已经安装并且启动了ssh。 12345#查看ssh安装包情况dpkg -l | grep ssh #查看是否启动ssh服务ps -e | grep ssh 如果系统中没有ssh服务,需要先安装:sudo apt-get install openssh-server我安装时出现了connect to host localhost port 22: Connection refused问题,在CSDN博客上找到解决方法: 先用:sudo apt-get install -f 解决依赖问题 然后:sudo apt-get install openssh-server 就可以了 安装完成之后,启动服务:sudo /etc/init.d/ssh start 三、安装Hadoop及配置根据上述地址下载hadoop-2.7.3.tar.gz文件 解压缩下载的文件到指定文件夹tar -zxvf hadoop-2.7.3 -C /usr/local/hadoop/ 配置/usr/local/hadoop/hadoop-2.7.3/etc/hadoop/目录下三个文件 core-site.xml配置如下:12345678910<configuration> <property> <name>fs.default.name</name> <value>hdfs://localhost:9000</value> </property> <property> <name>hadoop.tmp.dir</name> <value>/home/mindyu/tmp</value> </property></configuration> hdfs-site.xml配置如下: 12345678910<configuration> <property> <name>dfs.replication</name> <value>1</value> </property> <property> <name>dfs.permissions</name> <value>false</value> </property></configuration> hadoop-env.sh中进行对应的配置: 1234# The java implementation to use.export JAVA_HOME=/usr/lib/jvm/java-8-oracleexport HADOOP_HOME=/usr/local/hadoop/hadoop-2.7.3export PATH=$PATH:/usr/local/hadoop/hadoop-2.7.3/bin 在系统环境变量中写入hadoop路径:vim /etc/environment然后在文件尾追加:/usr/local/hadoop/hadoop-2.7.3/bin:/usr/local/hadoop/hadoop-2.7.3/sbin 重启系统 验证Hadoop单机模式安装完成hadoop version 启动hdfs 使用伪分布模式,首先完成格式化hadoop namenode -format 启动hdfssbin/start-all.sh 显示进程jps若显示以上内容即说明hdfs已经成功 Hadoop资源管理GUI:Hadoop节点管理GUI: 停止hdfssbin/stop-all.sh Ubuntu16.04 上运行 Hadoop2.7.3 自带wordCount摸索记录 启动hadoop: 运行命令:ps -ef|grep hadoop查询是否有hadoop进程: 在hdfs下创建input文件夹。准备两个文件,比如abc.txt和def.txt 里面各写上一句话(用于统计)。然后导入到hdfs文件系统中的input文件夹下。hadoop fs -put *.txt /input 执行:bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.3.jar wordcount /input /output下次执行时需要将当前的output目录删除或者更换目录hadoop fs -rmr /output 查看hdfs下output中的文件: 查看结果:hadoop fs -cat /output/part-r-00000 myeclipse下搭建hadoop2.7.3开发环境 下载对应版本的myeclipse-hadoo-plugins插件 把解压后的插件(jar文件)放到myeclipse安装目录下的plugins文件夹下 重启MyEclipse window->preferences中会出现Hadoop Map/Reduce选项,选中并设置hadoop 的安装路径 在show view中把map/reduce显示到工具栏 打开 Hadoop Location配置窗口: 配置Map/Reduce Master和DFS Mastrer,Host和Port配置成与core-site.xml的一致 配置完成之后。(如果出现错误,检查hadoop集群是否启动成功,看日志是否有错误,先把错误调完,正确启动集群后再干别的) 新建测试项目File—>Project,选择Map/Reduce Project,输入项目名称等。 将Hadoop源码(源码可在上文hadoop下载链接中下载)中的WordCount类拷贝到项目中 设置Run Configration的arguments参数: 执行WordCount类,查看结果。结果如下:]]></content>
<categories>
<category>Ubuntu学习</category>
</categories>
<tags>
<tag>Ubuntu16.04</tag>
<tag>Hadoop</tag>
<tag>MyEclipse</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ubuntu16.04 常用软件安装]]></title>
<url>%2F2018%2F04%2F26%2FUbuntu%E7%B3%BB%E7%BB%9F%E7%9A%84%E4%BD%BF%E7%94%A8%E5%8F%8A%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4%E9%9B%86%E5%90%88%2F</url>
<content type="text"><![CDATA[Ubuntu系统尝鲜 不知何时起,对Linux操作系统充满了好奇心,想去接触一番。 都说学编程搞开发的都离不开Linux操作系统,个人觉得Linux操作系统更适合学习,少了各种弹窗,界面更加清爽,系统任由你自己去操控,系统也更加稳定,许多的服务器也基本上都是Linux操作系统。然后就自己倒腾,在Windows上装了一个Ubuntu16.04LTS的双系统,通过日常是的使用去渐渐熟悉Linux操作系统,以及各种命令的使用~ 常用软件安装 Shadowsocks安装 12345sudo add-apt-repository ppa:hzwhuang/ss-qt5 sudo apt-get update sudo apt-get install shadowsocks-qt5 在Ubuntu系统上使用还需要设置系统代理,在 设置-》网络-》网络代理 中设置代理模式,最好设置为自动代理方式,然后添加一个pac文件,应用到整个系统即可。 小书匠安装一款Markdown编辑器,支持多平台使用。详细的安装及添加桌面快捷方式可见我的另一篇博客。 安装搜狗输入法其实个人感觉系统自带的输入法也还可以。 3.1 在官网下载搜狗输入法安装包 3.2 切换到deb包所在的目录,并执行以下命令:sudo dkpg -i deb包名 3.3 若出现由于缺少相关依赖文件的错误。执行以下命令,安装所缺少的依赖文件,然后再次使用3.2命令完成安装:sudo apt-get -f install 3.4 在“语言支持”面板,添加汉语支持,并将输入法系统设置为fcitx。然后重启 3.5 在Dash中搜索fcitx configuartion,在Input Method中添加和配置sogoupinyin即可 JDK安装 添加ppa 12sudo add-apt-repository ppa:webupd8team/javasudo apt-get update 安装oracle-java-installer 123456JDK8sudo apt-get install oracle-java8-installerJDK7sudo apt-get install oracle-java7-installer 安装时会提示你同意Oracle的服务条款,选择ok,然后选择yes Eclipse安装安装Eclipsesudo apt-get install eclipse启动Eclipseeclipse以及快捷方式制作,可见小书匠安装的详细博客 MyEclipse安装 在官方中文网下载MyEclipse安装包 我现在的是离线版,解压后为.run为后缀名的文件 双击执行,然后就和windows安装过程一样。选择安装目录比如/opt/MyEclipse/,一路Next即可。最后一步取消勾选。不立即打开MyEclipse。 然后下载对应的破解包,解压之后,执行cracker.jar包java -jar cracker.jar 之后的步骤和Windows破解过程一样。如果你一遍成功,那么就恭喜你。 我第一次安装失败了,因为破解包的问题,所以要确保破解包的正确性。 失败之后存在一个卸载不干净的问题。我也没有找到解决办法。 换了一台电脑,另外找了一个破解包,完美破解。 Google浏览器安装 下载 Chrome(Linux 版)https://www.google.cn/intl/zh-CN/chrome/ 终端下输入如下内容来安装.deb文件sudo dpkg -i 软件包名.deb 然后在Dash中搜索Google,打开即可。 使用中可能遇到两个问题: (1)重启之后,Google浏览器就打不开了 (2)设置新标签页打开网页 解决方案: 问题1: 网上的解决办法,大多是删除chrome的配置文件 rm -r~/.config/google-chrome 但是指标不治本,下次打开依然会出现问题 真正的解决方案是: 安装gnome-keyring。因为在登录谷歌账户之后,chrome会使用gnome-keyring来保护你的账户。 sudo apt install gnome-keyring 问题2: 打开google页面,任意搜索一个内容,在搜索结果页面的上面靠中间位置有一个设置,点击设置然后选择 搜索设置 ,即进入如下页面。 然后勾选结果打开方式的 在新的浏览器窗口中打开所选的每条搜索结果即可。 系统监视器 实时查看电脑的cpu,内存占用率,更可以查看网速安装: 12345sudo add-apt-repository ppa:fossfreedom/indicator-sysmonitor sudo apt-get update sudo apt-get install indicator-sysmonitor WPS 安装 在WPS官网下载alpha版本,我下载的是wps-office_10.1.0.5672~a21_amd64.deb 执行安装命令:sudo dpkg -i wps-office_10.1.0.5672~a21_amd64.deb 安装完成后,在Dash中即可搜索到,打开会发现字体缺失的问题 下载字体包 创建目录:sudo mkdir /usr/share/fonts/wps-office 将下载的字体复制到创建的目录:sudo cp -r wps_symbol_fonts.zip /usr/share/fonts/wps-office 切换到/usr/share/fonts/wps-office目录解压字体包:sudo unzip wps_symbol_fonts.zip 解压后删除字体包:sudo rm -r wps_symbol_fonts.zip再次打开就不会提示缺失字体包了! 下载工具uget安装 12345sudo add-apt-repository ppa:plushuang-tw/uget-stablesudo apt-get updatesudo apt-get install uget 终端和浏览器下载东西比较慢的时候可以试试,下载速度还是比较可观! 参考博客:https://www.cnblogs.com/wadxy1314/p/6880264.html https://blog.csdn.net/Jesse_Mx/article/details/52816928 https://blog.csdn.net/u011324454/article/details/78497021 https://blog.csdn.net/VectorWWW/article/details/78820156 https://my.oschina.net/renwofei423/blog/635798]]></content>
<categories>
<category>Ubuntu学习</category>
</categories>
<tags>
<tag>Ubuntu16.04</tag>
<tag>Linux命令</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ubuntu安装小书匠并创建桌面快捷方式]]></title>
<url>%2F2018%2F04%2F24%2FUbuntu%E5%AE%89%E8%A3%85%E5%B0%8F%E4%B9%A6%E5%8C%A0%E5%B9%B6%E5%88%9B%E5%BB%BA%E6%A1%8C%E9%9D%A2%E5%BF%AB%E6%8D%B7%E6%96%B9%E5%BC%8F%2F</url>
<content type="text"><![CDATA[小书匠简介 小书匠编辑器是一款专为markdown写作而设计的编辑器。 支持多种编辑模式。单栏编辑,双栏编辑,三栏编辑,实时预览,全屏写作,全屏阅读…想怎么切换,就怎么切换,就是这样随心所欲。 多种主题选择、丰富的语法支持、第三方同步等功能特色。原先在windows上习惯了使用小书匠,然后就想着在Ubuntu上也装一个。 小书匠安装 小书匠已经托管到github,首先下载对应版本到本地。https://github.com/suziwen/markdownxiaoshujiang 解压下载下来的zip文件到指定文件夹:# unzip Story-writer-linux64.zip.zip -d /opt/Story-Writer/ 切换到解压出来的文件夹根目录,然后启动软件# ./Story-writer即可启动软件 存在的问题 但是存在两个问题: 无快捷方式,每次打开过于麻烦 不能设置小书匠为系统默认打开.md文件 创建小书匠桌面快捷方式 对于没有快捷方式这个问题,让我很容易想到当初安装Eclipse的时候也没有快捷启动方式。然后在网上找到解决方案。 UBuntu的菜单图标保存在/usr/share/applications目录下,所以先在该目录下建一个eclipse.desktop文件sudo gedit /usr/share/applications/eclipse.desktop 在文件中输入以下内容 12345678910[Desktop Entry]Encoding=UTF-8Name=Eclipse Platfrom //图标名字,可修改Comment=Eclipse IDE //注释,可修改Exec=/opt/eclipse/eclipse //Eclipse文件夹下eclipse的路径Icon=/opt/eclipse/icon.xpm //Eclipse文件夹下图标icon.xpm的路径Terminal=falseStartupNotify=trueType=ApplicationCategories=Application;Development; 对该文件进行赋权 chmod u+x /usr/share/applications/eclipse.desktop 双击eclipse的图标就可以运行,然后复制到桌面就可以了 根据Ecllipse图标的制作,同理,然后试了小书匠是否也可以。 12345678910[Desktop Entry]Encoding=UTF-8Name=Story_Writer Comment=Story_WriterExec=/opt/Story_Writer/Story-writerIcon=/opt/Story_Writer/Story-writer.pngTerminal=falseStartupNotify=trueType=ApplicationCategories=Application;Development; 如图: 然后就出现了一个Story_Writer的快捷方式,双击即可打开小书匠。是不是很简单~ 对于另一个问题目前还没找到解决方案…. 原谅我还只是一个小白。如果有幸你可以看到,请留言我!]]></content>
<categories>
<category>Ubuntu学习</category>
</categories>
<tags>
<tag>Ubuntu16.04</tag>
<tag>小书匠</tag>
<tag>Markdown</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浅析素数环问题]]></title>
<url>%2F2018%2F03%2F18%2F%E6%B5%85%E6%9E%90%E7%B4%A0%E6%95%B0%E7%8E%AF%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[素数环的实现问题描述 从1到n这n个整数围成一个圆环,若其中任意2个相邻的数字相加,结果均为素数,那么这个环就成为素数环。 现在要求输入一个n,求n个数围成一圈有多少种素数环,规定第一个数字是1。 设计思路 数据结构:res数组用于保存最后满足描述的情况,isUsed数组用于标记1-n这些数字是否被使用,已达到不重复的效果。 规定第一个数字为1,res[0] = 1; 依次后面的值有两个限制 a.不能和前面的数字重复 b.和前一个数字相加为素数。 代码实现12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364import java.util.Scanner;public class Main { final static int max = 20; static int N; static int[] res = new int[max]; static boolean[] isUsed = new boolean[max]; public static void main(String[] args) { Scanner sc = new Scanner(System.in); int k = 1; for (int i = 0; i < isUsed.length; i++) { res[i] = i+1; isUsed[i] = false; } N = sc.nextInt(); while(N!=0){ System.out.println("Case "+ k++ +":"); if (N%2==0) { primeRing(1); }else { System.out.println("No Answer"); } N = sc.nextInt(); } sc.close(); } // 递归实现,输出素数环 public static void primeRing(int cur){ if (cur==N && isPrime(res[cur-1]+res[0])) { // 执行完成,并且首尾相加也是素数 for (int i = 0; i < N-1; i++) { System.out.print(res[i]+" "); }System.out.print(res[N-1]); System.out.println(); return; }else { for (int i = 2; i <= N; i++) { // 选取一个满足条件的值,继续递归 if (!isUsed[i] && isPrime(i+res[cur-1])) { res[cur] = i; isUsed[i] = true; primeRing(cur+1); isUsed[i] = false; } } } } // 判断是否为素数 public static boolean isPrime(int x) { if (x<3) { // 两数相加必定大于2,所以小于3的值不用考虑 return false; }else { for (int i = 2; i <= Math.sqrt(x); i++) { if (x%i==0) { return false; } } } return true; }} 运行结果 素数表格问题描述 前两天遇到一个问题和素数有关,从1到n中选择9个数字,填入3*3的表格中,使得相邻的数字相加和为素数。输出所有的情况。 设计思路 在CSDN上面看到过一片关于这个问题的博客。相邻两方格内的两个整数之和为质数-经典算法详解。理解起来可能还要一时半会儿。但是我觉得有了上述素数环的理解以及实现,只需要稍微修改一下上面的代码就可以达到表格的要求。 将3*3二维表格转换为一维数组。以下表格的数值代表数组中的位置。 0 1 2 7 8 3 6 5 4 问题可以简化为数组0-7下标组成一个素数环。然后数组res[8]同时与res1、res3、res[5]、res[7]分别相加合为素数。 代码实现 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465import java.util.Scanner;public class Main { final static int max = 20; static int N; static int[] res = new int[max]; static boolean[] isUsed = new boolean[max]; public static void main(String[] args) { Scanner sc = new Scanner(System.in); for (int i = 0; i < isUsed.length; i++) { res[i] = i+1; isUsed[i] = false; } N = sc.nextInt(); primeRing(1); sc.close(); } // 递归实现,输出素数环 public static void primeRing(int cur){ if (cur==8 && isPrime(res[cur-1]+res[0])) { // res[0]+res[7]和也为素数 for (int i = 2; i <= N; i++) { // 选取最后一个满足条件的值,填入res[8]位置 if (!isUsed[i] && isPrime(i+res[1]) && isPrime(i+res[3]) && isPrime(i+res[5]) && isPrime(i+res[7])) { for (int j = 0; j < 3; j++) // 输出第一行 System.out.print(res[j]+" "); System.out.println(); System.out.print(res[7]+" "); // 输出第二行 System.out.print(i+" "); System.out.print(res[3]+" "); System.out.println(); for (int j = 6; j > 3; j--) // 输出第三行 System.out.print(res[j]+" "); System.out.println(); System.out.println(); } } return; }else if(cur<8){ // 数组0-7下标组成一个素数环 for (int i = 2; i <= N; i++) { // 选取一个满足条件的值,继续递归 if (!isUsed[i] && isPrime(i+res[cur-1])) { res[cur] = i; isUsed[i] = true; primeRing(cur+1); isUsed[i] = false; } } } } // 判断是否为素数 public static boolean isPrime(int x) { if (x<3) { // 两数相加必定大于2,所以小于3的值不用考虑 return false; }else { for (int i = 2; i <= Math.sqrt(x); i++) { if (x%i==0) { return false; } } } return true; }} 运行截图]]></content>
<categories>
<category>算法分析</category>
</categories>
<tags>
<tag>数据结构</tag>
<tag>素数环问题</tag>
<tag>3*3素数表格</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浅析n元素出栈序列]]></title>
<url>%2F2018%2F02%2F24%2F%E6%B5%85%E6%9E%90n%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%E5%BA%8F%E5%88%97%2F</url>
<content type="text"><![CDATA[栈的简介 栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。遵循后进先出的原则。 n个元素出栈顺序种数 问题描述有n个元素依次进栈,请问总共有多少种出栈序列? 算法分析首先列举出简单的情况:当1个元素进栈,有1种出栈顺序;当2个元素进栈,有2种出栈顺序;当3个元素进栈,有5种出栈顺序 ;我们把n个元素的出栈个数的记为f(n), 则对于1,2,3有 123f(1) = 1 //即 1f(2) = 2 //即 12、21f(3) = 5 //即 123、132、213、321、231 对于f(4),我们假定是a,b,c,d四个元素。任意一个元素在其出栈序列中只有4个位置。取元素a分别讨论其在出栈序列中的位置: 当a元素在位置1时,只可能是a先入栈,然后出栈。接下来就是其它三个元素b,c,d的出栈序列,也就是子问题f(3)。 元素a在位置2处,有一个元素比a先出栈可能的序列即f(1),另外两个元素在位置3,4即f(2)。 元素a在位置3处,有两个元素比a先出栈可能的序列即f(2),另外两个元素在位置4即f(1)。 元素a在位置4处,有三个元素比a先出栈可能的序列即f(3). 那么f(4) = f(3) + f(2) f(1) + f(1) f(2) + f(3); 然后推广到n,按同理我们可以很容易的得到:f(n) = f(0)f(n-1) + f(1)f(n-2) + … + f(n-1)*f(0) 上式也就是卡特兰数(Catalen):卡特兰数的通项公式为:变换形式:另类递归式: C(n)=((4n-2)/(n+1))C(n-1); 卡特兰数的应用: 括号化问题。 矩阵链乘: P=a1×a2×a3×……×an,依据乘法结合律,不改变其顺序,只用括号表示成对的乘积,试问有几种括号化的方案?(C(n-1)种) 出栈次序问题。 n个元素依次进栈,请问总共有多少种出栈序列? 案例:2012腾讯实习招聘笔试题在图书馆一共6个人在排队,3个还《面试宝典》一书,3个在借《面试宝典》一书,图书馆此时没有了面试宝典了,求他们排队的总数? 解析:还书相当于入栈,借书相当于出栈。只有当栈内元素不为空时才可以借书。C(3) = 5。借书3个人,还书3个人求全排列。 总数为53!3! = 180。 将多边行划分为三角形问题。 将一个凸N+2多边形区域分成三角形区域的方法数?类似:一位大城市的律师在她住所以北n个街区和以东n个街区处工作。每天她走2n个街区去上班。如果她从不穿越(但可以碰到)从家到办公室的对角线,那么有多少条可能的道路? 类似:在圆上选择2n个点,将这些点成对连接起来使得所得到的n条线段不相交的方法数? 给顶节点组成二叉树的问题。 给定N个节点,能构成多少种不同的二叉树?(能构成Cn个)Catalan数的解法:Catalan数的组合公式为 Cn=C(2n,n) / (n+1);此数的递归公式为 C(n ) = C(n-1)(4n-2) / (n+1) 代码实现 123456789101112131415161718192021222324252627282930313233343536373839import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int[] arr = new int[n+1]; arr[0] = 1;arr[1] = 1; Catalen_1(arr, n);// Catalen_2(arr, n); for (int i = 1;i<=n;i++){ System.out.println(arr[i]); } sc.close(); } public static void Catalen_1(int[] arr, int n) { // 递推关系式 f(n) = f(0)*f(n-1) + f(1)*f(n-2) + … + f(n-1)*f(0) for (int i=2; i<=n; ++i) { for (int j=0; j<i; ++j) { arr[i] += arr[j] * arr[i-1-j]; } } } public static void Catalen_2(int[] arr, int n) { // 递推关系式 C(n)=((4*n-2)/(n+1))*C(n-1) for (int i=2; i<=n; ++i) { arr[i] = arr[i-1] * (4*i-2) / (i+1); } } } 运行结果 n个元素出栈序列输出 设计思路 所需数据结构,1栈2队列,输入队列从头部取出数据压入栈中,数据出栈进入输出队列。最终输出队列即为出栈序列的情况。采用递归的方式,将大问题分解为小问题。栈存在两种状态:1.输入队列入栈,2.输出队列出栈。 代码实现 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950import java.util.LinkedList;import java.util.Queue;import java.util.Scanner;import java.util.Stack;public class Main { static int n; public static void main(String[] args) { Scanner sc = new Scanner(System.in); n = sc.nextInt(); // 元素个数 Stack<Integer> s = new Stack<Integer>(); Queue<Integer> in = new LinkedList<Integer>(); for (int i = 1; i <= n; i++) { in.offer(i); } Queue<Integer> out = new LinkedList<Integer>(); printAllOutStackSeq(in, s, out); sc.close(); } @SuppressWarnings("unchecked") public static void printAllOutStackSeq(Queue<Integer> in, Stack<Integer> s, Queue<Integer> out) { if (out.size()==n) { // 所有元素都出栈了 while( !out.isEmpty() ){ System.out.print(out.poll()+"");; } System.out.println(); return; } Queue<Integer> inCopy = new LinkedList<Integer>(in); Stack<Integer> sCopy = (Stack<Integer>) s.clone(); Queue<Integer> outCopy = new LinkedList<Integer>(out); if( !s.empty() ) { // 出栈,将元素出栈,push到结果队列中 out.offer( s.pop() ); printAllOutStackSeq( inCopy, s, out ); } if ( !in.isEmpty() ) { // 入栈,将输入队列出队,进行入栈 sCopy.push(in.poll()); printAllOutStackSeq( in, sCopy, outCopy ); } return; }} 运行结果 判断一个序列是否是可能的出栈序列 设计思路 输入一个序列,判断该序列是否可能是正确的出栈序列(反向推理)。代码中A代表着有序的输入队列(1,2,3….,n)代码中B代表着所输入的目标序列的下标,从下标为1的位置开始匹配。代码中S代表栈,进行入栈、出栈操作。A == target[B] 当前输入队列头元素与目标序列B位置元素相同,也就是输入队列元素取头元素进行入栈,然后立即出栈。!s.isEmpty() && s.peek() == target[B] 当前栈顶元素与目标序列B位置元素相同,也就是栈内元素出栈。A <= n 上述两种情况都不满足时,也就是输入队列的头元素一直进行入栈操作。最后也不入栈也不出栈,也不就意味着目标序列不是出栈序列。 代码实现 123456789101112131415161718192021222324252627282930313233343536import java.util.Scanner;import java.util.Stack;public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); while(sc.hasNext()){ int n = sc.nextInt(); // 元素个数 if (n==0) break; int[] target = new int[n+1]; Stack<Integer> s = new Stack<Integer>(); for (int i = 1; i <= n; i++) { target[i] = sc.nextInt(); } int A = 1, B = 1, flag = 1; while(B <= n){ if (A == target[B]) { A++;B++; }else if (!s.isEmpty() && s.peek() == target[B]) { s.pop(); B++; }else if (A <= n ) { s.push(A++); }else { flag = 0; break; } } System.out.println(flag==1?"Yes":"No"); } sc.close(); }} 运行结果]]></content>
<categories>
<category>算法分析</category>
</categories>
<tags>
<tag>栈</tag>
<tag>数据结构</tag>
<tag>卡特兰数</tag>
<tag>Catalen</tag>
</tags>
</entry>
<entry>
<title><![CDATA[操作系统经典算法之银行家算法]]></title>
<url>%2F2017%2F12%2F13%2F%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%BB%8F%E5%85%B8%E7%AE%97%E6%B3%95%E4%B9%8B%E9%93%B6%E8%A1%8C%E5%AE%B6%E7%AE%97%E6%B3%95%2F</url>
<content type="text"><![CDATA[需求分析1. 银行家算法的实现思想 允许进程动态地申请资源,系统在每次实施资源分配之前,先计算资源分配的安全性,若此次资源分配安全(即资源分配后,系统能按某种顺序来为每个进程分配其所需的资源,直至最大需求,使每个进程都可以顺利地完成),便将资源分配给进程,否则不分配资源,让进程等待。 2. 死锁的概念 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。 此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。银行家算法是避免死锁的一种重要方法。 操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程已占用的资源数与本次申请的资源数之和是否超过了该进程对资源的最大需求量。若超过则拒绝分配资源,若没有超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配。 3. 产生死锁的必要条件① 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。② 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。③ 不可抢占条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。④ 循环等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,•••,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。 4.功能实现 理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何能够不让这四个必要条件同时成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源,在系统运行过程中,对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若分配后系统可能发生死锁,则不予分配,否则予以分配 。因此,对资源的分配要给予合理的规划。 概要设计1.数据结构 1) 可利用资源向量Available。这是一个含有m个元素的数组,其中的而每一个元素代表一类可利用资源数目,其初始值是系统中所配置的该类全部可用资源的数目,其数值随该类资源的分配和回收而动态的改变。如果Available[j]=K,则表示系统中现有Rj类资源K个。 2) 最大需求矩阵Max。这是一个n * m的矩阵,它定义了系统中n个进程中的每一个进程对m类资源的最大需求。如果Max[i,j]=K;则表示进程i需要Rj类资源的最大数目为K。 3) 分配矩阵Allocation。这也是一个n * m的矩阵,它定义了系统中每一类资源当前已分配给每一进程的资源数。如果Allocation[i,j]=K,则表示进程i当前已分得Rj类资源的数目为K。 4) 需求矩阵Need。这也是一个n * m的矩阵,用以表示每一个进程尚需的各类资源数。如果Need[i,j]=K,则表示进程i还需要Rj类资源K个,方能完成任务。 上述三个矩阵间存在下述关系: Need[i,j]=Max[i,j]-Allocation[i,j] 设计思路第一部分:银行家算法模块 如果Request<=Need,则转向2;否则,出错 如果Request<=Available,则转向3,否则等待 系统试探分配请求的资源给进程 系统执行安全性算法 第二部分:安全性算法模块 设置两个向量① 工作向量:Work=Available(表示系统可提供给进程继续运行所需要的各类资源数目)② Finish:表示系统是否有足够资源分配给进程(True:有;False:没有).初始化为False 若Finish[i]=False&&Need<=Work,则执行3;否则执行4(i为资源类别) 进程P获得第i类资源,则顺利执行直至完成,并释放资源: Work=Work+Allocation; Finish[i]=true;转2 若所有进程的Finish[i]=true,则表示系统安全;否则,不安全! 详细设计1.银行家算法 设Request i是进程Pi的申请向量,如果Request i[j]=K,则表示进程Pi需要K个Rj类型的资源。当Pi发出资源请求后,系统按下述步骤进行检查: 1) 如果Request i[j]<=Need[i,j],便转向步骤2);否则认为出错,因为它所需要的资源数已经超过它所宣布的最大值。 2) 如果Request i[j]<=Available[i,j],便转向步骤3);否则,表示尚无足够资源,Pi需等待。 3) 系统试探着把资源分配给进程Pi,并修改下面数据结构中的数值: 123Available[j]:=Available[j]-Request i[j];Allocation[i,j]:=Allocation[i,j]+Request i[j];Need[i,j]:=Need[i,j]-Request i[j]; 4) 系统执行安全性算法,检查此次资源分配后系统是否处于安全状态。若安全,才正式将资源分配给进程Pi,以完成本次分配;否则,将本次的试探分配作废,恢复原来的资源分配状态,让进程Pi等待。 2.安全性算法 系统所执行的安全性算法可描述如下: 1) 设置两个向量 ① 工作向量Work,它表示系统可提供给进程继续运行所需的各类资源数目,它含有m个元素,在执行安全算法开始时,Work:=Available。 ② Finish,它表示系统是否有足够的资源分配给进程,使之运行完成。开始时先做Finish[i]:=false;当有足够资源分配给进程时,再令Finish[i]:=ture. 2) 从进程集合中找到一个满足下述条件的进程: ① Finish[i]=false; ② Need[i,j]<=Work[j];若找不到,执行步骤3),否则,执行步骤4)。 3) 当进程Pi获得资源后,可顺利执行,直至完成,并释放出分配给它的资源,故应执行: 123Work[j]:=Work[j]+Allocation[i,j];Finish[i]:=true;Go to step 2; 4) 如果所有进程的Finish[i]=true都满足,则表示系统处于安全状态;否则,系统处于不安全状态。 代码流程图: 代码实现123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296#include<stdio.h>#include<stdlib.h>int Available[10]; //可使用资源向量int Max[10][10]; //最大需求矩阵int Allocation[10][10] = { 0 }; //分配矩阵int Need[10][10] = { 0 }; //需求矩阵int Work[10]; //工作向量bool Finish[10]; //是否有足够的资源分配,状态标志int Request[10][10]; //进程申请资源向量int Pause[10];int arr[] = { 0 }; //各类资源总数int List[10];int i, j;int n; //系统资源种类数int m; //总的进程数int a; //当前申请的进程号int l, e, z = 0; //计数器int b = 0, c = 0, f = 0, g; //显示菜单void menu(){ printf("*************************银行家算法**************************\n\n"); printf("\n\n\t1:初始化数据"); printf("\n \t2:设置进程资源申请"); printf("\n \t3:查看资源分配状态"); printf("\n \t4:退出程序"); printf("\n\n\t\t\t 请输入你的选择: ");}//显示资源分配状态void mainshow(){ printf("\n\n"); if (n == 3) { printf(" 最大需求量 已分配 尚需要量 \n进程"); } if (n == 2) { printf(" 最大需求 已分配 尚需要量 \n进程"); } for (i = 1; i <= 3; i++) { for (j = 1; j <= n; j++) { printf(" %d类", j); } } for (i = 1; i <= m; i++) { printf("\nP[%d]", i); for (j = 1; j <= n; j++) { printf(" %2d ", Max[i][j]); //最大需求 } for (j = 1; j <= n; j++) { printf(" %2d ", Allocation[i][j]); //分配 } for (j = 1; j <= n; j++) { printf(" %2d ", Need[i][j]); //需求 } } printf("\n\n系统剩余资源量: "); for (i = 1; i <= n; i++) { printf(" %d ", Available[i]); } printf("\n");}//安全性检测int securitycheck(){ printf("\n\n"); printf("**************************安全性检测**************************\n\n"); printf(" 工作向量 尚需求量 已分配 工作向量+已分配 Finish \n进程 "); for (c = 1; c <= 4; c++) { for (j = 1; j <= n; j++) { printf(" %d类", j); } } for (j = 1; j <= n; j++) { Pause[j] = Available[j]; //Pause[i]为防止在下面安全性检查时修改到Available[i]而定义的备份 } for (i = 1; i <= m; i++) { Finish[i] = false; } for (i = 1; i <= m; i++) { b = 0; //计数器初始化 if (!Finish[i]){ for (j = 1; j <= n; j++) //资源种类数 { if (Need[i][j] <= Pause[j]) //可使用资源向量备份,若3类资源均满足条件 { b = b + 1; } else { break; } if (b == n) { Finish[i] = true; printf("\nP[%d] ", i); //依次输出进程安全序列 for (l = 1; l <= n; l++) { printf(" %2d ", Pause[l]); } for (j = 1; j <= n; j++) { printf(" %2d ", Need[i][j]); } for (j = 1; j <= n; j++) { //Allocation[i][j]=Pause[j]-Need[i][j]; printf(" %2d ", Allocation[i][j]); } for (j = 1; j <= n; j++) { printf(" %2d ", Pause[j] + Allocation[i][j]); } printf(" true"); for (l = 1; l <= n; l++) { Pause[l] = Pause[l] + Allocation[i][l]; //回收该进程资源 } i = 0; break; } } } } printf("\n\n"); for (i = 1; i <= m; i++) { if (Finish[i] == true) f = f + 1; //统计Finish[i]==true的个数 } if (f == m) //全为true { printf("处于安全状态"); printf("\n\n系统剩余资源量: "); for (i = 1; i <= n; i++) { printf(" %d ", Available[i]); } f = 0; //将计数器f重新初始化,为下一次提出新的进程申请做准备 printf("\n\n"); return 1; } else { printf("处于不安全状态"); for (i = 1; i <= n; i++) { Available[i] = Available[i] + Request[a][i]; Allocation[a][i] = Allocation[a][i] - Request[a][i]; Need[a][i] = Need[a][i] + Request[a][i]; } printf("\n\n"); return 0; }}//初始化数据void initialize(){ printf("请输入系统的资源种类数:"); scanf_s("%d", &n); printf("请输入%d类资源总数(以空格分隔): ", n); for (i = 1; i <= n; i++) { scanf_s("%d", &arr[i]); } printf("请输入进程总数:"); scanf_s("%d", &m); for (i = 1; i <= m; i++) { printf("进程P[%d]对这%d类资源的最大需求量(以空格分隔): ", i, n); for (j = 1; j <= n; j++) { scanf_s("%d", &Max[i][j]); } } for (i = 1; i <= m; i++) { printf("进程P[%d]对这%d类资源已分配数(以空格分隔): ", i, n); for (j = 1; j <= n; j++) { scanf_s("%d", &Allocation[i][j]); Need[i][j] = Max[i][j] - Allocation[i][j]; //尚需资源数为 最大-已分配 } } for (i = 1; i <= n; i++) { for (j = 1; j <= m; j++) { arr[i] -= Allocation[j][i]; //各类资源总数-各进程分配的资源数 } } for (i = 1; i <= n; i++) Available[i] = arr[i]; securitycheck();}//进程申请资源void mainrequest(){ printf("请输入申请资源的进程:"); scanf_s("%d", &a); for (i = 1; i <= n; i++) { printf("请输入进程P[%d]对%d类资源的申请量:", a, i); scanf_s("%d", &Request[a][i]); if (Request[a][i] > Need[a][i]) { printf("\n出错!进程申请的资源数多于它自己申报的最大需求量\n"); return; } if (Request[a][i] > Available[i]) { printf("\nP[%d]请求的资源数大于可用资源数,必须等待\n", a); return; } } for (i = 1; i <= n; i++) { //以下是试探性分配 Available[i] = Available[i] - Request[a][i]; Allocation[a][i] = Allocation[a][i] + Request[a][i]; Need[a][i] = Need[a][i] - Request[a][i]; } int ret = securitycheck(); if (ret == 1) { int key = 0; for (j = 1; j <= n; j++) { if (Need[a][j] == 0) { key++; } } if (key == n) { for (j = 1; j <= n; j++) { Available[j] += Allocation[a][j]; Allocation[a][j] = 0; } } }}int main(){ int key = 0; printf("\n\n"); while (1) { menu(); scanf_s("%d", &key); printf("\n\n"); switch (key) { case 1: initialize(); break; case 2: mainrequest(); break; case 3: mainshow(); break; case 4: printf("\n\n\t\t谢谢使用 \n"); printf("\n\t\tMade by 杨陈强!\n\n\n"); system("pause"); return 0; } } system("pause"); return 0;} 结果分析程序菜单界面: 初始化数据:(模拟课本113例子) T0时刻的安全性: 由上图可知,存在安全序列{P2,P4,P1,P3,P5},(存在多种情况,此处只寻找到一种可行的情况)故系统处于安全状态。 P2请求资源: P2发出请求向量Request2(1,0,2),系统按银行家算法进行检查。然后再通过安全性算法检查此时系统是否处于安全状态。上图为安全状态! 银行家算法测试: 当进程1申请的资源大于系统剩余的资源时,提示进程必须等待! 安全性算法测试: 当进程1申请的资源过多时,导致各进程处于请求与保持状态,提示系统处于不安全状态!并且此次资源申请无效 查看资源分配情况:(依然处于第一次P2请求资源结束的状态) 通过这次课程设计,让我对银行家算法(避免死锁)有了更深入的理解,同时借鉴网上一些优秀的实现过程,自己理解并修改而记之!]]></content>
<categories>
<category>学习笔记</category>
</categories>
<tags>
<tag>操作系统</tag>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用Android Canvas绘制折线图]]></title>
<url>%2F2017%2F12%2F08%2F%E4%BD%BF%E7%94%A8Android-Canvas%E7%BB%98%E5%88%B6%E6%8A%98%E7%BA%BF%E5%9B%BE%2F</url>
<content type="text"><![CDATA[优化画图方法 原先磁感应力检测仪项目的绘图方法一直是采用的Android AChartEngine方法,简单介绍一下AChartEngine,它是 Android 平台的图表开发库, 能绘制 折线图, 饼图, 气泡图, 柱状图, 散点图, 面积图等统计图表。网上有很多例子,推荐一个Android 图表绘制 achartengine 示例解析。该方法优点:使用简单,只需要将数据传入到数据集,并设置相应渲染器的绘制颜色,线的粗细等等。缺点:当数据量较大时,在移动与缩放时会显得比较卡顿。 接下来就介绍一种新的绘制方法:Android Canvas方法 当我们在自定义 View的时候,我们经常需要绘制一些自己想要的效果。通过Canvas对象我们就可以绘制出我们自己想要的效果。比如折线图、平面图等等。对于一些简单的原理操作网上有很多Demo,推荐一个我在简书上面看到的一篇Android Canvas 方法总结,里面介绍了关于Canvas的平移、缩放、旋转,以及后面使用Canvas对象绘制直线、各种形状以及文字。通过图形的展示,我相信很快就可以看懂啦! 正式开始我做的内容: 首先贴一张我们所需要最终达到的效果图 坐标线的绘制: 此时需要注意的是Canvas画布的坐标是以 左上角为坐标原点的,水平向右为X轴的正方向,垂直向下为Y轴的正方向,而为了美观以及需求,我们需要将坐标原点设置在左下角,同时Y轴的正方向为垂直向上。而达到最终效果。坐标原点设置在左下角可以通过简单的平移画布而实现坐标(0,0)位于View的左下角,但是方向岂不是还要通过翻转? 这里其实并没有翻转这个方法,我指的其实就是通过坐标数据计算它对应的位置。画布的坐标原点并非你所显示的坐标原点。这样会使得画图麻烦很多。此时通过一个巧妙地方法,坐标数据的Y值我给它一个负号,那么如果是(2,5)和(3,7),而我实际将它们绘制在画布上的(2,-5)和(3,-7);而-7比-5小,正好-7就在-5的上方,从而模拟出正方形为垂直向上。 123456789//绘制坐标线@Overridepublic void drawAxis(Canvas canvas) { canvasWidth -= 140f; //留有边距以显示坐标对应的值 canvasHeight -= 140f; canvas.translate(110f, canvasHeight + 40f); // 使画布向 x 轴正向移动 110f,向 y 轴移动 40f canvas.drawLine(0, 0, canvasWidth, 0, paint); // 绘制 x 轴 canvas.drawLine(0, 0, 0,-canvasHeight, paint); // 绘制 y 轴} 坐标刻度的绘制 坐标主要需要完成动态的适配,当平移和缩放的时候坐标动态显示,并且完成它们之间的间距控制,所显示的画布上坐标轴上只显示2-6个坐标,避免坐标刻度太密而使得刻度值挤在一块无法显示。实现思路是:a.通过float interval = measureInterval(xDistance/xScale); //每组值得间隔计算当前显示区域的相邻坐标的间距。b.通过int n = (int) Math.ceil((xDistance/xScale)/interval); //一组有几个值计算当前显示区域的坐标数量也就对于上述的(2-6)c.通过 1int first = (int) Math.floor((xStart-((1-xScale)/2+xTranslate/canvasWidth)*xDistance/xScale)/interval); 计算出所显示的首坐标的位置。 d.循环n次,依次显示n个从first+i开始的坐标刻度值。 123456// 所需要的坐标属性:protected float xDistance; // x 方向总距离protected float yDistance; // y 方向总距离protected float xStart=0, yStart=0; // 起始点坐标protected float xTranslate = 0, yTranslate = 0; // 分别控制 x 和 y 方向的平移距离,单位为屏幕像素,达到手指移动多少,坐标平移多少protected float xScale = 1, yScale = 1; // 分别控制 x 和 y 方向的缩放程度 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667//绘制坐标值及刻度@Overridepublic void drawCoordinates(Canvas canvas) { String number; DecimalFormat df1 = new DecimalFormat("0.0"); DecimalFormat df2 = new DecimalFormat("0.00"); DecimalFormat df3 = new DecimalFormat("0.000"); // 绘制 x 轴坐标 canvas.save(); // 保存画布状态 canvas.clipRect(0, 0, canvasWidth, 60f); // 切割画布,使坐标显示在一定范围内 canvas.translate((1 - xScale) / 2 * canvasWidth, 0); // 缩放时,平移使得与折线图一致 canvas.translate(xTranslate, 0); // 使坐标跟着图形一起平移 float interval = measureInterval(xDistance/xScale); //每组值得间隔 int n = (int) Math.ceil((xDistance/xScale)/interval); //一组有几个值 int first = (int) Math.floor((xStart-((1-xScale)/2+xTranslate/canvasWidth)*xDistance/xScale)/interval); for (int i = 0; i <= n; i++){ if (interval < 0.01){ number = df3.format((first+i)*interval); }else if (interval <= 0.1){ number = df2.format((first+i)*interval); }else if (interval < 1){ number = df1.format((first+i)*interval); }else { number = String.valueOf((first+i)*(int)interval); } canvas.drawLine((canvasWidth * (first+i) * interval / xDistance * xScale), 0f, (canvasWidth * (first+i) * interval / xDistance * xScale), 10f,paint); canvas.drawText( number, (canvasWidth * (first+i) * interval / xDistance * xScale), 40f, paint); } canvas.restore(); // 使画布返回上一个状态 // 绘制 y 轴坐标 canvas.save(); // 保存画布状态 canvas.clipRect(-110f, -canvasHeight, 0, 0); // 切割画布,使坐标显示在一定范围内 canvas.translate(0, -(1 - yScale) / 2 * canvasHeight); // 缩放时平移使得与折线图一致 canvas.translate(0, (yTranslate/yScale-yStart/yDistance*canvasHeight)*yScale); // 使坐标跟着图形一起平移 interval = measureInterval(yDistance/yScale); //每组值得间隔// Log.d("Chart","interval="+interval); n = (int) Math.ceil((yDistance/yScale)/interval); //一组有几个值// Log.d("Chart","n="+n); first = -(int) Math.floor((yStart+yDistance/yScale/2*(1-yScale)-yTranslate/yScale/canvasHeight*yDistance)/interval);// Log.d("Chart","first="+first); for (int i = 0; i <= n; i++){ if (interval < 0.01){ number = df3.format((first+i)*interval); }else if (interval <= 0.1){ number = df2.format((first+i)*interval); }else if (interval < 1){ number = df1.format((first+i)*interval); }else { number = String.valueOf((first+i)*(int)interval); } canvas.drawLine(-10f, -(canvasHeight * (first+i) * interval / yDistance * yScale), 0f, -(canvasHeight * (first+i) * interval / yDistance * yScale),paint); canvas.drawText( number, -5f, -(canvasHeight * (first+i) * interval / yDistance * yScale)-4, paint); } canvas.restore(); // 使画布返回上一个状态} 以下为坐标完成动态适配的效果图: 折线与图形的绘制 由于需要绘制两种不同的图形,去噪图(折线图)和扫描图(平面图),此处将画图方法抽象出来:子类通过重写drawAction方法来完成各自所需绘制的图形。同时后期如果需要绘制其他类型的图,继承此抽象类然后重写自己的drawAction方法即可实现不同类型的图形绘制。 以下为折线图的drawAction方法: 根据平移与缩放值,将画布进行相应的平移与缩放,同时设置图形的缩放中心,以所显示图形的中心点为缩放中心。然后设置画笔样式及颜色,通过所获取的xList与yList值连接相邻点达到绘制折线的效果。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950//绘制曲线@Overridepublic void drawAction(Canvas canvas) { // 裁切矩形,把画面控制在坐标平面内 canvas.clipRect(0, 0, canvasWidth, -canvasHeight); // 手势缩放移动 canvas.translate(xTranslate/xScale, yTranslate/yScale-yStart/yDistance*canvasHeight); float px = (canvasWidth / 2 - xTranslate/xScale); float py = (canvasHeight / 2 + yTranslate/yScale-yStart/yDistance*canvasHeight); canvas.scale(xScale, yScale, px, -py); //以图的中心点缩放 paint.setDither(true); //设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰 paint.setFilterBitmap(true); //如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作,加快显示速度,本设置项依赖于dither和xfermode的设置 paint.setStyle(Paint.Style.FILL_AND_STROKE); //设置画笔的样式,Style.FILL: 实心 STROKE:空心 FILL_OR_STROKE:同时实心与空心 paint.setStrokeJoin(Paint.Join.ROUND); //设置绘制时各图形的结合方式,如平滑效果等 BEVEL斜角 //paint.setStrokeWidth(4/xScale); //当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度 int[] colors = GlobalParameter.getInstance().getColors(); // 绘制图形 for (int i=0; i<showIDs.size(); i++) { //通道数 for (int j=0; j< yList.get(showIDs.get(i)).size()-1; j++) { paint.setColor(colors[showIDs.get(i)]); if ((yList.get(showIDs.get(i)).get(j+1)-yList.get(showIDs.get(i)).get(j))<15&&(yList.get(showIDs.get(i)).get(j+1)-yList.get(showIDs.get(i)).get(j))>-15){ paint.setStrokeWidth(3/yScale); }else { paint.setStrokeWidth(3/xScale); } canvas.drawLine( //xDistance x方向的总距离 xList.get(j) / xDistance * canvasWidth, -yList.get(showIDs.get(i)).get(j) / yDistance * canvasHeight, xList.get(j+1) / xDistance * canvasWidth, -yList.get(showIDs.get(i)).get(j+1) / yDistance * canvasHeight, paint ); } } if (points!=null){ paint.setColor(Color.RED); for (int i=0;i<points.size()-1;i++){ canvas.drawLine( //xDistance x方向的总距离 points.get(i).getX() / xDistance * canvasWidth, -points.get(i).getY() / yDistance * canvasHeight, points.get(i+1).getX() / xDistance * canvasWidth, -points.get(i+1).getY() / yDistance * canvasHeight, paint ); } } initPaints(); //画笔reset} 以下为PopWindow弹出框中横屏显示折线图的效果: Touch手势事件的监听 Android Canvas 没有提供有关手势缩放的功能,但我们可以利用onTouchListener 来监测手势,并根据手势的不同对扫描图作不同处理,比如移动和缩放。采用接口回调机制,在所需用到AbstractChartService对象处,进行手势监控,减少抽象类间的耦合性。 通过设置OnTouchListener监听,只要有手指触碰到绘制的图形,就会触发 onTouch 方法,同时通过判断event.getAction() 获取到手势的不同动作,来完成你所重写的响应事件。重写onTouch方法,通过 event.getAction() 获取到的值,自动判断执行哪一个 case 中的代码,即通过监测不同的动作来对图形作出相应处理。我们的处理主要就是移动和缩放,通过event.getX、event.getY方法所获取的手势点击的屏幕坐标,计算图形的平移及缩放值。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970// 设置折线图的触摸事件,使扫描图保持同步平移和缩放chartView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent event) { switch (event.getAction()) { // 一根手指按下时 case MotionEvent.ACTION_DOWN: if (!mPopupWindow.isShowing()) { //大图未显示时 // 双击扫描图跳转到大图 if (event.getEventTime() - chartView.getUpTime() < 200) { addToBigchartLayout(chartView); } } chartView.setxDown(event.getX()); chartView.setyDown(event.getY()); break; // 手指抬起时 case MotionEvent.ACTION_UP: chartView.setUpTime(event.getEventTime()); break; // 手指移动时 case MotionEvent.ACTION_MOVE: // 只有一根手指移动时 if (event.getPointerCount() == 1 && event.getAction() != 261 && chartView.getxDown() != 0 && chartView.getyDown() != 0 ) { // 实现图形平移 chartView.setxTranslate(chartView.getxTranslate()+(event.getX() - chartView.getxDown()) ); chartView.setyTranslate(chartView.getyTranslate()+(event.getY() - chartView.getyDown()) ); chartView.setxDown(event.getX()); chartView.setyDown(event.getY()); } // 有两根手指移动时 else if (event.getPointerCount() == 2) { // 实现扫描图缩放 double xLenMove = Math.abs(event.getX(0) - event.getX(1)); double yLenMove = Math.abs(event.getY(0) - event.getY(1)); double lenMove = Math.sqrt(xLenMove * xLenMove + yLenMove * yLenMove); // 动态更新 // 设置最小缩放比例为 0.4 if (chartView.getxScale() + (lenMove / chartView.getLenDown() - 1) > 0.4) { chartView.setxScale((float) (chartView.getxScale() + (lenMove / chartView.getLenDown() - 1))); chartView.setyScale((float) (chartView.getyScale() + (lenMove / chartView.getLenDown() - 1))); chartView.setLenDown(lenMove); } chartView.setxDown(0); chartView.setyDown(0); } break; // 有两根手指按下时 case 261: double xLenDown = Math.abs(event.getX(0) - event.getX(1)); double yLenDown = Math.abs(event.getY(0) - event.getY(1)); chartView.setLenDown(Math.sqrt(xLenDown * xLenDown + yLenDown * yLenDown)); break; // 两根手指中的一根抬起时 case MotionEvent.ACTION_POINTER_UP: chartView.setxDown(0); chartView.setyDown(0); break; default: break; } chartView.postInvalidate(); //手势完成时重绘 scanView.setxTranslate(chartView.getxTranslate()); scanView.setxScale(chartView.getxScale()); scanView.postInvalidate(); // 只有当返回 false 时才会开启手势检测效果,否则折线图将无法移动和缩放 return false; }}); 最后通过计算的平移值以及缩放值,重绘图形。 完成以上步骤就可以在其它Activity中使用自己封装的对象来完成各种不同图形的绘制。如下是适配设备校准的绘图方法: 项目源码可见我的Github仓库漏磁检测系统。]]></content>
<categories>
<category>安卓学习</category>
</categories>
<tags>
<tag>安卓</tag>
<tag>漏磁检测</tag>
<tag>Andriod</tag>
</tags>
</entry>
<entry>
<title><![CDATA[校准数据处理篇-漏磁检测]]></title>
<url>%2F2017%2F12%2F04%2F%E6%A0%A1%E5%87%86%E6%95%B0%E6%8D%AE%E5%A4%84%E7%90%86%E7%AF%87-%E6%BC%8F%E7%A3%81%E6%A3%80%E6%B5%8B%2F</url>
<content type="text"><![CDATA[优化校准数据处理问题描述: 校准模块通过正反方向两次校准。每次进行数据处理获取到该次校准四个梯度(20%、40%、60%、80%)的去噪值以及对应步长(X坐标),以便后续的曲线拟合。 处理方法1:(维护极大值间的顺序关系) 所有极大值按Y值(去噪值)排序。. 根据最大的两个极大值点,计算x坐标满足的关系(x坐标递增或者递减),通过改关系以剔除错误数据。. 选择最大的四个极大值 该方法的处理效果: 去噪值排序后,X轴也满足递增或者递减的顺序。可以排除当20%的缺陷去噪值小于其他区间的误差值。如下图: 上图阴影部分为可去噪的效果。在阴影部分出现的缺陷值都可以过滤掉。 缺点:当某些地方误差值过大时,可能掩盖真实缺陷比处的值。(A、C为真实缺陷百分比对应的值,而B点为错误值。因为B>C而过滤掉C点) 处理方法2:(改进方法,x坐标之间间隔控制,同时维护间距) 所有极大值按Y值(去噪值)排序。. 首先获取数据最大为80%缺陷对对应的去噪值,以此寻找第二个值,通过这两个值计算步数间隔。. 继续计算下一个点的位置,在第二个点的基础上增加间隔internal,从该X坐标左右寻找存在的去噪值。. 更新间隔internal,按此方法计算第四个满足条件的值。. 若无满足的条件,依次选取第二大的去噪值为80%对应的缺陷值。重复2-4步骤。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148import com.tomatoLCJC.tools.Parameter.SystemParameter;import java.util.ArrayList;import java.util.Collections;import java.util.HashMap;import java.util.List;import java.util.Map;/** * Created by YCQ on 2017/10/10. */public class InstrumentCalibration { private static double range = 0.1; //左右寻找10%的区域 通过修改这个确定精确度 /******************************************************************************************* * 函数名称:getMaximumValue * 函数介绍:获取曲线的极大值, 原理是:已知输入曲线有4个峰,该算法的思想就是求出这每个峰的起始x坐标和结束x坐标(即求出峰所在的区间) 然后在该区间中搜最大值从而求出极大值。 * 输入参数:order 表示第几次获取极大值 * 输出参数:无 * 返回值 :无 ********************************************************************************************/ public static List<Point> getMaximumValue(List<Double> yValue) //获取极大值 { List<Point> maxValue=new ArrayList<>(); double key = 10; //设置标记值为10(即认为从某个大于60的点开始到某个小于60的点结束为峰的区间)也可以为其他值,根据需要来定 int end = yValue.size(); List<Integer> xValue=new ArrayList<>(); //记录所有峰的起始坐标和结束坐标(就是记录所有峰的区间) int flag = 0; //信号量 因为算法要求记录峰值的起始和结束坐标,当某一点大于标记值key以后因为峰的单调递增性会有很多点也大于可以 //所以用flag标记当获取了第一个大于标记值key以后让程序不再记录x坐标直到条件再次满足 for (int i = 0; i < end; ++i) //这是搜索峰的区间部分 { if (key < yValue.get(i) && 0 == flag) { xValue.add(i); flag = 1; } if (key > yValue.get(i) && 1 == flag) { xValue.add(i); flag = 0; } } xValue.add(end - 1); int length = xValue.size(); for (int i = 0; i < length / 2; ++i) //根据区间搜索极大值 { double temp = yValue.get(xValue.get(2*i)); int k = xValue.get(2*i) + 1; for (int j = k; j < xValue.get(2*i+1); ++j) { if (temp < yValue.get(j)) { temp = yValue.get(j); k = j; } } maxValue.add(new Point(k,Float.valueOf(String.valueOf(temp)))); } Collections.sort(maxValue); //根据Y值 从大到小排序 if (maxValue.size()<4) { return maxValue; //极大值个数小于4,在后续过程会滤掉该情况,直接返回 } else {// return adjustOrder(maxValue); //第一种校准方法 return adjustOrder2(maxValue); //第二种校准方法 } } //第一种调整方法,根据递增和递减顺序 public static List<Point> adjustOrder(List<Point> value ){ List<Point> resValue=new ArrayList<>(); resValue.add(value.get(0)); //滤掉可能出现的错误 double flag_x = value.get(0).getX(); int flag_pOm = value.get(1).getX()-value.get(0).getX() > 0 ? 1:-1; //设置正序标记 //剔除x非正序的Point for (int i=1;i<value.size();i++){ if ((value.get(i).getX()-flag_x > 0 ? 1:-1)==flag_pOm){ resValue.add(value.get(i)); flag_x = value.get(i).getX(); } } resValue = new ArrayList<>(resValue.subList(0 , Math.min(4,resValue.size()))); //size>4时仅获取前四个值, return resValue; } //第二种调整方法,根据递增和递减顺序 public static List<Point> adjustOrder2(List<Point> value ){ //x为步数,y为去噪值 Map<Integer,Float> map = new HashMap<>(); for (int i= 0;i<value.size();i++){ map.put((int) value.get(i).getX(), value.get(i).getY()); } List<Point> resValue=new ArrayList<>(); for(int i=0;i<4;i++) resValue.add(new Point(0.0f, 0.0f)); //初始化4个point int interval; //间隔 for (int i=0 ; i<value.size()-4 ; i++){ resValue.set(0,value.get(i)); //循环以此向后寻找一个最优的缺陷 80% for(int j=i+1;j<value.size()-3; j++){ resValue.set(1,value.get(j)); //60% interval = (int) (resValue.get(1).getX()-resValue.get(0).getX()); //80与60之间的间距(步数) for (int k=0;k<Math.abs((int)(interval*range));k++){ if (map.containsKey((int)(resValue.get(1).getX()+interval+k)) && map.get((int)(resValue.get(1).getX()+interval+k)) < resValue.get(1).getY()){ resValue.set(2,new Point((resValue.get(1).getX()+interval+k), map.get((int)(resValue.get(1).getX()+interval+k)))); int interval1= (((int) (resValue.get(2).getX()-resValue.get(1).getX()))+interval)/2; for (int m=0;m<Math.abs((int)(interval1*range));m++){ if (map.containsKey((int)(resValue.get(2).getX()+interval1+m)) && map.get((int)(resValue.get(2).getX()+interval1+m)) < resValue.get(2).getY()){ resValue.set(3,new Point((resValue.get(2).getX()+interval1+m), map.get((int)(resValue.get(2).getX()+interval1+m)))); return resValue; } if (map.containsKey((int)(resValue.get(2).getX()+interval1-m)) && map.get((int)(resValue.get(2).getX()+interval1-m)) < resValue.get(2).getY()){ resValue.set(3,new Point((resValue.get(2).getX()+interval1-m), map.get((int)(resValue.get(2).getX()+interval1-m)))); return resValue; } } } if (map.containsKey((int)(resValue.get(1).getX()+interval-k)) && map.get((int)(resValue.get(1).getX()+interval-k)) < resValue.get(1).getY() ){ resValue.set(2,new Point((resValue.get(1).getX()+interval-k), map.get((int)(resValue.get(1).getX()+interval-k)))); int interval1= (((int) (resValue.get(2).getX()-resValue.get(1).getX()))+interval)/2; for (int m=0;m<Math.abs((int)(interval1*range));m++){ if (map.containsKey((int)(resValue.get(2).getX()+interval1+m)) && map.get((int)(resValue.get(2).getX()+interval1+m)) < resValue.get(2).getY()){ resValue.set(3,new Point((resValue.get(2).getX()+interval1+m), map.get((int)(resValue.get(2).getX()+interval1+m)))); return resValue; } if (map.containsKey((int)(resValue.get(2).getX()+interval1-m)) && map.get((int)(resValue.get(2).getX()+interval1-m)) < resValue.get(2).getY()){ resValue.set(3,new Point((resValue.get(2).getX()+interval1-m), map.get((int)(resValue.get(2).getX()+interval1-m)))); return resValue; } } } } } } return new ArrayList<>(); } //list逆序 public static List<Point> invertedOrder(List<Point> value ){ List<Point> resValue = new ArrayList<>(); for (int i=1;i <= value.size();i++){ //从小到大排列,逆序填充 resValue.add(new Point( value.get(value.size()-i).getX()*(float) SystemParameter.getInstance().disSensorStepLen/1000 , value.get(value.size()-i).getY() )); } return resValue; }}]]></content>
<categories>
<category>安卓学习</category>
</categories>
<tags>
<tag>安卓</tag>
<tag>漏磁检测</tag>
<tag>Andriod</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Python入门(1)]]></title>
<url>%2F2017%2F11%2F07%2FPython%E5%85%A5%E9%97%A8%EF%BC%881%EF%BC%89%2F</url>
<content type="text"><![CDATA[自学廖雪峰教程代码笔记 通过自己在廖雪峰Python教程的学习,自己也尝试实现里面的一些基本语法,加强自己的理解,是学习一门语言最重要的部分! Python是一种解释型、面向对象、动态数据类型的高级程序设计语言。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355# -*- coding: utf-8 -*-L = [ ['Apple', 'Google', 'Microsoft'], ['Java', 'Python', 'Ruby', 'PHP'], ['Adam', 'Bart', 'Lisa']]# 打印Apple:print(L[0][0])# 打印Python:print(L[1][1])# 打印Lisa:print(L[2][2])print('') # python每次输出默认为一行,print('') 相当于换行# 条件判断height = 1.75weight = 80.5bmi = weight / (height * height)if bmi < 18.5: print('过轻')elif bmi < 25: print('正常')elif bmi < 28: print('过重')elif bmi < 32: print('肥胖')else: print('严重肥胖')print('')# 循环计算高斯公式,range的使用sum = 0# for x in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:for x in range(11): # 生成0-10的11个整数 sum = sum + xprint(sum)print('')# List的使用L = ['Bart', 'Lisa', 'Adam']for x in L: print('Hello,', x)print('')# 循环n = 1while n <= 20: if n == 5: n = n + 1 # while循环,+1运算在循环尾,此处需要加1,才可保证 continue if n > 10: # 当n = 11时,条件满足,执行break语句 break # break语句会结束当前循环 print(n) n = n + 1print('END')print('')# list的特点:# 1.查找和插入的时间随着元素的增加而增加;# 2.占用空间小,浪费内存很少。# dict(字典-相当于map)的特点:(以空间换时间)# 1.查找和插入的速度极快,不会随着key的增加而变慢;# 2.需要占用大量的内存,内存浪费多d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}print(d['Bob'])print('')# set可以看成数学意义上的无序和无重复元素的集合s = set([1, 1, 2, 2, 3, 3]) # 重复数据自动过滤掉for x in s: print(x)print('')s = set((1, 2, 3)) # 将元组赋给setfor x in s: print(x)print('')# s = set((1,[2,3])) # 不可以将tuple(不可变对象)赋给set# for x in s:# print(x)# print('')# python内置函数n1 = 255n2 = 1000print(hex(n1))print(hex(n2))print('')def my_abs(x): if x >= 0: return x else: return -xx = -20print(my_abs(x))print('')def power(x, n=2): # x为必选参数,n为默认参数 s = 1 while n > 0: n = n - 1 s = s * x return sprint(power(5))print(power(5, 3))print('')def enroll(name, gender, age=6, city='Beijing'): print('name:', name) print('gender:', gender) print('age:', age) print('city:', city)enroll('Yang', 'A', 18)print('-------------')enroll('Chen', 'B', city='wuhan') # 当不按顺序提供部分默认参数时,需要把参数名写上print('')def add_end(L=[]): # 默认参数L也是一个变量,它指向对象[] L.append('END') print(L)add_end([1, 2, 3])add_end() # ['END']add_end() # ['END', 'END']print('')def add_end_1(L=None): # 默认参数必须指向不变对象! None为不变对象 if L is None: L = [] L.append('END') print(L)add_end_1()add_end_1()print('')# 可变参数 (可变参数在函数调用时自动组装为一个tuple元组)def calc(*numbers): sum = 0 for n in numbers: sum = sum + n * n return sumprint(calc(1, 2, 3, 4))print('')# 关键字参数 (关键字参数在函数内部自动组装为一个dict字典) 作用:可以扩展函数的功能def person(name, age, **kw): print('name:', name, 'age:', age, 'other:', kw)person('Adam', 45, gender='M', job='Engineer')print('')extra = {'city': 'Beijing', 'job': 'Engineer'}person('Jack', 24, **extra) # **kw关键字参数获取到extra字典(dict)的一个拷贝print('')# 必选参数、默认参数、可变参数、关键字参数和命名关键字参数的参数组合def f1(a, b, c=0, *args, **kw): print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)def f2(a, b, c=0, *, d, **kw): # * 后面的参数被视为命名关键字参数 print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)f1(1, 2)f1(1, 2, c=3)f1(1, 2, 3, 'a', 'b') # ('a', 'b')可变参数f1(1, 2, 3, 'a', 'b', x=99)f2(1, 2, d=99, ext=None)print('')# 递归函数 与尾递归(在函数返回的时候,调用自身本身,并且return语句不能包含表达式)# 计算n的阶乘def fact(n): if n == 1: return 1 return n * fact(n - 1)print(fact(100))print('')# 上述递归中,如果调用次数过多就会出现栈溢出。 解决方法就是使用尾递归。def fact_back(n): return fact_iter(n, 1)def fact_iter(num, product): if num == 1: return product return fact_iter(num - 1, num * product) # 每次递归时就把该次结果作为参数传递给下一轮循环print(fact_back(100))print('')# 切片的使用L = list(range(100))print(L)print('')# 切片的三个参数的意义:1.首位置 2.末尾置 3.每n一取 [ )左闭右开区间print(L[:10])print(L[-10:])print(L[10:20])print(L[:10:2])print(L[::5])print('')# 迭代器 可迭代对象有:字符串、list、tuple(元组)、dict、setfor ch in 'ABC': print(ch)print('')for i, value in enumerate(['A', 'B', 'C']): # enumerate把一个list变成索引-元素对 print(i, value)print('')for x, y in [(1, 1), (2, 4), (3, 9)]: # 两个变量的迭代 print(x, y)print('')# 列表生成器print(list(range(1, 11))) # 生成1-10的list,左闭右开print('')print([x * x for x in range(1, 11) if x % 2 == 0]) # 生成1-10之间偶数的平方print('')print([m + n for m in 'ABC' for n in 'XYZ']) # 两层循环生成全排列print('')import os # 导入os模块,模块的概念后面讲到print([d for d in os.listdir('.')]) # [] 生成listprint('')d = {'x': 'A', 'y': 'B', 'z': 'C'} # dictprint([k + '=' + v for k, v in d.items()]) # 使用两个变量来生成listprint('')L1 = ['Hello', 'World', 18, 'Apple', None]print(L1)print('')L2 = [s.lower() for s in L1 if isinstance(s, str)] # 如果s为字符串,则转换为小写生成列表print(L2)print('')# 生成器:generator。 (用于一边循环一边推到下一个值)g = (x * x for x in range(10))print(g) # 输出:<generator object <genexpr> at 0x0000009576A0A4C0># next(g) 使用生成器生成下一个元素的值print('')for n in g: print(n)print('')def fib(max): # 斐波纳契数列 n, a, b = 0, 0, 1 # 变量初始化,相当于 n=0;a=0;b=1 while n < max: print(b) a, b = b, a + b n = n + 1 return 'done'print(fib(6))print('')def fibonacci(max): n, a, b = 0, 0, 1 while n < max: yield b # 每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行 a, b = b, a + b n = n + 1 return 'done'g = fibonacci(6)print(next(g))print(next(g))print(next(g))print(g)print('')while True: # 必须捕获StopIteration错误,才可以获取到fibonacci()的返回值 try: x = next(g) print('g:', x) except StopIteration as e: print('Generator return value:', e.value) breakprint('')# 杨辉三角,生成器写法def triangles(): L = [1] while True: yield L L.insert(0, 0) #首尾添加一个0,便于循环计算 L.append(0) L = [L[i] + L[i + 1] for i in range(len(L)-1)]n=0for t in triangles(): print(t) n = n + 1 if n == 10: breakprint('')# 迭代器# 可迭代对象(Iterable) (特点:可直接作用于for循环的)# 1.集合数据类型,如list、tuple、dict、set、str等;# 2.generator,包括生成器和带yield的generator function# 迭代器(Iterator)对象:生成器# list、tuple、dict、set、str可以通过iter()返回Iterator对象from collections import Iterableprint(isinstance([], Iterable))print('')from collections import Iteratorprint(isinstance([], Iterator))print(isinstance(iter([]), Iterator)) 身边有许多人都赶上AI学习的浪潮,我还是想努力做好一件事,在学习之余,自己看了看python,对这门语言很是好奇!很想先接触一下,虽然对python以后的方向并不是很清楚,适当扩展扩展自己的知识面也是灰常重要的!]]></content>
<categories>
<category>Python学习</category>
</categories>
<tags>
<tag>Python</tag>
</tags>
</entry>
<entry>
<title><![CDATA[static虽好,可不要滥用]]></title>
<url>%2F2017%2F10%2F30%2Fstatic%E8%99%BD%E5%A5%BD%EF%BC%8C%E5%8F%AF%E4%B8%8D%E8%A6%81%E6%BB%A5%E7%94%A8%2F</url>
<content type="text"><![CDATA[Java中static作用详解在自己的项目中已经两次入了static的坑,正如title所讲,static虽好但是请别滥用。 static表示"静态"、"全局"的意思,可以修饰成员变量、方法,以及静态代码块,一下列举几种常用的static用法: 静态域(静态变量)如果将类中的域定义为static,那么所有该类的对象共享这一个静态域。域分为实例域与静态域。静态域属于类而不属于任何一个独立的对象。 这也是我做项目是出现问题的所在,因为静态域可以更方便的访问,直接使用类名.静态域就可以轻松的访问,而忽略该类所有对象共用这一个域导致初始化出现遗漏。第一次是在图表类ChartService中将通道数设置为了静态变量,因为纵向梯度曲线比原始数据曲线数量少1,而导致原始数据数据集与数据渲染器初始化少了1,无法显示完整的曲线。第二次在读数据线程中,将暂停的flag设置为了静态变量,导致后面继续测量无法获取到数据,而调试了好久。 静态常量(使用比静态变量多)比如Math类中定义到: 12345public class Math{ ... public static final double PI = 3.14159265358979323846; ...} 然后就可以通过Math.PI的形式访问。 另外我们输出经常会用到System.out,它其实也是一个静态常量 1public static final PrintStream out = ...; 但是Syetem类中却有一个setOut的方法,并且可以将System.out设置为不同的流。原因是setOut是一个本地方法,而不是通过java语言实现,本地方法可以绕过java语言的存取控制机制。 静态方法静态方法是一种不能向对象实施操作的方法。Math.pow(x,a)计算x的a次方。静态方法是没有this参数的方法。静态方法不能访问实例域(静态方法不能操作对象),只能访问自身类中的静态域。 静态代码块 123static { ...} 静态代码块在类中独立于类成员,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果 static 代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。执行顺序先于构造函数。 静态导包 对于静态工具类,可以采用静态导包的方式引入包,就可以直接使用该包中的静态方法。]]></content>
<categories>
<category>安卓学习</category>
</categories>
<tags>
<tag>安卓</tag>
<tag>Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[A New Begining]]></title>
<url>%2F2017%2F09%2F23%2FA-New-Begining%2F</url>
<content type="text"><![CDATA[许久没有更新博客了… 开学已经好久,上一次更新还是两个月前,都说坚持一件事情很难,果不其然。总是找各种理由(借口)不想写。大三学期已经开始三周了,除了上课感觉自己还是不知道该做些什么,浑浑噩噩,想学点东西,做点东西,却又不知道从何开始。不想考研,可是自己的能力却又远远不够。想学的东西很多,还是得静下心来,好好完成一件事。一步一个脚印。 坚持一个有规划的大学生活 学好java,从项目中多学习经验与技巧 感觉设计模式是一门很有用的课,对以后项目开发都很有用,学好设计模式 坚持早起,有时间就去去图书馆、实验室。 计算机网络还挺有意思,老师讲的还挺好 上课得提高效率,平时多做做感兴趣的事情 想学算法,期待和大神一样的厉害 想接触人工智能,机器学习,等等。。。 总之,好多好多…… 突然想起一句话,想的太多,做的太少。好像用来描述此时的我,太合适不过了。还不如选定一个小小的方向,努力去做好一件事情,坚持下去。我想收获会更多,你是否也这样觉得?]]></content>
<categories>
<category>每日一记</category>
</categories>
<tags>
<tag>心情</tag>
<tag>学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[DatePicker与TimePicker的使用]]></title>
<url>%2F2017%2F07%2F20%2FDatePicker%E4%B8%8ETimePicker%E7%9A%84%E4%BD%BF%E7%94%A8%2F</url>
<content type="text"><![CDATA[安卓系统DatePicker与TimePicker的妙用 其实安卓系统自带的DatePicker和TimePicker的界面都已经很好看啦~可以直接拿过来直接用,当然也可以自定义。 它们可以直接以界面的形式显示,也可以用new DatePickerDialog()的方法以弹框的形式显示,使用特别方便。不信?你看~ main.xml:定义一个需要用到的Datepicker和TimePicker,首先然后先设置visibility为gone,我想通过按钮来实现它们的显示效果,而不会两个同时显示时,无法完全显示在界面上。12345678910111213<DatePicker android:id="@+id/datePicker" android:layout_width="match_parent" android:layout_height="0dp" android:visibility="gone" android:layout_weight="1" /><TimePicker android:id="@+id/timePicker" android:layout_width="match_parent" android:visibility="gone" android:layout_height="0dp" android:layout_weight="1" /> MainActivity.java类:添加四个按钮,分别实现不同的点击事件(事件的监听,三种方法:1.匿名内部类 2.在MainActivity类中实现 View.OnClickListener接口,3.自定义监听类,实现接口),完成TimePicker、DatePicker以及它们的弹框形式的分别显示。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143package com.example.datetimepicker;import android.app.DatePickerDialog;import android.app.TimePickerDialog;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.DatePicker;import android.widget.TimePicker;import java.util.Calendar;public class MainActivity extends AppCompatActivity implements View.OnClickListener{ private TimePicker timePicker; private DatePicker datePicker; private Calendar calendar; private int year; private int month; private int day; private int hour; private int minute; private Button timeBtn; private Button dateBtn; private Button timeBtnDia; private Button dateBtnDia; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); timeBtn= (Button) findViewById(R.id.timePickerBtn); dateBtn= (Button) findViewById(R.id.datePickerBtn); timeBtnDia= (Button) findViewById(R.id.timePickerDia); dateBtnDia= (Button) findViewById(R.id.datePickerDia); //获取日历对象 calendar=Calendar.getInstance(); //获取年月日时分秒的信息 year=calendar.get(Calendar.YEAR); month=calendar.get(Calendar.MONTH)+1; //Calendar.MONTH获取的月份从0开始 day=calendar.get(Calendar.DAY_OF_MONTH); hour=calendar.get(Calendar.HOUR_OF_DAY); minute=calendar.get(Calendar.MINUTE); setTitle(year+"-"+month+"-"+day+" "+hour+":"+minute); datePicker= (DatePicker) findViewById(R.id.datePicker); timePicker= (TimePicker) findViewById(R.id.timePicker); //datePicker的初始化日期Picker的年月日初始值 datePicker.init(year, month-1, day, new DatePicker.OnDateChangedListener() { @Override public void onDateChanged(DatePicker datePicker, int i, int i1, int i2) { setTitle(i+"-"+(i1+1)+"-"+i2); } }); timePicker.setOnTimeChangedListener(new TimePicker.OnTimeChangedListener() { @Override public void onTimeChanged(TimePicker timePicker, int i, int i1) { setTitle(i+":"+i1); } }); //添加监听,在MainActivity中,直接添加this timeBtn.setOnClickListener(this); timeBtnDia.setOnClickListener(this); dateBtn.setOnClickListener(this); dateBtnDia.setOnClickListener(this); //使用匿名内部类的方法实现监听 //initEvent(); } public void showDatePickerDialog(){ new DatePickerDialog(this, new DatePickerDialog.OnDateSetListener() { @Override public void onDateSet(DatePicker datePicker, int i, int i1, int i2) { setTitle(i+"-"+(i1+1)+"-"+i2); } },year,month-1,day).show(); } public void showTimePickerDialog(){ new TimePickerDialog(this, new TimePickerDialog.OnTimeSetListener() { @Override public void onTimeSet(TimePicker timePicker, int i, int i1) { setTitle(i+":"+i1); } },hour,minute,true).show(); } @Override public void onClick(View view) { switch (view.getId()){ case R.id.datePickerBtn: datePicker.setVisibility(View.VISIBLE); timePicker.setVisibility(View.GONE); break; case R.id.timePickerBtn: datePicker.setVisibility(View.GONE); timePicker.setVisibility(View.VISIBLE); break; case R.id.timePickerDia: showTimePickerDialog(); break; case R.id.datePickerDia: showDatePickerDialog(); break; } } public void initEvent(){ dateBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { datePicker.setVisibility(View.VISIBLE); timePicker.setVisibility(View.GONE); } }); timeBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { datePicker.setVisibility(View.GONE); timePicker.setVisibility(View.VISIBLE); } }); dateBtnDia.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showDatePickerDialog(); } }); timeBtnDia.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { showTimePickerDialog(); } }); }} 注意事项: 最后的显示效果:]]></content>
<categories>
<category>安卓学习</category>
</categories>
<tags>
<tag>andriod</tag>
<tag>安卓</tag>
</tags>
</entry>
<entry>
<title><![CDATA[AndriodManifest.xml配置]]></title>
<url>%2F2017%2F07%2F19%2FAndriodManifest-xml%E9%85%8D%E7%BD%AE%2F</url>
<content type="text"><![CDATA[AndriodManifest的配置基础 全局篇 应用的包名以及版本信息,控制安卓的版本信息。 组件篇(四大组件) Activity(活动) 启动一个没有在清单中定义的Activity会抛出异常 Service(服务) 做为后台运行的一个逻辑代码的处理 Content Provider(内容提供者) 是用来管理数据库访问以及程序内和程序间共享的 Broadcast Receiver(广播接收者) 其属性可以设置: 图标:android:icon标题:android:label主题样式:android:theme注意:只能包含一个application节点 权限篇 申请系统权限自定义权限,限制其它应用使用该应用的效果。 123456789<permission andriod:name="com.example.test" andriod:protectionLevel="normal"></permission><activity andriod:permission="com.example.test" .... > 然后在其他应用中若希望调用该应用则必须声明权限:1<uses-permission andriod:name="com.example.test"> ListView的使用数据适配器:纯文字使用ArrayAdapter,有图片有文字又复杂的一系列内容的用SimpleAdapter. ArrayAdapter(context, 当前listview加载的每一个列表项所对应的布局文件,数据源) SimpleAdapter( context, data , resource , from ,to ): context:上下文data:数据源(List<? extends Map<String,?>> data)一个由Map组成的List合集 每一个Map都对应ListView列表中的一行 每一个Map(键-值对)中的键都必须包含所有在from中所指定的键resource:列表项的布局文件IDfrom:Map中的键名to:绑定数据视图中的ID,与from成对应关系。 监听器:]]></content>
<categories>
<category>安卓学习</category>
</categories>
<tags>
<tag>andriod</tag>
<tag>安卓</tag>
</tags>
</entry>
<entry>
<title><![CDATA[安卓自定义控件]]></title>
<url>%2F2017%2F07%2F17%2F%E5%AE%89%E5%8D%93%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8E%A7%E4%BB%B6%2F</url>
<content type="text"><![CDATA[自定义UI组件许久没有更新自己的博客了,感觉这些天,并没有做什么额。想想接下来这些时间还是多学习学习安卓的基础,多花时间尝试做一些小Demo。 昨天,跟着网易云课堂自己做了一个自定义UI控件的小例子,分为以下三个步骤: 设计需要的属性: 新建一个attrs.xml文件声明以下内容 12345678910111213141516171819202122232425262728<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="Topbar"> <!--自定义属性--> <!--中间的标题--> <attr name="title" format="string" /> <attr name="titleTextSize" format="dimension" /> <attr name="titleTextColor" format="color" /> <!--左边的按钮--> <attr name="leftTextColor" format="color"/> <attr name="leftTextSize" format="dimension"/> <attr name="leftText" format="string"/> <attr name="leftBackground" format="color|reference"/> <!--右边的按钮--> <attr name="rightTextColor" format="color"/> <attr name="rightTextSize" format="dimension"/> <attr name="rightText" format="string"/> <attr name="rightBackground" format="color|reference"/> </declare-styleable> <!--"reference" //引用--> <!--"color" //颜色--> <!--"boolean" //布尔值--> <!--"dimension" //尺寸值--> <!--"float" //浮点值--> <!--"integer" //整型值--> <!--"string" //字符串--> <!--"fraction" //百分数,比如20%--></resources> 实现一个我们的自定义View 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136package com.example.topbardemo;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Color;import android.graphics.drawable.Drawable;import android.support.annotation.Dimension;import android.util.AttributeSet;import android.view.View;import android.view.ViewGroup;import android.widget.Button;import android.widget.RelativeLayout;import android.widget.TextView;public class Topbar extends RelativeLayout{ //自定义控件中的组件 private Button leftButton,rightButton; private TextView tvTitle; //各组件的属性 private String title; private float titleTextSize; private int titleTextColor; private int leftButtonColor; private String leftText; private Drawable leftBack; private float leftTextSize; private int rightButtonColor; private String rightText; private Drawable rightBack; private float rightTextSize; private LayoutParams leftParam,rightParam,titleParam; private topbarClickListener listener; public interface topbarClickListener{ void leftClick(); void rightClick(); } //接口回调机制,实现组件的事件 public void setTopbarClickListener(topbarClickListener listener){ this.listener=listener; } public Topbar(Context context, AttributeSet attrs) { super(context, attrs); //引用自定义的属性name TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.Topbar); //获取到自定义的各个属性 title=typedArray.getString(R.styleable.Topbar_title); titleTextColor=typedArray.getColor(R.styleable.Topbar_titleTextColor,0); titleTextSize=typedArray.getDimension(R.styleable.Topbar_titleTextSize,1); leftText = typedArray.getString(R.styleable.Topbar_leftText); leftButtonColor = typedArray.getColor(R.styleable.Topbar_leftTextColor,0); leftTextSize = typedArray.getDimension(R.styleable.Topbar_leftTextSize,1); leftBack = typedArray.getDrawable(R.styleable.Topbar_leftBackground); rightText = typedArray.getString(R.styleable.Topbar_rightText); rightButtonColor = typedArray.getColor(R.styleable.Topbar_rightTextColor,0); rightTextSize = typedArray.getDimension(R.styleable.Topbar_rightTextSize,1); rightBack = typedArray.getDrawable(R.styleable.Topbar_rightBackground); //调用结束后务必调用recycle()方法,否则这次的设定会对下次的使用造成影响 typedArray.recycle(); //定义各个组件 leftButton= new Button(context); rightButton = new Button(context); tvTitle =new TextView(context); //为组件添加属性 leftButton.setText(leftText); leftButton.setTextColor(leftButtonColor); leftButton.setTextSize(leftTextSize); leftButton.setBackground(leftBack); rightButton.setText(rightText); rightButton.setTextColor(rightButtonColor); rightButton.setTextSize(rightTextSize); rightButton.setBackground(rightBack); tvTitle.setText(title); tvTitle.setTextColor(titleTextColor); tvTitle.setTextSize(titleTextSize); setBackgroundColor(0xFFF1233); //布局参数样式 leftParam = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT); leftParam.addRule(RelativeLayout.ALIGN_PARENT_LEFT,TRUE); //组件以leftParam样式添加到View中 addView(leftButton,leftParam); rightParam = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT); rightParam.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,TRUE); addView(rightButton,rightParam); titleParam = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT); titleParam.addRule(RelativeLayout.CENTER_IN_PARENT,TRUE); addView(tvTitle,titleParam); //组件的点击事件 leftButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { listener.leftClick(); } }); rightButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { listener.rightClick(); } }); } //其他事件的完善 public void setLeftButtonVisable(boolean flag){ if (flag){ leftButton.setVisibility(VISIBLE); }else { leftButton.setVisibility(GONE); } }} 引用我们的View 在activity_main.xml中引入xmlns:custom="http://schemas.android.com/apk/res-auto"声明引用的组件并设置属性值 123456789101112131415<com.example.topbardemo.Topbar android:id="@+id/topbar" android:layout_width="match_parent" android:layout_height="50dp" custom:leftText="Left" custom:leftTextColor="@color/colorPrimaryDark" custom:leftTextSize="8sp" custom:rightText="Right" custom:rightTextColor="@color/colorPrimaryDark" custom:rightTextSize="8sp" custom:title="自定义标题" custom:titleTextColor="@color/colorAccent" custom:titleTextSize="10sp" ></com.example.topbardemo.Topbar> 在Activity中定义各个组件的事件。1234567891011121314151617181920topbar= (Topbar) findViewById(R.id.topbar); topbar.setTopbarClickListener(new Topbar.topbarClickListener() { @Override public void leftClick() { Toast.makeText(getApplicationContext(),"Left",Toast.LENGTH_SHORT).show(); } @Override public void rightClick() { Toast.makeText(getApplicationContext(),"Right",Toast.LENGTH_SHORT).show(); } }); button = (Button) findViewById(R.id.button1); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { topbar.setLeftButtonVisable(false); } }); 最后的完成的效果:]]></content>
<categories>
<category>安卓学习</category>
</categories>
<tags>
<tag>安卓</tag>
<tag>自定义控件</tag>
</tags>
</entry>
<entry>
<title><![CDATA[安卓学习之2048]]></title>
<url>%2F2017%2F07%2F07%2F%E5%AE%89%E5%8D%93%E5%AD%A6%E4%B9%A0%E4%B9%8B2048%2F</url>
<content type="text"><![CDATA[一个叫做2048的小游戏 许久未更新我的博客了,只是因为,这几天觉得并没有什么好写的,好记录下来的。有时候只是学习到编码中的一点点小技巧,大多也是自己通过在网上学习,自己敲代码所总结出来的。可能是自己做的不多吧。 这两天重新把自己开始接触安卓时想做的一个小游戏继续完善了一下,两个月之后回头再看看自己当时的代码,还是学的有许多许多需要去改进的地方,这也说明我还是不断的在学习^ - ^。 以前直接将每一种方块看成图片,然后自己做2~2048的图片,以图片的形式填充到每一个ImageView中,现在则是以TextView形式,动态设置每一方块的数字以及背景色。这样使得游戏占得空间小多啦,这也是网上所推荐的方法。 12345678910111213141516171819202122232425262728private LinearLayout layout; private Button mainMenu; private Button btnReset; private Button btnUp; private TextView textScore; private TextView textBestScore; private int[][] oldFlag = new int[4][4]; private static int score = 0; private static int best_score=0; //触摸事件手指按下和松开的两个坐标 private float x1 = 0; private float x2 = 0; private float y1 = 0; private float y2 = 0; //16个方块的id static int[][] btnBlock = { {R.id.btn00, R.id.btn01, R.id.btn02, R.id.btn03}, {R.id.btn10, R.id.btn11, R.id.btn12, R.id.btn13}, {R.id.btn20, R.id.btn21, R.id.btn22, R.id.btn23}, {R.id.btn30, R.id.btn31, R.id.btn32, R.id.btn33} }; //16个方块的对应的值 static int[][] flag = { {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0} }; 以二维数组的形式存储每一个方块对应的值,然后根据方块中的值,来动态绘制每一个方块的数字以及背景色。123456789101112131415161718192021222324252627282930//手势滑动 @TargetApi(Build.VERSION_CODES.M) @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: x1 = event.getX(); y1 = event.getY(); break; case MotionEvent.ACTION_MOVE: x2 = event.getX(); y2 = event.getY(); break; case MotionEvent.ACTION_UP: if (x2 - x1 > 50 && Math.abs(x2 - x1) > Math.abs(y2 - y1)) {// Toast.makeText(this,"向右滑动",Toast.LENGTH_SHORT).show(); moveToRight(); } else if (x1 - x2 > 50 && Math.abs(x2 - x1) > Math.abs(y2 - y1)) {// Toast.makeText(this,"向左滑动",Toast.LENGTH_SHORT).show(); moveToLeft(); } else if (y2 - y1 > 50 && Math.abs(x2 - x1) < Math.abs(y2 - y1)) {// Toast.makeText(this,"向下滑动",Toast.LENGTH_SHORT).show(); moveToBottom(); } else if (y1 - y2 > 50 && Math.abs(x2 - x1) < Math.abs(y2 - y1)) {// Toast.makeText(this,"向上滑动",Toast.LENGTH_SHORT).show(); moveToUp(); } } return true; } 重写触摸事件,按下的时候记录当前的坐标,手指滑动时记录手指所在坐标。滑动结束时(也就是手指放开时),计算坐标之间的变化来判断手势的滑动方向以进行方块的滑动。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354//向右滑动 public void moveToRight() { for (int i = 0; i < 16; i++) { oldFlag[i / 4][i % 4] = flag[i / 4][i % 4]; } for (int j = 0; j < 4; j++) { ArrayList<Integer> list1 = new ArrayList<>(); for (int i = 0; i < 4; i++) { if (flag[j][3 - i] != 0) { list1.add(flag[j][3 - i]); } } ArrayList<Integer> temp = new ArrayList<>(); int i = 0; for (i = 0; i < list1.size() - 1; i++) { if (list1.get(i) == 2048){ }else if (list1.get(i) == list1.get(i + 1)) { temp.add(list1.get(i) * 2); score += list1.get(i) * 2; i++; } else { temp.add(list1.get(i)); } } if (i == list1.size() - 1) { temp.add(list1.get(i)); } if (list1.size() > 1) { list1 = temp; } for (i = 0; i < list1.size(); i++) { flag[j][3 - i] = list1.get(i); fillBlock(list1.get(i), j, 3 - i); } for (i = list1.size(); i < 4; i++) { flag[j][3 - i] = 0; fillBlock(0, j, 3 - i); } } //判断是否变化 boolean isNext = false; for (int i = 0; i < 16; i++) { if (oldFlag[i / 4][i % 4] != flag[i / 4][i % 4]) { isNext = true; break; } } if (isNext) { nextBlock(); } else if (isGameOver()) { Toast.makeText(MainActivity.this, "GAME OVER", Toast.LENGTH_SHORT); reset(); //重新开始 } } 首先记录滑动之前的二维数组(1.判断此次滑动时候有方块变化,2.便于返回上一步),然后根据手势滑动所得的方向,将方块中的值依次添加到四个List,接下来遍历一遍,如果当前list.get(i)与list.get(i+1)相等,则两个数据合并,保存到一个temp的List中。最后将temp中的值一次填到二维数组的对应位置中去。 最后将方块显示出来,通过设置每一个方块的数字以及背景色。(这里还是差一个动画的效果,就更完美了!) 还有最高的记录,是用的安卓的SharedPreferences存储,以XML标签的形式将数据保存与本地手机上。每次打开游戏的时候,初始化界面的时候从score_data.xml文件中读取到对应的最高分。12345678//保存数据 public void saveBest_score(Score score){ SharedPreferences.Editor editor =getApplicationContext().getSharedPreferences("score_data", Context.MODE_PRIVATE).edit(); editor.putString("name",score.getName()); editor.putInt("score",score.getScore()); editor.putString("time",score.getTime()); editor.commit(); } 我的2048游戏源码]]></content>
<categories>
<category>安卓学习</category>
</categories>
<tags>
<tag>andriod</tag>
<tag>安卓</tag>
<tag>2048</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JNI(java 本地接口)开发]]></title>
<url>%2F2017%2F07%2F02%2FJNI%EF%BC%88java-%E6%9C%AC%E5%9C%B0%E6%8E%A5%E5%8F%A3%EF%BC%89%E5%BC%80%E5%8F%91%2F</url>
<content type="text"><![CDATA[JNI开发之NDK环境搭建 首先得先说说JNI是什么,Java Native Interface(java本地接口)的缩写,主要是实现Java与其他语言的通信(主要是C,C++)。当实际项目中一些接口并不支持java安卓的时候,就得靠自己进行封装,来达到目的。缺点是java与本地以编译好的代码交互,可能会丧失平台可移植性,另外程序安全性降低使用不当可能是整个程序崩溃,使用时尽量降低语言之间的耦合性。开发JNI项目前提是需要有NDK(Native Development Kit)的支持。因此,在开发前需要先安装和配置NDK。推荐一个NDK安装教程 对于windows系统依然适用。 项目配置文件gradle.properties中添加 1android.useDeprecatedNdk=true local.properties文件添加:(如果是在AS->SDK Manager->Appearance&Behavior->System Settings->Andriod SDK->SDK Tools安装,会自动配置) ndk.dir=NDK的路径 打开模块的build.gradle文件,在android/defaultConfig下面添加ndk节点,如下所示: 12345ndk { moduleName "JNIDemo" stl "stlport_static" ldLibs "log"} 创建JNIUtil类,声明本地方法。编译后在Terminal终端面板中定位到cd app/build/intermediates/classes/debug/ 执行javah 包名.JNIUtil(JNIUtil是刚刚定义的类名)执行成功之后会在刚刚的debug目录下生成一个.h的头文件(文件名为:Java.类名.包名.h(‘.’换成’_‘))。 在app目录下新建JNI目录,拷贝第四步生成的.h文件,再创建一个.cpp文件实现头文件中的方法。 ok~ 编译运行。完美~]]></content>
<categories>
<category>安卓学习</category>
</categories>
<tags>
<tag>安卓</tag>
<tag>NDK</tag>
<tag>JNI</tag>
</tags>
</entry>
<entry>
<title><![CDATA[又见周末...]]></title>
<url>%2F2017%2F07%2F02%2F%E5%8F%88%E8%A7%81%E5%91%A8%E6%9C%AB%2F</url>
<content type="text"><![CDATA[一周过去了… 回想起这周做的事情,感觉并不是很满意。差不多也花了两天时间,各种找如何用github+hexo搭建博客,一步一步尝试,最后达到了现在的效果,感觉还是挺不错的效果。很喜欢这个网页!希望自己可以坚持做好这件事情,尽量把每天每周做的事情都写下来,一是总结自己所做的事情,规划自己的时间,二是也可以记录一些学习中遇到的问题,解决技巧等等。 下一周,多看看安卓的基础,感觉很多事还是得从基础做起。另外学习学习JNI(java 本地接口)的开发。 下图为如何上传本地项目到github的步骤: 这个评论好尴尬呀,发个链接就提示请文明发言。我也是很无奈。就只好加在这里。Github上传项目步骤及常见问题的分享]]></content>
<categories>
<category>每日一记</category>
</categories>
<tags>
<tag>心情</tag>
<tag>学习</tag>
</tags>
</entry>
<entry>
<title><![CDATA[磁感应力检测Bug修复]]></title>
<url>%2F2017%2F06%2F30%2F%E7%A3%81%E6%84%9F%E5%BA%94%E5%8A%9B%E6%A3%80%E6%B5%8BBug%E4%BF%AE%E5%A4%8D%2F</url>
<content type="text"><![CDATA[又是忙碌的一天… 又是一天过去,昨天的生成报告的插图始终还是没有头绪,一直停留在原地。今天测试的时候发现原来做的通道选择的弹框和地磁场校准的位置有两个小小的bug,通道选择,点击全部通道之后,再点击其它的通道,取消该通道时,全部通道的选择并没取消。在点击每个通道时,如果该通道已被选则判断全部通道是否被选,若被选则取消全部通道的选择。 修改后的效果: (function(){var player = new DPlayer({"container":document.getElementById("dplayer0"),"theme":"#FADFA3","loop":true,"video":{"url":"http://hexoblog-1253306922.cosgz.myqcloud.com/video2017/%E9%80%9A%E9%81%93%E9%80%89%E6%8B%A9.mp4"},"danmaku":{"id":"9E2E3368B56CDBB4","api":"http://dplayer.daoapp.io","token":"tokendemo"}});window.dplayers||(window.dplayers=[]);window.dplayers.push(player);})() 另外通道校准画图时,ChartService是以系统参数的通道数初始化的,导致校准非全部通道时,未被选择的通道也绘制了折线。 项目差不多接近尾声了,但是实际调试的过程中,依然会遇到许多许多的Bug,而这些是你在开始设计时、编码时未注意到的小细节。事情往往都是看起来挺简单,但是想要做好,可并不容易哦~ 暑假这两个月,得好好规划一下自己的时间。 学好安卓,看看web。另外算法的能力也得提高,学会如何把代码写的优雅~]]></content>
<categories>
<category>安卓学习</category>
</categories>
<tags>
<tag>安卓</tag>
<tag>Andriod</tag>
<tag>磁感应力</tag>
</tags>
</entry>
<entry>
<title><![CDATA[我想吐个槽~]]></title>
<url>%2F2017%2F06%2F29%2F%E6%88%91%E6%83%B3%E5%90%90%E4%B8%AA%E6%A7%BD%2F</url>
<content type="text"><![CDATA[贴图贴图你真烦~ 短学期已经开始了好几天,我的任务就是继续完成实验室的安卓磁感应力检测仪的功能完善,实验室大佬去实习了,留下我跟刘昊东两个人并肩作战,先是画图过程比较卡,特别是后退的时候,很是难理解,后来修改了主线程的数据添加过程,将数据添加放在数据处理的线程中,然后通过Handler消息机制通知UI线程重绘。 因为安卓改变UI必须放在主线程中,主线程也被称之为UI线程,所以无法做到多线程绘制界面的效果。另外后退过程避免了多余数据的传递与处理,使得最后过程更加流畅。 另外修复了一个bug,安卓6.0及以上系统版本,对软件权限的管理更加严格,分为普通权限和危险权限,危险权限在软件安装时并不会获取权限,只能在使用过程中动态获取权限,因为生成报告中必须使用存储权限(危险权限),如果没有权限,会导致文件生成失败,显示无效的问价,所以得增加一个动态权限申请的功能,体现用户的交互性。 最后就是一个最难的问题,word文档中插入图片,找了许多资料,安卓上并没有很好的解决方案,Apache POI主要是对Excel操作,对word的操作显得有点尴尬,只想吐槽网上的博客方法都似乎是一个人写的,都一样…很是无奈, 今天想到了一个点子,可是可行性并不是很好,实现起来也并不简单,将doc转成xml文件 然后进行操作,添加图片,最后改成doc的格式,网上也看见过这样的想法,帖子下面还想并没想法,就很尴尬。1234567891011121314151617181920212223242526272829303132public byte[] bitmap2Byte(Bitmap bm) { Bitmap outBitmap = Bitmap.createScaledBitmap(bm, 150, bm.getHeight() * 150 / bm.getWidth(), true); if (bm != outBitmap) { bm.recycle(); bm = null; } byte[] compressData = null; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { try { outBitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); } catch (Exception e) { e.printStackTrace(); } compressData = baos.toByteArray(); baos.close(); } catch (IOException e) { e.printStackTrace(); } return compressData; } public String getBase64(byte[] image) { String string = ""; try { BASE64Encoder encoder = new BASE64Encoder(); string = encoder.encodeBuffer(image).trim(); } catch (Exception e) { e.printStackTrace(); } return string; } 这是将图片(Bitmap格式)转成二进制数组再转成Base64编码,而这种编码正好是xml文件中图片的编码方式,在电脑上试着用暴力更改文件后缀的方式测试,还真行的通,想法归想法, 实现起来 似乎也并不简单… 哎~ 心累~ 睡觉~ 明天继续………]]></content>
<categories>
<category>每日一记</category>
</categories>
<tags>
<tag>安卓</tag>
<tag>心情</tag>
<tag>随笔</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo博客完善]]></title>
<url>%2F2017%2F06%2F28%2FHexo%E5%8D%9A%E5%AE%A2%E5%AE%8C%E5%96%84%2F</url>
<content type="text"><![CDATA[你的Blog得优化~ 至此,我的博客已经建成,但是总觉得不够完美,还差一点,于是今天花了半天的时间在博客的优化上面,遇到了很多问题,自己一个一个去百度,按照其他博客的步骤来做,当然无法避免有些博客写的并不是很好,有些问题并没有很好的解决。 今天下午主要完成了三件事: 博客分类的完善,添加了博客分类,博客标签以及关于我。目的是为了后期更好的管理这些博客,对了还有搜索的功能,可以检索博客中的关键字词。在博客目录下右键 Git Bash 执行命令:$ hexo new pages tags,在对应 blog/source/ 下新建 tags 文件夹,用于标签的显示界面,此时你可能会问,为什么不自己直接建一个文件夹呀? 当然你也可以自己建,只不过用命令的方式会默认在 tags 文件夹下建立一个index.md文件,改一个自己喜欢的 title,并添加一行 type: "tags"即可。如下图所示: 博客分类亦是如此,对应类型为type: "cateagories"。关于我并没有添加类型。 站点内容搜索功能 安装 hexo-generator-searchdb对应博客目录下 git bash 执行:$ npm install hexo-generator-searchdb --save站点配置文件 _config.yml 中添加 search 字段,如下: 12345search: path: search.xml field: post format: html limit: 10000 第二是添加评论的功能: 注册一个友言账号,友言官网,注册成功后进入后台管理获取uid,然后在站点配置文件_config.yml中找到youyan_uid:,附上刚刚的uid,最后部署一下网页即可。 最后就是添加站点阅读量,这个也是我最花时间的一件事情,开始尝试了百度统计,代码也安装成功,只不过访问数据得在百度统计上查看,需求是想在自己的网页footer位置添加一条网页阅读量的消息。尝试了好久,并没有找到好的方法。这里附上一个推荐的教程,对于hexo博客的创建有详细的步骤~ 不说了,我也去加一个打赏的功能~]]></content>
<categories>
<category>搭建博客</category>
</categories>
<tags>
<tag>github</tag>
<tag>hexo</tag>
<tag>blog</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何上传文章]]></title>
<url>%2F2017%2F06%2F27%2F%E5%A6%82%E4%BD%95%E4%B8%8A%E4%BC%A0%E6%96%87%E7%AB%A0%2F</url>
<content type="text"><![CDATA[文章怎么上传了? 有了博客网页,首先第一步得熟悉如何上传文章,熟悉hexo的基本操作。一般分为三个步骤: (1)第一步新建文章文件,在Git Bash执行命令:$ hexo new "my new post",然后电脑对应 \bolg\source\_post 中打开my-new-post.md,打开方式可以使用记事本或notepad++。使用Markdown语法进行编辑文章。【当然也可以直接在目录下面新建一个my-new-post.md文件】 (2)Git Bash执行命令:$ hexo g生成静态文件。 (3)Git Bash执行命令:$ hexo d部署到github。稍后就可以在自己的网站上看到自己的文章【第2.3步可以直接执行命令 $ hexo d -g即可完成生成文件并部署到github】 另外可以在Git Bash执行命令:$ hexo s可以本地localhost:4000查看预览效果! 清除缓存文件db.json和已生成的静态文件public。在某些情况(尤其是更换主题后),如果发现您对站点的更改无论如何也不生效,您可能需要运行该命令$ hexo clean。]]></content>
<categories>
<category>搭建博客</category>
</categories>
<tags>
<tag>github</tag>
<tag>hexo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[我的第一篇文章]]></title>
<url>%2F2017%2F06%2F27%2F%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%2F</url>
<content type="text"><![CDATA[GitHub+hexo搭建我的博客 首先了,先讲讲为什么搭建这个博客,去年暑假的时候,因为邹萌学姐的一个个人网站,让我有了做一个属于自己的网站,可以贴一贴自己的博客,自己学习的心得,还是蛮有趣的一件事情! 因为自己原来实验室学过一点点java和javaweb的知识,但是觉得自己真的并没有学到一些什么实在的东西。缺乏自己实践的能力,寒假有想过做一个网页,看了一些jsp的后台开发技术,也可以连接数据库,做个用户登录与注册的界面,后来来到学校之后,突然发现自己的时间也并不多,就一直留了下来。后来退了原来的实验室,来到了现在的107,学长学姐们都还是特别好,很热心,就像一家人一样,经常也会参加一些活动,丰富我们在实验室学习的经历~~ 前几天,因为看了实验室刘昊东的个人网页,觉得挺炫的,虽然实质上是一个静态的网页,不过也是挺好的学习机会。自己尝试着百度,自己搭建环境,第一天晚上,用github建了一个网站,以为就是这样的了… 后来总觉得自己界面未免也太丑了,人家的咋那么炫了,然后才有了hexo,又花了一个下午的时间自己去尝试用hexo在github上搭建自己的博客,最后在本地localhost:4000上看到了效果,始终没能部署到github上。昨天下午问了问刘昊东,两个人误打误撞,最后部署到了github上。也就是现在的这个样子~ 前几天,因为看了实验室刘昊东的个人网页,觉得挺炫的,虽然实质上是一个静态的网页,不过也是挺好的学习机会。自己尝试着百度,自己搭建环境,第一天晚上,用github建了一个网站,以为就是这样的了… 后来总觉得自己界面未免也太丑了,人家的咋那么炫了,然后才有了hexo,又花了一个下午的时间自己去尝试用hexo在github上搭建自己的博客,最后在本地localhost:4000上看到了效果,始终没能部署到github上。昨天下午问了问刘昊东,两个人误打误撞,最后部署到了github上。也就是现在的这个样子~]]></content>
<categories>
<category>搭建博客</category>
</categories>
<tags>
<tag>github</tag>
<tag>hexo</tag>
</tags>
</entry>
</search>