-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
1258 lines (1258 loc) · 889 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
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
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[从进程到协程]]></title>
<url>%2F2018%2F07%2F27%2F%E4%BB%8E%E8%BF%9B%E7%A8%8B%E5%88%B0%E5%8D%8F%E7%A8%8B%2F</url>
<content type="text"><![CDATA[进程早期的计算机执行程序,是顺序执行的。按顺序一次做一件事情,只有当前的程序执行完了,才能执行下一个程序。 这样做有什么问题呢? 程序只能按顺序执行,如果当前的程序计算量比较大,运行时间比较长,后序的程序就长时间得不到运行。系统会表现的像死机一样。 属于同一个程序的计算和IO直接也是顺序执行的。在程序进行IO的时候,CPU只能等待。资源利用率很低。 为此,在系统中引入多道程序技术,使得程序直接可以并发执行。 程序并发执行,在单CPU环境下,表现为时间分片。程序快速切换,看起来像是大家一块跑。 程序并发执行,系统中的资源由各个程序共享,那么将失去其封闭性,并具有间断性和不可再现性。比如,某个程序进行多次方程的解运算,计算到一半,别的程序突然插进来,此时的中间状态怎么办?内存会不会被覆盖?所以,跑并发需要处理上下文切换的问题。 进程就是这样抽象出来的一个概念,进程是指在系统中能够独立运行并作为资源分配的基本单位,由一组机器指令、数据和堆栈等组成。这样就可以对并发执行的程序加以描述和控制,管理独立的程序运行、切换。 多CPU环境下,同一时间,不同的进程可以跑在不同的CPU上,这就是并行。 线程进程的管理,是由操作系统来做的。程序运行期间遇到了IO访问,阻塞了后面的计算,为了不浪费CPU,此时操作系统就会将当前进程挂起,把CPU让给其他进程使用。一切换进程,就得陷入内核,置换掉一大堆的状态,这个代价的很大的。如果系统中的进程数一多,IO操作也多,进程切换频繁发生,系统资源就都被进程切换吃掉了。整个系统就会变得很慢。 于是又提出了线程的概念,大致意思就是,这个地方阻塞了,但我还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的切换页表、刷新TLB,只要把寄存器刷新一遍就行,能比切换进程开销少点。比如,进程内的线程遇到IO操作,就切换到其他线程执行,该进程并不需要被切换出去,避免了大量进程切换,提高了工作效率。 同时,一个进程中的多个线程可以并发执行,不同进程中的线程也能并发执行,使得操作系统具有更好的并发性,从而能够更加有效的提高系统资源的利用率和系统的吞吐量。 异步IO多路复用在写一个服务器程序的时候,我们为每一个连接进来的用户创建一个线程为其服务。当用户量很小的时候,这么做是没有问题的。后来用户量上来了,为每个用户创建线程的做法就不行了,因为线程虽然比进程更轻量,但是创建、切换、销毁线程也是一笔很大的开销,线程也会占用系统资源,操作系统所能支持的线程数目也是有限的。 于是,有了线程池。程序维护一定数量的线程,线程不会被轻易的创建和销毁,而是得到了复用。如果连接超出了线程池能承受的范围,就将其放入队列,等待有空闲线程了再行分配。 这样看起来好像一定程度上解决了问题,但是也有其致命的缺陷,因为其本质还是依赖线程: 线程很占内存 线程的切换带来的资源消耗。有可能恰好轮到一个线程的时间片,但此时这个线程被io阻塞,这时会发生线程切换(无意义的损耗) 如果线程池定义了100个线程,意味着同时只能为100个用户服务。倘若服务器同故障节点通信,由于其io是阻塞的,如果所有可用线程被故障节点阻塞,那么新的请求在队列中排队,直到连接超时。 所以,面对数十万的连接请求,线程池也是无能为力的。 于是,IO多路复用登场。IO复用的优势在于它可以同时处理多个connection。这意味着单个线程就有可能处理成千上万个连接。 12345678910// epoll// 事先调用epoll_ctl注册感兴趣的事件到epollfdwhile true { // 返回触发注册事件的流 active_stream[] = epoll_wait(epollfd) // 无须遍历所有的流 for i in active_stream[] { read or write till }} 很多工具都使用了IO多路复用的技术,比如,Netty、Redis、Nginx等等。 异步IO比IO多路复用更为理想的IO模型是异步IO:应用程序发起异步调用,而不需要进行轮询,进而处理下一个任务,只需在I/O完成后通过信号或是回调将数据传递给应用程序即可。 1234var fs = require('fs');fs.open('./test.txt', "w", function(err, fd) { //..do something}); 上面是一个典型的NodeJs读取文件的操作。调用fs.open的线程不会阻塞,他只是发起了一个调用,然后就马上返回了,紧接着便可以处理后续的代码。原因在于fs.open这个函数是异步函数,调用函数发起了一个IO读操作便可直接返回。而IO操作是由别的线程异步执行的,当读取文件这个IO操作完成后,NodeJs会调用传入的callback函数进行处理。 回想一下Java读取文件的操作: 12345678910111213File file = new File(fileName);InputStream in = null;try { in = new FileInputStream(file); int tempbyte; while ((tempbyte = in.read()) != -1) { System.out.write(tempbyte); } in.close();} catch (IOException e) { e.printStackTrace(); return;} 如果忽略掉缓存的话,每次调用in.read方法,便发起了一个阻塞IO,当前线程便会被挂起,CPU切换到其他线程执行。这样便发生了线程切换。 异步IO与之相比,让单线程远离阻塞,同时规避了线程切换(恢复现场)的开销,让单一线程在执行 I/O 操作后立即进行其他操作。 那么,NodeJs是如何实现异步IO的呢?答案是线程池+IO复用|阻塞IO模拟异步IO。 由于Windows平台和*nix平台的差异,Node.js提供了libuv来作为抽象封装层,Linux下,采用线程池+IO复用|阻塞IO模拟异步IO,Windows下,采用其独有的内核异步IO实现IOCP(IOCP的思路也是通过线程实现,不同在于这些线程由系统内核接手管理)。 有了异步IO,再搭配事件循环,单线程的NodeJS便可以处理成千上万条连接。 注意,单线程的NodeJS只适合处理IO密集型的任务,IO操作较多时,NodeJS才能快速的执行事件循环,各个任务能够得到执行的机会;一旦涉及到大量的计算,那么线程便会阻塞,影响到事件循环的进行。 协程异步回调的缺点异步IO的后续操作需要通过回调进行,回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假定读取A文件之后,再读取B文件,代码如下。 12345fs.readFile(fileA, function (err, data) { fs.readFile(fileB, function (err, data) { // ... });}); 不难想象,如果依次读取多个文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。这种情况就称为”回调地狱”。 怎么解决这个问题?也就是说,异步的代码如何用同步的方式来书写?答案就是协程。 NodeJs中的协程协程有点像线程,是运行与线程之内的。它的运行流程大致如下。 1234567第一步,协程A开始执行。第二步,协程A执行到一半,进入暂停,执行权转移到协程B。第三步,(一段时间后)协程B交还执行权。第四步,协程A恢复执行。 在单个线程内,协程A和协程B交互运行。这种情况类似于单CPU下的多个线程的执行。 本质上,协程就是用户空间下的线程。在NodeJs里,对于协程的支持就是Generator。 TJ Holowaychuk编写的co模块,可以帮助程序员把异步执行的代码封装成同步的写法。其原理就是利用Promise对象。将异步操作包装成Promise对象,用then方法交回执行权。可以让Generator函数的自动执行,从而实现Generator函数的自动流程管理。 关于co的原理,详细的解释参考阮一峰的博客: Generator 函数的含义与用法 Thunk 函数的含义和用法 co 函数库的含义和用法 async 函数的含义和用法 就性能而言,调度协程有CPU开销,保存协程上下文有内存开销,性能可能反而不如事件驱动异步回调的编程模型。 Golang中的协程Golang对于协程的支持则更为先进,他在语言层面实现了协程的调度器。 协程是基于线程的。内部实现上,维护了一组数据结构和n个线程,真正的执行还是线程,协程执行的代码被扔进一个待执行队列中,有这n个线程从队列中拉出来执行。这就解决了协程的执行问题。那么协程是怎么切换的呢?答案是:golang对各种io函数进行了封装,这些封装的函数提供给应用程序使用,而其内部调用了操作系统的异步io函数,当这些异步函数返回busy或bloking时,golang利用这个时机将现有的执行序列压栈,让线程去拉另外一个协程的代码来执行,基本原理就是这样,利用并封装了操作系统的异步函数。包括linux的epoll,select和windows的iocp,event等。 在NodeJs中,协程的切换需要手动控制,而Golang在协程阻塞时自动切换协程,在写Golang的时候所有的代码可以都写同步代码,然后用go关键字去调用。 Golang中可以启用多个线程并行执行相同数量的协程,线程:协程 = m:n。 而NodeJs的用户代码只能跑在单线程中,无法并行执行,故无法处理计算密集型应用场景。 (完)]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>nodejs</tag>
<tag>协程</tag>
<tag>异步</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Synchronized的锁机制]]></title>
<url>%2F2018%2F07%2F22%2FJava%E4%B8%AD%E5%90%84%E7%A7%8D%E9%94%81%E7%9A%84%E6%A6%82%E5%BF%B5%2F</url>
<content type="text"><![CDATA[这段时间了解了一下Java中的Synchronized和J.U.C,从中整理出一些关于锁的概念。 关于Synchronized的用法,在之前的这篇博客中也学习到了,现在来看看Synchronized在JVM中的实现。 Java对象在JVM中的结构在Hotspot虚拟机当中,对象在内存中的存储布局可以分为3块区域:对象头、实例数据、对齐填充。 对象头 相当于对象的元数据部分,存储了对象自身的运行时数据和类型指针 实例数据 对象实例数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定 对齐填充 Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的倍数。这意味着如果对象头加上实例数据的长度不是8字节的整数倍,就需要加上大小合适的对齐填充进行8字节对齐 HotSpot虚拟机的对象头包括三部分信息: Mark Word 第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等。这部分数据的长度在32位和64位虚拟机(未开启指针压缩)中分别位32bit和64bit 类型指针 对象头的另外一部分是类型指针,即对象只想他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。 数组长度 如果对象是一个Java数组,那么对象头中还必须有一块用于记录数组长度的数据 Java SE1.6为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”,所以在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。 而上述提到的各种锁状态,需要依靠对象的Mark Word来实现。 上文提到,MarkWord用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等。这部分数据的长度在32位和64位虚拟机(未开启指针压缩)中分别位32bit和64bit。但是,对象需要存储的运行时数据很多,已经超出了32bit和64bit结构所能记录的限度。考虑到虚拟机的空间效率,Mark Word被设计程了一个非固定的数据结构,他会根据对象的状态复用自己的存储空间。 上图描述了在32位虚拟机上,在对象不同状态时 mark word各个比特位区间的含义。 偏向锁 偏向锁是JDK1.6中引入的一项锁优化,目的是消除数据在无竞争情况下的同步原语,进一步提高程序运行的性能。 当锁对象第一次被线程获取时(无锁状态),虚拟机会把Mark Word中的锁标识位设为”01“,将是否是偏向锁标识设为”1“,然后使用CAS操作把获取到这个锁的线程ID记录在Mark Word当中,表示该线程持有了这个对象的偏向锁。 持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机只是简单的判断Mark Word当中的线程ID是否是当前线程的ID,然后直接进入临界区执行,不再进行任何的同步操作。 轻量级锁假设对象的偏向锁已经被一个线程A持有。当有另外一个线程B尝试获取这把锁时,偏向模式宣告结束,升级为轻量级锁。 虚拟机首先将锁对象恢复为无锁状态,然后在线程A和B的栈帧中分别分配了一个名为”Lock Record“的空间,并把锁对象的Mark Word复制到”Lock Record“,叫做”Displaced Mark Word“。 虚拟机使用CAS操作尝试将对象的Mark Word更新为指向某个线程栈帧Lock Record的指针,如果这个动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word中的锁标识位更新为”00“,表示此对象处于轻量级锁定状态。 而没有获取锁的线程也不会阻塞,而是通过自旋进行等待。持有轻量级锁的线程退出临界区时,需要进行解锁。通过CAS操作把线程当前栈帧中的Displaced Mark Word复制回对象的Mark Word。自旋等待的线程此时就可以以同样的方式获取对象的轻量级锁了。 很明显,如果没有竞争或轻度竞争,轻量级锁仅仅使用CAS操作和Lock Record就避免了重量级互斥锁的开销。 重量级锁在轻量级锁中,没有获取锁的线程通过自旋进行等待。这个依据是持有锁的线程会很快的释放掉锁,倘若持有锁的线程一直不释放锁,那么自旋等待的线程就会白白的浪费CPU。为了避免这种情况,等待的线程等到一定的自旋循环次数,就会放弃等待而挂起,让出CPU。此时,轻量级锁就升级为重量级锁。如果有两条以上的线程争用同一把锁,轻量级锁也会升级为重量级锁。 重量级锁依赖于操作系统底层的互斥量(Mutex Lock)。这个重量级锁就是我们常提到的监视器锁(Monitor)。 为什么称这种锁为重量级锁?升级到重量级锁后,没有获取到锁的线程就会被阻塞。由于Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。 升级为重量级锁后,虚拟机将Mark Word锁标识位更新为10,并且将moniter对象的地址更新到Mark Word当中。 moniter对象可以理解为一种同步工具,其同步的过程类似于之前介绍到的AQS。与AQS通过state变量表示同步状态类似,moniter通过操作系统底层的互斥量来实现同步。moniter也包含了同步队列和条件队列,故Java的Object对象拥有notify、wait方法来进行线程同步。 总结轻量级锁能提升程序同步性能的依据是”对于绝大部分的锁,在整个同步周期内都是不存在竞争的“,如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,通过自旋等待,避免了线程切换的开销。 而偏向锁的目的是消除数据在无竞争情况下的同步原语,在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。 但是,如果存在大量的锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在竞争频繁的情况下,轻量级锁反而比传统的重量级锁更慢。 其他上文提到了对象的Mark Word是非固定的数据结构,其空间是可以重用的,所谓的重用就体现在在轻量级锁或重量级锁的状态下,原来无锁状态下存放hash code等数据的空间被放入了指向锁记录的指针。那么,hash code这部分数据去哪里了呢? 这是一个针对HotSpot VM的锁实现的问题。简单答案是:当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁可以存下identity hash code。请一定要注意,这里讨论的hash code都只针对identity hash code。用户自定义的hashCode()方法所返回的值跟这里讨论的不是一回事。Identity hash code是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。 参考自当Java处在偏向锁、重量级锁状态时,hashcode值存储在哪?]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JUC概述]]></title>
<url>%2F2018%2F07%2F13%2FJUC%E6%A6%82%E8%BF%B0%2F</url>
<content type="text"><![CDATA[JUC概述java.util.concurrent的缩写,该包参考自EDU.oswego.cs.dl.util.concurrent,是JSR 166标准规范的一个实现; JSR 166,是一个关于Java并发编程的规范提案,在JDK中,该规范由java.util.concurrent包实现。 即,JUC是Java提供的并发包,其中包含了一些并发编程用到的基础组件。 JUC JUC这个包下的类基本上包含了我们在并发编程时用到的一些工具。大致可以分为以下几类: 原子更新 Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环 境下,无锁的进行原子操作。 在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新 数组,原子更新引用和原子更新字段。 锁和条件变量 java.util.concurrent.locks包下包含了同步器的框架 AbstractQueuedSynchronizer,基于AQS构建的Lock以及与Lock配合可以实现等待/通知模式的Condition。 JUC 下的大多数工具类用到了Lock和Condition来实现并发。 线程池 涉及到的类比如:Executor、Executors、ThreadPoolExector、 AbstractExecutorService、Future、Callable、ScheduledThreadPoolExecutor等等。 阻塞队列 涉及到的类比如:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、LinkedBlockingDeque等等。 并发容器 涉及到的类比如:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、CopyOnWriteArraySet等等。 同步器 剩下的是一些在并发编程中时常会用到的工具类,主要用来协助线程同步。比如:CountDownLatch、CyclicBarrier、Exchanger、Semaphore、FutureTask等等。 CASCAS理论是实现整个java并发包的基石,谈到AQS之前,我们还需要对CAS有所了解。 在JAVA中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现CAS。 java.util.concurrent 包下的大量类都使用了这个 Unsafe 类的CAS操作。 乐观锁和悲观锁Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有共享变量的锁,都采用独占的方式来访问这些变量。独占锁其实就是一种悲观锁,所以可以说synchronized是悲观锁。 悲观锁机制存在以下问题: 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。 一个线程持有锁会导致其它所有需要此锁的线程挂起。 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。 而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。 CAS实现乐观锁CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。) CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” 比如,Unsafe中的int类型的CAS操作方法: 123public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); 参数o就是要进行cas操作的对象,offset参数是内存位置,expected参数就是期望的值,x参数是需要更新到的值。 比如,把1这个数字属性更新到2的话,需要这样调用: 1compareAndSwapInt(this, valueOffset, 1, 2) 若此时内存位置的值为1,则更新为2,更新成功。否则更新失败,返回false。 AQSAQS(AbstractQueuedSynchronizer)是构建锁或者其他同步组件的基础框架,位于 java.util.concurrent.locks 下。 JUC(java.util.concurrent)里所有的锁机制都是基于AQS框架上构建的。 首先通过上面我画的结构图(只是一个大致的框架,很多类并未列出),可以大致的了解到,JUC当中,锁、条件变量和一些并发工具类都围绕AQS进行构建。同时,线程池、阻塞队列等又依赖于锁和条件变量实现并发。所以说,AQS是JUC并发包中的核心基础组件。 AQS在内部定义了一个int state变量,用来表示同步状态,并通过一个双向的FIFO 同步队列来完成同步状态的管理,当有线程获取锁失败后,就被添加到队列末尾。 可以看到,ReentrantLock、Semaphore等类并没有直接继承AQS,而是通过一个内部类Sync继承AQS来使用这个同步器。原因在于,这些工具类面向的是用户,而同步器面向的则是线程控制,两者并不存在is-a的关系,故使用组合,而不是继承。 以ReentrantLock的lock方法为例,简单了解AQS的内部原理: 注意,lock有公平与非公平之分: 公平锁(Fair):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得 非公平锁(Nonfair):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待 ReentrantLock 默认的lock()方法采用的是非公平锁。 1234567// ReentrantLock.NonfairSync,继承自AbstractQueuedSynchronizer:final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1);} 注意,下文提到的【锁】,并不是正真的锁对象,而是一种同步状态,指向AbstractQueuedSynchronizer中的state变量。加锁即state加上某个值,释放锁即state减去某个值。 可以看到,lock()方法先通过CAS尝试将同步状态state从0修改为1。若恰好锁的状态为0,则直接修改成功。然后将独占锁的owner设置为当前线程。 若加锁失败,则调用acquire(1): 123456// AbstractQueuedSynchronizer:public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();} tryAcquire(arg)方法由AQS的子类(ReentrantLock.NonfairSync)实现,再次尝试获取锁,如果获取到,则执行完毕,否则,执行addWaiter(Node.EXCLUSIVE)。 通过addWaiter(Node.EXCLUSIVE)方法生成一个新的节点node,并将该节点节点添加到同步队列末尾,并返回该节点。 在把node插入队列末尾后,它并不立即挂起该节点中线程,因为在插入它的过程中,前面的线程可能已经执行完成,所以它会先进行自旋操作acquireQueued(node, arg),尝试让该线程重新获取锁。 1234567891011121314151617181920// AbstractQueuedSynchronizer:final boolean acquireQueued(final Node node, int arg) { try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } catch (RuntimeException ex) { cancelAcquire(node); throw ex; }} 上述方法中是一个for循环,首先判断前驱节点是否是head(head是持有锁的节点),若是则再次尝试获取锁。若成功则返回,否则,执行shouldParkAfterFailedAcquire(p, node),判断此时是否应该挂起。若shouldParkAfterFailedAcquire(p, node)返回true,表示应挂起,执行parkAndCheckInterrupt(),将当前线程挂起。 该线程被唤醒后,继续执行for循环中的代码,尝试获取锁。 有关AQS的详细介绍,参考深入学习java同步器AQS、AQS的原理浅析 条件变量(Condition)条件变量用于实现等待/通知模式,比如LinkedBlockingQueue: 123456789101112131415161718192021//首先创建一个可重入锁,它本质是独占锁private final ReentrantLock takeLock = new ReentrantLock();//创建该锁上的条件队列private final Condition notEmpty = takeLock.newCondition();//使用过程public E take() throws InterruptedException { //首先进行加锁 takeLock.lockInterruptibly(); try { //如果队列是空的,则进行等待 notEmpty.await(); //取元素的操作... //如果有剩余,则唤醒等待元素的线程 notEmpty.signal(); } finally { //释放锁 takeLock.unlock(); } //取完元素以后唤醒等待放入元素的线程} Condition接口由AQS内部类ConditionObject实现。ConditionObject在内部也维护了一个队列,与同步队列相对应的,称之为条件队列。该队列与同步队列类似,其节点为AQS内部类Node。 当持有锁的线程调用了Condition.await()方法时,代表该线程的节点进入该Condition对象(ConditionObject)的条件队列,同时释放其持有的锁并挂起,等待被唤醒。 当在条件队列中的节点被其他线程调用Condition.signal()唤醒,该节点从条件队列中移除并被加入到同步队列中,同时尝试获取锁,若获取失败则继续挂起。 需要注意的是,条件队列只能用于独占锁。Condition对象由ReentrantLock.newCondition()方法返回,其内部是返回了AQS内部类ConditionObject对象。 对于同一个ReentrantLock对象,每调用一次newCondition()方法,便返回一个新的ConditionObject实例。这些ConditionObject实例之间是独立的,拥有各自的条件队列。但是这些ConditionObject实例都被绑定到了同一个同步队列上,即他们竞争的是同一把锁。 其原理是,ConditionObject是AQS的内部类,内部类中隐含了指向外部类AQS的引用。所有由同一个AQS对象实例化的ConditionObject,他们的外部类的引用指向了相同的AQS对象,故他们访问的外部类的同步队列也是同一个。 关于条件队列的详细解释,参考深入浅出AQS之条件队列]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[gitlab备份恢复方案]]></title>
<url>%2F2018%2F06%2F17%2Fgitlab%E5%A4%87%E4%BB%BD%E6%81%A2%E5%A4%8D%E6%96%B9%E6%A1%88%2F</url>
<content type="text"><![CDATA[最近公司内部的gitlab私服打算上线了,其中,数据备份是很重要的一课。 研究制定了一下备份恢复的方案~记录一下 备份策略 有 machine-A、machine-B、machine-C 三台实体机,统一为windows系统; vm-A、vm-B、vm-C、vm-D、vm-E 为Linux系统虚拟机,其中: vm-A 提供 gitlab 服务,vm-B 提供 redmine 服务; vm-C 为远程备份服务器; vm-D 是 vm-B 的备份镜像,vm-E 是vm-A的备份镜像。 vm-D 和 vm-E 平时处于关闭状态,只有在 vm-A 或 vm-B 不可用,或者做备份恢复测试时再进行启用。 注:虚拟机管理工具统一使用 VirtualBox 宿主机备份vm-A 设置定时任务将每天的 gitlab 备份传送到其宿主机 machine-A 上; vm-B 设置定时任务将每天的 redmine 备份传送到其宿主机 machine-B 上。 远程备份vm-A 设置定时任务将每天的 gitlab 备份传送到 vm-C 上; vm-B 设置定时任务将每天的 redmine 备份传送到 vm-C 上。 备份 宿主机备份 vm-A 与宿主机 machine-A 建立共享文件夹,其中 vm-A 目录 /gitlab 映射到 machine-A 目录 E:\gitlab; vm-B 与宿主机 machine-B 建立共享文件夹,其中 vm-B 目录 /redmine 映射到 machine-B 目录 E:\redmine; vm-A 定时将 gitlab 备份拷贝到其 /gitlab 目录;vm-B 定时将 redmine 备份拷贝到其 /redmine 目录。 远程备份 vm-C 与宿主机 machine-C 建立共享文件夹,其中: vm-C 目录 /gitlab 映射到 machine-C 目录 E:\gitlab; vm-C 目录 /redmine 映射到 machine-C 目录 E:\redmine。 vm-C 开启 rsync 服务,vm-A 与 vm-B 作为 rsync 客户端,定时使用 rsync 将备份推送到 vm-C 的 /gitlab 和 /redmine 目录。 配置备份环境根据宿主机备份和远程备份两种策略,配置备份环境。 配置共享文件夹由上文可知,vm-A、vm-B、vm-C均需要进行共享文件夹的设置。下面以 vm-A 为例进行演示: 安装 VBoxLinuxAdditions 安装 linux-headers 有两种解决办法: 一、安装与当前 kernel 相同版本的 kernel-headers 和 kernel-devel 123yum remove kernel-headers -yyum install kernel-headers-$(uname -r) kernel-devel-$( uname -r) -yyum install gcc make -y 二、升级到最新内核版本 1234567yum update kernel -yyum install kernel-headers kernel-devel gcc make -y# 重启虚拟机sudo reboot# 查看安装的内核版本和kernel-headers版本rpm -qa|grep -e kernel-devel -e kernel-headers uname -r 添加虚拟光驱 在虚拟机关闭状态下,右键虚拟机->设置->存储->添加虚拟光驱: 选择 VirtualBox 安装目录,默认为 C:\Program Files\Oracle\VirtualBox,选择光 盘映像文件 VBoxGuestAdditions.iso。 安装增强功能 启动虚拟机,挂载刚刚添加的虚拟光驱: 1234sudo mkdir /winsharesudo mount /dev/cdrom /winsharecd /winsharesudo ./VBoxLinuxAdditions.run 设置共享文件夹右键虚拟机->设置->共享文件夹: 配置共享文件夹路径和名称 进入虚拟机,执行 12sudo mkdir /gitlabsudo mount -t vboxsf gitlabwin /gitlab 此时,共享文件夹配置完毕,vm-A 目录 /gitlab 映射到宿主机 E:\gitlab 配置 rsync安装 rsyncvm-A、vm-B、vm-C 需要安装 rsync: centos: sudo yum install rsync ubuntu: sudo apt-get install rsync 配置 rsync 服务vm-C 作为远程备份服务器,需要配置并启动 rsync daemon。 以下操作均在 vm-C 上进行: 编辑 rsyncd.conf sudo vim /etc/rsyncd.conf 12345678910111213141516uid = rootgid = rootlog file=/var/log/rsyncd.logmax connections = 4pid file = /var/run/rsyncd.pid[gitlab] path=/gitlab/backups secrets file=/etc/rsyncd.secrets auth users=root read only=false[gitlab] path=/redmine/backups secrets file=/etc/rsyncd.secrets auth users=root read only=false uid 与 gid 确定了访问 path 指定目录的权限,即 uid 和 gid 指定的用户必须拥有 path 指定目录的读写权限 path 指定了备份目录 secrets file 指定了用户密码文件 auth users 需要是在 rsyncd.secrets 中定义的用户名 编辑 rsyncd.secrets sudo vim /etc/rsyncd.secrets 1root:123456 rsyncd.secrets 中的用户名和密码可自定义 编辑完毕后需要修改 rsyncd.secrets 的访问权限: 1sudo chmod 600 rsyncd.secrets 启动 rsync 服务 12345678910111213# 设置开机启动sudo systemctl enable rsyncd.service# 启动 rsync --daemonsudo systemctl start rsyncd.service# 开放rsync服务端口sudo firewall-cmd --permanent --add-service=rsyncd# 重启防火墙sudo firewall-cmd --reload# 关闭selinux,避免产生权限问题# 永久关闭sudo vim /etc/selinux/config # SELINUX=disabled# 临时关闭sudo setenforce 0 测试 在 vm-A 或 vm-B 执行: rsync -avz --progress test root@vm-C::gitlab 其中, test 为测试文件 输入密码,正常情况下文件传送成功。若失败,检查端口和权限等是否配置正确。 备份 Gitlab备份配置备份 /etc/gitlab 文件夹下的内容。 目的:备份双因素认证用户登录信息、备份 Gitlab-CI 中的安全变量 1sudo sh -c 'umask 0077; tar -cf $(date "+etc-gitlab-%s.tar") -C /etc/gitlab' 这部分内容只需要在gitlab服务配置好之后,备份一次即可。 备份数据备份 Git 仓库和 SQL 数据。 修改默认备份路径: 1234sudo vim /etc/gitlab/gitlab.rb# 默认备份路径 /var/opt/gitlab/backups# gitlab_rails['backup_path'] = '/mnt/backups' // 修改为指定的备份路径sudo gitlab-ctl reconfigure 修改备份过期时间 1234sudo vim /etc/gitlab/gitlab.rb# limit backup lifetime to 7 days - 604800 seconds# gitlab_rails['backup_keep_time'] = 604800sudo gitlab-ctl reconfigure 执行备份 1sudo gitlab-rake gitlab:backup:create 这部分内容需要设置定时任务,每天进行备份。 编写备份脚本脚本如下(以root用户执行): vim /etc/gitlab/backup.sh 123456789101112131415#/bin/shbackupsLocal = /var/opt/gitlab/backupsexport RSYNC_PASSWORD=123456backupsRemote = root@vm-C::gitlabbackupsWin = /gitlabgitlabRakeLog = /var/log/gitlab-rake.logrsyncLog = /var/log/gitlab-rsync.log# 执行gitlab备份/opt/gitlab/bin/gitlab-rake gitlab:backup:create > $gitlabRakeLog 2>&1# 同步备份到共享文件夹/usr/bin/rsync -avz --progress --delete $backupsLocal $backupsWin > $rsyncLog 2>&1# 同步备份到远程rsync服务器/usr/bin/rsync -avz --progress --delete $backupsLocal $backupsRemote >> $rsyncLog 2>&1 修改脚本权限: chmod +x backup.sh 设置定时备份12su -crontab -e 增加内容如下:(每天1点执行一次远程备份) 10 1 * * * /usr/bin/sh /etc/gitlab/backup.sh 恢复恢复 Gitlab要求 执行备份与恢复的 Gitlab 版本要一致! 恢复前至少执行过一次 sudo gitlab-ctl reconfigure 命令 gitlab 处于运行状态(如未启动,执行 sudo gitlab-ctl start) 恢复 gitlab 配置1234# Rename the existing /etc/gitlab, if anysudo mv /etc/gitlab /etc/gitlab.$(date +%s)# Change the example timestamp below for your configuration backupsudo tar -xf etc-gitlab-1399948539.tar -C / 拷贝备份数据到 gitlab_rails['backup_path'] 指定的位置 12# /var/opt/gitlab/backups/ 是默认的位置sudo cp 1493107454_2017_04_25_9.1.0_gitlab_backup.tar /var/opt/gitlab/backups/ 恢复 gitlab 数据 停止与数据库交互的进程 1234sudo gitlab-ctl stop unicornsudo gitlab-ctl stop sidekiq# Verifysudo gitlab-ctl status 恢复gitlab数据并重启 1234# This command will overwrite the contents of your GitLab database!sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2017_04_25_9.1.0sudo gitlab-ctl restartsudo gitlab-rake gitlab:check SANITIZE=true 管理员须知备份日志管理员需要定期查看备份情况: 分别检查 machine-A E:\gitlab、machine-B E:\redmine、machine-C E:\redmine E:\gitlab 是否有当天或前一天的备份内容。 若出现备份失败的情况,查看当天的备份日志: Gitlab gitlab 备份日志位于 vm-A /var/log/gitlab-rake.log,rsync 日志位于 vm-A /var/log/gitlab-rsync.log 查看当天的备份日志,在 vn-A 执行: 1sudo less /var/log/gitlab-rake.log 定期检查周期建议不超过三天。 恢复测试管理员需定期检查 gitlab 备份是否可用: 在 machine-C 上启动 vm-E,参考恢复 Gitlab章节,使用 machine-C E:\gitlab 目录下最新的备份文件进行恢复测试。 若 gitlab 能正常启动,且登录 gitlab 可以看到最近日期的提交记录,则证明备份可用。 定期检查周期建议不超过一周。]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SSH端口转发]]></title>
<url>%2F2018%2F06%2F01%2FSSH%E7%AB%AF%E5%8F%A3%E8%BD%AC%E5%8F%91%2F</url>
<content type="text"><![CDATA[端口转发在之前的总结中提到过端口映射即端口转发:如何从外网访问家里的电脑。即将数据从一台机器的某个端口转发到另一台机器的某个端口。 A 的数据通过 B 转发 到 C,C 的响应再从 B 转发到 A 之前研究了一段时间小程序,微信官方只允许小程序访问在公众平台配置好的域名,这个域名往往指向一个服务器,该服务器一般是腾讯云等Vps。那么,我们在自己本地写小程序代码的时候如何调试呢?小程序是无法直接访问我们的本地机器的。 在寻找解决办法的时候,了解到了 SSH 端口转发的功能。 我们知道,SSH 会自动加密和解密所有 SSH 客户端与服务端之间的网络数据。但是,SSH 还同时提供了一个非常有用的功能,这就是端口转发。它能够将其他 TCP 端口的网络数据通过 SSH 链接来转发,并且自动提供了相应的加密及解密服务。这一过程有时也被叫做“隧道”(tunneling)。 SSH 端口转发能够提供两大功能: 加密 SSH Client 端至 SSH Server 端之间的通讯数据。 突破防火墙的限制完成一些之前无法建立的 TCP 连接。 在介绍端口转发的内容之前,先学习几个别的参数: 12345-g 在-L/-R/-D参数中,允许远程主机连接到建立的转发的端口,如果不加这个参数,只允许本地主机建立连接。-N 不打开远程shell,不执行远程命令. 用于转发端口.-T 不为这个连接分配TTY-f SSH连接成功后,转入后台运行,通常和-N连用 本地转发本地端口转发,适用于以下场景: A 与 B 两台主机无法连通,但是主机 C 可以同时连通 A 和 B。 此时我们可以通过 C 来达到 A、B 通信的目的。 比如,你购买了一台VPS(B),现在想在公司的主机(A)上访问这台VPS。但是由于保密原因,公司的主机不可以访问外网,只有一台外网机(C)可以访问外网(没错,这就是我们公司)。 在主机 A 上执行: 12# ssh -L <本地主机端口>:<目标主机>:<目标主机端口> <远程主机>ssh -NTf -L 1219:B:22 C 先明确几个概念: 本地主机:执行 ssh 连接命令的主机 远程主机:ssh 登录到的主机 目标主机:最终想要在本地主机去访问的主机 其中 A 就是本地主机,1219即本地端口;目标主机为 B,目标主机端口为 22。这条命令的意思,就是指定 SSH 绑定本地端口1219,然后指定 C 将所有的数据,转发到目标主机 B 的22端口(B 主机ssh运行在22端口) 此时,我们只要访问 A 的1219端口,就相当于登录 B 了。 在 A 上执行: 1ssh -p 1219 localhost 登录 B 后,从 A 到 B 的数据流应该是这样的: 我们在 A 上输入的数据发送到 A 的1219端口上, 而 A 的 SSH Client 会将 1219 端口收到的数据加密并转发到 C 的 SSH Server 上。 SSH Server 会解密收到的数据并将之转发到 B 22端口上 最后再将从 B 返回的数据原路返回以完成整个流程。 远程转发我们在做本地转发时,A 无法直接访问 B,需要借助 C 进行转发。所以,要求 A 可以访问 C。 如果 A 也不能直接访问C,但是反过来可以, C 可以连接 A。此时若 A 想访问 B,就需要使用远程端口转发。 比如,你在家里有一台电脑(A),想要访问公司测试机(B)上的数据库服务。但是公司的测试机是与外网隔绝的,只有一台外网机(C)可以访问外网,并且这台外网机只能主动访问外网,不能被外网访问。 利用之前学到的技能:如何从外网访问家里的电脑,我们可以让 C 访问 A。此时: A 无法直接访问 B,C; B、C 之间可以互相访问; C 可以访问 A。 如果想从 A 访问 B,既然 C 可以连 A,那么就从 C 上建立与 A 的SSH连接,然后在A 上使用这条连接访问 B 就可以了。 在主机 C 上执行: 12# ssh -R <远程主机端口>:<目标主机>:<目标主机端口> <远程主机>ssh -NTf -R 1219:B:2003 A 这条命令的意思,就是让 A 监听它自己的1219端口,然后将所有数据经由 C,转发到 B 的2003端口。 此时,我们只要访问 A 的1219端口,就相当于连接 B 的数据库服务了。 在 A 上执行: 1isql -p 1219 -h localhost 从 A 到 B 的数据流应该是这样的: 我们在 A 上输入的数据发送到 A 的1219端口上, 而 A 的 SSH Server 会将 1219 端口收到的数据加密并转发到 C 的 SSH Client 上。 SSH Client 会解密收到的数据并将之转发到 B 2003端口上 最后再将从 B 返回的数据原路返回以完成整个流程。 动态转发动态转发可以用来科学上网。 如果想要科学上网,一般我们需要购买一台可以访问“外网”的 VPS 主机,然后在上面搭建 shadowsocks 服务,参考VPS使用笔记。 不用在 VPS 上搭建 shadowsocks 服务,通过 ssh 我们同样可以通过 VPS 进行 fg。 在你本地主机上执行下面的命令: 1ssh -NTfg -D 2018 <SSH Server> 上面的命令实际上在你本机创建了一个 socks 代理服务,所有发往2018端口的请求都将被转发到 SSH Server 上面。 现在需要一个 socks 客户端来进行访问,Chrome 上有一个 SwitchyOmega 的插件可以满足我们的需求: 我在 192.168.172 上执行了上述命令,通过配置 SwitchyOmega 实现了科学上网。]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[VirtualBox设置共享文件夹]]></title>
<url>%2F2018%2F05%2F02%2FVirtualBox%E5%85%B1%E4%BA%AB%E6%96%87%E4%BB%B6%E5%A4%B9%2F</url>
<content type="text"><![CDATA[最近在做 Gitlab 备份,Gitlab本身运行在虚拟机当中,备份策略是使用 Rsync 备份到远程主机一份;同时在宿主机备份一份。宿主机使用了windows,想到了一个较为简单的办法,即设置共享文件夹,将需要备份的内容定时复制到共享文件夹即可。在设置共享文件夹的过程中遇到一些问题,在此记录下来。 因为虚拟机所在的 IP 无法访问外网,只能访问公司内部网络。所以在进行下面的操作之前,需要配置一下 yum 的代理: 我在本机开启了 shadowsocks 服务,ip:192.168.1.70,port:1090 虚拟机内执行: sudo vim /etc/yum.conf 增加下面内容: 1proxy=socks5h://192.168.1.70:1090 保存,退出。yum代理设置成功。 虚拟机安装增强功能 安装 linux-headers 有两种解决办法: 安装与当前 kernel 相同版本的 kernel-headers 和 kernel-devel 123yum remove kernel-headers -yyum install kernel-headers-$(uname -r) kernel-devel-$( uname -r) -yyum install gcc make -y 升级到最新内核版本 123456yum update kernel -yyum install kernel-headers kernel-devel gcc make -y# 重启虚拟机# 查看安装的内核版本和kernel-headers版本rpm -qa|grep -e kernel-devel -e kernel-headers uname -r 添加虚拟光驱 在虚拟机关闭状态下,右键虚拟机->设置->存储->添加虚拟光驱: 选择 VirtualBox 安装目录,默认为 C:\Program Files\Oracle\VirtualBox,选择光 盘映像文件 VBoxGuestAdditions.iso。 安装增强功能 启动虚拟机,挂载刚刚添加的虚拟光驱: 123456789101112131415sudo mkdir /winsharesudo mount /dev/cdrom /winsharecd /winsharesudo ./VBoxLinuxAdditions.run# 输出如下# Verifying archive integrity... All good.# Uncompressing VirtualBox 5.2.6 Guest Additions for Linux........# VirtualBox Guest Additions installer# Removing installed version 5.2.6 of VirtualBox Guest Additions...# Copying additional installer modules ...# Installing additional modules ...# VirtualBox Guest Additions: Building the VirtualBox Guest Additions kernel modules.# VirtualBox Guest Additions: Running kernel modules will not be replaced until the system restarted# VirtualBox Guest Additions: Starting. 设置共享文件夹右键虚拟机->设置->共享文件夹: 配置共享文件夹路径和名称。 进入虚拟机,执行: 12sudo mkdir /gitlabwinsudo mount -t vboxsf gitlabwin /gitlabwin 此时,共享文件夹配置完毕,/gitlabwin 映射到宿主机 E:\gitlab 可以在 /gitlabwin 下面新建文件,然后查看宿主机 E:\gitlab 是否存在对应的文件。 开机自动挂载目前还没有找到好的解决办法,参考这里 如果对挂载的目录没有特殊要求,可以选择自动挂载,右键虚拟机->设置->共享文件夹 共享文件夹会开机自动挂载到/media/sf_XXX 目录。 设置挂载目录权限 VirtualBox shared folders present a very simplified file system implementation, just enough to read/write files from/to the guest. Many applications can error when using shared folders, because they expect advanced features, like file locking or access controls, which don’t exist for shared folders. 由于共享文件夹并不是虚拟机的本地目录,我们在虚拟机中可以配置共享文件夹的权限是有限的。 手动挂载或自动挂载的目录,所属用户默认为root,组为vboxsf,并且使用 chmod chown 等命令是无法改变的。 如果想要配置挂载目录的权限,需要在手动挂载的时候指定一些选项: 12345// uid gid指定挂载目录的所属用户和组sudo mount -t vboxsf -o uid=1000,gid=1000 <folder name given in VirtualBox>// fmode指定文件权限,dmode指定目录权限// 注意,若同时指定挂载目录的所属用户和组,则fmode和dmode选项失效sudo mount -t vboxsf -o fmode=700,dmode=700 <folder name given in VirtualBox>]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Centos7美化]]></title>
<url>%2F2018%2F05%2F01%2FCentos7%E7%BE%8E%E5%8C%96%2F</url>
<content type="text"><![CDATA[五一假期比较闲,把我自己PC上的Centos7美化了一下,能够让自己看着舒服点。 先上一张效果图: 基础知识 Gnome vs KDE KDE 和 GNOME 是 LINUX 里最常用的图形界面操作环境,他们不仅仅是一个窗口管理器那么简单, KDE 是 K Desktop Environment 的缩写.他不仅是一个窗口管理器,还有很多配套的应用软件和方便使用的桌面环境,比如任务栏,开始菜单,桌面图标 等等. GNOME 是 GNU Network Object Model Environment 的缩写.和KDE一样,也是一个功能强大的综合环境. KDE 早于 Gnome 出现,但是 KDE 基于的 Qt 是不遵循 GPL 开源协议的,GNOME 选择完全遵循 GPL 的 GTK 图形界面库为基 础。Gnome 用的是GTK 库,KDE用的是 QT 库。 QT vs GTK vs GTK+ GTK,GTK + 和 Qt 都是 GUI 工具包。 Qt 是一个跨平台的 C++ 图形用户界面库,与 Qt 基于C++语言不同,GTK 采用较传统的C语言。 这三者类似于 Windows 下的 MFC。 Gnome-shell GNOME shell 是 GNOME 桌面的用户界面,是 GNOME 3 的关键技术。它提供了一些基本的用户界面功能,比如切换窗口,启动应用程序或者显示通知。 gnome shell 是一款类似gnome的桌面管理器,相对gnome 它更加智能。 gnome shell 本质上来说,是窗口管理器、应用启动器、桌面布局的集合。 Centos7 使用 Gnome 作为桌面环境。 安装主题首先打开 应用程序->工具->优化工具(或输入gnome-tweak-tool命令启动): 可以看到,我们可以对以下几个部分进行定制: 扩展 GTK+ 图标 光标 Shell 主题 安装 Gnome 扩展安装Firefox插件: 1sudo yum install gnome-shell-browser-plugin Firefox打开https://extensions.gnome.org/,在线安装扩展。我安装了以下几款: Clipboard Indicator 剪贴板管理 Coverflow Alt-Tab Alt+Tab 切换应用 3D 效果 Dash to Dock 定制你的启动器 Drop Down Terminal 下拉式模拟终端 Dynamic Top Bar 顶栏透明化 User Themes 从文件夹加载 gnome-shell 主题 NetSpeed 顶栏显示网速 扩展可以在优化工具中进行管理。 安装 GTK+ 主题GTK+ 主题主要针对的是使用 GTK+ 图形库的应用程序进行定制。 在 GTK3 Themes 中挑选自己喜欢的主题。 我选择了一款名为X-Arc-Collection 的 GTK 主题。 下载主题到本地,然后将其解压到 /usr/share/themes/目录下。 通过优化工具在 GTK+ Theme 菜单下找到新安装的主题,选择即可。 安装图标主题在 Icon Themes 中挑选自己喜欢的图标主题。 我选择了一款名为 Papirus 的图标主题。 下载图标主题到本地,将其解压到 /usr/share/icons/ 目录下。 通过优化工具在 Icon Theme 菜单下找到新安装的主题,选择即可。 安装光标主题在 Cursors 中挑选自己喜欢的光标主题。 我选择了一款名为 Bibata 的光标主题。 下载图标主题到本地,将其解压到 /usr/share/icons/ 目录下。 通过优化工具在 Cursor Theme 菜单下找到新安装的主题,选择即可。 安装 shell 主题shell 主题主要针对顶栏进行定制。 先备份原来的主题目录 /usr/share/gnome-shell/theme/ 12cd /usr/share/gnome-shell/mv theme theme.backup 到gnome-look寻找喜欢的主题包,下载并拷贝到 /usr/share/gnome-shell/ 目录下,解压缩主题包并重命名为 theme。 在优化工具->Shell 主题中进行选择。 注意,如果没有安装User Themes扩展,Shell 主题下拉框是灰色的不可编辑。 隐藏底栏1234567cd /usr/share/gnome-shell/# 先备份cp -r /usr/share/gnome-shell/extensions/ /usr/share/gnome-shell/extensions.backup/cp -r /usr/share/gnome-shell/modes/ /usr/share/gnome-shell/modes.backup/cp -r /usr/share/gnome-shell/theme/ /usr/share/gnome-shell/theme.backup/# 删除任务栏rm -fr /usr/share/gnome-shell/extensions/window-list@gnome-shell-extensions.gcampax.github.com 安装搜狗输入法启用了新的主题后,原来默认的ibus输入法显示不太友好,灰底白字,但是又无法配置底色和前景色,所以更换了一下输入法。 下载搜狗输入法的rpm包 http://pan.baidu.com/s/1c0yR6Ac 加入epel源和mosquito-myrepo源 12yum-config-manager --add-repo=https://copr.fedoraproject.org/coprs/mosquito/myrepo/repo/epel-(rpm -E %?rhel).repoyum-config-manager --add-repo=https://copr.fedoraproject.org/coprs/mosquito/myrepo/repo/epel-7/mosquito-myrepo-epel-7.repo 安装搜狗输入法 安装刚刚下载的rpm包 1sudo rpm -ivh sogoupinyin-1.2.0.0056-1.fc22.x86_64.rpm 如果提示有依赖的安装包未安装,则先根据提示安装其依赖。直到可以完成安装。 关闭 gnome-shell 对键盘的监听,然后切换输入法为fcitx 12gsettings set org.gnome.settings-daemon.plugins.keyboard active false imsettings-switch fcitx]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[联通华为HG8347R设置桥接模式]]></title>
<url>%2F2018%2F04%2F30%2F%E8%81%94%E9%80%9A%E5%8D%8E%E4%B8%BAHG8347R%E8%AE%BE%E7%BD%AE%E6%A1%A5%E6%8E%A5%E6%A8%A1%E5%BC%8F%2F</url>
<content type="text"><![CDATA[前面的笔记中有提到如何通过外网访问家里的服务。在某些场景下可能会用到,比如想要在单位ssh家里的电脑,或者访问家里的NAS等设备等等。之前用的电信的宽带,是通过自己的路由器拨号上网,并且外网IP也是直接分配好的,配一下DDNS和端口映射即可。前一段时间换了联通的宽带。 更换宽带的时候,联通提供了一个光猫,华为HG8347R。这个光猫取代了之前的路由器,拨号也由他进行。通过该光猫的无线功能就可以直接连接WIFI上网了。但是这个光猫的无线性能较差,于是在 Lan1 口接了一个自己的TP-link路由器,通过该路由器提供的wifi信号可以正常上网。此时出现了两个WIFI信号,一个是光猫提供的,一个是TP-link路由器提供的。家里的终端设备都使用TP-link提供的WIFI上网。此时的网络拓扑大致如下: 上面的拓扑显示了从Internet到家里的终端设备经过了三层NAT转发,分别是联通到光猫,光猫到TP-link,TP-link到终端。 换成联通的宽带后,NAT转发的层数多了,一定程度上会影响网速。另一方面,如果还想从外网访问家里终端的服务,面临两个问题: 第一,联通分给用户的是一个 172.16 打头的局域网IP,肯定是不可以从外网直接访问的; 第二,联通提供的光猫没有提供DDNS和端口映射等高级功能。 此时就有必要更改一下光猫的配置了。 基础知识首先了解一下路由器的几种工作模式: AP(接入点)模式: AP(接入点)模式下,只需要把一根可以上网的网线插在路由器上,无需任何配置就可以通过有线和无线上网了,相当于一台拥有无线功能的交换机。 适用场合:宾馆、酒店或者其它提供了一根网线上网的场所。 Router(无线路由)模式 在Router(无线路由)模式下,路由器就相当于一台普通的无线宽带路由器。家里的ADSL或者光猫安装好之后,可以通过路由模式,让无线路由器使用PPPoE(虚拟拨号)连接光猫拨号上网。 适用场合:用户自己办理了宽带业务情况下使用。 Repeater(中继)模式 Repeater(中继)模式下,路由器会通过无线的方式与一台可以上网的无线路由器建立连接,用来放大可以上网的无线路由器上的无线信号。 适用场合:有一台可以上网的无线路由器,但是该无线路由器的无线信号覆盖有限,希望无线信号可以覆盖更广泛的范围时使用。 Bridge(桥接)模式 Bridge(桥接)模式,路由器会通过无线的方式与一台可以上网的无线路由器建立连接,用来放大可以上网的无线路由器上的无线信号; 适用场合:有一台可以上网的无线路由器,但是该无线路由器的无线信号覆盖有线,希望无线信号可以覆盖更广泛的范围时使用。 Client(客户端)模式 像笔记本电脑上的无线网卡那样工作,仅连接其它的无线网络,而不发射自己的无线网络信号。这种模式相当于启用了一个无线的WAN口,且下面的电脑只能通过有线方式接到此设备。 适用场合:附近有无线信号,并且用户知道该无线信号密码,用户的台式电脑想连接该无线信号上网时使用。 注意,中继和桥接这两种模式看起来相似。其内部本质上的不同为: 中继模式下无线路由器仍然提供DHCP及NAT功能,即接入到该无线路由器的终端组成的是一个单独的局域网网段。和原来的无线路由器的LAN口及无线客户接入不在同一个网段。 桥接模式下接入到该无线路由器上的终端,是和主无线网网络处在相同的网段。内部的DHCP请求,也会被转发到主无线网络上。 修改光猫配置有了上面的基础,我们回头看此时的拓扑情况: 光猫工作在路由模式下,拨号上网; TP-link 工作在接入点模式下,连接光猫的Lan1口。 如果我们想要使用TP-link的DDNS和端口转发功能,那么首先就需要TP-link获得外网IP的地址,也就是说让TP-link取代光猫的位置,由TP-link来拨号上网。 此时TP-link需要工作在【路由模式】下,而光猫则需要改为【桥接模式】,这样TP-link才能与光猫的Wan口处在同一网段,才可以进行拨号。 如何修改光猫的工作模式?网上翻了不少资料,大部分是讲解如何破解光猫的。不过,我找到了一个更加简单的办法,那就是直接使用光猫的超级管理员账户,就可以修改光猫的工作模式啦! 在此之前,首先需要获得拨号上网的账号密码。这个账号密码在光猫里配好的,但是密码是加密的,不太容易取得,直接拨打10010,询问客服,获得账号密码。 接下来修改光猫的配置: 登录光猫的Web管理页面 我的光猫的Web页面是:http://192.168.18.1/CU.html,使用账户 CUAdmin 密码 CUAdmin 登入。 注意,一定要加上CU.html,否则会提醒你该操作不允许! 进入基本设置->上行线路配置 有四个上行线路,分别是管理电话、互联网、IPTV和声音什么的。选择第二个 2_Internet_xxx; 模式该为桥接,选择端口1进行绑定; 关闭光猫的无线功能和DHCP功能。 TP-link接Lan1端口 进入TP-link提供的Web管理界面 http://192.168.1.1/,路由设置->上网设置; 上网方式选择宽带拨号上网,填写从客服处获取的宽带账号密码,点击连接。 不出意外的话,可以连接成功,已经可以上网了。 但是,此时在TP-link上看到的路由器Wan口IP地址仍然是内网IP 172.16.X.X,所以还是无法使用DDNS和端口映射等功能。 取得公网IP拨打10010,向客服索要外网IP地址,而不是联通NAT分配的私有地址。 做完以上的操作,DDNS、DMZ、端口映射等功能又可以用啦! 疑问修改光猫的配置为桥接模式后,就无法访问光猫的Web管理页面了。Ping 192.168.18.1 也显示超时。然后我将电脑用网线直接接光猫的Lan2口,是可以访问192.168.18.1的。 这说明光猫的地址没有变,但是为什么用TP-link上网之后却Ping不通该地址呢?TP-link接光猫的Lan1口,两者应该是相通的才对呀? TP-link的Wan口变成了外网IP,光猫与TP-link之间的网络拓扑是怎样的呢? 以上。]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[可能会用到的Git技巧(2)]]></title>
<url>%2F2018%2F03%2F29%2F%E5%8F%AF%E8%83%BD%E4%BC%9A%E7%94%A8%E5%88%B0%E7%9A%84Git%E6%8A%80%E5%B7%A72%2F</url>
<content type="text"><![CDATA[git worktree我们知道如果工作目录修改到一半的话,是不能随便切换 branch 的。解决方法可以通过 git stash 先暂存起来,随后执行 git stash apply 恢复。 但是,如果我们想同时修改两个分支呢?或者同时测试两个分支。能想到的方法就是单独再 clone 一份代码到其他目录。但是这种方法不仅麻烦,而且形成了两个独立的 git 目录,双方的同步也比较费劲。 git 为我们提供了一个命令来解决这个问题,那就是 git worktree add -b <新分支名> <新路径> <从此分支创建>。 比如,我们正在某个 feature 分支开发,现在希望从 master 分出一个分支来解决某个紧急的 BUG: 1git worktree add -b hotfix ../hotfix master 执行上面的命令,就会在上一层新建立一个 hotfix 目录,并新建一个分支 hotfix。 这两个工作目录在工作上看起来就像两个独立的仓库一样。因为所有工作目录共享一个仓库,所以一个更新意味着整个更新(A 目录里对分支做的改动,B 目录里切到此分支也是改动后的)。 使用 git worktree 创建的多个目录,不能有任何两个目录在同一个分支下,因为对于同一个分支的修改都会反映到各个工作目录当中,同时修改同一个分支就会造成混乱。 如果要删除其中一个工作目录,直接删除文件夹即可。随后使用命令清除已经被删的工作目录: 1git worktree prune 变基在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。 假设我们有两个分支 master 和 experiment,并在这两个分支上分别进行了提交: 现在我们希望把 experiment 上面的修改合并到 master 上去: 执行 merge 命令。 它会把两个分支的最新快照(C3 和 C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(合并提交)。 12git checkout master git merge experiment 还有一种合并方法:变基,将提交到 experiment 分支上的所有修改都移至 master 分支上:提取在 C4 中引入的修改,然后在 C3 的基础上应用一次: 12git checkout experimentgit rebase master 它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的【历次提交】,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改【依序应用】。 所以,变基的过程中,历次提交对于同一文件的修改可能产生冲突,如果遇到冲突,则解决冲突,然后执行: 12git add .git rebase --continue 如果想要跳过这个 patch,则执行 git rebase --skip。意味着这次提交将被抛弃。 直到所有 patch 应用完毕。 现在回到 master 分支,进行一次 fast-forward 合并。 12git checkout mastergit merge experiment 此时,C4’ 指向的快照就和上面使用 merge 命令的例子中 C5 指向的快照一模一样了。变基的目的是为了确保在向远程分支推送时能保持提交历史的整洁,提交历史是一条直线没有分叉。 使用变基需要遵循一个原则:不要对在你的仓库外有副本的分支执行变基。 压缩提交git 为我们提供了修改历史 commit 的功能,那就是 交互式变基。 通常在本地进行修改的时候,可能提交的粒度很小。一旦修改完毕,需要把修改推送到远程分支上去,这个时候我们希望能把本地的提交压缩成为一个或几个提交,使得提交历史变得清晰,不那么冗余。这时就需要用到交互式变基中的 squash 功能。 假设我们本地的最近三次提交历史如下: 12345git log --pretty=format:"%h %s" HEAD~3..HEADa5f4a0d added cat-file310154e updated README formatting and added blamef7f3f6d changed my name a bit 我们希望把这三次提交压缩成一次提交: 1git rebase -i HEAD~3 注意,-i 后面的 commitID 实际上是指向你要修改的提交的父提交,即我们要压缩的是 HEAD~2..HEAD 这三次提交。 运行这个命令会为我们的文本编辑器提供一个提交列表,看起来像下面这样: 1234567891011121314pick f7f3f6d changed my name a bitpick 310154e updated README formatting and added blamepick a5f4a0d added cat-file# Rebase 710f0f8..a5f4a0d onto 710f0f8## Commands:# p, pick = use commit# e, edit = use commit, but stop for amending# s, squash = use commit, but meld into previous commit## If you remove a line here THAT COMMIT WILL BE LOST.# However, if you remove everything, the rebase will be aborted.# 默认情况下,会省略 merge commit,详见What exactly does git’s “rebase –preserve-merges” do (and why?) 需要注意的是这些提交的顺序与我们通过log命令看到的是相反的,log命令显示的是由新到旧的提交,而上面显示的是由旧到新的几次提交。 可以看到其中分为两个部分,上方未注释的部分是填写要执行的指令,而下方注释的部分则是指令的提示说明。指令部分中由前方的命令名称、commit hash 和 commit message 组成。 现在我们只要知道 pick 和 squash 这两个命令即可。 pick 的意思是要会执行这个 commit squash 的意思是这个 commit 会被合并到前一个commit 我们将上面打开的脚本修改成下面这样: 123pick f7f3f6d changed my name a bitsquash 310154e updated README formatting and added blamesquash a5f4a0d added cat-file 输入:wq以保存并退出,同【变基】章节中介绍到的,其原理与【变基】类似,也是将【历次提交】的 patch 【依序应用】,所以可能会产生冲突。 冲突解决完毕后,这时我们会看到 commit message 的编辑界面: 1234567891011# This is a combination of 3 commits.# The first commit's message is:changed my name a bit# This is the 2nd commit message:updated README formatting and added blame# This is the 3rd commit message:added cat-file 其中,非注释部分就是两次的 commit message, 我们要做的就是将这两个修改成新的 commit message。 输入wq保存并退出,此时就拥有了一个包含前三次提交的全部变更的单一提交。 git merge –suqash上面的交互式变基,提供了压缩提交的功能。还有一种场景下,我们也需要压缩合并,比如合并 B 分支上的修改到 A 分支,我们可以选择在合并时将 B 分支的多个提交压缩成一个提交,合并到 A 分支上形成【一个】提交节点。 12git checkout Agit merge --squash B 此时 A 分支有一个线性的提交历史。 对比一下单纯的 merge: 12git checkout Agit merge B 如果不想生成提交节点,而是想把修改合并过来不进行提交,方便再次修改后统一提交,可以指定--no-commit选项: 1git merge --no-commit --squash B git apply patch如果一个软件有了新版本,我们可以完整地下载新版本的代码进行编译安装,但是每次全新下载是有相当大的代价的。然而,每次更新变动的代码可能只有一点点。因此,我们只要能够有两个版本代码的diff的数据,应该就可以以极低的代价更新程序了。这就是patch,它可以根据一个diff文件进行版本更新。 用 git diff 制作一个 patch 假设现在我们有两条分支,master 和 test,test 分支基于 master 分支而来,并且进行了几次新的提交。 12git checkout testgit diff master > patch 我们现在得到了一个 patch 文件,内容是 test 分支与 master 分支的 diff 结果。 应用 patch 123git checkout mastergit apply patchgit commit -a -m "Patch Apply" 我们切换到 master 分支,将 patch 文件中的更新内容应用到 master 分支上,然后进行提交。 此时 master 分支的内容已经与 test 分支的内容一模一样了。 gitlab 提供了一个类似于 git merge --suqash 命令的功能:Squash and merge。gitlab 内部的实现并不是使用了git merge --suqash命令,而是利用了 Git 提供的 patch 功能,原理如下: 首先找到提交的 merge request 中,source branch 和 target branch 的共同祖先节点,然后将 source branch 与 这个节点做对比得到 patch。 随后将这个 patch 应用到 target branch 的副本(git worktree)上面(在merge之前,已经保证解决了冲突),然后将这个副本与 target branch 进行 merge。 这样的话,source branch 上面的多个提交就都看不到了,只形成了一个提交,达到了类似于 git merge --suqash 的效果。 git 还提供了 git format-patch 生成一个 git 专用的 patch,不再赘述。详细内容可以参考 Git的Patch功能。]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[可能会用到的Git技巧(1)]]></title>
<url>%2F2018%2F03%2F01%2F%E5%8F%AF%E8%83%BD%E4%BC%9A%E7%94%A8%E5%88%B0%E7%9A%84Git%E6%8A%80%E5%B7%A71%2F</url>
<content type="text"><![CDATA[最近在看 gitlab 的源码,因为公司希望把 gitlab-ee 的 merge squash 功能集成到 gitlab-ce 上来,供公司内部使用….这个任务交给了我,现在这个功能已经改的差不多了,中间也了解了一些可能会用到的 git 技巧,现在记录下来 git cherry-pickgit cherry-pick 可以应用某个分支的某些提交到另一个分支上去。 比如,在我们的工作流中,有两个分支分别是特性分支 feature 和发布分支 stable。feature 新增了某个功能并进行了提交且通过了测试。过了一段时间到了发布日,此时我们想要将该功能集成到发布分支 stable 上面进行发布。将 feature 直接合并到 stable? 不行,stable分支只想集成这个功能,而特性分支很可能已经进行了别的提交。这种情况就需要用到 cherry-pick 的功能。cherry-pick 在 git 工作流中的使用比较常见。 12git checkout stablegit cherry-pick commit_id 首先我们需要在 feature 分支上通过 git log 查询得到我们需要的提交的 commitID,比如 41e59d4(这个提交位于 feature 分支当中)。 然后切换到 stable 分支,执行 git cherry-pick 41e59d4 如果没有冲突,就会正常提交。 如果出现了冲突: 12345$ git cherry-pick 41e59d4error: could not apply 41e59d4... featurehint: after resolving the conflicts, mark the corrected pathshint: with 'git add <paths>' or 'git rm <paths>'hint: and commit the result with 'git commit' 先使用 git status 查看哪些文件出现了冲突: 123456789101112$ git statusOn branch stableYou are currently cherry-picking commit 41e59d4. (fix conflicts and run "git cherry-pick --continue") (use "git cherry-pick --abort" to cancel the cherry-pick operation)Unmerged paths: (use "git add <file>..." to mark resolution) both modified: test.txtno changes added to commit (use "git add" and/or "git commit -a") 解决完冲突后,执行 git add 和 git commit 完成合并。 如果是一次 cherry-pick 多个 commit,则执行 git cherry-pick --continue 继续 cherry-pick。如果想要返回到执行 cherry-pick 前的状态,执行 git cherry-pick --abort。 git bisect设想如下场景:我们刚刚发布了一个新版本,运行了一段时间后偶然发现了某个表现不太正常。但是在上一个版本中是不存在这种表现的。新版本与上一个版本中间有一百多次的提交,如何确定是哪一次提交出了问题呢? 答案就是使用 git bisect 命令。 首先运行 git bisect start 启动,然后使用 git bisect bad 来告诉系统当前的提交已经有问题了。然后必须告诉 bisect 已知的最后一次正常状态是哪次提交,使用 git bisect good [good_commit]: 12345git bisect startgit bisect badgit bisect good v1.0Bisecting: 6 revisions left to test after this[ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] error handling on repo Git 发现在你标记为正常的提交(v1.0)和当前的错误版本之间有大约12次提交,于是它检出中间的一个。在这里,你可以运行测试来检查问题是否存在于这次提交。如果是,那么它是在这个中间提交之前的某一次引入的;如果否,那么问题是在中间提交之后引入的。如果这里是没有错误的,那么就运行 git bisect good 命令,如果这次提交已经出现了问题,运行 git bisect bad。 运行了上面的命令之后,就会此次提交和上个错误提交的中间点(git bisect good)或者和上个正常提交的中间点(git bisect bad)。这也是二分查找的原理。 反复执行上面的过程,最终可以找到第一个错误提交的 commit: 1b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit 再通过 git show b047b02ea8 就能看到这次错误提交的全部内容。 这样就可以找出缺陷被引入的根源。 git update-index –assume-unchanged我们的代码中经常会包含一些配置文件,每个成员的配置文件都有所不同。我们每次 git push 或 git merge时都有可能会重置这些配置文件,这样在每次合并远端代码后都需要我们手动修改他。更合理的办法是告诉 Git 忽略这些本地配置文件的变更: 1git update-index --assume-unchanged <your_file_path> 我们也可以重新跟踪被忽略的文件: 1git update-index --no-assume-unchanged <your_file_path> 注意,与 .gitignore 文件的作用不同,.gitignore 文件作用于 Untracked Files,也就是那些从来没有被 Git 记录过的文件(自添加以后,从未 add 及 commit 过的文件),比如日志文件、临时文件等。这些文件是不需要上传到远程仓库的。 而 git update-index --assume-unchanged <your_file_path> 忽略的文件是已经存在于代码仓库中,也是代码本身的一部分,比如配置文件等。 git loggit log 命令可一接受一个 --pretty 选项,来确定输出的格式. 123456789101112131415%H 提交对象(commit)的完整哈希字串%h 提交对象的简短哈希字串%T 树对象(tree)的完整哈希字串%t 树对象的简短哈希字串%P 父对象(parent)的完整哈希字串%p 父对象的简短哈希字串%an 作者(author)的名字%ae 作者的电子邮件地址%ad 作者修订日期(可以用 -date= 选项定制格式)%ar 作者修订日期,按多久以前的方式显示%cn 提交者(committer)的名字%ce 提交者的电子邮件地址%cd 提交日期%cr 提交日期,按多久以前的方式显示%s 提交说明 比如,git shortlog --format='%H|%cn|%s' | grep '#2230' 可以查找 commit 内容包括某个特定字符的提交。git shortlog 相当于 git log --pretty=short git log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short 是一个不错的格式化log的命令,我们可以把他做成 alias: 1git config --global alias.lg "log --pretty=format:'%h %ad | %s%d [%an]' --graph --date=short" 这样,我们就可以直接输入 git lg 查看格式化的 git log 啦。 遇到文件重命名的情况,使用 git log --follow <filename>更为合适,详细内容查看git log –follow奇遇记 git blamegit blame 命令可以查看某个文件现在的代码是由谁在哪一天修改的。使用 -L 参数可以指定文件的某几行的修改情况。 git blame -L 12,22 simplegit.rb 1234567891011^4832fe2 (Scott Chacon 2008-03-15 10:31:28 -0700 12) def show(tree = 'master')^4832fe2 (Scott Chacon 2008-03-15 10:31:28 -0700 13) command("git show #{tree}")^4832fe2 (Scott Chacon 2008-03-15 10:31:28 -0700 14) end^4832fe2 (Scott Chacon 2008-03-15 10:31:28 -0700 15)9f6560e4 (Scott Chacon 2008-03-17 21:52:20 -0700 16) def log(tree = 'master')79eaf55d (Scott Chacon 2008-04-06 10:15:08 -0700 17) command("git log #{tree}")9f6560e4 (Scott Chacon 2008-03-17 21:52:20 -0700 18) end9f6560e4 (Scott Chacon 2008-03-17 21:52:20 -0700 19)42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 20) def blame(path)42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 21) command("git blame #{path}")42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 22) end 第一列是最后一次修改那行(也就是现在看到的修改)的那次提交的 commitID,第二列和第三列分别是作者的姓名和修改日期,第四列是修改的行号,最后一列显示了这行当前的内容。 注意类似 ^4832fe2 的提交表示这个提交是文件第一次被加入项目时的提交,从那以后这行就未被改变过。 有时候,我们不仅仅想关注当前某个文件的某行是由谁在什么时候修改的,我们还想看到某行的修改历史。这个时候可以使用 git log -L start,end:file 达到这个目的。 git log -L 17,37:./services/merge_requests/merge_service.rb 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253commit 190ea021cd1fd89d20c9548a72034a7b941413caMerge: bddbb90fd9 011558ea4aAuthor: Sean McGivern <sean@mcgivern.me.uk>Date: Mon Nov 20 15:08:50 2017 +0000 Merge branch 'osw-merge-process-logs' into 'master' Add logs for monitoring the merge process See merge request gitlab-org/gitlab-ce!15425commit 011558ea4ac59bce74c18d2f7c55ac257de111c6Author: Oswaldo Ferreira <oswaldo@gitlab.com>Date: Thu Nov 16 12:49:01 2017 -0200 Add logs for monitoring the merge processdiff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb--- a/app/services/merge_requests/merge_service.rb+++ b/app/services/merge_requests/merge_service.rb@@ -13,28 +15,29 @@ def execute(merge_request) if project.merge_requests_ff_only_enabled && !self.is_a?(FfMergeService) FfMergeService.new(project, current_user, params).execute(merge_request) return end @merge_request = merge_request unless @merge_request.mergeable? return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true) end @source = find_merge_source unless @source return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true) end merge_request.in_locked_state do if commit after_merge clean_merge_jid success end end+ log_info("Merge process finished on JID #{merge_jid} with state #{state}") rescue MergeError => e handle_merge_error(log_message: e.message, save_message_on_model: true) end...... 如上,git log -L 17,37:./services/merge_requests/merge_service.rb 这个命令会显示 merge_service.rb 17到37行, 由近到远的修改历史。还会列出相邻两次 commit 的 diff 内容。其中 a/app/services/merge_requests/merge_service.rb 代表前一次提交,b/app/services/merge_requests/merge_service.rb 代表后一次提交。]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[安装Gitlab-Development-Kit]]></title>
<url>%2F2018%2F01%2F25%2F%E5%AE%89%E8%A3%85Gitlab-Development-Kit%2F</url>
<content type="text"><![CDATA[Gitlab-CE是开源项目,意味着我们可以针对官方的Gitlab源码进行二次开发,从而定制出符合自己的开发习惯或开发流程的代码管理工具。 一般来说只要把Gitlab-CE的代码仓库clone到本地,就可以在上面修改代码了。Gitlab-CE的地址:https://gitlab.com/gitlab-org/gitlab-ce/ 。但是,只有源代码是不能够直接在本地上跑起来的,整个开发环境还需要安装很多依赖,以及配置数据库。Gitlab为了方便开发者,提供了一个Gitlab开发工具Gitlab-Development-Kit,其地址是:https://gitlab.com/gitlab-org/gitlab-development-kit 。Gitlab-Development-Kit可以帮助开发者很方便地在本地搭建起开发环境,并且把Gitlab运行起来。 系统环境 ubuntu-16.04.3-desktop-amd64 The preferred way to use GitLab Development Kit is to install Ruby and dependencies on your ‘native’ OS. We strongly recommend the native install since it is much faster than a virtualized one. Due to heavy IO operations a virtualized installation will be much slower running the app and the tests. 最好【不要】使用虚拟机安装,而是直接安装在你本机系统上面,要不然会很慢(真的很慢~)。 如果需要在Windows下开发,也只能是安装在Windows10所带的Linux子系统下~~ Prepare 添加新用户 gitdev 12adduser gitdev# 赋予gitdev用户sudo权限 下面的安装使用gitdev用户执行。 安装git 1234add-apt-repository ppa:git-core/ppaapt updateapt install gitgit --version # 2.15.1 安装RVM 123456789# 安装curlsudo apt-get updatesudo apt install curl# 安装RVMgpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB\curl -sSL https://get.rvm.io | bash -s stablesource ~/.bashrcsource ~/.bash_profile 安装Ruby 检查GDK要求的Ruby版本:目前是2.3.6 123456# 安装Ruby2.3.6rvm install 2.3.6# 设置默认Rubyrvm use 2.3.6# 验证ruby -v 安装Node 123curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -sudo apt-get install -y nodejsnode -v # 8.9.4 安装Yarn 1234curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.listsudo apt-get update && sudo apt-get install yarnyarn -v # 1.3.2 安装 bunlder 1gem install bundler 安装Golang 123sudo add-apt-repository ppa:longsleep/golang-backportssudo apt-get updatesudo apt-get install golang-go 安装其他软件 1234567# Add apt-add-repository helper scriptsudo apt-get install software-properties-common python-software-properties# This PPA contains an up-to-date version of Gosudo add-apt-repository ppa:longsleep/golang-backportssudo apt-get updatesudo apt-get install git postgresql postgresql-contrib libpq-dev redis-server libicu-dev cmake g++ libre2-dev libkrb5-dev libsqlite3-dev ed pkg-config Set-Up-Gdk Fork 一份 Gitlab-CE 的代码到自己账户的仓库,例如:https://gitlab.com/Hikyu/gitlab-ce.git 新建一个用于开发 Gitlab-CE 代码的文件夹,例如:/home/gitdev/project/gdk 进入上述文件夹,执行: 12gem install gitlab-development-kitgdk init 进入 ./gitlab-development-kit,执行: 12gdk install gitlab_repo=https://gitlab.com/Hikyu/gitlab-ce.gitsupport/set-gitlab-upstream 启动 gitlab-development-kit 123gdk run# 管理员用户密码: root 5iveL!fe 遇到的问题 执行 gdk install gitlab_repo=https://gitlab.com/Hikyu/gitlab-ce.git 报错:support/bootstrap-rails failed Makefile:246: recipe for target 'postgresql/data' failed make: *** [postgresql/data] Error 1 重新执行一次该命令,错误消失了… 执行 gdk run,报错:gitaly.socket: bind: no such file or directory 编辑 /home/gitdev/project/gdk/gitlab-development-kit/gitaly/config.toml,修改两处: 123456789修改/home/gitdev/project/gdk/gitlab-development-kitdev/project/gdk/gitlab-development-kit/gitaly.socket为/home/gitdev/project/gdk/gitlab-development-kit/gitlab-development-kit/gitaly.socket修改/home/gitdev/project/gdk/gitlab-development-kitdev/project/gdk/gitlab-development-kit/gitaly/bin为/home/gitdev/project/gdk/gitlab-development-kit/gitlab-development-kit/gitaly/bin 估计是个bug]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Gitlab调研]]></title>
<url>%2F2018%2F01%2F17%2FGitlab%E8%B0%83%E7%A0%94%2F</url>
<content type="text"><![CDATA[最近公司想要替换原来的代码管理工具Starteam,一方面是这个 Starteam bug不少,用起来有不少问题,随着开发团队的扩大,工具跟不上现在的开发节奏了;另一方面近几年 Git 已经成为趋势,作为开发人员,总应该顺应潮流…所以调研了一下 Gitlab 这个开源的项目管理工具。我觉得有必要把一些理解记下来,以备不时之需。 Gitlab Github Git Github 提起开源,我们总能想到鼎鼎有名的 Github。Github 是一个代码托管网站,提供源代码托管服务。简单点来说,就是你可以把自己的代码上传到 Github 进行保存,然后在别的地方下载下来进行修改。当然,Github 不仅仅可以托管代码,你可以把他类比成网盘,存放你想存的任何东西,Github 还提供了一些其他的服务,比如写文档、生成电子书、托管博客等等。 GitHub 同时提供付费账户和免费账户。这两种账户都可以创建公开的代码仓库,但是付费账户还可以创建私有的代码仓库。 Gitlab Gitlab 则是跟 Github 十分类似的网站。Gitlub 几乎提供了 Github 所拥有的一切功能。Gitlab 也提供付费账户和免费账户,与 Github 不同之处在于, Gitlab 的免费账户也可以创建私有代码仓库。 与 Github 不同之处在于,Gitlab 同时也是提供了自托管服务,即 Gitlab 也是一款应用程序。你可以下载 Gitlab 的源代码,在你本地的环境编译安装,生成与 gitlab.com 一样的网站。这意味着你可以在公司内部使用这款工具,而不用把代码上传到 Gitlab 或 Github 上面,保证了代码的私密性。 Gitlab 提供了社区版本和企业版本。 社区版本是免费的,企业版本是付费的。 与社区版本相比,企业版本提供了一些额外的功能。但是对于大多数小公司来说,免费版本已经足够使用了。 在git服务器的配置 这篇总结中,已经提到了如何在本地搭建 Gitlab 的社区版本。 Git Gitlab 与 Github 都是围绕 Git 展开的。 Git 是一款分布式版本控制系统,用来进行版本控制。 Git 与 SVN 是一个层次的概念(VCS),而 SVN 属于集中式的版本控制,Git 属于分布式的版本控制。 Gitlab 和 Github 以 Git 为核心,提供了远程代码托管的服务,同时展开了一些别的服务,比如项目管理、BUG 追踪等等。 几种工作流随着 Git 的流行,出现了几种工作流。 Git 作为一个源码管理系统,不可避免涉及到多人协作。协作必须有一个规范的工作流程,让大家有效地合作,使得项目井井有条地发展下去。”工作流程”在英语里,叫做”workflow”或者”flow”,原意是水流,比喻项目像水流那样,顺畅、自然地向前流动,不会发生冲击、对撞、甚至漩涡。 大致上有这样几种工作流: 集中式 Git工作流 Github工作流 Gitlab工作流 目前公司采用的就是 集中式 的工作流。也是SVN的代码提交方式,即多人协作开发同一个主分支代码,所有的代码都提交到同一个分支。 这几种工作流的详细介绍可以参考下面的链接: Git 工作流程 git-workflows-and-tutorials Code review多人协作,免不了要进行 Code review,即代码审查。Gitlab 提供了 Code review 的功能。 Gitlab 提供的 Code review 通过 Merge requests 来做。 你可能听过 Pull request ,这是 Github 提供的 Code review 手段,他俩的流程是一样的,只不过名字不同而已。 简单介绍一下 Gitlab Code review 流程:(假设我们现在针对master分支进行修改) 在此之前,先介绍一下保护分支。 所谓的保护分支即给某个分支设立一些操作权限,包括 push、merge 的权限。你可以将权限分配给 主程序员 或者 开发者。比如,我们现在设置 master 的保护权限为 禁止任何人进行push, 只允许主程序员进行merge。这样设置意味着任何人都不能通过 push 的方式修改 Gitlab 远程仓库 master 分支的代码,同时所有人的修改都需要提交 merge request 给主程序员,由他将修改合并到主分支。 通过给 master 分支设置权限,可以起到杜绝恶意代码提交或滥提交的作用。 新建远程分支 首先,你需要在 Gitlab 服务器上建立一个master分支的拷贝,有两种方式:fork到你自己的账户下(类似于Github协作的方式),或者在 master 分支的基础上新建一个 分支。 我们采用第二种,新建一个bug修改的分支:bug_No1 克隆远程分支到本地,并进行修改 我们将项目克隆(git clone)到自己的机器上。切换到 bug_No1 分支,开始修改bug。 过程大概是这样:git add -> git commit 、git add -> git commit、git add -> git commit… 压缩提交,推送到远程分支 此时代码修改的差不多了,可以进行 Code review 了。但是在此之前,我们应该进行一个操作:压缩提交。这个不是必须的,但是是一个好的习惯: 在第2步中,我们进行了多次提交,这些提交反应了你修改 bug 过程的细节。这些细节是否有必要推送出去呢?有可能我们只是修改了一个分号也会进行一次提交,这些提交只会造成 review 人员的迷惑,所以在推送代码之前,有必要整理一下这些 commit,即使用Git交互式变基的功能(git rebase -i)。 然后,将本地 bug_No1 分支push到 远程 bug_No1 分支。 提交 merge request 登录 Gitlab,在项目中选择 Merge Requests 标签页,点击 New merge request: 选择source branch为 bug_No1, target branch为 master,点击Compare branches and continue 可以添加一些描述,比如 close #1,表示如果这个merge request 被合并,就更新 issue #1 的状态为关闭。或者还可以在描述中 @别的开发人员,进行多人review。 选择reviewer,提交 merge request。 reviewer 查看 merge request reviewer 和你@过的开发人员 会收到merge request的提醒,然后到 Gitlab 查看你的 merge request: 他可以选择 点击 Merge,表示合并此次提交到 master,或者在评论里评论你的代码,可以具体到某行,指出一些错误和建议。 再次修改代码 你的 merge request 被别人评论之后,你就会收到邮件。根据评论,你可以在本地再次修改你 bug_No1上面的代码,git add -> git commit … 然后再次push。注意,这次的push不要再进行压缩提交了,因为此时你的代码已经被分享到远端,你在本地修改了提交历史,是无法再次提交到远程仓库的。如果需要push到远程仓库,则需要强制推送,这样的话远程仓库的提交历史也改变了。这样会造成 reviewer 的疑惑,因为原先的一些提交已经不再了,会使review历史变得不清晰。 再次 review 当你第二次push了你的代码之后,不再需要点击merge request了。Gitlab 会自动将此次的推送绑定到之前的 merge request 上。此时 reviewer 会收到邮件提醒,再次查看你的代码,并决定是否 merge。 6,7步可以多次进行。 merge reviewer 通过了此次 merge request,点击 merge,将 bug_No1 的代码合并到 master。如果有冲突(别人已经合并过了),则需要解决冲突。 以上就是一次代码 review 的流程,其中还有一些问题: 因为只有主程序员拥有 merge 的权限,那么主程序员需要解决每一次的 merge 冲突,有点蛋疼 你可以为开发者也分配 merge 权限,但是这样显得不是那么严谨。自己给自己 review? 每次 review->修改->review 造成了很多commit,这些commit也会随着 merge 被合并到master中 我们在第一次推送 bug_No1 到远程仓库的时候,压缩了提交。但是一旦你的代码已经分享到远程仓库,那么再进行变基是不明智的。 这样的话,每次review之后再 git add -> git commit ,我们对这些 commit 就无能为力了。一旦代码被 merge 到 master 非分支,你再查看 master 的 commits 时就会发现很多这样 review 代码之后的提交,是不是很蛋疼(这有点强迫症的意味)? 对于这两点问题,我想到了一个解决方案:Fast-forward merge requests 简单来说,就是 master 分支只接受 Fast-forward 的方式合并的代码。什么是快速推进(Fast-forward)?参考图解4种git合并分支方法 当你的代码 bug_No1 不能以快速推进的方式合并到 master 分支的时候,意味着可能出现了冲突: 注意上面图片中的提醒:To merge this request, first rebase locally. 需要你在本地执行变基。什么是变基(rebase)?还是参考图解4种git合并分支方法 此时,提交 merge request 的开发者就有责任解决这个冲突:在本地执行下面的指令 1234git checkout bug_No1// 之前提到的交互式变基也是变基的一种,我们现在在变基master的同时执行压缩提交,就能解决上面的第二个问题git rebase -i mastergit push --force 因为执行了变基,本地分支 bug_No1 的提交历史已经发生了改变,想要推送到远程仓库 bug_No1 分支时,就需要执行强制推送。 此时,主程序员只需要点一下 Rebase 按钮即可,不再会有合并冲突了。 这种解决方案其实也不是完美的,原因之前也提到了,使用了变基之后,提交历史改变了,而且需要强制推送,--force 总是让人嗅到危险的味道。此外,对于使用变基还是合并,一直有一些争议,使用变基的缺点大致有两个:1.回滚的时候可能比较麻烦 2.无法反应真实的合并历史 对于上面提到的第二个问题,Gitlab 企业版提供了解决方案:Squash and merge 实际上就是在 bug_No1 Merge 到 master 分支时,使用了git merge –squash bug_No1命令。什么是squash?还是参考[图解4种git合并分支方法] 这个功能其实是比较常用的一个功能点,Gitlab issue 上有针对这个的讨论。很可惜社区版本并不支持这个功能。 Gitlab 与 RedmineGitlab 本身的 issue 功能已经很强大了,但是还有一些瑕疵,比如 issue 状态不够多(只有新建和关闭)并且无法自定义(虽然可以使用Label,但是Label之间并不具有状态那样的互斥关系)。另外,公司原本使用 TD 作为bug追踪工具,TD 的bug序列号是全局唯一的,而 Gitlab issue 的序列号是与项目绑定的,并不是全局唯一,造成了从 T D迁移到 Gitlab issue 变得十分困难。 综上,决定使用 Redmine 作为 bug 追踪工具。 安装安装 Redmine 可以使用两种方式:原生安装 或 使用 bitnami 一键安装。 原生安装比较繁琐,这里直接选择 bitnami 的方式安装。 下载 bitnami redmine 安装包 Redmine Installers,双击,根据GUI提示安装即可。 Redmine 与 Gitlab 集成分为两部分,如下: 配置 Gitlab Redmine ServiceGitlab 项目与 Redmine 项目是一一对应的。 进入 Gitlab 的项目服务设置页面:settings->integrations,找到 redmine 的设置: 12345Active:TrueTrigger:PushProject url:Redmine 中相关的项目地址,例如 http://192.168.1.100:8888/redmine/projects/testIssues url:一般是 Project url + issues,例如 http://192.168.1.100:8888/redmine/issues/:idNew issue url:通常是 Issues url + new,例如 http://192.168.1.100:8888/redmine/projects/test/issues/new 保存设置,这样的话当你点击 merge request 等描述或者评论中 #number 时,就会自动跳转到redmine相关issue页面。 配置 Gitlab Redmine Webhook这个配置是要做到 Redmine 中的本地版本仓库自动同步 Gitlab 远程仓库,同时能够识别commit中的关键字,比如fixed #1,从而自动管理issue状态。 Redmine 配置 Git 管理员身份登录 Redmine,进入 管理->配置->版本库 1234Enabled SCM 中,我们要使用的 Gitlab,因此需要启用 Git 。如果 Git 不可选,则需要确认是否安装了 git,并在 Redmine 的 configuuration.yml 中进行设置Fetch commits automatically:True,这个选项只是在用户打开仓库页面时候会获取仓库内容Enable WS for repository management:TrueRepository management WS API key:这里需要生成一个 API Key,用于后面的 WebHook 触发 页面下方还可以配置一些关键字和对应的状态。 配置 Git 本地仓库 在 Redmine 所在的服务器上,Redmine可以访问到的路径中(一定要确定权限),使用 git 克隆对应Gitlab项目的远程仓库: git clone --mirror git@192.168.1.100:java/test.git 进入项目的仓库设置页面:配置->版本库->新建版本库; 库路径指向刚才克隆的本地仓库的位置,使用绝对路径; 在 项目->版本库 中,现在就可以看到版本库和文件了。 安装 redmine_gitlab_hook 插件 Github地址:https://github.com/phlegx/redmine_gitlab_hook 使用 git clone 下载该插件到Redmine插件目录,比如 redmine-3.4.4-1/apps/redmine/htdocs/plugins; 重启 Redmine 完成安装:在 redmine 安装目录下运行 ./ctlscript.sh restart apache 在 Redmine 的 管理->插件 中可以看到这一新安装的插件,此时配置 redmine_gitlab_hook,选中 All branches。 配置 Gitlab Webhook Webhook 是 Gitlab 的事件触发系统,这里我们借助这一功能,同 Redmine 的 Gitlab 插件协作,触发 Redmine 的自动更新。 浏览项目的 Webhook 页面:settings->integrations,新建一个Webhook,URL 栏目填写 http://redmine-url/gitlab_hook?project_id=[project-id]&key=[repository-token],key 部分就是前文中提到的Repository management WS API key。比如 http://192.168.1.100:8888/redmine/gitlab_hook?project_id=test&key=ntMQb9TedYR49MXXfN93 测试 提交一个新的提交到 Gitlab 项目分支,在提交信息中写入 fixed #1,观察Redmine 中的这一 Issue 是否发生更新(需要在Redmine中预先建好这个issue)。 同时也可以观察 Redmine 事件日志:进入 Redmine 安装目录,执行 tail -f apps/redmine/htdocs/log/production.log 结尾以上就是 Gitlab 对于 Code Review 和 issue 追踪这两个重要功能的调研。还有一个在开发过程中用到的比较重要的功能没有提到:CI(持续集成),目前的想法是 Gitlab 与 Jenkins 进行集成,等实际操作之后再来更新吧。]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java同步工具类]]></title>
<url>%2F2017%2F12%2F20%2FJava%E5%90%8C%E6%AD%A5%E5%B7%A5%E5%85%B7%E7%B1%BB%2F</url>
<content type="text"><![CDATA[CountDownLatchCountDownLatch(闭锁)的用法: CountDownLatch在实例化的时候需要传入一个int类型的计数器,表示需要等待事件的数量。CountDownLatch.countDown()方法递减这个计数器,表示一个事件已经发生了;而调用了CountDownLatch.await()方法的线程等待计数器值达到零,表示所有需要等待的事件已经发生了。若计数器值非零,那么await()方法会一直阻塞到计数器的值为零,或者等待超时。 CountDownLatch的应用场景: 确保某个计算在其所需要的所有资源都已经初始化后再继续执行。 确保某个服务在其依赖的所有其他服务都已经启动之后再启动。 等待某个操作的所有参与者都就绪后再执行。比如《荒野行动》,小队所有玩家点击“准备”之后房主才可以开始游戏。 比如,我们经常需要测试n个线程并发执行某个任务执行的时间: 123456789101112131415161718192021222324252627public long timeTasks(int nThreads, final Runnable task) throws InterruptedException { final CountDownLatch startGate = new CountDownLatch(1); final CountDownLatch endGate = new CountDownLatch(nThreads); for (int i = 0; i < nThreads; i++) { Thread t = new Thread() { public void run() { try { startGate.await(); try { task.run(); } finally { endGate.countDown(); } } catch (InterruptedException e) { } }; }; t.start(); } long start = System.nanoTime(); startGate.countDown(); endGate.await(); long end = System.nanoTime(); return end - start;} startGate保证了主线程能够同时释放所有的工作线程;endGate是主线程能够等待最后一个线程执行完成,而不是顺序的等待每个线程执行完成。 SemaphoreSemaphore(信号量)用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。Semaphore还可以用来实现某种资源池,或对容器添加边界。 Semaphore的原理是:Semaphore中管理着一组虚拟的”许可”,许可的初始数量可以通过构造函数指定。在执行某个操作时可以先获得许可(acquire),并在使用后释放许可(release)。如果当前没有许可,那么acquire将阻塞直到有许可;release方法将返回一个许可给Semaphore。注意,Semaphore并不受限于他在创建时初始化的许可数量,只要调用了release方法,Semaphore就会增加一个许可。 12345678Semaphore sem = new Semaphore(2);System.out.println(sem.availablePermits());//2sem.release();System.out.println(sem.availablePermits());//3sem.release(2);System.out.println(sem.availablePermits());//5sem.acquire();System.out.println(sem.availablePermits());//4 Semaphore还提供了一些其他的方法用来获得许可: 123456789101112131415161718192021222324252627282930313233343536/* * 获得一个许可,如果当前没有许可,将阻塞直到有许可或线程被中断 * 若线程被中断,抛出InterruptedException */public void acquire() throws InterruptedException/* * 获得指定数量的许可,如果当前没有这么多许可,将阻塞直到有许可或线程被中断 * 若线程被中断,抛出InterruptedException */public void acquire(int permits) throws InterruptedException/* * 获得一个的许可,如果当前没有许可,将阻塞直到有许可; * 不响应线程中断,若检测到线程中断,重新设置中断状态,代码返回后由上层代码处理中断 */public void acquireUninterruptibly()/* * 获得指定数量的许可,如果当前没有这么多许可,将阻塞直到有许可; * 不响应线程中断,若检测到线程中断,重新设置中断状态,代码返回后由上层代码处理中断 */public void acquireUninterruptibly(int permits)/* * 获得一个的许可,如果当前没有许可,将返回false; */public boolean tryAcquire()/* * 获得指定数量的许可,如果当前没有这么多许可,将返回false; */public boolean tryAcquire(int permits)/* * 获得一个的许可,如果当前没有许可,将阻塞直到有许可或线程被中断或超过指定时间; */public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException/* * 获得指定数量的许可,如果当前没有这么多许可,将阻塞直到有许可或线程被中断或超过指定时间; */public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException 此外,Semaphore构造函数还提供了一个bool参数,用于指定Semaphore是公平(true)还是非公平(false)的。 Semaphore维护了一个许可的阻塞等待队列; 公平策略是指:当一个线程A执行acquire方法时,如果阻塞队列有等待的线程,直接插入到阻塞队列尾节点并挂起,等待被唤醒。 非公平策略是指:当一个线程A执行acquire方法时,会直接尝试获取许可,而不管同一时刻阻塞队列中是否有线程也在等待许可。 例子:有界阻塞容器BoundedHashSet 123456789101112131415161718192021222324252627282930public class BoundedHashSet<T> { private final Set<T> set; private final Semaphore sem; public BoundedHashSet(int bound) { this.set = Collections.synchronizedSet(new HashSet<T>()); this.sem = new Semaphore(bound); } public boolean add(T o) throws InterruptedException { sem.acquire(); boolean wasAdd = false; try { wasAdd = set.add(o); return wasAdd; } finally { if (!wasAdd) { sem.release(); } } } public boolean remove(Object o) { boolean wasRemoved = set.remove(o); if (wasRemoved) { sem.release(); } return wasRemoved; }} CyclicBarrierCyclicBarrier(栅栏)从字面意思上来说,意为”循环栅栏”。所谓栅栏,就是屏障,CyclicBarrier所实现的功能就是让一组线程到达一个屏障点时阻塞(调用await方法),直到所有的线程都到达屏障的位置(都调用了await方法),此时所有的线程都被释放,CyclicBarrier也被重置便于下次使用。 CyclicBarrier提供了两组构造函数: 12public CyclicBarrier(int parties, Runnable barrierAction)public CyclicBarrier(int parties) parties 表示需要在屏障点阻塞的线程数。即,有 parties 个线程需要调用 CyclicBarrier 的 await 方法。 barrierAction 表示在所有线程到达屏障点之后需要执行的特殊动作。 若想理解 CyclicBarrier,看一下await的源码即可: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen; }}private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { final ReentrantLock lock = this.lock; lock.lock(); try { // 屏障 final Generation g = generation; if (g.broken) throw new BrokenBarrierException(); if (Thread.interrupted()) { breakBarrier(); throw new InterruptedException(); } int index = --count; // index ==0 表示此时所有线程都已经调用await方法,到达了屏障点 if (index == 0) { // tripped boolean ranAction = false; try { // 可以看到,由最后一个到达屏障点的线程执行了barrierAction final Runnable command = barrierCommand; if (command != null) command.run(); ranAction = true; // 唤醒所有等待的线程,设置新的屏障,恢复Count nextGeneration(); return 0; } finally { if (!ranAction) breakBarrier(); } } // loop until tripped, broken, interrupted, or timed out // 还有线程没有到达屏障(调用await),本线程阻塞 for (;;) { try { if (!timed) trip.await(); else if (nanos > 0L) nanos = trip.awaitNanos(nanos); } catch (InterruptedException ie) { if (g == generation && ! g.broken) { // await的线程被中断了,打破了栅栏 // 所有的线程都将被唤醒,抛出BrokenBarrierException breakBarrier(); throw ie; } else { // We're about to finish waiting even if we had not // been interrupted, so this interrupt is deemed to // "belong" to subsequent execution. Thread.currentThread().interrupt(); } } if (g.broken) throw new BrokenBarrierException(); // 线程被唤醒,可能是所有线程执行到屏障点被唤醒;也可能是到达了超时时间被唤醒 // 检查是否更新了屏障,如果更新了屏障,表示所有线程都执行到了屏障点,屏障被重置,返回当前索引 if (g != generation) return index; if (timed && nanos <= 0L) { breakBarrier(); throw new TimeoutException(); } } } finally { lock.unlock(); }}private void nextGeneration() { // signal completion of last generation trip.signalAll(); // set up next generation count = parties; generation = new Generation();}private void breakBarrier() { generation.broken = true; count = parties; trip.signalAll();} 例子:A,B,C 三个家庭开了三辆车自驾游,由于每辆车的速度不一致,约定每到达一个休息站,先到达的家庭需要等后面的家庭,等所有家庭都到齐了之后,买点东西,再同时出发。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758public class Traveling { private final CyclicBarrier barrier; private final Family[] families; /** 已经经过的休息站数量 */ private volatile int restingCount = 0; public Traveling(String[] names) { this.families = new Family[names.length]; for (int i = 0; i < families.length; ++i) { families[i] = new Family(names[i]); } int count = families.length; barrier = new CyclicBarrier(count, new Runnable() { @Override public void run() { restingCount ++; System.out.println("所有人到达休息站 " + restingCount + ", 休息一下,买点东西..."); } }); } public void start() { for (Family family : families) { new Thread(family).start(); } } class Family implements Runnable { private String name; public Family(String name) { this.name = name; } @Override public void run() { while (restingCount < 5) {// 总共需要经过5个休息站才到达终点 System.out.println(name + " 出发啦..."); int runTime = new Random().nextInt(5); try { Thread.sleep(runTime * 1000); } catch (InterruptedException e) { } System.out.println(name + " 到达休息站..."); try { barrier.await(); } catch (InterruptedException e) { } catch (BrokenBarrierException e) { } } } } public static void main(String[] args) { Traveling traveling = new Traveling(new String[]{"A", "B", "C"}); traveling.start(); }} ExchangerExchanger常用于两个线程之间安全的交换数据,在生产者-消费者线程模型中有可能会被用到。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263public class ExchangerTest { public static void main(String[] args) { Exchanger<List<Integer>> exchanger = new Exchanger<>(); new Consumer(exchanger).start(); new Producer(exchanger).start(); }}class Producer extends Thread { List<Integer> list = new ArrayList<>(); Exchanger<List<Integer>> exchanger = null; public Producer(Exchanger<List<Integer>> exchanger) { super(); this.exchanger = exchanger; } @Override public void run() { Random rand = new Random(); for (int i = 0; i < 10; i++) { list.clear(); // 生产数据 list.add(rand.nextInt(10000)); list.add(rand.nextInt(10000)); list.add(rand.nextInt(10000)); list.add(rand.nextInt(10000)); list.add(rand.nextInt(10000)); try { list = exchanger.exchange(list); } catch (InterruptedException e) { e.printStackTrace(); } } }}class Consumer extends Thread { List<Integer> list = new ArrayList<>(); Exchanger<List<Integer>> exchanger = null; public Consumer(Exchanger<List<Integer>> exchanger) { super(); this.exchanger = exchanger; } @Override public void run() { for (int i = 0; i < 10; i++) { try { list = exchanger.exchange(list); } catch (InterruptedException e) { e.printStackTrace(); } // 消费数据 System.out.print(list.get(0) + ", "); System.out.print(list.get(1) + ", "); System.out.print(list.get(2) + ", "); System.out.print(list.get(3) + ", "); System.out.println(list.get(4) + ", "); } }} FutureTask在并发场景下缓存的创建这篇文章中已经涉及到了FutureTask,FutureTask实现了Runnable和Future接口: 1234567public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;} 可以看出来,Future接口主要用来管理和查询一个任务的生命周期。FutureTask表示的任务是通过Callable来实现的,任务可能处于以下三种状态: 等待运行 正在运行 执行完毕,包括正常结束、由于取消而结束或由于异常而结束 Future.get方法的行为取决于任务的状态:如果任务已经完成,get方法会立即返回结果,否则get方法将一直阻塞直到任务进入完成状态,然后返回结果或抛出异常。FutureTask安全地将计算结果从计算结果的线程 传递到 获取这个结果的线程。 FutureTask可以表示一个异步的任务,用来执行一些时间较长的计算,这些计算可以在使用计算结果之前启动。 例子:实现一个Html页面渲染器。 最简单的方法是对HTML文档串行处理。遇到文本标签时,将其绘制到画布;遇到图片引用时,先通过网络获取他,然后绘制到画布。这种方法的缺点显而易见,那就是网络获取图片可能会比较耗时,用户需要等待较长的时间才能看到所有的文本。 在这里,我们找到了可以利用的并行性:将渲染过程分解为两个任务,一个是渲染所有的文本,另一个是下载所有的图片。绘制文本和下载图片可以同时进行。 123456789101112131415161718192021222324252627282930public class FutureRenderer { private final ExecutorService executor = Executors.newCachedThreadPool(); void renderPage(CharSequence source) { final List<ImageInfo> imageInfos = scanForImageInfo(source); Callable<List<ImageData>> task = new Callable<List<ImageData>>() { public List<ImageData> call() { List<ImageData> result = new ArrayList<ImageData>(); for (ImageInfo imageInfo : imageInfos) result.add(imageInfo.downloadImage()); return result; } }; Future<List<ImageData>> future = executor.submit(task); renderText(source); try { List<ImageData> imageData = future.get(); for (ImageData data : imageData) renderImage(data); } catch (InterruptedException e) { Thread.currentThread().interrupt(); future.cancel(true); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } }} 可以看到,在渲染文本的任务开始之前,已经将下载图片的任务提交到线程池了。文本渲染完毕后,等待Future.get获取结果,然后渲染图片。这样,图片下载与文本渲染这两个任务就实现了并行执行。 CompletionService上个小结页面渲染的不足在于:用户需要等待所有的图片下载完毕,没有办法下载一张显示一张;如果渲染文本的速度远远大于下载图片的速度,那么程序并行后的性能与串行执行差别不大。 我们可以利用CompletionService将每张图片的下载创建成为并行的任务,减少下载所有图形需要的总时间;然后实现下载一张图片就立刻显示的功能。 CompletionService将Executor和BlockingQueue的功能结合在了一起,可以将Callable类型的任务提交给他执行,然后使用类似于队列的操作take或poll等方法获取已经完成的结果。 ExecutorCompletionService实现了CompletionService接口。ExecutorCompletionService的实现很简单,ExecutorCompletionService内部维护了一个BlockingQueue,用来保存计算结果。同时将任务包装为FutureTask的子类QueueingFuture: 12345678private class QueueingFuture extends FutureTask<Void> { QueueingFuture(RunnableFuture<V> task) { super(task, null); this.task = task; } protected void done() { completionQueue.add(task); } private final Future<V> task;} 当任务执行完毕后,会调用done方法,将结果放入BlockingQueue当中,CompletionService的take和poll方法委托给BlockingQueue,这些方法会在得出结果之前阻塞。 123456789101112131415161718192021222324252627282930313233public class Renderer { private final ExecutorService executor; Renderer(ExecutorService executor) { this.executor = executor; } void renderPage(CharSequence source) { final List<ImageInfo> info = scanForImageInfo(source); CompletionService<ImageData> completionService = new ExecutorCompletionService<ImageData>(executor); for (final ImageInfo imageInfo : info) completionService.submit(new Callable<ImageData>() { public ImageData call() { return imageInfo.downloadImage(); } }); renderText(source); try { for (int t = 0, n = info.size(); t < n; t++) { Future<ImageData> f = completionService.take(); ImageData imageData = f.get(); renderImage(imageData); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { throw launderThrowable(e.getCause()); } }}]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java阻塞队列]]></title>
<url>%2F2017%2F12%2F10%2FJava%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97%2F</url>
<content type="text"><![CDATA[BlockingQueue(阻塞队列)是java.util.concurrent包下面的一个接口,在Java并发编程中,阻塞队列占有重要的一席之地。比如,线程池、消息队列等等。总结一下Java中的阻塞队列。 BlockingQueueBlockingQueue是所有阻塞队列的父接口,定义了一个队列应该具备的基本功能,即’存’和’取’的接口。阻塞队列常常运用于生产者消费者的编程模型当中,从中存入或取出元素都是线程安全的。 所谓的”阻塞队列”,即若队列为空,则 [消费者线程] 试图从队列取出元素时阻塞,直到 [生产者线程] 向其中放入一个元素。若队列已满,则 [生产者线程] 试图向队列插入元素时阻塞,直到 [消费者线程] 从中取出一个元素。 满足上述功能的接口即 put(o) 与 take()。除此以外,BlockingQueue还提供了其他类型的存取接口: 抛出异常 返回特定的值 阻塞 带超时的阻塞 插入 add(o) offer(o) put(o) offer(o,timeout,timeunit) 移除 remove(o) poll() take() poll(timeout,timeunit) 检查 element() peek() 插入:即向队列中添加一个元素; 移除:即从队列中取出一个元素,同时从队列中删除该元素; 检查:即从队列中取出一个元素,但不删除该元素 抛出异常:即当调用的方法不能顺利完成操作时(比如从空队列中取出一个元素),抛出异常 返回特定值:即当调用的方法不能顺利完成操作时,返回一个特定的值。比如add(o),如果此时队列已满插入失败,则返回false 阻塞:即当调用的方法不能顺利完成操作时,一直阻塞直到完成该操作 带超时的阻塞:即当调用的方法不能顺利完成操作时,阻塞直到操作完成或到达指定超时时间 ArrayBlockingQueueArrayBlockingQueue实现了BlockingQueue接口。从名字上就可以看出,ArrayBlockingQueue是基于数组实现的。 ArrayBlockingQueue是一个有界队列,同时,该队列具有先进先出的特性。ArrayBlockingQueue维护了两个索引,takeIndex和putIndex。takeIndex指向下一个要取出的元素位置,即队列头部;putIndex指出下一个元素要插入的位置,即队列尾部。 ArrayBlockingQueue的实现很简单,是生产者消费者模型的一个典型例子。其内部使用两个条件对象notEmpty、notFull完成生产者消费者线程的等待和唤醒。 LinkedBlockingQueueLinkedBlockingQueue同样实现了BlockingQueue接口。顾名思义,LinkedBlockingQueue底层是用链表结构存储元素的。与ArrayBlockingQueue一样,LinkedBlockingQueue也是先进先出队列。 LinkedBlockingQueue的默认构造函数会将其设为无界队列(Integer.MAX_VALUE),也可以调用LinkedBlockingQueue(int capacity)将其设为有界队列。 PriorityBlockingQueuePriorityBlockingQueue即优先级队列。PriorityBlockingQueue是一个无界队列,其底层存储使用了数组,存储空间不足时会发生自动扩容。 既然是优先级队列,表示该队列是按优先级排序的。PriorityBlockingQueue内部使用了最小堆进行排序,不允许插入不可比较的对象(包括NULL)。 有两种方法实现队列元素可比较: 第一种PriorityBlockingQueue接受一个实现了Comparatorj接口的对象作为构造函数参数。 第二种是插入的元素本身实现了Comparable接口。 对于优先级相同的元素(compare结果为0)PriorityBlockingQueue没有确定他们之间被取出的顺序,可以自定义元素的包装类实现相同元素FIFO的顺序: 1234567891011121314class FIFOEntry<E extends Comparable<? super E>> implements Comparable<FIFOEntry<E>> { static final AtomicLong seq = new AtomicLong(0); final long seqNum; final E entry; public FIFOEntry(E entry) { seqNum = seq.getAndIncrement(); this.entry = entry; } public E getEntry() { return entry; } public int compareTo(FIFOEntry<E> other) { if (res == 0 && other.entry != this.entry) res = (seqNum < other.seqNum ? -1 : 1); return res; } 还有一点需要注意的是,使用PriorityBlockingQueue返回的迭代器Iterator对队列进行迭代时,所迭代的顺序并不一定是具有优先级的。如果需要按照优先级顺序进行迭代,可以使用Arrays.sort(pq.toArray())先将PriorityBlockingQueue.toArray()得到的数组进行排序,然后对该数组进行遍历。PriorityBlockingQueue.toArray()返回的数组,其中的元素并不映射到PriorityBlockingQueue存储元素的数组,而是其拷贝。 DelayQueueDelayQueue在如何执行一个延迟任务?中已经提到过了。 DelayQueue内部元素的存储委托给了PriorityQueue。PriorityQueue与PriorityBlockingQueue相似,是具有优先级排序的队列,只不过没有实现BlockingQueue接口,同时也不是线程安全的。因为使用了PriorityQueue存储元素,所以DelayQueue也是无界队列。 DelayQueue的思想是:插入的元素按指定的优先级顺序排列,而这个优先级顺序是到期时间。即距离到期时间最短的元素具有最高优先级。同时,DelayQueue的元素也只有到了到期时间后才可被消费者线程取出。 这样的话,只要看队列优先级最高元素即可,所有的消费者线程阻塞在take方法上,直到优先级最高元素到期,被某个消费者线程取出,队列下一个元素成为优先级最高元素,其余消费者线程继续阻塞。 如何获取元素的到期时间?DelayQueue要求插入队列的元素必须实现Delayed接口。 123public interface Delayed extends Comparable<Delayed> { long getDelay(TimeUnit unit);} getDelay方法到期时间,返回值小于等于0时,认为到达过期时间。 可以看到,Delayed接口继承了Comparable接口,意味着两个Delayed对象之间可以进行比较。因此DelayQueue(PriorityQueue)可以对插入的元素进行排序。 ScheduledThreadPoolExecutor.ScheduledFutureTask实现了Delayed接口,ScheduledThreadPoolExecutor使用ScheduledFutureTask为任务进行排序。 ScheduledFutureTask:123456789101112131415161718192021222324public long getDelay(TimeUnit unit) { // 返回到期时间与当前时间相距的纳秒数 return unit.convert(time - now(), TimeUnit.NANOSECONDS);}public int compareTo(Delayed other) { if (other == this) // compare zero ONLY if sameobject return 0; if (other instanceof ScheduledFutureTask) { ScheduledFutureTask<?> x = (ScheduledFutureTask<>)other; long diff = time - x.time; if (diff < 0) return -1; else if (diff > 0) return 1; else if (sequenceNumber < x.sequenceNumber) return -1; else return 1; } long d = (getDelay(TimeUnit.NANOSECONDS) - other.getDelay(TimeUnit.NANOSECONDS)); return (d == 0) ? 0 : ((d < 0) ? -1 : 1);} SynchronousQueueSynchronousQueue严格意义上来说,并不能称之为队列。SynchronousQueue内部并没有数据缓存空间。当一个生产者线程试图插入一个元素到SynchronousQueue时会被阻塞,直到有消费者线程取走这个元素。同样的,当一个消费者线程试图从SynchronousQueue取出一个元素时会被阻塞,直到有生产者线程插入了一个元素。 有点类似于”一手交钱一手交货”的情景,数据是在配对的生产者和消费者线程之间直接传递的。SynchronousQueue提供了线程之间安全的交换元素的方法。 Executors.newCachedThreadPool()使用了SynchronousQueue,保证了如果有空闲线程,则使用空闲线程执行任务,若没有空闲线程,则创建新的线程来执行任务。适合任务执行时间短,生产者速度小于消费者速度的场景。 When should I use SynchronousQueue讨论了SynchronousQueue的应用场景。 BlockingDeque与BlockingQueue类似,对BlockingDeque的存取也是线程安全的。实际上,BlockingDeque接口继承自BlockingQueue和Deque。Deque,意味着”Double Ended Queue”,即可以从两端分别进行存取的队列。BlockingQueue只可以从队列头部取出,从对列尾部插入,与BlockingQueue相比,BlockingDeque既可以从头部插入、取出,也可以从尾部插入、取出。 在Java-ForkJoin框架这篇文章中提到过的”工作窃取”技术,就是基于双端队列实现的。 BlockingDeque提供了4组从队列中获取、插入、查看的方法: 抛出异常 返回特定的值 阻塞 带超时的阻塞 插入 addFirst(o) offerFirst(o) putFirst(o) offerFirst(o,timeout,timeunit) 移除 removeFirst(o) pollFirst() takeFirst() pollFirst(timeout,timeunit) 检查 elementFirst() peekFirst() 插入 addLast(o) offerLast(o) putLast(o) offerLast(o,timeout,timeunit) 移除 removeLast(o) pollLast() takeLast() pollLast(timeout,timeunit) 检查 elementLast() peekLast() BlockingDeque的实现类为LinkedBlockingDeque,是一个基于链表结构的双端队列。 参考java.util.concurrent - Java Concurrency Utilities]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java-ForkJoin框架]]></title>
<url>%2F2017%2F11%2F30%2F%E7%AE%80%E5%8D%95%E5%AD%A6%E4%B9%A0Java-ForkJoin%E6%A1%86%E6%9E%B6%2F</url>
<content type="text"><![CDATA[分治思想分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。 分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。 分治法在每一层递归上都有三个步骤: 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题; 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题 合并:将各个子问题的解合并为原问题的解。 归并排序运用了分治思想: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970public class MergeSort { static int number = 0; int[] arr; public static void main(String[] args) { int[] a = { 26, 5, 98, 108, 28, 99, 100, 56, 34, 1 }; printArray("排序前:", a); MergeSort mergeSort = new MergeSort(a); mergeSort.sort(); printArray("排序后:", a); } private static void printArray(String pre, int[] a) { System.out.print(pre + "\n"); for (int i = 0; i < a.length; i++) System.out.print(a[i] + "\t"); System.out.println(); } public MergeSort(int[] a) { this.arr = a; } public void sort() { sort(arr, 0, arr.length - 1); } private void sort(int[] a, int left, int right) { if (left >= right) return; int mid = (left + right) / 2; sort(a, left, mid); sort(a, mid + 1, right); merge(a, left, mid, right); } private void merge(int[] a, int left, int mid, int right) { int[] tmp = new int[a.length]; int r1 = mid + 1; int tIndex = left; int cIndex = left; // 逐个归并 while (left <= mid && r1 <= right) { if (a[left] <= a[r1]) tmp[tIndex++] = a[left++]; else tmp[tIndex++] = a[r1++]; } // 将左边剩余的归并 while (left <= mid) { tmp[tIndex++] = a[left++]; } // 将右边剩余的归并 while (r1 <= right) { tmp[tIndex++] = a[r1++]; } System.out.println("第" + (++number) + "趟排序:\t"); // 从临时数组拷贝到原数组 while (cIndex <= right) { a[cIndex] = tmp[cIndex]; // 输出中间归并排序结果 System.out.print(a[cIndex] + "\t"); cIndex++; } System.out.println(); }} fork/joinJava1.7 提供的fork/join框架运用了上面提到的分治思想。 fork/join框架由两部分组成: ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类: RecursiveAction:用于没有返回结果的任务。 RecursiveTask :用于有返回结果的任务。 ForkJoinPool :ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程(ForkJoinWorkerThread)所维护的双端队列中,进入队列的头部。 ForkJoinPool提供了三种接口来提交任务: execute(ForkJoinTask): Arrange asynchronous execution invoke(ForkJoinTask): Await and obtain result submit(ForkJoinTask): Arrange exec and obtain Future fork/join框架还运用到了一个技术:工作窃取 每个工作线程都有一个工作队列,这个队列是双端队列。对于该线程,新的任务进入队列头部,执行任务则从头部取出。若该线程的工作队列为空,也就是没有任务可以执行,则从其他线程的工作队列的尾部窃取任务执行。这样做很大程度上减少了对队列的访问冲突。 下面分别参考两个使用了fork/join框架的例子,针对RecursiveAction和RecursiveTask: 网络爬虫 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107public class WebCrawer implements LinkHandler { private final Collection<String> visitedLinks = Collections.synchronizedSet(new HashSet<String>()); private String url; private ForkJoinPool mainPool; public WebCrawer(String startingURL, int maxThreads) { this.url = startingURL; mainPool = new ForkJoinPool(maxThreads); } private void startCrawling() { mainPool.invoke(new LinkFinderAction(this.url, this)); } @Override public int size() { return visitedLinks.size(); } @Override public void addVisited(String s) { visitedLinks.add(s); } @Override public boolean visited(String s) { return visitedLinks.contains(s); } /** * @param args the command line arguments */ public static void main(String[] args) throws Exception { new WebCrawer("http://www.baidu.com", 8).startCrawling(); }}interface LinkHandler { /** * Returns the number of visited links * @return */ int size(); /** * Checks if the link was already visited * @param link * @return */ boolean visited(String link); /** * Marks this link as visited * @param link */ void addVisited(String link);}class LinkFinderAction extends RecursiveAction { private String url; private LinkHandler cr; /** * Used for statistics */ private static final long t0 = System.nanoTime(); public LinkFinderAction(String url, LinkHandler cr) { this.url = url; this.cr = cr; } @Override public void compute() { if (!cr.visited(url)) { System.out.println(url); try { List<RecursiveAction> actions = new ArrayList<RecursiveAction>(); URL uriLink = new URL(url); Parser parser = new Parser(uriLink.openConnection()); NodeList list = parser.extractAllNodesThatMatch(new NodeClassFilter(LinkTag.class)); for (int i = 0; i < list.size(); i++) { LinkTag extracted = (LinkTag) list.elementAt(i); if (!extracted.extractLink().isEmpty() && !cr.visited(extracted.extractLink())) { actions.add(new LinkFinderAction(extracted.extractLink(), cr)); } } cr.addVisited(url); if (cr.size() == 1500) { System.out.println("Time for visit 1500 distinct links= " + (System.nanoTime() - t0)); } //invoke recursively invokeAll(actions); } catch (Exception e) { //ignore 404, unknown protocol or other server errors } } }} 词频统计 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193class Document { private final List<String> lines; Document(List<String> lines) { this.lines = lines; } List<String> getLines() { return this.lines; } static Document fromFile(File file) throws IOException { List<String> lines = new LinkedList<>(); try(BufferedReader reader = new BufferedReader(new FileReader(file))) { String line = reader.readLine(); while (line != null) { lines.add(line); line = reader.readLine(); } } return new Document(lines); }}/* ......................................................................................... */class Folder { private final List<Folder> subFolders; private final List<Document> documents; Folder(List<Folder> subFolders, List<Document> documents) { this.subFolders = subFolders; this.documents = documents; } List<Folder> getSubFolders() { return this.subFolders; } List<Document> getDocuments() { return this.documents; } static Folder fromDirectory(File dir) throws IOException { List<Document> documents = new LinkedList<>(); List<Folder> subFolders = new LinkedList<>(); for (File entry : dir.listFiles()) { if (entry.isDirectory()) { subFolders.add(Folder.fromDirectory(entry)); } else { documents.add(Document.fromFile(entry)); } } return new Folder(subFolders, documents); }}/* ......................................................................................... */public class WordCounter { /* ......................................................................................... */ String[] wordsIn(String line) { return line.trim().split("(\\s|\\p{Punct})+"); } Long occurrencesCount(Document document, String searchedWord) { long count = 0; for (String line : document.getLines()) { for (String word : wordsIn(line)) { if (searchedWord.equals(word)) { count = count + 1; } } } return count; } /* ......................................................................................... */ Long countOccurrencesOnSingleThread(Folder folder, String searchedWord) { long count = 0; for (Folder subFolder : folder.getSubFolders()) { count = count + countOccurrencesOnSingleThread(subFolder, searchedWord); } for (Document document : folder.getDocuments()) { count = count + occurrencesCount(document, searchedWord); } return count; }/* ......................................................................................... */ class DocumentSearchTask extends RecursiveTask<Long> { private final Document document; private final String searchedWord; DocumentSearchTask(Document document, String searchedWord) { super(); this.document = document; this.searchedWord = searchedWord; } @Override protected Long compute() { return occurrencesCount(document, searchedWord); } }/* ......................................................................................... */ class FolderSearchTask extends RecursiveTask<Long> { private final Folder folder; private final String searchedWord; FolderSearchTask(Folder folder, String searchedWord) { super(); this.folder = folder; this.searchedWord = searchedWord; } @Override protected Long compute() { long count = 0L; List<RecursiveTask<Long>> forks = new LinkedList<>(); for (Folder subFolder : folder.getSubFolders()) { FolderSearchTask task = new FolderSearchTask(subFolder, searchedWord); forks.add(task); task.fork(); } for (Document document : folder.getDocuments()) { DocumentSearchTask task = new DocumentSearchTask(document, searchedWord); forks.add(task); task.fork(); } for (RecursiveTask<Long> task : forks) { count = count + task.join(); } return count; } } /* ......................................................................................... */ private final ForkJoinPool forkJoinPool = new ForkJoinPool(); Long countOccurrencesInParallel(Folder folder, String searchedWord) { return forkJoinPool.invoke(new FolderSearchTask(folder, searchedWord)); }/* ......................................................................................... */ public static void main(String[] args) throws IOException { args = new String[3]; args[0] = "D:\\code\\driver"; args[1] = "java"; WordCounter wordCounter = new WordCounter(); Folder folder = Folder.fromDirectory(new File(args[0])); // int repeatCount = Integer.decode(args[2]); int repeatCount = 1; long counts; long startTime; long stopTime; long[] singleThreadTimes = new long[repeatCount]; long[] forkedThreadTimes = new long[repeatCount]; for (int i = 0; i < repeatCount; i++) { startTime = System.currentTimeMillis(); counts = wordCounter.countOccurrencesOnSingleThread(folder, args[1]); stopTime = System.currentTimeMillis(); singleThreadTimes[i] = (stopTime - startTime); System.out.println(counts + " , single thread search took " + singleThreadTimes[i] + "ms"); } for (int i = 0; i < repeatCount; i++) { startTime = System.currentTimeMillis(); counts = wordCounter.countOccurrencesInParallel(folder, args[1]); stopTime = System.currentTimeMillis(); forkedThreadTimes[i] = (stopTime - startTime); System.out.println(counts + " , fork / join search took " + forkedThreadTimes[i] + "ms"); } System.out.println("\nCSV Output:\n"); System.out.println("Single thread,Fork/Join"); for (int i = 0; i < repeatCount; i++) { System.out.println(singleThreadTimes[i] + "," + forkedThreadTimes[i]); } System.out.println(); }} 上面的代码分别参考自Java Tip: When to use ForkJoinPool vs ExecutorService和Fork and Join: Java Can Excel at Painless Parallel Programming Too! fork/join框架适用于以下几种场景: 可以通过将任务分割-合并得到结果,fork/join可以并行处理子问题,提高处理效率。 fork/join善于处理并行问题。可并行处理问题要么是彼此完全独立的问题,要么是可分解单独处理的问题。可分解单独处理的问题即1中提到的,彼此完全独立的问题譬如 事件类型(不需要join)的任务(akka)。 fork/join应该用于处理cup密集型的计算任务。fork/join不适用于执行包含阻塞io的任务类型。 ForkJoin与MapReduce很类似,都是基于分治思想,用于执行并行任务。他们之间的差异在于: MapReduce用于分布式环境(利用很多机器做分布式计算),ForkJoin用于单机多核(充分利用多处理器)。 MapReduce一开始就先切分好任务然后再执行,并且彼此间在最后合并之前不需要通信,而ForkJoin任务自己知道该如何切分自己,递归地切分到一组合适大小的子任务来执行。 参考10: ♦ ExecutorService Vs Fork/Join & Future Vs CompletableFuture Interview Q&As Fork and Join: Java Can Excel at Painless Parallel Programming Too! Java Tip: When to use ForkJoinPool vs ExecutorService]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java基础-字节]]></title>
<url>%2F2017%2F11%2F23%2FJava%E5%9F%BA%E7%A1%80-%E5%AD%97%E8%8A%82%2F</url>
<content type="text"><![CDATA[这两天在实施一个加密通信方案时,涉及到了字节的读写和转换,有一些知识点需要学习和记录下来… 原码 反码 补码原码:第一位表示符号,其余位表示数值。比如8位2进制: [+1]原 = 0000 0001 [-1]原 = 1000 0001 第一位是符号位,8位2进制的取值范围: [11111111, 01111111] = [-127, 127] 反码:正数的反码是等于原码,负数的反码是在其原码的基础上,符号位不变,其余位取反: [+1] = [00000001]原 = [00000001]反 [-1] = [10000001]原 = [11111110]反 补码:正数的补码等于原码,负数的补码是在其原码的基础上,符号位不变,其余位取反,然后加1: [+1] = [00000001]原 = [00000001]反 = [00000001]补 [-1] = [10000001]原 = [11111110]反 = [11111111]补 在Java中,存储的数值都是有符号的,同时也是使用补码存储的。 1234public static void main(String[] args) { int a = -1; System.out.println(Integer.toBinaryString(a));} 输出: 111111111111111111111111111111111 Primitive Data Types: 123456789101112131415byte: The byte data type is an 8-bit signed two's complement integer. It has a minimum value of -128 and a maximum value of 127 (inclusive). The byte data type can be useful for saving memory in large arrays, where the memory savings actually matters. They can also be used in place of int where their limits help to clarify your code; the fact that a variable's range is limited can serve as a form of documentation.short: The short data type is a 16-bit signed two's complement integer. It has a minimum value of -32,768 and a maximum value of 32,767 (inclusive). As with byte, the same guidelines apply: you can use a short to save memory in large arrays, in situations where the memory savings actually matters.int: By default, the int data type is a 32-bit signed two's complement integer, which has a minimum value of -231 and a maximum value of 231-1. In Java SE 8 and later, you can use the int data type to represent an unsigned 32-bit integer, which has a minimum value of 0 and a maximum value of 232-1. Use the Integer class to use int data type as an unsigned integer. See the section The Number Classes for more information. Static methods like compareUnsigned, divideUnsigned etc have been added to the Integer class to support the arithmetic operations for unsigned integers.long: The long data type is a 64-bit two's complement integer. The signed long has a minimum value of -263 and a maximum value of 263-1. In Java SE 8 and later, you can use the long data type to represent an unsigned 64-bit long, which has a minimum value of 0 and a maximum value of 264-1. Use this data type when you need a range of values wider than those provided by int. The Long class also contains methods like compareUnsigned, divideUnsigned etc to support arithmetic operations for unsigned long.float: The float data type is a single-precision 32-bit IEEE 754 floating point. Its range of values is beyond the scope of this discussion, but is specified in the Floating-Point Types, Formats, and Values section of the Java Language Specification. As with the recommendations for byte and short, use a float (instead of double) if you need to save memory in large arrays of floating point numbers. This data type should never be used for precise values, such as currency. For that, you will need to use the java.math.BigDecimal class instead. Numbers and Strings covers BigDecimal and other useful classes provided by the Java platform.double: The double data type is a double-precision 64-bit IEEE 754 floating point. Its range of values is beyond the scope of this discussion, but is specified in the Floating-Point Types, Formats, and Values section of the Java Language Specification. For decimal values, this data type is generally the default choice. As mentioned above, this data type should never be used for precise values, such as currency.boolean: The boolean data type has only two possible values: true and false. Use this data type for simple flags that track true/false conditions. This data type represents one bit of information, but its "size" isn't something that's precisely defined.char: The char data type is a single 16-bit Unicode character. It has a minimum value of '\u0000' (or 0) and a maximum value of '\uffff' (or 65,535 inclusive). 有符号 无符号上文可以看出,Java 中的 byte 是 1 字节,short 是 2 字节,int 是 4 字节,long 是 8 字节。他们都是有符号的数值。 类型 最小值 最大值 byte -2^7 2^7-1 short -2^15 2^15-1 int -2^31 2^31-1 long -2^63 2^63-1 发现byte类型跟上文所说的取值范围[-127, 127]不太一样,这是因为使用了补码的缘故。查看原码, 反码, 补码 详解了解。 C 语言中的整数类型都提供了对应的”无符号”版本,第一位不再表示符号位。比如C语言中的无符号类型byte,其取值范围为[0, 256]。 所以,当C程序向Java程序通过网络传递了一个无符号数时,我们需要怎么存他呢? 答案就是:使用比要用的无符号类型更大的有符号类型。 比如:使用 short 来处理无符号的字节,使用 long 来处理无符号整数等。下面看一个例子,使用int(4字节)存储一个无符号byte(1字节): 1234567public static void main(String[] args) { int a = 250;// 无符号byte的取值范围[0, 256] byte b = (byte) a; // 强制转换,直接截取int低8位 // b 相当于C后台发来的无符号数 int c = b & 0xff; System.out.println(c);// 250} 上面的程序中,b 当做C后台发来的无符号数,最后我们使用int存下了这个无符号的byte。为什么需要 b & 0xff这步操作呢? 1234567public static void main(String[] args) { int a = 250;// 无符号byte的取值范围[0, 256] byte b = (byte) a; // 强制转换,直接截取int低8位 // b 相当于C后台发来的无符号数 int c = b; System.out.println(c);// -6} 可以看到,无符号byte直接转为int,丢失了原本的数值。为什么执行 b & 0xff这步操作就可以了呢? 看下文 符号位扩展上文中,b直接转换为 int,丢失了无符号byte原本的数值。是因为,byte在向int转换的过程中,发生了符号位扩展: [250]无符号 = 1111 1010 转为4字节int: 1111 1111 1111 1111 1111 1111 1111 1010 = [-6]补 在Java中,当较窄的整型扩展为较宽的整型时,发生符号位扩展: 对于正数而言,将需要扩展的高位全部赋为0; 对于负数而言,将需要扩展的高位全部赋为1。 观察下面的代码: 1System.out.println((int)(char)(byte)-1); //65535 为什么没有输出-1呢? 因为如果最初的类型是char,那么不管他将要被提升成什么类型,都执行0扩展,即需要扩展的高位全部赋0。 byte是有符号的类型,所以在将byte数值-1(二进制为:11111111)提升到char时,会发生符号位扩展,又符号位为1,所以就补8个1,最后为16个1;然后从char到int的提升时,由于是char型提升到其他类型,所以采用零扩展而不是符号扩展,结果int数值就成了65535。 总结: 窄的整型转换成较宽的整型时符号扩展规则:如果最初的数值类型是有符号的,那么就执行符号扩展(即如果符号位为1,则扩展为1,如果为零,则扩展为0);如果它是char,那么不管它将要被提升成什么类型,都执行零扩展。 回顾上文提到的0xff问题:int c = b & 0xff; 对于0xff,是Java中的字面常量,本身是个int值。0xff 表示为 11111111 ,Java对于这种字面常量,不把他前面的1看做符号位,当发生符号位扩展时,扩展成的是”000…ff”。 当 执行 b & 0xff 时,b发生符号位扩展: 1111 1111 1111 1111 1111 1111 1111 1010&0000 0000 0000 0000 0000 0000 1111 1111=0000 0000 0000 0000 0000 0000 1111 1010= [250]补 逻辑右移 算数右移Java中有三种移位操作:左移、算数右移、逻辑右移 注意: short, byte,char 在移位之前首先将数据转换为int,然后再移位 算数右移:>>,有符号的移位操作,右移之后的空位用符号位补充,如果是正数用 0 补充,负数用1补充。 -4>>1 [-4]原= 10000000 00000000 00000000 00000100[-4]补= 11111111 11111111 11111111 111111000 向右移出 1 位后 11111111 11111111 11111111 11111110 = [-2]补 逻辑右移:>>>,不管正数、负数,左端都用0补充。 -1>>>1[-1]原= 10000000 00000000 00000000 00000001[-1]补= 11111111 11111111 11111111 111111111 向右移出1位 01111111 11111111 11111111 11111111 = [2^31-1]补 算数左移:<<,左移后右端用0补充。 字节序字节顺序是指占用内存多于一个字节类型的数据在内存中的存放顺序,有小端、大端两种顺序。 小端字节序(little endian):低字节数据存放在内存低地址处,高字节数据存放在内存高地址处; 大端字节序(bigendian):高字节数据存放在低地址处,低字节数据存放在高地址处。 int value = 0x01020304;采用不同的字节序,在内存中的存储情况如下: 小端字节序: 内存地址编号 字节内容 0x00001000 04 0x00001001 03 0x00001002 02 0x00001003 01 大端字节序: 内存地址编号 字节内容 0x00001000 01 0x00001001 02 0x00001002 03 0x00001003 04 显然大字节序,比较符合人类思维习惯。 JAVA字节序都为大端字节序,所谓JAVA字节序,是指在JAVA虚拟机中多字节类型数据的存放顺序。 java.nio包中提供了 ByteOrder.nativeOrder()方法来查看主机的字节序。 还可以指定ByteBuffer读写操作时的字节序:byteBuffer.order(ByteOrder.LITTLE_ENDIAN) 例子byte 数组 转为 int (默认为大端字节序) 12345678910111213141516// 第一种方法public int convertByteToInt(byte[] b){ int value= 0; for(int i=0; i<b.length; i++) value = (value << 8) | b[i]; return value; }// 第二种方法public static int byteArrayToInt(byte[] b) { return b[3] & 0xFF | (b[2] & 0xFF) << 8 | (b[1] & 0xFF) << 16 | (b[0] & 0xFF) << 24; }// 第三种方法ByteBuffer.wrap(byteBarray).getInt(); int 转为 byte 数组 (默认为大端字节序) 1234567891011121314151617181920212223// 第一种方法private byte[] intToByteArray ( final int i ) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeInt(i); dos.flush(); return bos.toByteArray();}// 第二种方法public byte[] intToBytes( final int i ) { ByteBuffer bb = ByteBuffer.allocate(4); bb.putInt(i); return bb.array();}// 第三种方法public static byte[] intToByteArray(int a) { return new byte[] { (byte) ((a >> 24) & 0xFF), (byte) ((a >> 16) & 0xFF), (byte) ((a >> 8) & 0xFF), (byte) (a & 0xFF) };} 从流中读取指定长度的整数: 12345678910 public static int ReceiveIntegerR(InputStream input, int siz) throws IOException { int n = 0; for (int i = 0; i < siz; i++) { int b = input.read(); if (b < 0) throw new IOException(); n = b | (n << 8); } return n;} 向流中写入指定长度的整数: 12345678public static void SendInteger(OutputStream output, int val, int siz) throws IOException { byte[] buf = new byte[siz]; while (siz-- > 0) { buf[siz] = (byte) (val & 0xff); val >>= 8; } output.write(buf);}]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[理解Java-Reference]]></title>
<url>%2F2017%2F11%2F21%2F%E7%90%86%E8%A7%A3Java-Reference%2F</url>
<content type="text"><![CDATA[最近重读《深入理解Java虚拟机》,讲到Java中的几种引用类型,结合源码总结梳理一遍。 引用类型JDK1.2之后,Java扩充了引用的概念,将引用分为强引用、软引用、弱引用和虚引用四种。 强引用 类似于”Object a = new Object()”这类的引用,只要垃圾强引用存在,垃圾回收器就不会回收掉被引用的对象。 软引用 对于软引用关联的对象,在系统将要发生内存溢出异常之前,会把这些对象列入垃圾回收范围中进行回收。如果这次回收还没有足够内存,则抛出内存异常。 使用SoftReference类实现软引用 弱引用 强度比软引用更弱,被弱引用关联的对象只能存活到下一次垃圾回收发生之前。当发生GC时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 使用WeakReference类实现弱引用 虚引用 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能够在这个对象被垃圾回收器回收掉后收到一个通知。 使用PhantomReference类实现虚引用 使用场景强引用代码中随处可见,对于其他几种引用则不太熟悉,他们有什么作用呢? 假设有这样一个需求:每次创建一个数据库Connection的时候,需要将用户信息User与之关联。典型的用法就是在一个全局的Map中存储Connection和User的映射。 12345678910111213public class ConnManager { private Map<Connection,User> m = new HashMap<Connection,User>(); public void setUser(Connection s, User u) { m.put(s, u); } public User getUser(Connection s) { return m.get(s); } public void removeUser(Connection s) { m.remove(s); }} 这种方法的问题是User的生命周期与Connection挂钩,我们无法准确预支Connection在什么时候结束,所以需要在每个Connection关闭之后,手动从Map中移除键值对,否则Connection和User将一直被Map引用,即使Connection的生命周期已经结束了,GC也无法回收对应的Connection和User。这些对象留在内存中不受控制,可能会造成内存溢出。 那么,如何避免手动的从Map中删除对象呢? 利用 WeakHashMap 即可实现: 12345678910public class ConnManager { private Map<Connection,User> m = new WeakHashMap<Connection,User>(); public void setUser(Connection s, User u) { m.put(s, u); } public User getUser(Connection s) { return m.get(s); }} WeakHashMap 与 HashMap类似,但是在其内部,key是经过WeakReference包装的。使用WeakHashMap情况会变得怎样呢? 每当垃圾回收发生时,那些已经结束生命周期的Connection对象(没有强引用指向它)不受WeakHashMap中key(WeakReference)的影响,可以直接回收掉。同时,WeakHashMap利用ReferenceQueue(下文会提到) 可以做到删除那些已经被回收的Connection对应的User。是不是做到了内存的自动管理呢? 可达性分析算法Java执行GC时,需要判断对象是否存活。判断一个对象是否存活使用了”可达性分析算法”。 基本思路就是通过一系列称为GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即从GC Roots到这个对象不可达时,证明此对象不可用。 可以作为GC Roots的对象包括: 虚拟机栈中引用的对象 方法区中类静态属性引用的对象 方法区中常量引用的对象 本地方法栈JNI引用的对象 往往到达一个对象的引用链会存在多条,垃圾回收时会依据两个原则来判断对象的可达性: 单一路径中,以最弱的引用为准 多路径中,以最强的引用为准 Reference && ReferenceQueueSoftReference,WeakReference,PhantomReference拥有共同的父类Reference,看一下其内部实现: Reference的构造函数最多可以接受两个参数:Reference(T referent, ReferenceQueue<? super T> queue) referent:即Reference所包装的引用对象 queue:此Reference需要注册到的引用队列 ReferenceQueue本身提供队列的功能,ReferenceQueue对象同时保存了一个Reference类型的head节点,Reference封装了next字段,这样就是可以组成一个单向链表。 ReferenceQueue主要用来确认Reference的状态。Reference对象有四种状态: active GC会特殊对待此状态的引用,一旦被引用的对象的可达性发生变化(如失去强引用,只剩弱引用,可以被回收),GC会将引用放入pending队列并将其状态改为pending状态 pending 位于pending队列,等待ReferenceHandler线程将引用入队queue enqueue ReferenceHandler将引用入队queue inactive 引用从queue出队后的最终状态,该状态不可变 Reference与ReferenceQueue之间是如何工作的呢? Reference里有个静态字段pending,同时还通过静态代码块启动了Reference-handler thread。当一个Reference的referent被回收时,垃圾回收器会把reference添加到pending这个链表里,然后Reference-handler thread不断的读取pending中的reference,把它加入到对应的ReferenceQueue中。 当reference与referenQueue联合使用的主要作用就是当reference指向的referent回收时,提供一种通知机制,通过queue取到这些reference,来做额外的处理工作。 PhantomReference的一个使用案例上文提到 当reference与referenQueue联合使用的主要作用就是当reference指向的referent回收时,提供一种通知机制,通过queue取到这些reference,来做额外的处理工作。通过PhantomReference的一个例子来加深体会:用PhantomReference来自动关闭文件流 使用PhantomReference封装引用 12345678910111213141516171819202122public class ResourcePhantomReference<T> extends PhantomReference<T> { private List<Closeable> closeables; public ResourcePhantomReference(T referent, ReferenceQueue<? super T> q, List<Closeable> resource) { super(referent, q); closeables = resource; } public void cleanUp() { if (closeables == null || closeables.size() == 0) return; for (Closeable closeable : closeables) { try { closeable.close(); System.out.println("clean up:"+closeable); } catch (IOException e) { e.printStackTrace(); } } }} 守护者线程利用ReferenceQueue做自动清理 123456789101112131415161718192021222324public class ResourceCloseDeamon extends Thread { private static ReferenceQueue QUEUE = new ReferenceQueue(); //保持对reference的引用,防止reference本身被回收 private static List<Reference> references=new ArrayList<>(); @Override public void run() { this.setName("ResourceCloseDeamon"); while (true) { try { ResourcePhantomReference reference = (ResourcePhantomReference) QUEUE.remove(); reference.cleanUp(); references.remove(reference); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void register(Object referent, List<Closeable> closeables) { references.add(new ResourcePhantomReference(referent,QUEUE,closeables)); }} 封装的文件操作 12345678910111213141516171819public class FileOperation { private FileOutputStream outputStream; private FileInputStream inputStream; public FileOperation(FileInputStream inputStream, FileOutputStream outputStream) { this.outputStream = outputStream; this.inputStream = inputStream; } public void operate() { try { inputStream.getChannel().transferTo(0, inputStream.getChannel().size(), outputStream.getChannel()); } catch (IOException e) { e.printStackTrace(); } }} 测试 1234567891011121314151617181920212223242526272829303132333435ublic class PhantomTest { public static void main(String[] args) throws Exception { //打开回收 ResourceCloseDeamon deamon = new ResourceCloseDeamon(); deamon.setDaemon(true); deamon.start(); // touch a.txt b.txt // echo "hello" > a.txt //保留对象,防止gc把stream回收掉,其不到演示效果 List<Closeable> all=new ArrayList<>(); FileInputStream inputStream; FileOutputStream outputStream; for (int i = 0; i < 100000; i++) { inputStream = new FileInputStream("/Users/robin/a.txt"); outputStream = new FileOutputStream("/Users/robin/b.txt"); FileOperation operation = new FileOperation(inputStream, outputStream); operation.operate(); TimeUnit.MILLISECONDS.sleep(100); List<Closeable>closeables=new ArrayList<>(); closeables.add(inputStream); closeables.add(outputStream); all.addAll(closeables); ResourceCloseDeamon.register(operation,closeables); //用下面命令查看文件句柄,如果把上面register注释掉,就会发现句柄数量不断上升 //jps | grep PhantomTest | awk '{print $1}' |head -1 | xargs lsof -p | grep /User/robin System.gc(); } }} 参考自Java Reference详解 WeakHashMapWeakHashMap实现原理很简单,它除了实现标准的Map接口,里面的机制也和HashMap的实现类似。从它entry子类中可以看出,它的key是用WeakReference封装的。 WeakHashMap里声明了一个queue,Entry继承WeakReference,构造函数中用key和queue关联构造一个weakReference。当key所封装的对象被GC回收后,GC自动将key注册到queue中。 WeakHashMap中有代码检测这个queue,取出其中的元素,找到WeakHashMap中相应的键值对进行remove。这部分代码就是expungeStaleEntries方法: 123456789101112131415161718192021222324252627private void expungeStaleEntries() { for (Object x; (x = queue.poll()) != null; ) { synchronized (queue) { @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) x; int i = indexFor(e.hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> p = prev; while (p != null) { Entry<K,V> next = p.next; if (p == e) { if (prev == e) table[i] = next; else prev.next = next; // Must not null out e.next; // stale entries may be in use by a HashIterator e.value = null; // Help GC size--; break; } prev = p; p = next; } } }} 这段代码会在resize,getTable,size里执行,清除失效的entry。 FinalReference在Reference的子类中,还有一个名为FinalReference的类,这个类用来做什么呢? FinalReference仅仅继承了Reference,没有做其他的逻辑,只是将访问权限声明为package,所以我们不能够直接使用它。 需要关注的是其子类 Finalizer,看一下他的实现: 首先,哪些类对象是Finalizer reference类型的referent呢? 只要类覆写了Object 上的finalize方法,方法体非空。那么这个类的实例都会被Finalizer引用类型引用。这个工作是由虚拟机完成的,对于我们来说是透明的。 Finalizer 中有两个字段需要关注: queue:private static ReferenceQueue queue = new ReferenceQueue() 即上文提到的ReferenceQueue,用来实现通知 unfinalized:private static Finalizer unfinalized 维护了一个未执行finalize方法的reference列表。维护静态字段unfinalized的目的是为了一直保持对未未执行finalize方法的reference的强引用,防止被gc回收掉。 在Finalizer的构造函数中通过add()方法把Finalizer引用本身加入到unfinalized列表中,同时关联finalizee和queue,实现通知机制。 Finalizer静态代码块里启动了一个deamon线程 FinalizerThread,FinalizerThread run方法不断的从queue中去取Finalizer类型的reference,然后调用Finalizer的runFinalizer方法,该方法最后执行了referent所重写的finalize方法。 12345678910111213141516private void runFinalizer(JavaLangAccess jla) { synchronized (this) { if (hasBeenFinalized()) return; remove(); } try { Object finalizee = this.get(); if (finalizee != null && !(finalizee instanceof java.lang.Enum)) { jla.invokeFinalize(finalizee); /* Clear stack slot containing this variable, to decrease the chances of false retention with a conservative GC */ finalizee = null; } } catch (Throwable x) { } super.clear();} 观察上面的代码,hasBeenFinalized()判断了finalize是否已经执行,如果执行,则把这个referent从unfinalized队列中移除。所以,任何一个对象的finalize方法只会被系统自动调用一次。当下一次GC发生时,由于unfinalized已经不再持有该对象的referent,故该对象被直接回收掉。 从上面的过程也可以看出,覆盖了finalize方法的对象至少需要两次GC才可能被回收。第一次GC把覆盖了finalize方法的对象对应的Finalizer reference加入referenceQueue等待FinalizerThread来执行finalize方法。第二次GC才有可能释放finalizee对象本身,前提是FinalizerThread已经执行完finalize方法了,并把Finalizer reference从Finalizer静态unfinalized链表中剔除,因为这个链表和Finalizer reference对finalizee构成的是一个强引用。 参考Java Reference详解]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何执行一个延迟任务?]]></title>
<url>%2F2017%2F11%2F20%2F%E5%A6%82%E4%BD%95%E6%89%A7%E8%A1%8C%E4%B8%80%E4%B8%AA%E5%BB%B6%E8%BF%9F%E4%BB%BB%E5%8A%A1%EF%BC%9F%2F</url>
<content type="text"><![CDATA[背景前段时间做到临时版本管理系统的项目中有这样一个需求: 最新发布的版本发布记录状态为未处理,用户手动确认后状态修改为已处理。超过七天后状态为未处理的记录状态自动修改为过期提醒,同时向用户发送一封邮件。 “修改状态为过期提醒” 的任务就是一个延迟任务,如何执行这样一个任务呢?思考了两种方案: 轮询启动一个定时任务,每隔一天执行一次,执行的内容如下: select id from release where status = 2 and release_time > 7days update release set status = 3 where id in (…) 向用户发送过期通知邮件(可以交给专门的线程去做) 在代码中,我们可以使用ScheduledThreadPoolExecutor 实现定时任务。ScheduledThreadPoolExecutor的核心使用了DelayQueue这样一个数据结构。DelayQueue是一个使用优先队列(PriorityQueue)实现的BlockingQueue,优先队列的比较基准值是时间。由于PriorityQueue内部使用最小堆来实现排序队列,其时间复杂度为O(logN)。 这样的方案是我们第一时间可以想到的,也是实现起来最简单的方案。那么,他有什么不足呢? 查询语句返回的数据量可能会很大,可能需要分页查询 查询语句的执行可能需要全表扫描 时效性不好,最大的发送邮件时间误差可以达到一天 环形队列参考Netty的HashedWheelTimer 需要实现两个数据结构: 环形队列 创建一个长度为24的队列(本质上是数组) 任务集合 队列中的每个元素是一个任务集合Set 然后,启动一个定时任务,每隔一个小时,在环形队列中移动一格,使用一个currentIndex指向队列当前的元素。 Task有两个属性: cycleNum:当currentIndex第几圈扫描到这个元素时,执行任务 runnable:需要执行的任务,任务内容取出对应id的版本发布记录,查询其状态如果是未处理,则更新其状态为过期提醒并发送通知邮件 设现在currentIndex指向第一格,此时发布了一条版本发布记录,希望在7天之后,触发一个延迟任务: 计算这个Task应该放在哪个元素的集合当中 现在指向1,7天之后(7*24),应该放在第一个元素的Set当中 计算这个Task的cycleNum 环形队列是24格,每隔一小时currentIndex移动一次,即移动一圈是24小时,所以应该是 (7*24)/24 = 7 圈后再执行 currentIndex每隔一个小时移动一格,每移动一次,就取出当前位置的Set,观察每一个Task的cycleNum: 如果不是0,说明还需要再移动几圈才可执行,将cycleNum减一 如果是0,说明这个Task可以马上执行,取出这个Task中的runnable执行(可以交给别的线程执行),将此Task从Set中删除 使用环形队列之后,每次发布一条版本发布记录时,在环形队列中插入一个Task即可,7天之后,这个Task将会被触发: 不需要全表扫描,只需查询对应id的记录即可(主键索引) 时效性好,精确到一个小时。可以通过修改环形队列的大小和控制currentIndex的移动频率来控制精度。 操作环形队列时间复杂度为O(1) 使用环形队列的缺点是增加了复杂度,如果数据量不大,对性能要求不高的场景下,使用DelayQueue即可。 参考1分钟实现“延迟消息”功能]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从通信协议到装饰者模式]]></title>
<url>%2F2017%2F11%2F15%2F%E4%BB%8E%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AE%E5%88%B0%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F%2F</url>
<content type="text"><![CDATA[背景最近有个新的需求,jdbc与后台的通信要求使用第三方提供的硬件加密卡进行加密。当前数据库与前台工具的通信有两种方式:分别是基于明文或基于ssl。 基于明文的通信如下: 在应用层定义了前后台交互协议,封装的数据包结构大致如下: 基于ssl的通信如下: 在应用层和传输层之间新加了ssl加密层,对于应用层而言,整个加密过程都是透明的。代码上的表现为直接替换普通socket为sslsocket。 现在的需求同样需要实现加密,方法是调用第三方提供的加密算法实现应用层数据的加密。第三方只提供了加密算法,所以需要从应用层入手,来实现数据加密。 思考两种方式如下: 对于现有的数据包,对其数据部分加密后传输。 第一直觉可能会想到这种方式。但是仔细想想,这样的修改对于现有的应用层协议侵入性太大,每个类型的数据包均需要修改、测试,工作量也不小。同时,对于原有的明文传输方式兼容性不强。 对于计算机中遇到的问题,都可以通过"加一层"的方式解决。 加密数据包结构如下: 通过这种加一层的方式,可以做到对原有的协议零侵入性,同时使用tag标识位标识数据包是否是加密包还是明文包,兼容明文传输。修改量也最小,故采用这种方式实现前后台加密通讯。 大致的思路有了,那么在代码中如何实现呢?这就用到了装饰者模式。 装饰者模式 装饰者模式在Java IO总结中也提到了: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152Component:定义一个对象接口,可以给这些对象动态地添加职责。public interface Component{ void operation();} Concrete Component:定义一个对象,可以给这个对象添加一些职责。动作的具体实施者。public class ConcreteComponent implements Component{ public void operation() { // Write your code here }} Decorator:维持一个指向Component对象的引用,并定义一个与 Component接口一致的接口。public class Decorator implements Component{ public Decorator(Component component) { this.component = component; } public void operation() { component.operation(); } private Component component;} Concrete Decorator:在Concrete Component的行为之前或之后,加上自己的行为,以“贴上”附加的职责。public class ConcreteDecorator extends Decorator{ public void operation() { //addBehavior也可以在前面 super.operation(); addBehavior(); } private void addBehavior() { //your code }} 使用装饰模式来实现扩展比继承更加灵活,它以对客户透明的方式动态地给一个对象附加更多的责任。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。 Java中的流家族就是装饰者模式的典型案例。 那么,与我们上面提到的加密层有什么联系呢? 在session建立一开始,协议协商确定使用加密通信后,将明文通信使用的BufferedOutputStream与BufferedInputStream替换成我们自定义的装饰者流:EncryptedOutputStream和EncryptedInputStream。 123456public void wrapEnprytedStream() { //osr_input 是BufferedInputStream 实例 osr_input = new EncryptedInputStream(osr_input, con); //osr_output 是 BufferedOutputStream 实例 osr_output = new EncryptedOutputStream(osr_output, con);} 在EncryptedOutputStream.write方法和EncryptedInputStream.read方法中分别进行加密数据包的封包和拆包工作。把原有的应用层协议包封装在一个个加密数据包当中,相当于”加了一层”。 贴出这两个装饰者类的实现: EncryptedInputStream.java 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182public class EncryptedInputStream extends FilterInputStream { private static final byte encryptedTag = (byte) 0xA4;// 加密数据包标识 private static final byte unEncryptedTag = (byte) 0xA5;// 非加密数据包标识 private byte[] buffer; private InputStream in; private byte[] packetHeaderBuffer = new byte[3]; private int pos = 0; private BaseConnection con; private byte[] lock = new byte[0]; StringBuffer sb = new StringBuffer(); boolean logFlag = Driver.getLogLevel() >= TrackLog.PROTOCOLDETAIL_LEVEL; protected EncryptedInputStream(InputStream in, BaseConnection con) { super(in); this.in = in; this.con = con; } public int available() throws IOException { if (this.buffer == null) { return this.in.available(); } return this.buffer.length - this.pos + this.in.available(); } public void close() throws IOException { this.in.close(); this.buffer = null; } private void getNextPacketFromServer() throws Exception { byte[] decryptedData = null; /** * 数据头 第一个字节存放是否进行了加密。 第二三个字节存放加密后的数据的长度 */ int lengthRead = readFully(this.packetHeaderBuffer, 0, 3); if (lengthRead < 3) { throw new IOException("Unexpected end of input stream"); } /** * 加密数据的长度 */ int encryptedPacketLength = ((int) this.packetHeaderBuffer[1] & 0xff) << 8 | (int) this.packetHeaderBuffer[2] & 0xff; if (encryptedPacketLength < 0) { throw new Exception("数据包长度为负值:" + encryptedPacketLength + "----hb:" + this.packetHeaderBuffer[1] + "---lb:" + this.packetHeaderBuffer[2]); } /** * 对数据进行了加密 */ decryptedData = new byte[encryptedPacketLength]; readFully(decryptedData, 0, encryptedPacketLength); if (this.packetHeaderBuffer[0] == encryptedTag) { synchronized (lock) { decryptedData = decryptData(con.getPublicKey(), decryptedData); } } if ((this.buffer != null) && (this.pos < this.buffer.length)) { int remaining = this.buffer.length - this.pos; byte[] newBuffer = new byte[remaining + decryptedData.length]; int newIndex = buffer.length - pos; System.arraycopy(buffer, pos, newBuffer, 0, newIndex); System.arraycopy(decryptedData, 0, newBuffer, newIndex, decryptedData.length); decryptedData = newBuffer; } this.pos = 0; this.buffer = decryptedData; } private byte[] decryptData(String publicKey, byte[] src) { byte[] data = DataEncryptUtil.instance.decryptDataByPublicKey(publicKey, src, src.length); return data; } private void getNextPacketIfRequired(int numBytes) throws Exception { if ((this.buffer == null) || ((this.pos + numBytes) > this.buffer.length)) { getNextPacketFromServer(); } } synchronized public int read() throws IOException { try { getNextPacketIfRequired(1); } catch (IOException ioEx) { return -1; } catch (Exception e) { e.printStackTrace(); } return this.buffer[this.pos++] & 0xff; } public int read(byte[] b) throws IOException { return read(b, 0, b.length); } public int read(byte[] b, int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } if (len <= 0) { return 0; } try { getNextPacketIfRequired(len); } catch (IOException ioEx) { return -1; } catch (Exception e) { e.printStackTrace(); } int bufferLen = buffer.length; if (bufferLen < len) { System.arraycopy(this.buffer, this.pos, b, off, bufferLen); this.pos += bufferLen; return bufferLen; } else { System.arraycopy(this.buffer, this.pos, b, off, len); this.pos += len; return len; } } private final int readFully(byte[] b, int off, int len) throws IOException { if (len < 0) { throw new IndexOutOfBoundsException(); } int n = 0; int count = 0; while (n < len) { count = this.in.read(b, off + n, len - n); if (count < 0) { throw new EOFException(); } n += count; } return n; } public long skip(long n) throws IOException { long count = 0; int bytesRead = 0; for (long i = 0; i < n; i++) { bytesRead = read(); if (bytesRead == -1) { break; } count++; } return count; } private void append(StringBuffer sb, byte[] value) { if (value == null) { sb.append("null"); } else { for (int i = 0; i < value.length; i++) { sb.append(value[i]).append(" "); } } }} EncryptedOutputStream.java 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116public class EncryptedOutputStream extends BufferedOutputStream { private static final byte encryptedTag = (byte) 0xA4;// 加密数据包标识 private static final byte unEncryptedTag = (byte) 0xA5;// 非加密数据包标识 private static final int K = 1024; private static final int defaultBufferSize = 128 * K; // 每个加密包的数据长度不超过48k private static final int slice = 48 * K; private BaseConnection con; private OutputStream out; private byte[] buffer; // buffer 当前写入位置 private int pos = 0; // 标志位 占一个字节 public byte[] singleBuf = new byte[1]; // 长度 占两个字节 public byte[] integerBuf = new byte[2]; public EncryptedOutputStream(OutputStream out, BaseConnection con) { this(out, con, defaultBufferSize); } public EncryptedOutputStream(OutputStream out, BaseConnection con, int size) { super(out); this.out = out; this.con = con; this.buffer = new byte[size]; } public void write(byte[] b, int off, int len) throws IOException { if (len > buffer.length) { flushBuffer(); writeWithEncrypted(b, off, len); return; } if (pos + len > buffer.length) { flushBuffer(); } System.arraycopy(b, off, buffer, pos, len); pos += len; } private void flushBuffer() throws IOException { if (pos > 0) { writeWithEncrypted(buffer, 0, pos); pos = 0; } } private void writeWithEncrypted(byte[] b, int off, int len) throws IOException { int remainLen = len; int offset = off; byte[] buf = new byte[slice];; while (remainLen > slice) { System.arraycopy(b, offset, buf, 0, slice); sendSlice(buf); offset += slice; remainLen -= slice; } if (remainLen > 0) { buf = new byte[remainLen]; System.arraycopy(b, offset, buf, 0, remainLen); sendSlice(buf); } } /** * 发送一个加密数据包 * @param buf 原始数据 * @throws IOException */ private void sendSlice(byte[] buf) throws IOException { byte[] encryptedData = DataEncryptUtil.instance.encryptDataByPublicKey(con.getPublicKey(), buf, buf.length); int encryptedDataLen = encryptedData.length; writeChar(encryptedTag); writeInteger(encryptedDataLen, 2); out.write(encryptedData); } private void writeChar(int c) throws IOException { singleBuf[0] = (byte) c; out.write(singleBuf); } private void writeInteger(int val, int size) throws IOException { int count = size; while (size-- > 0) { integerBuf[size] = (byte) (val & 0xff); val >>= 8; } out.write(integerBuf, 0, count); } public void write(byte[] b) throws IOException { write(b, 0, b.length); } public void write(int b) throws IOException { byte[] buf = new byte[1]; buf[0] = (byte) b; write(buf); } public void flush() throws IOException { flushBuffer(); out.flush(); } public void close() throws IOException { out.close(); buffer = null; } } 可以看到,使用了装饰者模式后,很容易就解决了我们的问题。现在想一下,假如现在又有新的需求,要求实现后台数据的压缩传输呢?(前台发送的数据量小,不考虑使用压缩) 123456public void wrapEnprytedStream() { //osr_input 是BufferedInputStream 实例 osr_input = new CompressedInputStream(new EncryptedInputStream(osr_input), con); //osr_output 是 BufferedOutputStream 实例 osr_output = new EncryptedOutputStream(osr_output, con);} 装饰者与代理读过上面的代码,是否觉得跟代理模式的代码长得很像? 代理模式(Proxy Pattern),为其它对象提供一种代理以控制对这个对象的访问。装饰模式(Decorator Pattern),动态地给一个对象添加一些额外的职责。 从语意上讲,代理模式的目标是控制对被代理对象的访问,而装饰模式是给原对象增加额外功能。 比如,我们使用了某些远程RPC通讯的sdk,在我们的代码中调用一个方法的时候,实际上调用的是代理类提供的方法,在其内部封装了远程通信的细节,而这些对我们而言都是透明的。调用方直接调用代理而不需要直接操作被代理对象甚至都不需要知道被代理对象的存在。屏蔽了实际对象。 上文提及的装饰者模式代码中,装饰类可装饰的类并不固定,并且被装饰对象是在使用时通过组合确定。 装饰模式的本质是动态组合。动态是手段,组合是目的。每个装饰类可以只负责添加一项额外功能,然后通过组合为被装饰类添加复杂功能。由于每个装饰类的职责比较简单单一,增加了这些装饰类的可重用性,同时也更符合单一职责原则。]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[大型网站技术架构]]></title>
<url>%2F2017%2F11%2F05%2F%E5%A4%A7%E5%9E%8B%E7%BD%91%E7%AB%99%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84%2F</url>
<content type="text"><![CDATA[最近在读李智慧所著的《大型网站技术架构》,这是我第二遍读这本书,很好的一本书,让人开拓了视野。 原著分为四部分,分别包括大型网站架构演进、大型网站架构模式、大型网站核心要素以及大型网站案例。我把第二三部分做成了思维导图,就当做是读书笔记好了。点击图片下方的链接可以查看大图,思维导图工具使用XMind. 点击查看原图 点击查看原图]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>web</tag>
</tags>
</entry>
<entry>
<title><![CDATA[认识MVCC]]></title>
<url>%2F2017%2F11%2F02%2F%E8%AE%A4%E8%AF%86MVCC%2F</url>
<content type="text"><![CDATA[前一篇总结提到了数据库事务的隔离性可以由锁机制来实现。锁是传统数据库控制事务并发的主要手段,这里提到的锁(S锁和X锁)可以认为是悲观锁,在对数据实现读写之前加上相应的锁,实现了独占访问机制,从而控制并发行为。神通数据库7.0主要使用了锁来实现事务并发控制。 那么数据库事务的并发控制还有其他的实现方式吗?当然是有的。去年刚推出的神通数据库8.0,就使用了MVCC(Multiversion Concurrency Control) 即多版本控制来控制事务的并发行为。 研究一下什么是MVCC… MVCC 多版本并发控制技术,它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是,把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能. 多版本并发控制,意味着同一时刻看到的相同行的数据可能是不一样的,即一个行可能有多个版本.有点平行宇宙的意思。要实现这种效果,需要做一些操作: 对于每一行记录,新增两个隐藏列: 行的更新时间(创建版本号) 行的删除时间(删除版本号) 数据版本 同一行数据,在同一时刻对于不同的事务,可能看到不同的版本。即同一行数据有多个版本同时存在。 事务版本 每个事务有其唯一的事务版本号 版本有序 无论是数据版本,还是事务版本,其版本号随时间增长而增长,即版本号是递增的。 MVCC 与 数据库事务隔离级别对于INSERT,DELETE,UPDATE语句,MVCC是如何处理的? INSERT时,保存当前事务版本号为行的创建版本号 DELETE时,保存当前事务版本号为行的删除版本号 UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行(还有另外一种实现方式,即把旧数据放入回滚段当中,其他事务读数据时,从回滚段中读出) MVCC由于其实现原理,只支持read committed和repeatable read隔离等级: 提交读 SELECT时,读取当前查询语句之前已经提交的结果 因此,SELECT看得见其自身所在事务中前面更新执行结果或者在查询执行期间其它事务已提交的数据 可重复读 SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号 因此,事务内部的同样的SELECT命令总是看到相同的数据 上面提到的读操作,读取的都是数据的快照版本,即”快照读”,读取数据库当前版本数据的方式,叫做”当前读”。在MVCC中: 快照读:即select select * from table ….; 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。 select * from table where ? lock in share mode; select * from table where ? for update; insert; update ; delete; MVCC 与 乐观并发控制通过上面的总结可以看出,MVCC是主要用来解决”读-写冲突”的无锁并发控制。读操作时不用阻塞写操作,写操作不用阻塞读操作,同时避免了脏读。 那么乐观并发控制是什么呢?乐观并发控制假设认为数据一般情况下不会造成冲突,所以先进行修改,在提交事务前,检查一下事务开始后,有没有新提交改变,如果没有就提交,如果有就放弃并重试(或返回错误让用户处理)。可以看出,乐观并发控制是用来解决”写-写冲突”的无锁并发控制。 那么,在使用了MVCC之后,如何解决”写-写冲突”呢? 有两种方法: 结合传统的悲观锁控制写-写冲突 就像上面提到的”当前读”,遇到更新语句,对数据加X锁 MySQL中将MVCC与2PL结合起来,实现了事务并发控制。 使用乐观并发控制解决写-写冲突 PostgreSQL中将MVCC与乐观并发控制结合起来,实现了事务的并发控制。 总结由上文可以得出,MVCC在每次读时避免了加锁开销,适用于”读多写少”的场景。增加了事务的并发度,同时避免了死锁的发生; 但是当写-写冲突增加时,基于乐观并发控制的MVCC会有更多的retry发生,如果事务的执行时间很长,回滚后再执行反而会造成性能降低。 参考Databases 101: ACID, MVCC vs Locks, Transaction Isolation Levels, and Concurrency]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>数据库</tag>
</tags>
</entry>
<entry>
<title><![CDATA[数据库事务浅析]]></title>
<url>%2F2017%2F10%2F31%2F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BA%8B%E5%8A%A1%E6%B5%85%E6%9E%90%2F</url>
<content type="text"><![CDATA[ACID 一个数据库事务通常包含了一个序列的对数据库的读/写操作。它的存在包含有以下两个目的: 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。 上面对数据库事务的定义摘自维基百科。先不用着急的去理解这个定义的具体含义,我们从事务的四个特性来逐步了解什么是事务。 数据库事务拥有以下四个特性,习惯上被称之为ACID特性。 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。 用一个转账的例子来解释,这个例子被用烂了,却很经典: 从A账户向B账户转账100元,可能分为以下几个步骤: 读取A账户,将A账户余额减100 A 账户余额写回数据库 读取B账户,将B账户余额加100 B账户余额写回数据库 一致性 什么叫数据一致性?通常是由我们自己来定义的。在上面的场景中,就是在转账的前后,A账户和B账户的总额保持不变。 再举一个例子,之前做过一个版本管理系统,用户发布一个的版本(上传若干附件),版本记录表就要插入一行记录,相对应的,附件表也要插入若干的记录(一对多的关系)。对于这两个表的操作,要么全做,要么不做,如果附件表插入了记录,而版本记录表没有操作,不符合一致性定义;如果版本记录表插入了一条记录,而附件表没有插入记录或者插入记录少了几条,也不符合一致性的定义。想要保持一致性,那么需要对这两个表的插入操作都放在同一个事务内进行。 在事务处理的ACID属性中,一致性是最基本的属性,其它的三个属性都为了保证一致性而存在的。 原子性 上面的转账的四个步骤要么全做,要么不做。假如做完第一步之后,计算机突然断电了,那么数据库重启之后就需要执行一个crashrecovery的过程,之前的所有操作都应该回滚到执行事务之前的状态。即A向B转账的操作失败了。 上面提到,其他三个属性都是为了保持一致性而存在。只要原子性是否就可以保证一致性?答案当然是否定的。 比如,事务1 A向B转账100元,在第一步执行完毕之后,恰好另外一个事务2操作是C向A转账200元,并且已经执行完毕,此时执行事务1的第二步,将A账户余额写回数据库,此时事务2的执行结果就被事务1覆盖掉了,造成了数据的不一致(A + B + C 的账户总额保持一致)。 可见,即使事务1最终执行完毕,满足了原子性,因为另一个事务的影响,还是造成了数据的不一致状态。原子性并不能保证一致性。 那么,为什么会看到网上还有许多人再问原子性和一致性的问题呢? 我认为是程序员很容易从数据库事务原子性联想到做应用时多线程并发时的原子性。多线程并发时的原子性基本靠锁来维持,我们认为,有了锁的保护,临界区的资源就不可以被另一个线程访问了。事实上,数据库事务原子性与锁关系不大,锁涉及到了事务的另一个特性:隔离性。 隔离性 就像在上面谈到的,事务1 与事务2 并行发生,造成了数据的不一致状态。隔离性用来解决这个问题。 事务隔离性可以保证:如果在A给B转账的同时,有另外一个事务执行了C给B转账的操作,那么当两个事务都结束的时候,B账户里面的钱应该是A转给B的钱加上C转给B的钱再加上自己原有的钱。 持久性 持久性比较容易理解。即,一旦事务提交(转账成功),所有的数据都会被写入数据库,落地到磁盘。账户中的钱就真的发生了变化。 事务隔离性锁从上文可以看出,当并发事务同时访问一个资源时,有可能导致数据不一致,因此需要一种机制来将数据访问顺序化,以保证数据库数据的一致性。锁就是其中的一种机制。我们通过使用锁来保证事务隔离性。 为了理解下面提到的隔离级别,我们简单认识一下数据库中的几种锁: 从锁粒度划分 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。 从锁性质划分 读锁(S 锁):如果事务A对数据T加了该锁之后,其他事务可以并发读取T(获取该数据的读锁),但任何事务都不能对数据T进行修改(获取数据上的写锁),直到已释放所有读锁。 写锁(X 锁):如果事务A对数据T加上写锁后,则其他事务不能再对T加任任何类型的锁。获得写锁的事务既能读数据,又能修改数据。 意向锁: 设计目的主要是为了在一个事务中揭示下一行将要被请求锁的类型。参考 更新锁:引入它是因为多数数据库在实现加X锁时是执行了如下流程:先加S锁,添加成功后尝试更换为X锁。这时如果有两个事务同时加了S锁,尝试换X锁,就会发生死锁。因此增加U锁,U锁代表有更新意向,只允许有一个事务拿到U锁,该事务在发生写后U锁变X锁,未写时看做S锁。 隔离级别我们知道了并发事务的隔离性靠锁机制来实现,很多DBMS定义了多个不同的”事务隔离等级”来控制锁的程度和并发能力。 SQL定义的标准隔离级别有四种,从高到底依次为: 可序列化(Serializable) 可重复读(Repeatable reads) 提交读(Read committed) 未提交读(Read uncommitted) 随着数据库隔离级别的提高,数据的并发能力也会有所下降。 下面了解一下这几种隔离级别在数据库中可能的实现。注意,下面的实现都是基于传统数据库,而不是MVCC的。 未提交读 锁机制: 事务在读数据的时候并未对数据加锁; 事务在修改数据的时候对数据增加行级S锁。 举例: Transaction 1 Transaction 2 select * from users where id = 1 // will read 20 update users set age = 21 where id = 1 select age from users where is = 1 // will read 21 roll back 事务一在读取某行数据的时候并未加任何锁,事务二也能对这行数据进行读取和更新; 事务二在更新某行数据的时候对这行数据加了S锁,事务一可以对这行数据进行读取,因此看到了事务二未提交的更改; 事务二更新某行数据对这行数据加了S锁,事务一不能对这行数据进行更新,直到事务二结束。 可以看到,事务一第二次查询看到了事务二未提交的更改,之后这些数据被事务二进行了回滚,于是事务一查询到的数据就成了脏数据,这种现象称之为脏读。 未提交读会造成脏读。 提交读 锁机制 事务对当前被读取的数据加行级S锁(读到时才加),一旦读完该行就释放S锁; 事务在更新某数据时,必须先对其加行级X锁,直到事务结束才释放。 举例 Transaction 1 Transaction 2 select * from users where id =1 //will read 20 update users set age = 21 where id =1; commit select * from users where id = 1 //will read 21 事务二在更新数据的时候对数据加了X锁,直到事务结束才释放。所以事务一读取不到事务二未提交的数据。 事务二结束后事务一读取到了与第一次读取中不一致的数据。造成了事务一中两次读取的结果不一致,产生了不可重复读问题。 可重复读 锁机制 事务对当前被读取的数据加行级S锁,直到事务结束才释放; 事务在更新某数据时,对其加行级X锁,直到事务结束才释放。 举例 Transaction 1 Transaction 2 select * from users where id =1; commit update users set age = 21 where id =1; commit 事务一在读取数据时,对数据加了S锁,直到事务结束才释放。因此在此期间,事务二只能读取该数据,不能更新。这样保证了事务一在整个事务期间,无论读取多少次该数据,结果都是一致的,解决了不可重复读的问题。 事务二在更新数据时对数据加了X锁,直到事务结束才释放,在此期间事务一都无法访问和更新该数据,解决了脏读的问题。 Transaction 1 Transaction 2 select * from users where age between 20 and 30 insert into users values(3, ‘bob’, 25); commit select * from users where age between 20 and 30 上面的例子中: 事务一查询年龄20到30之间的用户,假设取到10条数据,那么对这10条数据加上了行级S锁; 事务二插入一条数据。由于此时没有任何事务对表添加了表级锁,因此顺利插入; 事务一再一次查询年龄20到30之间的用户,发现与第一次读取时的数据不一致了,多出了一条数据。 这种现象就是幻读。这是一种特殊的不可重复读现象。 可序列化 锁机制 事务对当前被读取的数据加表级S锁,直到事务结束才释放; 事务在更新某数据时,对其加表级X锁,直到事务结束才释放。 事务一在读取表记录时,事务二也可以读取该表,但不能对表进行更新、删除、插入等操作; 事务一在更新表记录时,事务二不能够读取该表的任何记录,也不能对表进行更新操作。 可序列化隔离级别避免了脏读、不可重复读和幻读,是最高的隔离级别。 参考理解MySql事务隔离机制、锁以及各种锁协议,mysql事务 事务并发的可能问题与其解决方案 Innodb中的事务隔离级别和锁的关系 深入分析事务的隔离级别]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>数据库</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java读取资源文件]]></title>
<url>%2F2017%2F10%2F31%2FJava%E8%AF%BB%E5%8F%96%E8%B5%84%E6%BA%90%E6%96%87%E4%BB%B6%2F</url>
<content type="text"><![CDATA[写java代码时常常需要加载一些外部的资源,通常我们会使用全路径名加载一份资源,比如:C:\Users\Yukai\Desktop\abc.jpg . 但是,有些时候我们需要加载的是源代码路径下的资源或者配置文件等等,更习惯于使用相对路径,或者直接给一个文件名,就希望能够找到我们需要的配置文件。如何做到?常见的方法是使用了 class.getResource 或 classloader.getResource class.getResource && classloader.getResource ?这两个方法看起来很相似,他们直接有什么区别? 直接上网搜索能够得到一些答案,但都不如查看源代码来的直接: Class.getResourceAsStream(String name) 123456789public InputStream getResourceAsStream(String name) { name = resolveName(name); ClassLoader cl = getClassLoader0(); if (cl==null) { // A system class. return ClassLoader.getSystemResourceAsStream(name); } return cl.getResourceAsStream(name);} 上面的代码可以看出, Class.getResourceAsStream(String name) 最终还是调用了 classloader.getResourceAsStream(String name) 。但是两者还是有一些区别的,注意 name =resolveName(name)这一行, Class.getResourceAsStream(String name)在这里做了一些处理: Class.resolveName(String name) 1234567891011121314151617181920private String resolveName(String name) { if (name == null) { return name; } if (!name.startsWith("/")) { Class<?> c = this; while (c.isArray()) { c = c.getComponentType(); } String baseName = c.getName(); int index = baseName.lastIndexOf('.'); if (index != -1) { name = baseName.substring(0, index).replace('.', '/') +"/"+name; } } else { name = name.substring(1); } return name;} 现在看这段代码还有点云里雾绕,不妨写几行代码测试一下,看看这段代码到底在干嘛: 12345678public class App { public static void main(String[] args) { System.out.println("App.class.getClassLoader().getResource(\"\") : " + App.class.getClassLoader().getResource("")); System.out.println("App.class.getClassLoader().getResource(\"/\") : " + App.class.getClassLoader().getResource("/")); System.out.println("App.class.getResource(\"\") : " + App.class.getResource("")); System.out.println("App.class.getResource(\"/\") : " + App.class.getResource("/")); }} 输出: 1234App.class.getClassLoader().getResource("") : file:/D:/workspace/eclipse/cluster/TestClassloader/bin/App.class.getClassLoader().getResource("/") : nullApp.class.getResource("") : file:/D:/workspace/eclipse/cluster/TestClassloader/bin/space/yukai/App.class.getResource("/") : file:/D:/workspace/eclipse/cluster/TestClassloader/bin/ 虽然上面的代码使用了getResource,但与getResourceAsStream大同小异。 可以看到, calssloder.getResource("")方法返回了classpath根路径(eclipse工程中,编译生成的类文件存放在/bin目录下); calssloder.getResource("/")方法返回null,说明calssloder.getResource不支持以”/“开头的参数; class.getResource("")方法返回了App.class所在的路径; class.getResource("/")与calssloder.getResource("")表现一致,返回了classpath的根路径 再回顾上面的代码,是否有一点明白了呢? 工程目录结构如下图: 读取根目录下的tmp(src/): 1234// 第一种方法InputStream in = App.class.getResourceAsStream("/tmp");// 第二种方法InputStream in =ClassLoader.getSystemClassLoader().getResourceAsStream("tmp"); 读取App类同级目录下的tmp(src/space/yukai) 123456// 第一种方法InputStream in = App.class.getResourceAsStream("tmp");// 第二种方法InputStream in =ClassLoader.getSystemClassLoader().getResourceAsStream("space/yukai/tmp");//第三种方法InputStream in = App.class.getResourceAsStream("/space/yukai/tmp"); 读取MyClassloader同级目录下的tmp(src/space/yukai/classloader): 123456// 第一种方法InputStream in = App.class.getResourceAsStream("classloader/tmp");// 第二种方法InputStream in =ClassLoader.getSystemClassLoader().getResourceAsStream("space/yukai/classloader/tmp");//第三种方法InputStream in = App.class.getResourceAsStream("/space/yukai/classloader/tmp"); classloader.getResource上面提到了Class.getResourceAsStream(String name) 最终还是调用了classloader.getResourceAsStream(String name),那么classloader.getResourceAsStream(String name)是如何寻找我们要读取的资源呢? classloadergetResourceAsStream(String name) 123456789101112131415161718192021public InputStream getResourceAsStream(String name) { URL url = getResource(name); try { return url != null ? url.openStream() : null; } catch (IOException e) { return null; }}public URL getResource(String name) { URL url; if (parent != null) { url = parent.getResource(name); } else { url = getBootstrapResource(name); } if (url == null) { url = findResource(name); } return url;} 上面getResource(String name)中的代码是否有中熟悉的感觉?这不就是classloader的双亲委托机制么? 首先使用自己的父类加载器寻找资源,如果父类加载器为null,表示此时的类加载器是启动类加载器,故调用getBootstrapResource(name)方法查询资源。如果所有的祖先类加载器都找不到指定的资源,那么调用该类加载器的findResource(name)方法。 那么这些类加载器是去哪查询资源是否存在呢?与加载类时查询的路径一致,对于我们的应用来说,我们应该关心AppClassLoader,我们自定义的资源往往存放于其查询的路径上,也就是classpath指定的路径。 classpath在上文的例子中,classpath指向eclipse自动生成的/bin目录。我们如何在eclipse中添加我们自定义的classpath呢? 有两种方法: 工程右键->Build Path->Configure Build Path->Source标签->点击右侧AddFolder 可以将工程目录下的文件夹设置为Source目录 比如常见的Maven中的resources目录,就是Source目录。 设置为Source目录之后,eclipse在编译源文件时,会自动将Source目录下的文件拷贝到/bin目录,自然也就是classpath下了。 这种方法的限制就是只能把工程目录下的文件夹添加进去。 设置运行时classpath 在菜单栏点击run->Run Configurations->Classpath 如图所示,点击右侧Advanced按钮,可以添加文件夹到运行时classpath。 当然,如果是在命令行下直接运行java程序的话,可以使用-classpath选项指定classpath 在maven中,可以使用下面的方法指定jar包的classpath: 12345678910111213141516171819202122<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <finalName>ReleaseTool</finalName> <archive> <manifest> <!-- 为依赖包添加路径, 这些路径会写在MANIFEST文件的Class-Path下 --> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>com.oscar.releasetool.app.App</mainClass> </manifest> <manifestEntries> <!-- 在Class-Path下添加配置文件的路径 --> <Class-Path>./</Class-Path> </manifestEntries> </archive> <excludes> <exclude>config.json</exclude> </excludes> </configuration></plugin> 我们可以将配置文件放到任何需要的地方,然后将配置文件所在的目录添加到classpath,使用classloader.getResourceAsStream方法来读取。利用这种方法可以做到配置文件与jar包分离,并且配置文件所在的目录是可以自定义的。Spring读取application.properties使用的是同样的原理。 可以使用下面的代码查看当前classpath: 12345String classpath = System.getProperty("java.class.path");String[] classpathEntries = classpath.split(File.pathSeparator);for (String str1 : classpathEntries) { System.out.println(str1);} 读取jar包所在的位置有时候需要知道jar包所在的位置,比如我们的项目需要一个默认的日志文件输出路径,这个路径就可以是运行时jar包所在的目录。如何获取jar包所在的目录? 12URL url = App.class.getProtectionDomain().getCodeSource().getLocation();String path = url.toURL().getPath(); 注意,上面的方法仅适用jdk1.5及以上]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>类加载</tag>
</tags>
</entry>
<entry>
<title><![CDATA[使用maven打包]]></title>
<url>%2F2017%2F10%2F15%2Fmaven%E6%89%93%E5%8C%85%2F</url>
<content type="text"><![CDATA[前段时间做的web项目涉及到了打包的问题,记录一下使用maven打包的过程… 项目打包为fatjar什么是fatjar?做过java项目的都知道,一个项目从开发到部署,一般需要经过打包,把一些资源文件和类文件压缩到一块,形成一个单独的文件,叫做jar(或者war)。如果这个项目依赖了一些第三方的jar包,在最终的部署阶段,这些jar有两种存在方式: 一种是单独放到一个与项目jar包并行的文件夹中(一般叫做lib),然后使用-classpath将这个文件夹下的jar加入到classpath 12345678--- |--VersionManager.jar | |--lib | |--a.jar | |--b.jar java -cp lib/*;VersionManager.jar com.oscar.App a.jar与b.jar分别为依赖的第三方jar包,VersionManager.jar是项目jar,com.oscar.App是启动类 注意,window下的变量分隔符为”;”,而linux下则为”:” 一种是直接把依赖的第三方jar包与项目打包到一块,形成一个单独的jar java -cp VersionManager.jar com.oscar.App 显然,fatjar的运行命令看起来更简洁一些。如何使用maven打包fatjar? 1234567891011121314151617181920212223<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>CHOOSE LATEST VERSION HERE</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <id>assemble-all</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins></build> 上面的配置将打包fatjar的目标绑定到了package阶段。使用maven package命令,在项目target目录下会生成两个jar文件:*with-dependencies.jar就是生成的 “fatjar”,另一个则是不包含第三方依赖的普通jar包。 项目单独打包有时候,fatjar并不是完美的发布方案。想到下面两个原因: 由于fatjar包含了所有第三方的依赖,这个单独的jar包往往会变得很大。修改了一些代码上的bug之后,需要远程部署到生产环境,如果jar包很大,传输起来不是很方便。 依赖的第三方jar拥有一些输出日志的功能。比如,在项目中依赖了某个jdbc的jar包,而这个jar提供了可以通过一些连接参数动态的输出sql日志到jdbc jar所在的同级目录的功能。此时如果jdbc的jar被打包到fatjar当中,输出日志时会报错。 考虑到上面两种情况,我们还是需要第一种部署方案: 1234567891011--- | |--src/main/java | | | |--.java | |--src/main/resource | |--application.yml | |--log4j.properties 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283<build> <plugins> <!-- 指定编译时使用的jdk版本 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <!-- 将依赖的jar拷贝到lib目录下 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin> <!-- 拷贝项目src/main/resources/目录下的指定配置文件到conf目录下 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-resource</id> <phase>package</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <encoding>UTF-8</encoding> <outputDirectory>${project.build.directory}/conf</outputDirectory> <resources> <resource> <directory>src/main/resources/</directory> <filtering>true</filtering> <includes> <include>application.yml</include> <include>log4j.properties</include> </includes> </resource> </resources> </configuration> </execution> </executions> </plugin> <!-- 指定打包时的一些选项 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <finalName>VersionManager</finalName> <archive> <manifest> <!-- 为依赖包添加路径, 这些路径会写在MANIFEST文件的Class-Path下 --> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>com.oscar.tempVersionManager.web.App</mainClass> </manifest> <manifestEntries> <!-- 在Class-Path下添加配置文件的路径 --> <Class-Path>conf/</Class-Path> </manifestEntries> </archive> <excludes> <exclude>application.yml</exclude> <exclude>log4j.properties</exclude> </excludes> </configuration> </plugin> </plugins></build> 注意几点: 将依赖的jar包复制到target/lib目录下 将application.yml、log4j.properties复制到target/conf目录下 将lib目录下的jar包加入到MANIFEST.MF配置的classpath中 将conf目录下的文件添加到MANIFEST.MF配置的classpath中 application.yml、log4j.properties将不被打包到jar中 配置生成jar包的名字为VersionManager 通过上面几点的配置,最终在target目录下生成了conf/,lib/,VersionManager.jar 将这三个文件拷贝到生产环境当中,然后执行java命令: nohup java -jar VersionManager.jar >logs/info.log 2>&1 & logs目录为日志文件的输出目录。 nohup的使用参阅Linux下的守护进程 存在的问题以上两种方式都存在一个问题,那就是无论是maven-assembly-plugin还是maven-dependency-plugin都会忽略下面的形式的依赖: 1234567<dependency> <groupId>com.oscar</groupId> <artifactId>starteam100</artifactId> <version>1.0</version> <scope>system</scope> <systemPath>${project.basedir}/lib/starteam100.jar</systemPath> </dependency> stackoverflow给出了解决方案: 12345678910111213141516171819202122232425262728293031323334353637If you really want this (understand, if you can't use a corporate repository), then my advice would be to use a "file repository" local to the project and to not use a system scoped dependency. The system scoped should be avoided, such dependencies don't work well in many situation (e.g. in assembly), they cause more troubles than benefits.So, instead, declare a repository local to the project:<repositories> <repository> <id>my-local-repo</id> <url>file://${basedir}/my-repo</url> </repository></repositories>Install your third party lib in there using install:install-file with the localRepositoryPath parameter: mvn install:install-file -Dfile=<path-to-file> -DgroupId=<myGroup> \ -DartifactId=<myArtifactId> -Dversion=<myVersion> \ -Dpackaging=<myPackaging> -DlocalRepositoryPath=<path>Update: It appears that install:install-file ignores the localRepositoryPath when using the version 2.2 of the plugin. However, it works with version 2.3 and later of the plugin. So use the fully qualified name of the plugin to specify the version:mvn org.apache.maven.plugins:maven-install-plugin:2.3.1:install-file \ -Dfile=<path-to-file> -DgroupId=<myGroup> \ -DartifactId=<myArtifactId> -Dversion=<myVersion> \ -Dpackaging=<myPackaging> -DlocalRepositoryPath=<path>maven-install-plugin documentationFinally, declare it like any other dependency (but without the system scope):<dependency> <groupId>your.group.id</groupId> <artifactId>3rdparty</artifactId> <version>X.Y.Z</version></dependency>This is IMHO a better solution than using a system scope as your dependency will be treated like a good citizen (e.g. it will be included in an assembly and so on).Now, I have to mention that the "right way" to deal with this situation in a corporate environment (maybe not the case here) would be to use a corporate repository. 类路径下查找配置文件上面的打包过程中,把配置文件application.yml、log4j.properties拷贝到/conf目录下,主要是为了可以在不用更新jar的情况下修改一些配置,只要重启进程便可以生效。否则,如果配置文件被打包到jar中,修改配置文件就会变得比较麻烦。 但是,代码中如何能够读取到指定的配置文件呢?spring中有一个类提供了在classpath中读取指定文件的功能: File dumpFile = new org.springframework.core.io.ClassPathResource("application.yml").getFile(); 但是每次做项目的时候不一定都会依赖spring。看了一下源码,写了一个类似的比较简单的util类,帮助我们在代码中读取类路径下的配置文件: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556import java.io.IOException;import java.net.URL;import org.apache.commons.io.IOUtils;public class ClassPathResource { private ClassLoader classLoader; private String path; public ClassPathResource(String path) { this.path = path; } public byte[] getBytes() throws IOException { URL url = resolveURL(); byte[] byteArray = IOUtils.toByteArray(url.openStream()); return byteArray; } public String getString() throws IOException { return new String(getBytes()); } private URL resolveURL() { if (this.classLoader == null) { classLoader = getDefaultClassLoader(); } URL url = classLoader.getResource(path); if (url != null) { return url; } url = ClassLoader.getSystemResource(path); if (url == null) { throw new IllegalArgumentException(String.format("Error opening %s", path)); } return url; } private ClassLoader getDefaultClassLoader() { ClassLoader cl = null; try { cl = Thread.currentThread().getContextClassLoader(); } catch (Throwable ex) { } if (cl == null) { cl = ClassPathResource.class.getClassLoader(); if (cl == null) { try { cl = ClassLoader.getSystemClassLoader(); } catch (Throwable ex) { } } } return cl; }} 以上]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>maven</tag>
</tags>
</entry>
<entry>
<title><![CDATA[VncServer的安装与使用]]></title>
<url>%2F2017%2F10%2F01%2FVncServer%E7%9A%84%E5%AE%89%E8%A3%85%E4%B8%8E%E4%BD%BF%E7%94%A8%2F</url>
<content type="text"><![CDATA[因为新做的版本发布系统需要部署在内网,找了两台空闲的机器做了Centos7的系统,并搭建读写分离的环境。因为读写分离环境的搭建需要用到可视化工具,所以决定在这两台机器上搭一个vnc服务。 centos7,IP 192.168.1.57 安装 安装VNCServer 1yum install tigervnc-server -y 设置防火墙 12sudo firewall-cmd --permanent --add-service vnc-serversudo systemctl restart firewalld.service 普通方式启动 单用户单连接 server端: 比如你想要连入vnc的用户为Java 123su javavncpasswd //配置java用户vnc密码,比如123456vncserver :n //启动vncserver,其中,n为大于等于1的整数比如 vncserver :1(可以不填,vnc会自动分配一个序号) client端: 安装VNC Viewer 打开VNC Viewer,File->New connection,其中: VNC Server: 192.168.1.57:n //其中,n为VNCServer启动时的参数,比如192.168.1.57:1 Name: 随意填写 点击OK,弹出密码对话框,填入VNCServer密码 比如123456 此时以Java用户的身份进入GUI界面 单用户多连接 同一用户多个连接连入VNCServer: 上述几步是以Java用户的身份连入VNCServer,如果需要多个连接都是以Java用户的身份连入VNCServer: server端: 12su Javavncserver :n //此时n应该填2,以此类推 client端: VNC Server: 192.168.1.57:n //此时n应该为2,依次类推 多用户 不同用户连入VNCServer: 如果要使用不同身份的用户接入VNC,比如root: server端: 123suvncpasswd //配置root用户vnc密码,比如123456vncserver :n //启动vncserver,其中,n为大于等于1的整数且尚未被分配,比如 vncserver :3 client端: VNC Server: 192.168.1.57:n //此时n应该为3,接入VNC后身份为root 服务方式启动将VNCServer配置为服务,可以开机启动 12# 配置.service文件,注意":1"相当于"vncserver :n"中的参数":n"cp /lib/systemd/system/vncserver@.service /etc/systemd/system/vncserver@:1.service 观察一下文件 /etc/systemd/system/vncserver@:1.service,发现配置说明: 12345678910# Quick HowTo:# 1. Copy this file to /etc/systemd/system/vncserver@.service# 2. Replace <USER> with the actual user name and edit vncserver# parameters appropriately# ("User=<USER>" and "/home/<USER>/.vnc/%H%i.pid")# 3. Run `systemctl daemon-reload`# 4. Run `systemctl enable vncserver@:<display>.service```` 1. 首先将<USER>中的USER替换为指定的用户,有两处需要替换: User=PIDFile=/home//.vnc/%H%i.pid12替换为java: User=java 注意,如果是root,配置为PIDFile=/root/.vnc/%H%i.pidPIDFile=/home/java/.vnc/%H%i.pid122. 重启systemd systemctl daemon-reload123. 设置vnc密码 su javavncpasswd124. 开启服务 sudo systemctl enable vncserver@:1.servicesudo systemctl start vncserver@:1.service```]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux下的守护进程]]></title>
<url>%2F2017%2F10%2F01%2FLinux%E4%B8%8B%E7%9A%84%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B%2F</url>
<content type="text"><![CDATA[这两天在搭建VNC服务的时候,遇到一个简单的问题,却困扰了我有一会:网上的教程大部分是修改.service文件,然后启动服务,但是我发现使用vncserver这个命令也可以达到同样的目的,这两者之间有什么区别呢?查了一些资料,简单的做一个总结。 工作管理一般情况下,我们可能会在命令行下这样启动一个程序,以新做的版本管理系统为例: 1java -jar VersionManager.jar 现在,内网中的其他机器可以通过80端口访问本机提供的web服务了。一切都很正常。 注意到一个问题,新启动的程序独占了命令行窗口,并随时打印一些程序运行期间的log出来。如果想在同一个命令行窗口再执行其他命令,那么需要Ctrl+c停止这个web服务才可以。 这个时候的web服务称为前台任务,一旦我们退出这个命令行窗口,该服务也随之关闭,无法访问。 如何将其变成一个后台执行的任务,从而不影响命令行再执行其他命令呢? 1java -jar VersionManager.jar & 只要在命令的尾部加上符号&,启动的进程就会成为”后台任务”。”后台任务”有两个特点: 继承当前 session (对话)的标准输出(stdout)和标准错误(stderr)。因此,后台任务的所有输出依然会同步地在命令行下显示。 不再继承当前 session 的标准输入(stdin)。你无法向这个任务输入指令了。如果它试图读取标准输入,就会暂停执行(halt)。 所以,我们以上述方式启动web服务,他的运行日志依然会打印在屏幕上面。但是与前台任务的一个区别就是,现在可以在命令行执行其他命令了,所有的输出都会混杂在一起打印在屏幕上。 有没有办法解决这种问题呢?那就是重定向: 1java -jar VersionManager.jar >info.log 2>&1 & 上述命令把web服务输出的标准输出和标准错误信息都重定向到了info.log这个文件,屏幕上不会再有任何的信息被打印出来了。你也可以像之前那样查看web服务的输出信息: 1tail -f info.log 此时,web服务的输出又动态的在屏幕上打印出来了。 如果要让正在运行的”前台任务”变为”后台任务”,可以先按ctrl + z,然后执行bg命令。(让最近一个暂停的”后台任务”继续执行) 如何查看当前session有哪些后台任务在运行呢? 123$ jobs -l //打印pid[1]- 17000 运行中 nohup java -jar VersionManager.jar > logs/info.log 2>&1 &[2]+ 22738 停止 vim cron.log 将指定的后台任务变成前台执行: 1fg 2 //继续编辑cron.log 最后做一个小结: 123456查看后台任务:jobs -l将后台任务取回前台:fg number //number为任务号暂停前台任务,并将任务放到后台:ctrl + z暂停的后台任务继续执行:bg number //number为任务号结束前台任务:ctrl + c结束后台任务:kill pid //pid可以通过jobs -l进行查看 脱机管理通过上面的内容,我们了解到如何将一个任务放在后台执行。后台任务都是基于当前session的,如果我们退出了当前的session(关闭了命令行窗口或执行exit),后台任务还会执行吗? 想起了之前有个现场的技术支持人员打电话跟我反映,一个rest服务总是无规律的宕掉。刚开始我也想不通,后来才想到是上面提到的原因… 看一下session退出的时候发生了什么: 用户准备退出 session 系统向该 session 发出SIGHUP信号 session 将SIGHUP信号发给所有子进程 子进程收到SIGHUP信号后,自动退出 上面的流程可以解释,随着session退出,前台任务也会结束。那么后台任务是否也会收到SIGHUP信号后退出呢? 这由 Shell 的huponexit参数决定的。 1shopt | grep huponexit 执行上面的命令,就会看到huponexit参数的值。如果显示off,表示session 退出的时候,不会把SIGHUP信号发给”后台任务”,从而”后台任务”不会随着 session 一起退出。 但是,为了确保我们的web服务变成一个可靠的守护进程,我们应该显式的指出 session 退出的时候,不把SIGHUP信号发给”后台任务”: nohup1nohup java -jar VersionManager.jar & nohup对java -jar VersionManager.jar命令做了几件事: 阻止SIGHUP信号发到这个进程。 关闭标准输入。该进程不再能够接收任何输入,即使运行在前台。 重定向标准输出和标准错误到文件nohup.out。 一般情况下,我们会重定向web服务的输出: 1nohup java -jar VersionManager.jar >info.log 2>&1 & 至此,我们的web服务已经变成了一个可靠的守护进程。 tmux还有一种方法,那就是使用tmux。tmux可以在当前session里新建一个session。当前的session退出不会影响到新建的session。重新登录之后还可以连上之前建立的session。 1234567891011121314// 新建 session$ tmux new -s session_name// 切换到指定 session$ tmux attach -t session_name// 列出所有 session$ tmux list-sessions// 退出当前 session,返回前一个 session $ tmux detach// 杀死指定 session$ tmux kill-session -t session-name 参考如何使用Tmux提高终端环境下的效率 systemd服务 systemd 是 Linux 下的一款系统和服务管理器。Systemd 并不是一个命令,而是一组命令,涉及到系统管理的方方面面。 在centos7中,我们也许会使用systemd来管理我们的一些程序,比如ssh: 12345// 启动ssh服务systemctl start sshd.service// 设置ssh服务开机启动systemctl enable sshd.service... 我们也可以通过systemd来管理我们的守护进程,成为真正意义上的系统服务。 关于systemd的使用不再赘述,参考Systemd (简体中文)) 参考Linux 守护进程的启动方法]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[读懂Java_Thread_Dump]]></title>
<url>%2F2017%2F09%2F23%2F%E8%AF%BB%E6%87%82Java-Thread-Dump%2F</url>
<content type="text"><![CDATA[前段时间有现场发现死锁的问题,查看发回来的jstack dump文件,发现虽然大致看懂,但是不是很透彻。从网上看了一些介绍jstack的文章,大部分复制粘贴,讲解的也不够透彻,现在有时间来自己整理一下jstack的知识,记录一下~ 获取Dumpjava 提供了查看当前用户启动的java进程的工具 JPS:jps 123455008 org.eclipse.equinox.launcher_1.3.201.v20161025-1711.jar10712 Main888 App15804 Jps34300 SynchronizedTest 根据对应的java进程,查看其Thread Dump:jstack 34300 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566672017-09-22 14:57:46Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b15 mixed mode):"DestroyJavaVM" #12 prio=5 os_prio=0 tid=0x0000000001c7e000 nid=0x7ca4 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE"Synchronized_second" #11 prio=5 os_prio=0 tid=0x000000005887c000 nid=0x7430 waiting for monitor entry [0x00000000593af000] java.lang.Thread.State: BLOCKED (on object monitor) at jstack.SynchronizedTest$1.run(SynchronizedTest.java:8) - waiting to lock <0x00000000d5ad8fe8> (a java.lang.Class for jstack.SynchronizedTest) at java.lang.Thread.run(Thread.java:745)"Synchronized_first" #10 prio=5 os_prio=0 tid=0x000000005887b000 nid=0x5bdc runnable [0x000000005969f000] java.lang.Thread.State: RUNNABLE at jstack.SynchronizedTest$1.run(SynchronizedTest.java:8) - locked <0x00000000d5ad8fe8> (a java.lang.Class for jstack.SynchronizedTest) at java.lang.Thread.run(Thread.java:745)"Service Thread" #9 daemon prio=9 os_prio=0 tid=0x0000000058812800 nid=0x7a6c runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE"C1 CompilerThread2" #8 daemon prio=9 os_prio=2 tid=0x000000005879a000 nid=0xc07c waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE"C2 CompilerThread1" #7 daemon prio=9 os_prio=2 tid=0x0000000058797000 nid=0x7e5c waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE"C2 CompilerThread0" #6 daemon prio=9 os_prio=2 tid=0x000000005877e000 nid=0x9474 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000000005876b800 nid=0x861c waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000005751a000 nid=0x1ff4 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x00000000574fb000 nid=0x2e84 in Object.wait() [0x000000005875f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000d5988ee0> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143) - locked <0x00000000d5988ee0> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x00000000574b2000 nid=0x9dc8 in Object.wait() [0x000000005840f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000d5986b50> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference.tryHandlePending(Reference.java:191) - locked <0x00000000d5986b50> (a java.lang.ref.Reference$Lock) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)"VM Thread" os_prio=2 tid=0x00000000574aa800 nid=0x3b08 runnable"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x000000000233c800 nid=0xcba4 runnable"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000000000233e000 nid=0x51f4 runnable"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000000000233f800 nid=0x2474 runnable"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000002341000 nid=0x6048 runnable"VM Periodic Task Thread" os_prio=2 tid=0x000000005886d000 nid=0x902c waiting on conditionJNI global references: 16 注意,无论是使用jps命令或者jstack,要确保执行该命令的用户和启动java进程的用户为同一用户。 格式分析观察上面的Dump文件,列出了进程12964所启动的所有线程的某一时刻的状态,我们要关注的是与业务逻辑相关的线程,比如: 12345"Synchronized_second" #11 prio=5 os_prio=0 tid=0x000000005887c000 nid=0x7430 waiting for monitor entry [0x00000000593af000] java.lang.Thread.State: BLOCKED (on object monitor) at jstack.SynchronizedTest$1.run(SynchronizedTest.java:8) - waiting to lock <0x00000000d5ad8fe8> (a java.lang.Class for jstack.SynchronizedTest) at java.lang.Thread.run(Thread.java:745) Synchronized_second:是线程的名字,可以在实例化Thread对象的时候指定。如果没有显示指定,java会按照”Thread-n”的方式命名线程,n为从0开始的序号。如果使用了jdk提供的线程池,线程的命名方式为”pool-m-thread-n” prio=5:线程优先级 os_prio:系统线程优先级 tid:线程ID,在jvm中的唯一标识 nid:本地线程ID,线程在操作系统中的标识 waiting for monitor entry:线程当前的动作,代表线程当前的执行情况 java.lang.Thread.State:线程当前状态,由Thread.State定义: 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;} 上面的这些状态反映了线程在虚拟机中的执行状态(而不是操作系统中的状态信息),同一时刻,一个线程只能有一种状态: State 描述 NEW 线程未启动,即创建了Thread实例,调用start方法之前,线程处于NEW状态 RUNNABLE 表示线程具备运行的所有条件,正在运行或在执行队列中等待内核调用。如果线程一直处于这个状态(对比不同时刻的Dump文件),表示该线程一直在执行或者一直处于等待队列中 BLOCKED 线程正在等待获取内置锁,进入Synchronized关键字修饰的代码块或方法 WAITING 线程正在等待某个事件的发生,可能是由于调用了下列方法:Thread.join、Object.wait、LockSupport.park TIMED_WAITING 线程正在等待某个事件的发生,如果达到限定的时间,线程也能恢复运行,可能是由于调用了下列方法:Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos、LockSupport.parkUntil TERMINATED 线程执行完毕,只剩Thread对象存在 waiting to lock:线程调用修饰, 即线程执行的额外信息 Entry Set & Wait Set注意到上面的Thread Dump中出现了waiting for monitor entry,这代表什么意思呢?还需要了解java的Synchronized机制: Synchronized使用了内置锁Monitor,每一个java对象都有且只有一个Monitor 123456Object lock = new Object();public void run() { synchronized (lock) { //do somthing }} 使用Synchronized修饰代码称为临界区,jvm使用lock对象的Monitor实现多线程对这段代码的互斥访问: Monitor对象有两个队列,WaitSet和EntrySet 线程通过synchronized要求获取对象的锁。首先进入EntrySet队列,如果此刻没有其他线程持有Monitor,直接得到Monitor,进入临界区,如果此时Monitor已经被其他线程持有,则在EntrySet队列中等待 持有Monitor的线程将Monitor释放(退出临界区或调用lock.wait),EntrySet队列中的线程开始竞争Monitor,得到Monitor的线程可以进入临界区,其他线程继续等待 持有Monitor的线程调用lock.wait方法,该线程立即释放Monitor,并且被放入WaitSet 持有Monitor的线程lock.notify方法,jvm从WaitSet中选择一个线程放入EntrySet,如果调用了lock.notifyAll方法,则WaitSet中的所有线程都被放入EntrySet Dump 实例下面实际看一下Thread.sleep、Synchronized、ReentrantLock、Thread.join等情况下的Dump文件内容: Thread.sleep 123456789101112131415161718public class SleepTest { public static void main(String[] args) { Runnable runnable = new Runnable() { public void run() { try { Thread.sleep(200000); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread thread = new Thread(runnable, "yukai"); thread.setDaemon(false); thread.start(); }} 12345"yukai" #10 prio=5 os_prio=0 tid=0x0000000058e1c000 nid=0x27dc waiting on condition [0x000000005950e000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at jstack.SleepTest$1.run(SleepTest.java:9) at java.lang.Thread.run(Thread.java:745) Thread.join 12345678910111213141516171819public class JoinTest { public static void main(String[] args) { Thread thread = new Thread() { public void run() { try { Thread.sleep(30000); } catch (InterruptedException e) { e.printStackTrace(); } } }; thread.start(); try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } }} 12345678"main" #1 prio=5 os_prio=0 tid=0x0000000001b9e000 nid=0xb2c8 in Object.wait() [0x00000000027de000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000d5adb268> (a jstack.JoinTest$1) at java.lang.Thread.join(Thread.java:1245) - locked <0x00000000d5adb268> (a jstack.JoinTest$1) at java.lang.Thread.join(Thread.java:1319) at jstack.JoinTest.main(JoinTest.java:16) Synchronized 12345678910111213141516171819202122public class SynchronizedTest { public static void main(String[] args) { Runnable runnable = new Runnable() { public void run() { synchronized (SynchronizedTest.class) { while (true) { } } } }; new Thread(runnable, "Synchronized_first").start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(runnable, "Synchronized_second").start(); }} 1234567891011"Synchronized_first" #10 prio=5 os_prio=0 tid=0x0000000058c09000 nid=0x3f0c runnable [0x00000000598ff000] java.lang.Thread.State: RUNNABLE at jstack.SynchronizedTest$1.run(SynchronizedTest.java:8) - locked <0x00000000d5ad8fe8> (a java.lang.Class for jstack.SynchronizedTest) at java.lang.Thread.run(Thread.java:745)"Synchronized_second" #11 prio=5 os_prio=0 tid=0x0000000058c0a000 nid=0xab20 waiting for monitor entry [0x0000000059a4f000] java.lang.Thread.State: BLOCKED (on object monitor) at jstack.SynchronizedTest$1.run(SynchronizedTest.java:8) - waiting to lock <0x00000000d5ad8fe8> (a java.lang.Class for jstack.SynchronizedTest) at java.lang.Thread.run(Thread.java:745) Object.wait 12345678910111213141516171819202122232425262728293031323334public class SynchronizedConditionTest { public static void main(String[] args) { new Thread(SynchronizedConditionTest.wait, "Synchronized_Condition").start(); new Thread(SynchronizedConditionTest.timeWait, "Synchronized_Condition_time").start(); } public static Runnable wait = new Runnable() { Object lock = new Object(); public void run() { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }; public static Runnable timeWait = new Runnable() { Object lock = new Object(); public void run() { synchronized (lock) { try { lock.wait(20000); } catch (InterruptedException e) { e.printStackTrace(); } } } };} 12345678910111213141516"Synchronized_Condition" #10 prio=5 os_prio=0 tid=0x000000005893e800 nid=0x3468 in Object.wait() [0x000000005995f000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000d5adb980> (a java.lang.Object) at java.lang.Object.wait(Object.java:502) at jstack.SynchronizedConditionTest$1.run(SynchronizedConditionTest.java:16) - locked <0x00000000d5adb980> (a java.lang.Object) at java.lang.Thread.run(Thread.java:745)"Synchronized_Condition_time" #11 prio=5 os_prio=0 tid=0x000000005893f000 nid=0x4f84 in Object.wait() [0x000000005905f000] java.lang.Thread.State: TIMED_WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000d5add2b8> (a java.lang.Object) at jstack.SynchronizedConditionTest$2.run(SynchronizedConditionTest.java:29) - locked <0x00000000d5add2b8> (a java.lang.Object) at java.lang.Thread.run(Thread.java:745) ReentrantLock 12345678910111213141516171819202122232425262728public class ReentrantLockTest { private static Lock lock = new ReentrantLock(); public static void main(String[] args) { Runnable runnable = new Runnable() { public void run() { try { lock.lock(); while (true) { } } finally { lock.unlock(); } } }; new Thread(runnable, "ReentrantLock_first").start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(runnable, "ReentrantLock_second").start(); }} 1234567891011121314151617"ReentrantLock_first" #10 prio=5 os_prio=0 tid=0x0000000058a53000 nid=0xb9a8 runnable [0x000000005988f000] java.lang.Thread.State: RUNNABLE at jstack.ReentrantLockTest$1.run(ReentrantLockTest.java:14) at java.lang.Thread.run(Thread.java:745)"ReentrantLock_second" #11 prio=5 os_prio=0 tid=0x0000000058a53800 nid=0x6440 waiting on condition [0x000000005998e000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000d5adb7d8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199) at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209) at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285) at jstack.ReentrantLockTest$1.run(ReentrantLockTest.java:13) at java.lang.Thread.run(Thread.java:745) Condition 12345678910111213141516171819202122232425262728293031323334353637383940414243444546public class ReentrantLockConditionTest { public static void main(String[] args) { new Thread(ReentrantLockConditionTest.timeWait, "ReentrantLock_Condition_Time").start(); new Thread(ReentrantLockConditionTest.wait, "ReentrantLock_Condition").start(); } public static Runnable wait = new Runnable() { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void run() { try { // 加锁, 进入临界区 lock.lock(); condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 解锁, 退出临界区 lock.unlock(); } } }; public static Runnable timeWait = new Runnable() { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void run() { try { // 加锁, 进入临界区 lock.lock(); condition.await(20000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 解锁, 退出临界区 lock.unlock(); } } };} 1234567891011121314151617"ReentrantLock_Condition" #11 prio=5 os_prio=0 tid=0x000000005899a000 nid=0x47ac waiting on condition [0x000000005999f000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000d5add5a0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at jstack.ReentrantLockConditionTest$1.run(ReentrantLockConditionTest.java:25) at java.lang.Thread.run(Thread.java:745)"ReentrantLock_Condition_Time" #10 prio=5 os_prio=0 tid=0x0000000058999000 nid=0x2744 waiting on condition [0x000000005935f000] java.lang.Thread.State: TIMED_WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000000d5adf550> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2163) at jstack.ReentrantLockConditionTest$2.run(ReentrantLockConditionTest.java:43) at java.lang.Thread.run(Thread.java:745) socket 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110public class Server { public static final int PORT = 12345;//监听的端口号 private static int NUMBER = 0; public static void main(String[] args) { System.out.println("服务器启动...\n"); Server server = new Server(); server.init(); } public void init() { try { ServerSocket serverSocket = new ServerSocket(PORT); while (true) { // 一旦有堵塞, 则表示服务器与客户端获得了连接 Socket client = serverSocket.accept(); // 处理这次连接 new HandlerThread(client); } } catch (Exception e) { System.out.println("服务器异常: " + e.getMessage()); } } private class HandlerThread implements Runnable { private Socket socket; public HandlerThread(Socket client) { socket = client; new Thread(this, "server" + NUMBER).start(); } public void run() { try { // 读取客户端数据 DataInputStream input = new DataInputStream(socket.getInputStream()); String clientInputStr = input.readUTF();//这里要注意和客户端输出流的写方法对应,否则会抛 EOFException // 处理客户端数据 System.out.println("客户端发过来的内容:" + clientInputStr); // 向客户端回复信息 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); System.out.print("请输入:\t"); // 发送键盘输入的一行 String s = new BufferedReader(new InputStreamReader(System.in)).readLine(); out.writeUTF(s); out.close(); input.close(); } catch (Exception e) { System.out.println("服务器 run 异常: " + e.getMessage()); } finally { if (socket != null) { try { socket.close(); } catch (Exception e) { socket = null; System.out.println("服务端 finally 异常:" + e.getMessage()); } } } } } }public class Client { public static final String IP_ADDR = "localhost";// 服务器地址 public static final int PORT = 12345;// 服务器端口号 public static void main(String[] args) { System.out.println("客户端启动..."); System.out.println("当接收到服务器端字符为 \"OK\" 的时候, 客户端将终止\n"); while (true) { Socket socket = null; try { // 创建一个流套接字并将其连接到指定主机上的指定端口号 socket = new Socket(IP_ADDR, PORT); // 读取服务器端数据 DataInputStream input = new DataInputStream(socket.getInputStream()); // 向服务器端发送数据 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); System.out.print("请输入: \t"); String str = new BufferedReader(new InputStreamReader(System.in)).readLine(); out.writeUTF(str); String ret = input.readUTF(); System.out.println("服务器端返回过来的是: " + ret); // 如接收到 "OK" 则断开连接 if ("OK".equals(ret)) { System.out.println("客户端将关闭连接"); Thread.sleep(500); break; } out.close(); input.close(); } catch (Exception e) { System.out.println("客户端异常:" + e.getMessage()); } finally { if (socket != null) { try { socket.close(); } catch (IOException e) { socket = null; System.out.println("客户端 finally 异常:" + e.getMessage()); } } } } }} Server 端的dump内容 123456789101112131415161718192021222324"main" #1 prio=5 os_prio=0 tid=0x000000000049e000 nid=0x6eec runnable [0x00000000027fe000] java.lang.Thread.State: RUNNABLE at java.net.DualStackPlainSocketImpl.accept0(Native Method) at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:131) at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409) at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:199) - locked <0x00000000d5adc508> (a java.net.SocksSocketImpl) at java.net.ServerSocket.implAccept(ServerSocket.java:545) at java.net.ServerSocket.accept(ServerSocket.java:513) at jstack.socket.Server.init(Server.java:24) at jstack.socket.Server.main(Server.java:16)"server0" #10 prio=5 os_prio=0 tid=0x0000000058a64000 nid=0x317c runnable [0x0000000059b3e000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:170) at java.net.SocketInputStream.read(SocketInputStream.java:141) at java.net.SocketInputStream.read(SocketInputStream.java:223) at java.io.DataInputStream.readUnsignedShort(DataInputStream.java:337) at java.io.DataInputStream.readUTF(DataInputStream.java:589) at java.io.DataInputStream.readUTF(DataInputStream.java:564) at jstack.socket.Server$HandlerThread.run(Server.java:44) at java.lang.Thread.run(Thread.java:745) 注意到,虽然线程阻塞在SocketInputStream.socketRead或者ServerSocket.accept方法上,但是dump文件中线程的状态依然为RUNNABLE。因为RUNNABLE表示的是线程在虚拟机中的状态信息,在操作系统中该线程有可能因为其他原因被阻塞。 有了上面各种情况下Dump文件的参照,再拿到真实环境下的Dump文件对照分析也应该容易一些了。 最后上一张Thread的状态转换图:]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[理解JavaScript作用域链]]></title>
<url>%2F2017%2F09%2F16%2F%E7%90%86%E8%A7%A3JavaScript%E4%BD%9C%E7%94%A8%E5%9F%9F%E9%93%BE%2F</url>
<content type="text"><![CDATA[最近在读《JavaScript权威指南》,读到“函数作用域和声明提前”这部分内容时有点晕,上网查了一些资料,算是弄明白了,所以把自己的理解记下来~ 作用域 全局作用域 在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域: 最外层函数和在最外层函数外面定义的变量拥有全局作用域,例如: 123456789101112var a="global";function doSomething(){ var b="local"; function innerSay(){ alert(b); } innerSay();}alert(a); //globalalert(b); //脚本错误doSomething(); //localinnerSay() //脚本错误 所有末定义直接赋值的变量自动声明为拥有全局作用域,例如: 12345678function doSomething(){ var a="local"; b="global"; alert(a);}doSomething(); //localalert(b); //globalalert(a); //脚本错误 所有window对象的属性拥有全局作用域 一般情况下,window对象的内置属性都拥有全局作用域,例如window.name、window.location、window.top等等。 局部作用域 和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所有在一些地方也会看到有人把这种作用域称为函数作用域,例如下列代码中的a和函数innerSay都只拥有局部作用域。 123456789function doSomething(){ var a="local"; function innerSay(){ alert(a); } innerSay();}alert(a); //脚本错误innerSay(); //脚本错误 执行上下文在JavaScript中有三种代码运行环境: Global Code JavaScript代码开始运行的默认环境 Function Code 代码进入一个JavaScript函数 Eval Code 使用eval()执行代码 为了表示不同的运行环境,JavaScript中有一个执行上下文(Execution context,EC)的概念。也就是说,当JavaScript代码执行的时候,会进入不同的执行上下文,这些执行上下文就构成了一个执行上下文栈(Execution context stack,ECS)。 1234567891011121314151617181920var a = "global var";function foo(){ console.log(a);}function outerFunc(){ var b = "var in outerFunc"; console.log(b); function innerFunc(){ var c = "var in innerFunc"; console.log(c); foo(); } innerFunc();}outerFunc() 代码首先进入Global Execution Context,然后依次进入outerFunc,innerFunc和foo的执行上下文,执行上下文栈就可以表示为: 当JavaScript代码执行的时候,第一个进入的总是默认的Global Execution Context,所以说它总是在ECS的最底部。 对于每个Execution Context都有三个重要的属性,变量对象(VO),作用域链(Scope chain)和this。 问题提出123456789var a = 'global';function echo() { alert(a); var a = 'local'; alert(a); alert(b);}echo(); 运行结果为: 123undefinedlocal[脚本出错] 是不是跟你预想的不同? 理解作用域链任何执行上下文时刻的作用域, 都是由作用域链(scope chain)来实现: 在一个函数被定义的时候, 这个函数对象的[[scope]]属性会指向它定义时刻的执行上下文的scope chain 在一个函数对象被调用的时候,会创建一个活动对象(AO),然后将这个活动对象做为此时执行上下文的作用域链(scope chain)最前端, 并将这个函数对象的[[scope]]加入到scope chain中 例子: 1234function add(num1,num2) { var sum = num1 + num2; return sum;} 在执行func的定义语句的时候, 会创建一个这个函数对象的[[scope]]属性, 并将这个[[scope]]属性, 指向定义它的执行上下文的作用域链上。 此时因为add定义在全局环境, 所以此时的scope chain只是指向全局活动对象window active object 1var total = add(5,10); 在调用add的时候, 会创建一个活动对象(假设为aObj),并创建arguments属性, 然后会给这个对象添加俩个命名属性aObj.num1, aObj.num2; 对于每一个在这个函数中申明的局部变量和函数定义, 都作为该活动对象的同名命名属性 然后将调用参数赋值给形参数,对于缺少的调用参数,赋值为undefined 然后将这个活动对象做为scope chain的最前端, 并将add的[[scope]]属性所指向的,scope chain, 加入到当前scope chain 有了上面的作用域链, 在发生标识符解析的时候, 就会逆向查询当前scope chain列表的每一个活动对象的属性,如果找到同名的就返回。找不到,那就是这个标识符没有被定义。 变量对象(VO)与活动对象(AO) 变量对象是在函数被调用,但是函数尚未执行的时刻被创建的,这个创建变量对象的过程实际就是函数内数据(函数参数、内部变量、内部函数)初始化的过程。 未进入执行阶段之前,变量对象中的属性都不能访问。但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。所以活动对象实际就是变量对象在真正执行时的另一种形式。 实例1234567891011121314function factory() { var a = 'local'; var intro = function(){ alert(a); } return intro;}function app(para){ var a = para; factory();}app('global'); 执行代码,在刚进入app函数体时,scope chain为: 123456789[[scope chain]] = [{ para : 'global', a : undefined, arguments : ['global']}, { window call object}] 当调用进入factory的函数体的时候,此时的scope chain为: 12345678[[scope chain]] = [{ a : undefined, intor : undefined}, { window call object}] 在定义intro函数的时候,intro函数的[[scope]]为: 12345678[[scope chain]] = [{ a : 'local', intor : undefined}, { window call object}] 当调用进入intor的时候, 此时的scope chain为: 12345678910[[scope chain]] = [{ intro call object}, { a : 'local', intor : undefined}, { window call object}] 运行结果为: 1local 问题解决回到”问题提出”部分: 当echo函数被调用的时候, echo的活动对象已经被预编译过程创建, 此时echo的活动对象为: 123[callObj] = {name : undefined} 当第一次alert的时候, 发生了标识符解析, 在echo的活动对象中找到了name属性, 所以这个name属性, 完全的遮挡了全局活动对象中的name属性 参考JavaScript的执行上下文 JavaScript 开发进阶:理解 JavaScript 作用域和作用域链 Javascript作用域原理 图解Javascript——变量对象和活动对象]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>nodejs</tag>
</tags>
</entry>
<entry>
<title><![CDATA[组合模式的妙用]]></title>
<url>%2F2017%2F09%2F16%2F%E7%BB%84%E5%90%88%E6%A8%A1%E5%BC%8F%E7%9A%84%E5%A6%99%E7%94%A8%2F</url>
<content type="text"><![CDATA[组合模式定义 Component 是组合中的对象声明接口,在适当的情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。 Leaf 在组合中表示叶子结点对象,叶子结点没有子结点。 Composite 定义有枝节点行为,用来存储子部件,在Component接口中实现与子部件有关操作,如增加(add)和删除 组合模式让多用于优化处理递归或分级数据结构,比如文件系统:文件系统由目录和文件组成。每个目录都可以装内容。目录的内容可以是文件,也可以是目录。按照这种方式,计算机的文件系统就是以递归结构来组织的。描述这样的数据结构,可以使用组合模式。 实践需求项目中有这样一个需求:上传一个zip格式的文件,服务器程序解压缩该文件,并生成json格式的文件层级描述,对于每个文件要求在json对象中包含该文件MD5、sha1、文件名以及大小。比如: 1234567891011121314151617181920212223242526272829303132333435363738{ "children": [ { "children": [ { "md5": "xxxxxxxxxxxx", "sha1": "xxxxxxxxxxxxx", "size": 1024, "path": "123.txt" }, { "md5": "xxxxxxxxxxxx", "sha1": "xxxxxxxxxxxxx", "size": 1024, "path": "456.txt" } ], "path": "yukai" }, { "children": [ { "children": [ { "md5": "xxxxxxxxxxxx", "sha1": "xxxxxxxxxx", "size": 1024, "path": "123.txt" } ], "path": "book" } ], "path": "zhanglei" } ], "path": "we.zip"} 表示:123456789101112we.zip | |----zhanglei | | | |----book | | | |----123.txt |----yukai | | |----123.txt | |----456.txt 实现 根据上面提到组合模式的定义,实现文件的层级描述: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970public abstract class Component { private String path; public Component(String path) { this.path = path; } public abstract void add(Component component); public String getPath() { return path; } public void setPath(String path) { this.path = path; }}public class Composite extends Component{ private List<Component> children; public Composite(String path) { super(path); children = new ArrayList<>(); } @Override public void add(Component component) { children.add(component); }}public class Leaf extends Component { public Leaf(String path) { super(path); } private String md5; private String sha1; private long size; @Override public void add(Component component) { return; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 = md5; } public String getSha1() { return sha1; } public void setSha1(String sha1) { this.sha1 = sha1; } public long getSize() { return size; } public void setSize(long l) { this.size = l; }} 写一个简单的测试类测试我们的实现: 123456789101112131415161718192021222324252627@Testpublic void testComponent() { Composite root = new Composite("we.zip"); Composite component1 = new Composite("yukai"); Leaf leaf1 = new Leaf("123.txt"); leaf1.setMd5("xxxxxxxxxxx"); leaf1.setSha1("xxxxxxxxxxxxx"); leaf1.setSize(1024); component1.add(leaf1); Leaf leaf2 = new Leaf("456.txt"); leaf1.setMd5("xxxxxxxxxxx"); leaf1.setSha1("xxxxxxxxxxxxx"); leaf1.setSize(1024); Composite component2 = new Composite("zhanglei"); Component component3 = new Composite("book"); Leaf leaf = new Leaf("123.txt"); leaf.setMd5("xxxxxxxx"); leaf.setSha1("xxxxxxx"); leaf.setSize(1024); component3.add(leaf); component2.add(component3); root.add(component1); root.add(component2); Gson gson = new Gson(); String json = gson.toJson(root); System.out.println(json);} 输出的结果正好是我们上面提到的json串。 递归遍历文件目录,生成json树 上面的数据结构可以满足我们对生成的json树的格式要求,接下来便是遍历文件目录,生成组合模式中的结构,代码很简单: 1234567891011121314151617181920212223public void walk(Path parentPath, Component parent) throws IOException { File file = parentPath.toFile(); File[] list = file.listFiles(); if (list == null) { return; } Composite composite; Leaf leaf; for (File f : list) { String currentName = f.getName(); if (f.isDirectory()) { composite = new Composite(currentName); parent.add(composite); walk(f.toPath(), composite); } else { leaf = new Leaf(currentName); leaf.setSize(f.length()); leaf.setMd5(FileProvider.md5Hex(f)); leaf.setSha1(FileProvider.sha1Hex(f)); parent.add(leaf); } }} 上面的代码使用递归的方式,遍历文件目录,生成目录结构。我们可以这样使用它: 1234567String zipFile = ".../..../..../abc.zip";File file = new File(zipFile);Composite root = new Composite(file.getName());walkDirectory(file.toPath(), root);Gson gson = new Gson();String json = gson.toJson(root);System.out.println(json); 通过上面的代码,看似复杂的生成文件json树的需求就被轻易的解决了,看起来真的很优雅! have a nice day~~~]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java-NIO-Reactor]]></title>
<url>%2F2017%2F09%2F13%2FJava-NIO-Reactor%2F</url>
<content type="text"><![CDATA[接着学习java-NIO。这次要从宏观架构上来学习NIO,并涉及到一个模型:Reactor线程模型。 传统的BIO模式 12345678910111213141516171819202122232425262728class Server { public static void main() { ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池 ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(8088); while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来 Socket socket = serverSocket.accept(); executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程 } } static class ConnectIOnHandler implements Runnable { private Socket socket; public ConnectIOnHandler(Socket socket){ this.socket = socket; } public void run(){ while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){ String someThing = socket.read();//读取数据 if(someThing!=null){ ......//处理数据 socket.write()....//写数据 } } } }} 上面的代码中,我们在主线程中处理客户端的连接请求,然后为每个建立的连接分配一个线程去执行。socket.read()、socket.write()是同步阻塞的,我们开启了多线程,就可以让CPU去处理更多的连接,这也是多线程的本质: 利用了多核的并行处理能力 当io阻塞系统,但CPU空闲时,利用多线程使用CPU资源 上面的方案也有其致命缺陷,因为其本质还是依赖线程: 线程创建和销毁的代价很高 线程很占内存 线程的切换带来的资源消耗。有可能恰好轮到一个线程的时间片,但此时这个线程被io阻塞,这时会发生线程切换(无意义的损耗) 上面的线程池定义了100个线程,意味着同时只能为100个用户服务。倘若服务器同故障节点通信,由于其io是阻塞的,如果所有可用线程被故障节点阻塞,那么新的请求在队列中排队,直到连接超时。 所以,当面对数十万的连接请求,传统的BIO是无能为力的。 NIO工作原理回顾前面的学习内容 Linux网络IO模型 BIO的read过程:发起系统调用,试图从内核空间读取数据到用户空间,如果数据没有就绪(数据还没有从硬件拷贝到内核),一直阻塞,直到返回数据 NIO的处理过程:发起系统调用,试图从内核空间读取数据到用户空间,如果数据没有就绪,直接返回0,永远也不会阻塞 需要注意的是: 从内核拷贝数据到用户空间这个io操作是阻塞的,而且需要消耗CPU(性能非常高,基本不耗时) BIO等待内核数据就绪的过程是空等,不需要CPU Reactor与NIO相结合所谓的Reactor模式,核心就是事件驱动,或者j叫回调的方式。这种方式就是,应用业务向一个中间人注册一个回调(event handler),当IO就绪后,就这个中间人产生一个事件,并通知此handler进行处理。 那么由谁来充当这个中间人呢?是由一个不断等待和循环的单独进程(线程)来做这件事,它接受所有handler的注册,并负责先操作系统查询IO是否就绪,在就绪后就调用指定handler进行处理,这个角色的名字就叫做Reactor。 回想一下 Linux网络IO模型 中提到的 IO复用,一个线程可以同时处理多个Connection,是不是正好契合Reactor的思想。所以,在java中可以使用NIO来实现Reactor模型。 单线程Reactor Reactor:负责响应事件,将事件分发给绑定了该事件的Handler处理; Handler:事件处理器,绑定了某类事件,负责执行对应事件的Task对事件进行处理; Acceptor:Handler的一种,绑定了connect事件。当客户端发起connect请求时,Reactor会将accept事件分发给Acceptor处理。 看一下其对应的实现: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384class Reactor implements Runnable { final Selector selector; final ServerSocketChannel serverSocket; Reactor(int port) throws IOException { //Reactor初始化 selector = Selector.open(); serverSocket = ServerSocketChannel.open(); serverSocket.socket().bind(new InetSocketAddress(port)); serverSocket.configureBlocking(false); //非阻塞 SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT); //分步处理,第一步,接收accept事件 sk.attach(new Acceptor()); //attach callback object, Acceptor } public void run() { try { while (!Thread.interrupted()) { selector.select(); Set selected = selector.selectedKeys(); Iterator it = selected.iterator(); while (it.hasNext()) dispatch((SelectionKey)(it.next()); //Reactor负责dispatch收到的事件 selected.clear(); } } catch (IOException ex) { /* ... */ } } void dispatch(SelectionKey k) { Runnable r = (Runnable)(k.attachment()); //调用之前注册的callback对象 if (r != null) r.run(); } class Acceptor implements Runnable { // inner public void run() { try { SocketChannel c = serverSocket.accept(); if (c != null) new Handler(selector, c); } catch(IOException ex) { /* ... */ } } }}final class Handler implements Runnable { final SocketChannel socket; final SelectionKey sk; ByteBuffer input = ByteBuffer.allocate(MAXIN); ByteBuffer output = ByteBuffer.allocate(MAXOUT); static final int READING = 0, SENDING = 1; int state = READING; Handler(Selector sel, SocketChannel c) throws IOException { socket = c; c.configureBlocking(false); // Optionally try first read now sk = socket.register(sel, 0); sk.attach(this); //将Handler作为callback对象 sk.interestOps(SelectionKey.OP_READ); //第二步,接收Read事件 sel.wakeup(); } boolean inputIsComplete() { /* ... */ } boolean outputIsComplete() { /* ... */ } void process() { /* ... */ } public void run() { try { if (state == READING) read(); else if (state == SENDING) send(); } catch (IOException ex) { /* ... */ } } void read() throws IOException { socket.read(input); if (inputIsComplete()) { process(); state = SENDING; // Normally also do first write now sk.interestOps(SelectionKey.OP_WRITE); //第三步,接收write事件 } } void send() throws IOException { socket.write(output); if (outputIsComplete()) sk.cancel(); //write完就结束了, 关闭select key }} NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。 缺点: 一个连接里完整的网络处理过程一般分为accept、read、decode、process(compute)、encode、send这几步,如果在process这个过程中需要处理大量的耗时业务,比如连接DB或者进行耗时的计算等,整个线程都被阻塞,无法处理其他的链路 单线程,不能充分利用多核处理器 单线程处理I/O的效率确实非常高,没有线程切换,只是拼命的读、写、选择事件。但是如果有成千上万个链路,即使不停的处理,一个线程也无法支撑 单线程,一旦线程意外进入死循环或者抛出未捕获的异常,整个系统就挂掉了 对于缺点1,通常的解决办法是将decode、process、encode扔到后台业务线程池中执行,避免阻塞reactor。但对于缺点2、3、4,单线程的reactor是无能为力的。 多线程的Reactor 有专门一个reactor线程用于监听服务端ServerSocketChannel,接收客户端的TCP连接请求; 网络IO的读/写操作等由一个worker reactor线程池负责,由线程池中的NIO线程负责监听SocketChannel事件,进行消息的读取、解码、编码和发送。 一个NIO线程可以同时处理N条链路,但是一个链路只注册在一个NIO线程上处理,防止发生并发操作问题。 注意,socketchannel、selector、thread三者的对应关系是: socketchannel只能注册到一个selector上,但是一个selector可以被多个socketchannel注册; selector与thread一般为一一对应。 12345678910Selector[] selectors; // 一个selector对应一个线程int next = 0;class Acceptor { public synchronized void run() { ... Socket connection = serverSocket.accept(); if (connection != null) new Handler(selectors[next], connection); if (++next == selectors.length) next = 0; }} 主从多线程Reactor 在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是在极个别特殊场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。比如,建立连接时需要进行复杂的验证和授权工作等。 服务端用于接收客户端连接的不再是个1个单独的reactor线程,而是一个boss reactor线程池; 服务端启用多个ServerSocketChannel监听不同端口时,每个ServerSocketChannel的监听工作可以由线程池中的一个NIO线程完成。 NIO实战 参考老外写的一个 Java-NIO-Server:Java NIO: Non-blocking Server,代码在 github上。不错的一个参考,解决了NIO中半包粘包的问题,但是代码可读性不高; 另外一个NIO-Server,代码比较简单,可读性较高,代码风格值得学习。但避开了半包粘包的问题,也不算是正真意义上的Reactor模型。 参考Scalable IO in Java netty学习系列:NIO Reactor模型 & Netty线程模型 Java NIO浅析]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>NIO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[炒茄子]]></title>
<url>%2F2017%2F09%2F09%2F%E7%82%92%E8%8C%84%E5%AD%90%2F</url>
<content type="text"><![CDATA[原料茄子、姜片、葱、蒜、干辣椒、郫县豆瓣酱、盐、花椒粒、料酒、醋、生抽、白糖、味精 做法 茄子一个,洗净,切成条 锅烧热,直接将切好的茄条倒入,小火翻炒,直到茄子变软,且水分炒调一些,盛出 锅中倒油,油七成热后加入姜片、葱丝、蒜末、干辣椒、花椒粒炒香 放入一勺郫县豆瓣酱,继续翻炒出红油和香味 倒入茄子条,翻炒几下 将香醋3勺,生抽2勺,盐半勺,料酒1勺,白糖1勺,味精半勺调成汁,倒入锅中 翻炒均匀,待调味汁收干入味后撒葱花出锅 成品加了洋葱一起炒~]]></content>
<categories>
<category>生活</category>
<category>食物</category>
</categories>
<tags>
<tag>食物</tag>
</tags>
</entry>
<entry>
<title><![CDATA[git服务器的配置]]></title>
<url>%2F2017%2F09%2F06%2Fgit%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E9%85%8D%E7%BD%AE%2F</url>
<content type="text"><![CDATA[公司一直在使用Starteam作版本控制,这是个很古老的工具,也有不少的BUG…最近为公司搭建了Git服务器试用,把搭建的过程记录下来… ssh配置可以使用ssh协议搭建ssh服务,适合于几个人的小团队,每个人都拥有读写的权限。 配置git服务器通过创建一个专门的git用户,作为访问git服务的账户 123456$ ssh root@192.168.1.110$ sudo adduser git$ su git$ cd$ mkdir .ssh && chmod 700 .ssh$ touch .ssh/authorized_keys && chmod 600 .ssh/authorized_keys 配置客户机 安装git 本机生成ssh key 123456右键,打开Git Bash Here$ ssh-keygen -t rsa -b 4096 -C "your_email@example.com"Generating public/private rsa key pair.Enter a file in which to save the key (/c/Users/you/.ssh/id_rs[Press enter]Enter passphrase (empty for no passphrase): [Type a passphrase]Enter same passphrase again: [Type passphrase again] 将公钥传送到远程主机上(git服务器所在的主机) 1$ ssh ssh-copy-id git@192.168.1.110 新建仓库以JDBC为例,仓库目录结构参照 Starteam:oscar5.7-> OSCAR7.0_64BITS_DEV-> OSCAR_RELEASE_2012 新建裸仓库 123456$ ssh git@192.168.1.110$ cd git$ mkdir OSCAR_RELEASE_2012/Cross_Platform/jdbc.git$ cd OSCAR_RELEASE_2012/Cross_Platform/jdbc.git$ git init --bareInitialized empty Git repository in /home/git/git/OSCAR_RELEASE_2012/Cross_Platform/jdbc.git/ 将项目推送到远程仓库 客户机yukai: 1234567右键,打开Git Bash Here$ cd jdbc //进入JDBC源码所在的根目录$ git init$ git add .$ git commit -m 'initial commit'$ git remote add origin git@192.168.1.110:/home/git/git/OSCAR_RELEASE_2012/Cross_Platform/jdbc.git/$ git push origin master 克隆git服务器上的仓库 客户机fuqiuying: 12右键,打开Git Bash Here$ git clone git@192.168.1.110:/home/git/git/OSCAR_RELEASE_2012/Cross_Platform/jdbc.git/ 修改git用户登录shell 需要注意的是,目前所有(获得授权的)开发者用户都能以系统用户 git 的身份登录服务器从而获得一个普通 shell 借助一个名为 git-shell 的受限 shell 工具,你可以方便地将用户 git 的活动限制在与 Git 相关的范围内layout: post如果将 git-shell 设置为用户 git 的登录 shell,那么用户 git 便不能获得此服务器的普通 shell 访问权限 1234$ cat /etc/shells #查看git-shell是否已经存在于 /etc/shells 文件中$ which git-shell #查看git-shell的安装位置$ sudo vim /etc/shells #将上一步查询得到的git-shell安装位置加入到/etc/shells文件末尾$ sudo chsh git #执行此命令,修改git用户的shell,会提示输入修改的shell,这里修改为git-shell的安装位置 这样,用户 git 就只能利用 SSH 连接对 Git 仓库进行推送和拉取操作,而不能登录机器并取得普通 shell。 如果试图登录,你会发现尝试被拒绝,像这样: 1234$ ssh git@gitserverfatal: Interactive git shell is not enabled.hint: ~/git-shell-commands should exist and have read and execute access.Connection to gitserver closed. gitlab配置gitlab可以说是一个翻版的GitHub,拥有权限管理,review等功能。适合于公司内部使用。 centos7为例: 配置安装环境 12345678$ sudo yum install curl policycoreutils openssh-server openssh-clients$ sudo systemctl enable sshd$ sudo systemctl start sshd$ sudo yum install postfix$ sudo systemctl enable postfix$ sudo systemctl start postfix$ sudo firewall-cmd --permanent --add-service=http$ sudo systemctl reload firewalld 下载并安装 12$ curl -sS http://packages.gitlab.com.cn/install/gitlab-ce/script.rpm.sh | sudo bash$ sudo yum install gitlab-ce 配置gitlab 12$ sudo vim /etc/gitlab/gitlab.rb# external_url="192.168.1.110:8888" 开放端口并应用配置 1234$ sudo firewall-cmd --zone=public --add-port=8888/tcp --permanent$ sudo firewall-cmd --reload$ firewall-cmd --list-all$ sudo gitlab-ctl reconfigure 检测是否安装成功 1234567$ sudo gitlab-ctl status[sudo] password for firehare: run: nginx: (pid 13334) 16103s; run: log: (pid 4244) 22211srun: postgresql: (pid 4153) 22280s; run: log: (pid 4152) 22280srun: redis: (pid 4070) 22291s; run: log: (pid 4069) 22291srun: sidekiq: (pid 4234) 22212s; run: log: (pid 4233) 22212srun: unicorn: (pid 4212) 22218s; run: log: (pid 4211) 22218s gitlab汉化 1$ sudo cat /opt/gitlab/embedded/service/gitlab-rails/VERSION 假设当前版本为 v9.5.2,并确认汉化版本库是否包含该版本的汉化标签(-zh结尾),也就是是否包含 v9.5.2-zh。 如果版本相同,首先在本地 clone 仓库。 1234# 克隆汉化版本库$ git clone https://gitlab.com/xhang/gitlab.git# 如果已经克隆过,则进行更新$ git fetch 然后比较汉化标签和原标签,导出 patch 用的 diff 文件。 1$ git diff v9.5.2 v9.5.2-zh > ../9.5.2-zh.diff 上传 9.5.2-zh.diff 文件到服务器 1234$ cd ..$ sudo gitlab-ctl stop$ sudo yum -y install patch$ sudo patch -d /opt/gitlab/embedded/service/gitlab-rails -p1 < 9.5.2-zh.diff 重启gitlab 12$ sudo gitlab-ctl start$ sudo gitlab-ctl reconfigure 参考 git book gitlab中文网 gitlab汉化]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[红烧排骨]]></title>
<url>%2F2017%2F09%2F01%2F%E7%BA%A2%E7%83%A7%E6%8E%92%E9%AA%A8%2F</url>
<content type="text"><![CDATA[原料排骨、葱片、姜片、八角、花椒粒、料酒、生抽、蚝油、盐、白糖 做法 锅中倒水,将剁好洗净的肋排放入,煮几分钟,撇去杂质后捞出 锅中放少许油烧热,放入葱花、姜片、八角、花椒炒香 放入肋排,翻炒至肉变色(注意时间不要太长,否则肉会变老) 倒入料酒、生抽、蚝油,翻炒几下 加入清水(高汤)刚好没过排骨,大火烧开 小火慢炖四十分钟左右,大火收汁出锅 成品]]></content>
<categories>
<category>生活</category>
<category>食物</category>
</categories>
<tags>
<tag>食物</tag>
</tags>
</entry>
<entry>
<title><![CDATA[状态机与状态模式]]></title>
<url>%2F2017%2F08%2F10%2F%E7%8A%B6%E6%80%81%E6%9C%BA%E4%B8%8E%E7%8A%B6%E6%80%81%E6%A8%A1%E5%BC%8F%2F</url>
<content type="text"><![CDATA[又是很长时间没有写博客了(一个月)…最近在做一个SpringBoot+Vue的项目,所以一直在看spring相关的东西。今天要学习的跟spring没有关系,是我在之前维护的一个测试工具是遇到的一个知识点–状态机 这个测试的一个功能就是解析自己定义的一套脚本语法规则,涉及到对输入的语句进行解析,然后下发到对应的执行器去执行。 之前的解析逻辑是用一个while循环,对每一个字符判断,然后各种if…else和临时变量…总之读起来十分费劲,并且总容易出BUG,而且十分不容易修改,因为每一个修改都很容易影响到原来的解析结果。于是我把这段解析的代码重构了一遍,就是使用了状态机的思想。 什么是状态机 有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。 状态机可归纳为4个要素,即现态、条件、动作、次态。“现态”和“条件”是因,“动作”和“次态”是果: 现态:是指当前所处的状态。 条件:又称为“事件”。当一个条件被满足,将会触发一个动作,或者执行一次状态的迁移。 动作:条件满足后执行的动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。 次态:条件满足后要迁往的新状态。“次态”是相对于“现态”而言的,“次态”一旦被激活,就转变成新的“现态”了。 不是太好理解,我也是copy网上的概念,我们下面会举例子说明。 什么是状态模式 Context(环境类) 环境类又称为上下文类,它是拥有多种状态的对象。由于环境类的状态存在多样性且在不同状态下对象的行为有所不同,因此将状态独立出去形成单独的状态类。在环境类中维护一个抽象状态类State的实例,这个实例定义当前状态,在具体实现时,它是一个State子类的对象。 State(抽象状态类) 它用于定义一个接口以封装与环境类的一个特定状态相关的行为,在抽象状态类中声明了各种不同状态对应的方法,而在其子类中实现类这些方法,由于不同状态下对象的行为可能不同,因此在不同子类中方法的实现可能存在不同,相同的方法可以写在抽象状态类中。 ConcreteState(具体状态类) 它是抽象状态类的子类,每一个子类实现一个与环境类的一个状态相关的行为,每一个具体状态类对应环境的一个具体状态,不同的具体状态类其行为有所不同。 同样的,下面会举例说明。 一个例子假设现在有这么一个需求:给出一段java程序,要求删除其中的注释并返回删除注释之后的代码。 想想怎么去实现这个功能?初步的思路是在一个while循环里面,遍历这个String,对每个字符进行判断,然后是if else等等…功能肯定是可以实现的,但是我们有一个更加合适的套路,就是使用状态机。 设计状态机如下: 设正常状态为0,并且初始为正常状态 每遍历一个字符,就依次检查下列条件,若成立或全部检查完毕,则回到这里检查下一个字符 状态0中遇到/,说明可能会遇到注释,则进入状态1 例子: int a = b; / 状态1中遇到/,说明进入单行注释部分,则进入状态2 例子: int a = b; // 状态1中遇到,说明进入多行注释部分,则进入状态3 例子: int a= b; / 状态1中没有遇到*或/,说明/是路径符号或除号,则恢复状态0 例子: 8/3 状态2中遇到回车符\n,说明单行注释结束,则恢复状态0 例子: int a = b; //hehe 状态2中不是遇到回车符\n,说明单行注释还在继续,则维持状态2 例子: int a = b; //hehe 状态3中遇到,说明多行注释可能要结束,则进入状态4 例子: int a = b; /heh* 状态3中不是遇到,说明多行注释还在继续,则维持状态3 例子: int a = b; /hehe 状态4中遇到/,说明多行注释要结束,则恢复状态0 例子: int a = b; /hehe/ 状态4中不是遇到/,说明多行注释只是遇到,还要继续,则恢复状态3 例子: int a = b; /hehe*h 状态图: if else实现状态机1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192package space.kyu.mode.state;public class CodeProcessor1 { private StringBuilder codeWithoutComment; private String originCode; public CodeProcessor1(String code) { originCode = code; } public String clearComment() { codeWithoutComment = new StringBuilder(); char c, state; state = 0; for (int i = 0; i < originCode.length(); ++i) { c = getChar(i); if (state == 0) { if (c == '/') { state = 1; } else { putChar(c); // action } } else if (state == 1) { if (c == '/') // 例子: int a = b; // { state = 2; } else if (c == '*') // 例子: int a= b; /* { state = 3; } else // 例子: <common/md5.h> or 8/3 { state = 0; putChar('/'); // action putChar(c); // action } } else if (state == 2) { if (c == '\n') // 例子: int a = b; //hehe { state = 0; putChar(c); // action } // 例子: int a = b; //hehe } else if (state == 3) { if (c == '*') // 例子: int a = b; /*heh* { state = 4; } // 例子: int a = b; /*hehe } else if (state == 4) { if (c == '/') // 例子: int a = b; /*hehe*/ { state = 0; } else // 例子: int a = b; /*hehe*h { state = 3; } } else { System.out.println("state error!"); } } return codeWithoutComment.toString(); } private char getChar(int i) { return originCode.charAt(i); } private void putChar(char c) { codeWithoutComment.append(c); } public static void main(String[] args) { String code = " public static void main(String[] args) {" + "\n" + " /*hehe " + "\n" + " hehe " + "\n" + " */ " + "\n" + " /*hehe*/" + "\n" + " int a, int b; " + "\n" + " /* hehe */ " + "\n" + " //hehe" + "\n" + " a = 4+2; //hehe" + "\n" + " b = a;" + "\n" + " String file = \"/tmp/log.log\"" + "\n" + " }"; System.out.println(code); System.out.println("*******************************"); CodeProcessor1 process = new CodeProcessor1(code); String str = process.clearComment(); System.out.println(str); }} 输出结果: 1234567891011121314151617181920212223public static void main(String[] args) { /*hehe hehe */ /*hehe*/ int a, int b; /* hehe */ //hehe a = 4+2; //hehe b = a; String file = "/tmp/log.log" }******************************* public static void main(String[] args) { int a, int b; a = 4+2; b = a; String file = "/tmp/log.log" } 状态模式实现状态机123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115package space.kyu.mode.state.interfac;public class CodeProcessor2 { InputState currentState; StringBuilder codeWithoutComment; String originCode; public CodeProcessor2(String code) { originCode = code; currentState = new Normal(); } public String clearComment() { codeWithoutComment = new StringBuilder(); for (int i = 0; i < originCode.length(); ++i) { char charAt = getChar(i); currentState.handleInput(charAt, this); } return codeWithoutComment.toString(); } private char getChar(int i) { return originCode.charAt(i); } public void putChar(char c) { codeWithoutComment.append(c); } public static void main(String[] args) { String code = " public static void main(String[] args) {" + "\n" + " /*hehe " + "\n" + " hehe " + "\n" + " */ " + "\n" + " /*hehe*/" + "\n" + " int a, int b; " + "\n" + " /* hehe */ " + "\n" + " //hehe" + "\n" + " a = 4+2; //hehe" + "\n" + " b = a;" + "\n" + " String file = \"/tmp/log.log\"" + "\n" + " }"; System.out.println(code); System.out.println("*******************************"); CodeProcessor2 process = new CodeProcessor2(code); String str = process.clearComment(); System.out.println(str); }}abstract class InputState { protected char backslash = '/'; protected char asterisk = '*'; protected char lineBreaks = '\n'; abstract void handleInput(char charAt, CodeProcessor2 processor);}class Normal extends InputState { @Override public void handleInput(char charAt, CodeProcessor2 processor) { if (charAt == backslash) { processor.currentState = new CommentSymbol(); } else { processor.putChar(charAt); } }}class CommentSymbol extends InputState { @Override public void handleInput(char charAt, CodeProcessor2 processor) { if (charAt == backslash) { processor.currentState = new SinglelineComment(); } else if (charAt == asterisk) { processor.currentState = new MutilineComment(); } else { processor.putChar('/'); processor.putChar(charAt); processor.currentState = new Normal(); } }}class SinglelineComment extends InputState { @Override public void handleInput(char charAt, CodeProcessor2 processor) { if (charAt == lineBreaks) { processor.putChar(charAt); processor.currentState = new Normal(); } }}class MutilineComment extends InputState { @Override public void handleInput(char charAt, CodeProcessor2 processor) { if (charAt == asterisk) { processor.currentState = new MutilineCommentEnding(); } }}class MutilineCommentEnding extends InputState { @Override public void handleInput(char charAt, CodeProcessor2 processor) { if (charAt == backslash) { processor.currentState = new Normal(); } else { processor.currentState = new MutilineComment(); } }} 其中: CodeProcessor2 为 Context(环境类) InputState 为 State(抽象状态类) Normal等继承了InputState的类 为 ConcreteState(具体状态类) 输出结果: 1234567891011121314151617181920212223public static void main(String[] args) { /*hehe hehe */ /*hehe*/ int a, int b; /* hehe */ //hehe a = 4+2; //hehe b = a; String file = "/tmp/log.log" }******************************* public static void main(String[] args) { int a, int b; a = 4+2; b = a; String file = "/tmp/log.log" } enum实现状态机利用java中提供的enum实现状态机也是状态模式的一种,这样让代码更整洁并且不会产生很多的类导致类膨胀。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129package space.kyu.mode.state;public class CodeProcessor { InputState currentState; StringBuilder codeWithoutComment; String originCode; public CodeProcessor(String code) { originCode = code; currentState = States.NORMAL; } public String clearComment() { codeWithoutComment = new StringBuilder(); for(int i = 0; i < originCode.length(); ++i){ char charAt = getChar(i); currentState.handleInput(charAt, this); } return codeWithoutComment.toString(); } private char getChar(int i) { return originCode.charAt(i); } public void putChar(char c){ codeWithoutComment.append(c); } public static void main(String[] args) { String code = " public static void main(String[] args) {" + "\n" + " /*hehe " + "\n" + " hehe " + "\n" + " */ " + "\n" + " /*hehe*/" + "\n" + " int a, int b; " + "\n" + " /* hehe */ " + "\n" + " //hehe" + "\n" + " a = 4+2; //hehe" + "\n" + " b = a;" + "\n" + " String file = \"/tmp/log.log\"" + "\n" + " }"; System.out.println(code); System.out.println("*******************************"); CodeProcessor process = new CodeProcessor(code); String str = process.clearComment(); System.out.println(str); } } interface InputState { void handleInput(char charAt, CodeProcessor processor); } enum States implements InputState { /** * 正常状态 */ NORMAL{ @Override public void handleInput(char charAt, CodeProcessor processor) { if (charAt == backslash) { processor.currentState = COMMENT_SYMBOL; } else { processor.putChar(charAt); } } }, /** * 遇到注释符 / */ COMMENT_SYMBOL{ @Override public void handleInput(char charAt, CodeProcessor processor) { if (charAt == backslash) { processor.currentState = SINGLE_LINE_COMMENT; } else if (charAt == asterisk) { processor.currentState = MUTI_LINE_COMMENT; } else { processor.putChar('/'); processor.putChar(charAt); processor.currentState = NORMAL; } } }, /** * 进入单行注释 */ SINGLE_LINE_COMMENT{ @Override public void handleInput(char charAt, CodeProcessor processor) { if (charAt == lineBreaks) { processor.putChar(charAt); processor.currentState = NORMAL; } } }, /** * 进入多行注释 */ MUTI_LINE_COMMENT{ @Override public void handleInput(char charAt, CodeProcessor processor) { if (charAt == asterisk) { processor.currentState = MUTI_LINE_COMMENT_ENDDING; } } }, /** * 多行注释 遇到 * */ MUTI_LINE_COMMENT_ENDDING{ @Override public void handleInput(char charAt, CodeProcessor processor) { if (charAt == backslash) { processor.currentState = NORMAL; } else { processor.currentState = MUTI_LINE_COMMENT; } } }; char backslash = '/'; char asterisk = '*'; char lineBreaks = '\n'; } 输出结果:1234567891011121314151617181920212223 public static void main(String[] args) { /*hehe hehe */ /*hehe*/ int a, int b; /* hehe */ //hehe a = 4+2; //hehe b = a; String file = "/tmp/log.log" }******************************* public static void main(String[] args) { int a, int b; a = 4+2; b = a; String file = "/tmp/log.log" } 参考有限状态机]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[springboot配置https]]></title>
<url>%2F2017%2F07%2F12%2Fspringboot%E9%85%8D%E7%BD%AEhttps%2F</url>
<content type="text"><![CDATA[这两天打算在组内做个培训,关于Https方面的。于是一直在查资料,理解,顺便把今天了解的内容总结一下。 之前已经写过两篇有关ssl方面的笔记:关于加密的一点总结,ssl总结。当时的理解现在看来还有一些欠缺的地方,温故而知新嘛! 数字签名 数字签名是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术实现,用于鉴别数字信息的方法。(很空洞有木有~) 数字签名的作用:身份认证与完整性检验 下面模拟一个场景,小明给小红写一封信。 首先将摘要信息用发送者小明的私钥加密,生成数字签名,与原始信息一起传送给接收者小红。(什么是摘要信息,公钥私钥在前面的笔记中提到了,不再赘述) 小红使用小明的公钥对小明私钥加密后的摘要信息进行解密,如果解密成功,则说明接收到的内容确实由小明发出。这就完成了发送者小明的身份认证。 小红对收到的原始信息使用信息摘要算法得到其哈希值,与2中解密后的摘要信息进行比较,如果一致,证明原始信息未经篡改。保证了信息完整性(完整性检验)。 上面的过程有一个明显的疑问,就是小红是如何得到小明提供的公钥呢? 把公钥放到互联网的某个地方的一个下载地址,事先给客户去下载? 每次和客户开始通信时,服务器把公钥发给客户? 客户无法确定这个下载地址是不是服务器发布的,你凭什么就相信这个地址下载的东西就是服务器发布的而不是别人伪造的呢,万一下载到一个假的怎么办?另外要所有的客户都在通信前事先去下载公钥也很不现实。 任何人都可以自己生成一对公钥和私钥,他只要向客户发送他自己的私钥就可以冒充服务器了。 所以。数字证书出现了。 数字证书 数字证书是一种权威性的电子文档,可以由权威公正的第三方机构,即CA(例如中国各地方的CA公司)中心签发的证书,也可以由企业级CA系统进行签发。 小红无法确定自己获得的公钥是不是小明的,她想到了一个办法: 要求小明去找“证书中心”(certificate authority,简称CA),为公钥做认证。证书中心用自己的私钥,对小明的公钥和一些相关信息一起加密,生成”数字证书”(Digital Certificate)。 小明以后再给小红写信,只要在签名的同时,再附上数字证书就行了。 小红收信后,用CA的公钥解开数字证书,就可以拿到小明真实的公钥了,然后就能证明“数字签名”是否真的是小明签的。 Https演化上面小明给小红写信的过程类似于https的工作原理。首先我们知道,所谓https是在http协议的基础上加了一层ssl/tls协议: ssl/tls能起到什么作用呢? 所有信息都是加密传播,第三方无法窃听(窃听风险) 具有校验机制,一旦被篡改,通信双方会立刻发现(篡改风险) 配备身份证书,防止身份被冒充(冒充风险) 如何做到以上几点,参考之前的笔记中关于通信演化过程的内容。 https通信的过程如下图: 握手阶段服务器发送了自己的证书,确认了服务器的身份并且双方商议好了对称加密的算法好密钥,应用数据传输阶段就使用对称加密算法加密信息进行通信了。 证书链在握手阶段,服务器会把自己的证书发送到客户端(以浏览器为例),那么客户端是怎么使用这个证书确认服务器的身份的呢? 首先观察一下证书里有什么内容: 上图只截取了一部分,我们看几个比较重要的: Issuer (证书的发布机构) 指出是什么机构发布的这个证书,也就是指明这个证书是哪个公司创建的(只是创建证书,不是指证书的使用者)。 Valid from , Valid to (证书的有效期) 也就是证书的有效时间,或者说证书的使用期限。 过了有效期限,证书就会作废,不能使用了。 Public key (公钥) 公钥是用来对消息进行加密的,也就是服务器使用的公钥。 Subject (使用者) 这个证书是发布给谁的,或者说证书的所有者,一般是某个人或者某个公司名称、机构的名称、公司网站的网址等。 Thumbprint, Thumbprint algorithm (指纹以及指纹算法) 这个是用来保证证书的完整性的,也就是说确保证书没有被修改过。 其原理就是在发布证书时,发布者根据指纹算法(一个hash算法)计算整个证书的hash值(指纹)并和证书放在一起,使用者在打开证书时,自己也根据指纹算法计算一下证书的hash值(指纹),如果和刚开始的值对得上,就说明证书没有被修改过,因为证书的内容被修改后,根据证书的内容计算的出的hash值(指纹)是会变化的。 Signature algorithm (签名所使用的算法) 指纹的加密结果就是数字签名。 注意,这个指纹会使用证书发布机构的私钥用签名算法(Signature algorithm)加密后生成数字签名和证书放在一起。 Signature algorithm就是指的这个数字证书的数字签名所使用的加密算法,这样就可以使用证书发布机构的证书里面的公钥,根据这个算法对指纹进行解密。 什么是证书链呢? 上图中的Issuer’s DN即证书颁发机构,这样的证书颁发机构可能不止一个,上图表示一个3级证书链。 Owner’s DN 代表服务器发来的证书,浏览器得到这个证书之后根据Issuer’s DN获得该证书的颁发机构的证书,以此类推,最终得到根证书。比如 A->B->C->root 什么是根证书呢?一般是内嵌在操作系统中的,公认的可以信任的证书机构颁发的自签名证书。这些证书发布机构自己持有与他自己的数字证书对应的私钥,他会用这个私钥加密所有他发布的证书的指纹作为数字签名。浏览器无条件的信任根证书。 得到根证书后,拿到其公钥,然后使用这个公钥对证书链的上一级证书c依据Signature algorithm对指纹的加密结果(签名)进行解密,得到证书c的指纹。解密成功,证明c确实是由root颁发的,然后使用Thumbprint algorithm计算一下证书c的指纹,如果和解密得到的指纹相同,证明证书没有被篡改。因此,证书c是可以信任的。以此类推,证明证书A是可以信任的。也就是服务器发来的证书是可以信任的。 问题一:服务器可不可以随便下载一个认证的证书自己使用呢? 证书中包含了服务器端的公钥,浏览器会使用该公钥加密一个随机字符串要求服务器解密。服务器没有该证书的私钥,故解密失败 问题二:可不可以篡改一个已经认证的服务器为我所用? 证书中包含了证书的指纹,证书一经篡改就会被察觉 可以看到,https已经是很安全了。但是要注意,一旦信任根证书,则意味着信任了该证书所签发的所有下级证书,所以,安装根证书一定要谨慎! 使用keytool生成证书使用下面的方法生成一个二级证书链: 生成根证书密钥 12keytool -genkey -alias CA -keyalg RSA -validity 365 -keystore server.keystore -storepass 123456 -keypass 123456 keytool -list -v -keystore server.keystore 生成自签名证书密钥 12keytool -genkey -keystore server.keystore -alias server -keyalg RSA -validity 365 -storepass 123456 -keypass 123456 keytool -list -keystore server.keystore -alias server 提交申请 12keytool -certreq -keystore server.keystore -alias server -storepass 123456 -file server.certreq keytool -printcertreq -file server.certreq -v 颁发证书 12// 新升级的chrome58,要求证书中至少包含一个[Subject Alternative Name](https://support.dnsimple.com/articles/what-is-ssl-san/)keytool -gencert -keystore server.keystore -alias CA -ext san=ip:192.168.1.70 -infile server.certreq -outfile server.crt -storepass 123456 自签名证书导入keystore 12keytool -importcert -keystore server.keystore -alias server -file server.crt -storepass 123456keytool -list -keystore server.keystore -v -alias server 导出根证书 1keytool -export -alias CA -keystore server.keystore -file CA.crt -storepass 123456 目录下得到以下几个文件: 1234CA.crt //根证书server.certreq // server证书申请server.crt // server证书server.keystore //密钥库 Chrome导入根证书设置->高级->管理证书->受信任的根证书颁发机构->导入 springboot配置https在application.properties中增加以下配置:(跟上一步对应) 1234567server.ssl.key-store=server.keystoreserver.ssl.key-store-password=123456server.ssl.keyStoreType=JKSserver.ssl.keyAlias:server 开启http配置好https后发现http方式无法访问,服务器强制使用https方式,可以在 @Configuration注解的类中增加下面的配置同时开启http 1234567891011121314151617181920212223242526@Beanpublic EmbeddedServletContainerFactory servletContainer() { TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory() { @Override protected void postProcessContext(Context context) { SecurityConstraint securityConstraint = new SecurityConstraint(); securityConstraint.setUserConstraint("CONFIDENTIAL"); SecurityCollection collection = new SecurityCollection(); collection.addPattern("/*"); securityConstraint.addCollection(collection); context.addConstraint(securityConstraint); } }; tomcat.addAdditionalTomcatConnectors(initiateHttpConnector()); return tomcat;}private Connector initiateHttpConnector() { Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); connector.setScheme("http"); connector.setPort(8080); connector.setSecure(false); connector.setRedirectPort(8443); return connector; } http重定向到https1234567891011121314151617181920212223242526272829303132@Value("${server.port}") private int port;@Beanpublic EmbeddedServletContainerFactory servletContainer() { TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory() { @Override protected void postProcessContext(Context context) { SecurityConstraint securityConstraint = new SecurityConstraint(); securityConstraint.setUserConstraint("CONFIDENTIAL"); SecurityCollection collection = new SecurityCollection(); collection.addPattern("/*"); securityConstraint.addCollection(collection); context.addConstraint(securityConstraint); } }; tomcat.addAdditionalTomcatConnectors(initiateHttpConnector()); return tomcat;}private Connector initiateHttpConnector() { Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); connector.setScheme("http"); connector.setPort(getHttpPort()); connector.setSecure(false); connector.setRedirectPort(port); return connector;} private int getHttpPort() { return 80;} https配置到此完成,美滋滋:]]></content>
<categories>
<category>技术</category>
<category>加密</category>
</categories>
<tags>
<tag>加密</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux网络IO模型]]></title>
<url>%2F2017%2F07%2F10%2FLinux%E7%BD%91%E7%BB%9CIO%E6%A8%A1%E5%9E%8B%2F</url>
<content type="text"><![CDATA[学习Java-NIO在网络端的应用,就需要了解Linux的网络IO模型,才能够体会为什么需要NIO和NIO的好处在哪里。 什么是同步与异步、阻塞与非阻塞引用知乎 怎样理解阻塞非阻塞与同步异步的区别? 上面的一个回答,很生动的说明了同步异步,阻塞非阻塞之间的区别联系: 老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。 1 老张把水壶放到火上,立等水开。(同步阻塞) 老张觉得自己有点傻 2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。 3 老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大 4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞) 老张觉得自己聪明了。 所谓同步异步,只是对于水壶而言。普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。 所谓阻塞非阻塞,仅仅对于老张而言。立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。 看上面的例子,我们再来关注Linux网络IO模型,然后结合这个例子去理解。 Linux网络IO模型Unix提供了五种IO模式,分别是: 阻塞IO 非阻塞IO IO复用 信号驱动IO 异步IO 在之前的学习中我们也了解了从用户进程到底层硬件执行IO的过程,以read为例: 数据需要从硬件设备拷贝到内核空间的缓冲区,然后从内核缓冲区拷贝到用户进程空间。 我们把数据需要从硬件设备拷贝到内核空间的缓冲区这个过程类比为烧水,从内核缓冲区拷贝到用户进程空间这个过程类比为用烧好的水泡茶。 阻塞IO 阻塞IO是最常用的IO模型,我们在java中调用传统BIO(InputStream、OutpuytStream)的读写方法都是这种IO模型。 观察上图,在进程空间中调用recvfrom,其系统调用直到数据从硬件设备拷贝到内核缓冲区并且从内核拷贝到用户进程空间时才会返回,在此期间一直是阻塞的,进程在从调用recvfrom到他返回这段时间一直都是阻塞的,故称为阻塞IO。 阻塞IO对应了我们上面提到的同步阻塞。在这种IO模式下整个过程相当于使用不会响的普通水壶烧水,并且老张一直在旁边盯着,干不了其他事。水烧好后老张再去泡茶。整个过程是同步阻塞的。 在阻塞IO模式下,在同一个线程当中,我们对于多个连接,只能依次处理: 123456while true { for i in stream[] { //可能会阻塞很长时间 read until available }} 非阻塞IO 用户进程发起一个recvfrom调用的时候,如果内核缓冲区的数据还没有准备好(没有完全从硬件拷贝到内核),那么他不会阻塞用户进程,而是立刻返回一个error。用户发起一个recvfrom操作之后,不需要等待,而是马上会得到一个结果,用户可以判断这个结果,如果是一个error,表示数据还没有准备好,于是可以再次发起recvfrom操作,一旦内核数据准备好了,就可以把数据拷贝到用户进程空间,然后返回。 这种IO模型称之为非阻塞IO,整个过程可以类比为:在这种IO模式下调用recvfrom相当于使用不会响的普通水壶烧水,老张时不时跑到厨房看看水烧开了没(这个过程是同步非阻塞的),如果水烧开了,他就用烧开的水泡茶(相当于从内核copy数据到用户空间这一段,这个过程其实是同步阻塞的) 在非阻塞IO模式下,我们发现可以在一个线程中处理多个连接了: 1234567// 忙轮询while true { for i in stream[]; { // 如果数据没有准备好,就立即返回,处理下一个流 read until unavailable }} 我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。 为了避免CPU空转,可以引进了一个代理: select或poll(两者本质上相同) IO复用 Linux 提供了select/poll,进程将一个或多个fd传递给select或poll系统调用,并且阻塞在select或poll方法上。同时,kernel会侦测所有select负责的fd是否处于就绪状态,如果有任何一个fd就绪,select或poll就会返回,这个时候用户进程再调用recvfrom,将数据从内核缓冲区拷贝到用户进程空间。 这个图和blocking IO的图有些相似,但是还有一些区别。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。 12345678while true { // 在select上阻塞 select(streams[]) // 无差别轮询 for i in streams[] { read until unavailable }} 于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。 Linux还提供了一个epoll系统调用,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的(复杂度降低到了O(1))。 123456789// 事先调用epoll_ctl注册感兴趣的事件到epollfdwhile true { // 返回触发注册事件的流 active_stream[] = epoll_wait(epollfd) // 无须遍历所有的流 for i in active_stream[] { read or write till }} 信号驱动IO 首先开启套接口信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数,此系统调用立即返回。当数据准备就绪时,就为该进程生成一个sigio信号,通过信号回调通知进程。进程调用recvfrom读取数据,将数据从内核缓冲区拷贝到用户进程空间。 上面的过程可以类比为:老张使用会响的水壶烧水,然后就去客厅看电视了。水烧好后水壶响起来(这个过程是异步非阻塞的),老张再来厨房用烧好的水泡茶(这个过程是同步阻塞的)。 异步IO 用户进程发起recvfrom操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它收到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。 这种IO模式与信号驱动IO的区别在于:信号驱动IO由内核通知我们什么时候可以开始一个IO操作,异步IO则由内核告诉我们IO操作何时完成。 异步IO模式可以类比为:在这种IO模式下整个过程相当于使用会响的水壶烧水,并且,这个水壶更加智能,水烧好后可以自动泡茶,然后发出声响通知老张。老张把水放到火上就去客厅看电视了,水烧好并且茶叶泡好之后,水壶发出声响通知老张。 参考Linux IO模式及 select、poll、epoll详解 怎样理解阻塞非阻塞与同步异步的区别? epoll 或者 kqueue 的原理是什么? 《Netty权威指南》 电子工业出版社]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
<tag>java</tag>
<tag>NIO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java-NIO-Channel]]></title>
<url>%2F2017%2F07%2F08%2FJava-NIO-Channel%2F</url>
<content type="text"><![CDATA[Java NIO 学习第三篇–Channel Channel通道(Channel)的作用有类似于流(Stream),用于传输文件或者网络上的数据。 上图中,箭头就相当于通道。一个不是很准确的例子:把通道想象成铁轨,缓冲区则是列车,铁轨的起始与终点则可以是socket,文件系统和我们的程序。假如当我们在代码中要写入数据到一份文件的时候,我们先把列车(缓冲区)装满,然后把列车(缓冲区)放置到铁轨上(通道),数据就被传递到通道的另一端,文件系统。读取文件则相反,文件的内容被装到列车上,传递到程序这一侧,然后我们在代码中就可以读取这个列车中的内容(读取缓冲区)。 通道与传统的流还是有一些区别的: 通道可以同时支持读写(不是一定支持),而流只支持单方向的操作,比如输入流只能读,输出流只能写。 通道可以支持异步的读或写,而流是同步的。 通道的读取或写入是通过缓冲区来进行的,而流则写入或返回字节。 FileChannel通道大致上可以分为两类:文件通道和socket通道。看一下文件通道: 文件通道可以由以下几个方法获得: 12345678910RandomAccessFile file = new RandomAccessFile(new File(fileName), "rw");FileChannel channel = file.getChannel();FileInputStream stream = new FileInputStream(new File(fileName));FileChannel channel = stream.getChannel();FileOutputStream stream = new FileOutputStream(new File(fileName));FileChannel channel = stream.getChannel();FileChannel channel = FileChannel.open(Paths.get(fileName)); FileChannel 类结构: 可见FileChannel实现了读写接口、聚集、发散接口,以及文件锁功能。下面会提到。 看一下FileChannel的基本方法: 123456789101112public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // 这里仅列出部分API public abstract long position() public abstract void position (long newPosition) public abstract int read (ByteBuffer dst) public abstract int read (ByteBuffer dst, long position) public abstract int write (ByteBuffer src) public abstract int write (ByteBuffer src, long position) public abstract long size() public abstract void truncate (long size) public abstract void force (boolean metaData)} 在通道出现之前,底层的文件操作都是通过RandomAccessFile类的方法来实现的。FileChannel模拟同样的 I/O 服务,因此它们的API自然也是很相似的。 上图是FileChannel、RandomAccessFile 和 POSIX I/O system calls 三者在方法上的对应关系。 POSIX接口我们在上一篇文章中也略有提及,他是一个系统级别的接口。下面看一下这几个接口,主要也是和上一篇文章文件描述符的介绍做一个呼应。 position()和position(long newPosition) position()返回当前文件的position值,position(long newPosition)将当前position设置为指定值。当字节被read()或write()方法传输时,文件position会自动更新。 position的含义与Buffer类中的position含义相似,都是指向下一个字节读取的位置。 回想一下介绍文件描述符的文章当中提到,当进程打开一个文件时,内核就会创建一个新的file对象,这个file对象有一个字段loff_t f_pos描述了文件的当前位置,position相当于loff_t f_pos的映射。由此可知,如果是使用同一文件描述符读取文件,那么他们的position是相互影响的: 12345RandomAccessFile file = new RandomAccessFile(new File(fileName), "rw");FileChannel channel = file.getChannel();System.out.println("position: " + channel.position());file.seek(30);System.out.println("position: " + channel.position()); 打印如下: 12position: 0position: 30 这是因为,file与channel使用了同一个文件描述符。如果新建另一个相同文件的通道,那么他们之间的position不会相互影响,因为使用了不同的文件描述符,指向不同的file对象。 truncate(long size) 当需要减少一个文件的size时,truncate()方法会砍掉指定的size值之外的所有数据。这个方法要求通道具有写权限。 如果当前size大于给定size,超出给定size的所有字节都会被删除。如果提供的新size值大于或等于当前的文件size值,该文件不会被修改。 12345678RandomAccessFile file = new RandomAccessFile(new File(fileName), "rw");FileChannel channel = file.getChannel();System.out.println("size: " + channel.size());System.out.println("position: " + channel.position());System.out.println("trucate: 90");channel.truncate(90);System.out.println("size: " + channel.size());System.out.println("position: " + channel.position()); 打印如下: 12345size: 100position: 0trucate: 90size: 90position: 0 force(boolean metaData) force()方法告诉通道强制将全部待定的修改都应用到磁盘的文件上。 如果文件位于一个本地文件系统,那么一旦force()方法返回,即可保证从通道被创建(或上次调用force())时起的对文件所做的全部修改已经被写入到磁盘。但是,如果文件位于一个远程的文件系统,如NFS上,那么不能保证待定修改一定能同步到永久存储器。 force()方法的布尔型参数表示在方法返回值前文件的元数据(metadata)是否也要被同步更新到磁盘。元数据指文件所有者、访问权限、最后一次修改时间等信息。 FileChannel对象是线程安全的。如果有一个线程已经在执行会影响通道位置或文件大小的操作,那么其他尝试进行此类操作之一的线程必须等待。 ReadableByteChannel、WritableByteChannel通道可以是单向或者双向的。 12345678910public interface ReadableByteChannel extends Channel{ public int read (ByteBuffer dst) throws IOException;}public interface WritableByteChannel extends Channel{ public int write (ByteBuffer src) throws IOException;}public interface ByteChannel extends ReadableByteChannel, WritableByteChannel{} 实现ReadableByteChannel或WritableByteChannel其中之一的channel是单向的,只可以读或者写。如果一个类同时实现了这两种接口,那么他就具备了双向传输的能力。 java为我们提供了一个接口ByteChannel,同时继承了上述两个接口。所以,实现了ByteChannel接口的类可以读,也可以写。 在FlieChannel这一节中我们知道,文件在不同的方式下以不同的权限打开。比如FileInputStream.getChannel()方法返回一个FileChannel实例,FileChannel是个抽象类,间接的实现了ByteChannel接口,也就意味着提供了read和write接口。但是FileInputStream.getChannel()方法返回的FileChannel实际上是只读的,很简单,因为FileInputStream本身就是个输入流啊~在这样一个通道上调用write方法将抛出NonWritableChannelException异常,因为FileInputStream对象总是以read-only的权限打开通道。看一下代码: FileInputStream.getChannel() 12345678910111213141516public FileChannel getChannel() { synchronized (this) { if (channel == null) { // 第三个参数指定通道是否可读,第四个参数指定通道是否可写 channel = FileChannelImpl.open(fd, path, true, false, this); /* * Increment fd's use count. Invoking the channel's close() * method will result in decrementing the use count set for * the channel. */ fd.incrementAndGetUseCount(); } return channel; }} 同样的,FileOutputStream.getChannel()返回的通道是不可读的。 InterruptibleChannelInterruptibleChannel是一个标记接口,当被通道使用时可以标示该通道是可以中断的。 如果一个线程在一个通道上处于阻塞状态时被中断(另外一个线程调用该线程的interrupt()方法设置中断状态),那么该通道将被关闭,该被阻塞线程也会产生一个ClosedByInterruptException异常。也就是说,假如一个线程的interrupt status被设置并且该线程试图访问一个通道,那么这个通道将立即被关闭,同时将抛出相同的ClosedByInterruptException异常。 在java任务取消中提到了,传统的java io 在读写时阻塞,是不会响应中断的。解决办法就是使用InterruptibleChannel,在线程被中断时可以关闭通道并返回。 可中断的通道也是可以异步关闭。实现InterruptibleChannel接口的通道可以在任何时候被关闭,即使有另一个被阻塞的线程在等待该通道上的一个I/O操作完成。当一个通道被关闭时,休眠在该通道上的所有线程都将被唤醒并接收到一个AsynchronousCloseException异常。接着通道就被关闭并将不再可用。 Scatter/Gather发散(Scatter)读取是将数据读入多个缓冲区(缓冲区数组)的操作。通道将数据依次填满到每个缓冲区当中。 汇聚(Gather)写出是将多个缓冲区(缓冲区数组)数据依次写入到通道的操作。 在FileChannel中提到的两个接口,提供了发散汇聚的功能: 12345678public interface ScatteringByteChannel extends ReadableByteChannel{ public long read (ByteBuffer[] dsts) throws IOException; public long read (ByteBuffer[] dsts, int offset, int length) throws IOException;}public interface GatheringByteChannel extends WritableByteChannel{ public long write(ByteBuffer[] srcs) throws IOException; public long write(ByteBuffer[] srcs, int offset, int length) throws IOException;} 发散汇聚在某些场景下是很有用的,比如有一个消息协议格式分为head和body(比如http协议),我们在接收这样一个消息的时候,通常的做法是把数据一下子都读过来,然后解析他。使用通道的发散功能会使这个过程变得简单: 12345678// head数据128字节ByteBuffer header = ByteBuffer.allocate(128);// body数据1024字节ByteBuffer body = ByteBuffer.allocate(1024);ByteBuffer[] bufferArray = { header, body };channel.read(bufferArray); 通道会依次填满这个buffer数组的每个buffer,如果一个buffer满了,就移动到下一个buffer。很自然的把head和body的数据分开了,但是要注意head和body的数据长度必须是固定的,因为channel只有填满一个buffer之后才会移动到下一个buffer。 FileLock摘抄一段oracle官网上FileLock的介绍吧,感觉说的挺清楚了。(因为懒,就不翻译了,读起来不是很费劲) A token representing a lock on a region of a file.A file-lock object is created each time a lock is acquired on a file via one of the lock or tryLock methods of the FileChannel class, or the lock or tryLock methods of the AsynchronousFileChannel class. A file-lock object is initially valid. It remains valid until the lock is released by invoking the release method, by closing the channel that was used to acquire it, or by the termination of the Java virtual machine, whichever comes first. The validity of a lock may be tested by invoking its >isValid method. A file lock is either exclusive or shared. A shared lock prevents other concurrently-running programs from acquiring an overlapping exclusive lock, but does allow them to acquire overlapping shared locks. An exclusive lock prevents other programs from acquiring an overlapping lock of either type. >Once it is released, a lock has no further effect on the locks that may be acquired by other programs. Whether a lock is exclusive or shared may be determined by invoking its isShared method. Some platforms do not support shared locks, in which case a request for a shared lock is automatically converted into a request for an exclusive lock. The locks held on a particular file by a single Java virtual machine do not overlap. The overlaps method may be used to test whether a candidate lock range overlaps an existing lock. A file-lock object records the file channel upon whose file the lock is held, the type and validity of the lock, and the position and size of the locked region. Only the validity of a lock is subject to change over time; all other aspects of a lock’s state are immutable. File locks are held on behalf of the entire Java virtual machine. They are not suitable for controlling access to a file by multiple threads within the same virtual machine. File-lock objects are safe for use by multiple concurrent threads. Platform dependencies This file-locking API is intended to map directly to the native locking facility of the underlying operating system. Thus the locks held on a file should be visible to all programs that have access to the file, regardless of the language in which those programs are written. Whether or not a lock actually prevents another program from accessing the content of the locked region is system-dependent and therefore unspecified. The native file-locking facilities of some systems are merely advisory, meaning that programs must cooperatively observe a known locking protocol in >order to guarantee data integrity. On other systems native file locks are mandatory, meaning that if one program locks a region of a file then other programs are actually prevented from accessing that region in a way that would violate the lock. On yet other systems, whether native file locks are >advisory or mandatory is configurable on a per-file basis. To ensure consistent and correct behavior across platforms, it is strongly recommended that the locks provided by this API be used as if they were advisory locks. On some systems, acquiring a mandatory lock on a region of a file prevents that region from being mapped into memory, and vice versa. Programs that combine locking and mapping should be prepared for this combination to fail. On some systems, closing a channel releases all locks held by the Java virtual machine on the underlying file regardless of whether the locks were acquired via that channel or via another channel open on the same file. It is strongly recommended that, within a program, a unique channel be used to >acquire all locks on any given file. Some network filesystems permit file locking to be used with memory-mapped files only when the locked regions are page-aligned and a whole multiple of the underlying hardware’s page size. Some network filesystems do not implement file locks on regions that extend past a certain position, often 230 >or 231. In general, great care should be taken when locking files that reside on network filesystems. FileLock可以由以下几个方法获得: 1234567public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {// 这里仅列出部分API public final FileLock lock() public abstract FileLock lock (long position, long size, boolean shared) public final FileLock tryLock() public abstract FileLock tryLock (long position, long size, boolean shared)} 其中,lock是阻塞的,tryLock是非阻塞的。position和size决定了锁定的区域,shared决定了文件锁是共享的还是独占的。 不带参数的lock方法等价于fileChannel.lock(0L, Long.MAX_VALUE, false),tryLock亦然。 lock方法是响应中断的,当线程被中断时方法抛出FileLockInterruptionException异常。如果通道被另外一个线程关闭,该暂停线程将恢复并产生一个 AsynchronousCloseException异常。 上面还提到了,文件锁是针对于进程级别的。如果有多个进程同时对一个文件锁定,并且其中有独占锁的话,这些锁的申请会被串行化。 如果是同一个进程(Jvm实例)的多个线程同时请求同一个文件区域的lock的话,会抛出OverlappingFileLockException异常。 Channel-to-ChannelFileChannel提供了接口,用于通道和通道之间的直接传输。 12345public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // 这里仅列出部分API public abstract long transferTo (long position, long count, WritableByteChannel target) public abstract long transferFrom (ReadableByteChannel src, long position, long count)} 只有FileChannel类有这两个方法,因此Channel-to-Channel传输中通道之一必须是FileChannel。不能在socket通道之间直接传输数据,不过socket通道实现WritableByteChannel和ReadableByteChannel接口,因此文件的内容可以用transferTo()方法传输给一个socket通道,或者也可以用transferFrom()方法将数据从一个socket通道直接读取到一个文件中。 直接的通道传输不会更新与某个FileChannel关联的position值。请求的数据传输将从position参数指定的位置开始,传输的字节数不超过count参数的值。实际传输的字节数会由方法返回。 直接通道传输的另一端如果是socket通道并且处于非阻塞模式的话,数据的传输将具有不确定性。比如,transferFrom从socket通道读取数据,如果socket中的数据尚未准备好,那么方法将直接返回。 例子: 12345678910111213141516171819202122RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");FileChannel fromChannel = fromFile.getChannel();RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");FileChannel toChannel = toFile.getChannel();long position = 0;long count = fromChannel.size();toChannel.transferFrom(fromChannel, position, count);RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");FileChannel fromChannel = fromFile.getChannel();RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");FileChannel toChannel = toFile.getChannel();long position = 0;long count = fromChannel.size();fromChannel.transferTo(position, count, toChannel); 参考Java nio入门教程详解]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>NIO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java文件描述符]]></title>
<url>%2F2017%2F07%2F07%2Fjava%E6%96%87%E4%BB%B6%E6%8F%8F%E8%BF%B0%E7%AC%A6%2F</url>
<content type="text"><![CDATA[文件描述符在Linux中,进程是通过文件描述符(file descriptors,简称fd)而不是文件名来访问文件的,文件描述符实际上是一个整数。 内核中,对应于每个进程都有一个文件描述符表,表示这个进程打开的所有文件。文件描述符就是这个表的索引。 文件描述表中每一项都是一个指针,指向一个用于描述打开的文件的数据块———file对象,file对象中描述了文件的打开模式,读写位置等重要信息。 当进程打开一个文件时,内核就会创建一个新的file对象。因此,我们在进程中使用多线程打开同一个文件,每个线程会有各自的文件描述符,每个线程也会有保存自己的读取位置,互不影响。 需要注意的是,file对象不是专属于某个进程的,不同进程的文件描述符表中的指针可以指向相同的file对象,从而共享这个打开的文件。比如,如果在调用fork之前父进程已经打开文件,则fork后子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件集合,因此共享相同的文件位置。 file对象有引用计数,记录了引用这个对象的文件描述符个数,只有当引用计数为0时,内核才销毁file对象,因此某个进程关闭文件,不影响与之共享同一个file对象的进程。 每个file结构体都指向一个file_operations结构体,这个结构体的成员都是函数指针,指向实现各种文件操作的内核函数。比如在用户程序中read一个文件描述符,read通过系统调用进入内核,然后找到这个文件描述符所指向的file结构体,找到file结构体所指向的file_operations结构体,调用它的read成员所指向的内核函数以完成用户请求。在用户程序中调用lseek、read、write、ioctl、open等函数,最终都由内核调用file_operations的各成员所指向的内核函数完成用户请求。file_operations结构体中的release成员用于完成用户程序的close请求,之所以叫release而不叫close是因为它不一定真的关闭文件,而是减少引用计数,只有引用计数减到0才关闭文件。 file对象中包含一个指针,指向dentry对象。“dentry”是directory entry(目录项)的缩写,dentry对象代表一个独立的文件路径,如果一个文件路径被打开多次,那么会建立多个file对象,但它们都指向同一个dentry对象。为了减少读盘次数,内核缓存了目录的树状结构,称为dentry cache,其中每个节点是一个dentry结构体。 每个dentry结构体都有一个指针指向inode结构体。inode结构体保存着从磁盘inode读上来的信息。在上图的例子中,有两个dentry,分别表示/home/akaedu/a和/home/akaedu/b,它们都指向同一个inode,说明这两个文件互为硬链接。inode结构体中保存着从磁盘分区的inode读上来信息,例如所有者、文件大小、文件类型和权限位等。 每个进程刚刚启动的时候,文件描述符0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。 java中的FileDescriptor在java中,有着与文件描述符对应的一个类对象:FileDescriptor。我们看一下FileDescriptor与Channel的关系: FileInputStream.getChannel(): 123456789101112131415public FileChannel getChannel() { synchronized (this) { if (channel == null) { channel = FileChannelImpl.open(fd, path, true, false, this); /* * Increment fd's use count. Invoking the channel's close() * method will result in decrementing the use count set for * the channel. */ fd.incrementAndGetUseCount(); } return channel; }} 其中的FileChannelImpl.open(fd, path, true, false, this)参数fd就是FileDescriptor实例。 看一下他是怎么产生的: 123456789101112131415161718192021public FileInputStream(File file) throws FileNotFoundException { String name = (file != null ? file.getPath() : null); SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkRead(name); } if (name == null) { throw new NullPointerException(); } if (file.isInvalid()) { throw new FileNotFoundException("Invalid file path"); } fd = new FileDescriptor(); fd.incrementAndGetUseCount(); this.path = name; open(name);}static { initIDs();} 注意到initIDs()这个静态方法: 123456jfieldID fis_fd; /* id for jobject 'fd' in java.io.FileInputStream */JNIEXPORT void JNICALLJava_java_io_FileInputStream_initIDs(JNIEnv *env, jclass fdClass) { fis_fd = (*env)->GetFieldID(env, fdClass, "fd", "Ljava/io/FileDescriptor;");} 在FileInputStream类加载阶段,fis_fd就被初始化了,fid_fd相当于是FileInputStream.fd字段的一个内存偏移量,便于在必要时操作内存给它赋值。 看一下FileDescriptor的实例化过程: 123456789public /**/ FileDescriptor() { fd = -1; handle = -1; useCount = new AtomicInteger();}static { initIDs();} FileDescriptor也有一个initIDs,他和FileInputStream.initIDs的方法类似,把设置IO_fd_fdID为FileDescriptor.fd字段的内存偏移量。 123456789/* field id for jint 'fd' in java.io.FileDescriptor */jfieldID IO_fd_fdID;/************************************************************** * static methods to store field ID's in initializers */JNIEXPORT void JNICALLJava_java_io_FileDescriptor_initIDs(JNIEnv *env, jclass fdClass) { IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "fd", "I");} 接下来再看FileInputStream构造函数中的open(name)方法,字面上看,这个方法打开了一个文件,他也是一个本地方法,open方法直接调用了fileOpen方法,fileOpen方法如下: 12345678910111213141516171819void fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags){ WITH_PLATFORM_STRING(env, path, ps) { FD fd;#if defined(__linux__) || defined(_ALLBSD_SOURCE) /* Remove trailing slashes, since the kernel won't */ char *p = (char *)ps + strlen(ps) - 1; while ((p > ps) && (*p == '/')) *p-- = '\0';#endif // 打开一个文件并获取到文件描述符 fd = handleOpen(ps, flags, 0666); if (fd != -1) { SET_FD(this, fd, fid); } else { throwFileNotFoundException(env, path); } } END_PLATFORM_STRING(env, ps);} 其中的handleOpen函数打开了一个文件描述符,相当于和文件建立了联系,并且将返回的文件描述符描述符赋值给了局部变量fd,然后调用了SET_FD宏: 123#define SET_FD(this, fd, fid) \ if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \ (*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd)) 注意到IO_fd_fdID,他是FileDescriptor.fd字段的内存偏移量。这个方法相当于设置FileDescriptor.fd的值等于文件描述符fd。 需要注意的是,FileDescriptor有两个字段:handle和fd,上面的代码表示我们只设置了fd字段为文件描述符,没有提到handle字段,这是因为: 在 win32 的实现中将 创建好的 文件句柄 设置到 handle 字段,在 linux 版本中则使用的是 FileDescriptor 的 fd 字段。 由此,可知 handle 和 fd 是共存的但并不同时在使用,在 win32 平台上使用 handle 字段,在 linux 平台上使用 fd 字段。 所以,FileInputStream打开文件的过程总结如下: 创建 FileDescriptor 对象 每一个 FileInputStream 有一个 FileDescriptor,代表这个流底层的文件的fd 调用 native 方法 open, 打开文件 内部调用 handleOpen 打开文件,返回文件描述符 fd 初始化 FileDescriptor 对象 将 文件描述符 fd 设置到,FileDescriptor 对象的 fd 中 再谈java文件读取在java-NIO-Buffer这篇文章中我们提到了FileInputStream.read方法,再来回顾一下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859JNIEXPORT jint JNICALL Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this, jbyteArray bytes, jint off, jint len) {//除了前两个参数,后三个就是readBytes方法传递进来的,字节数组、起始位置、长度三个参数 return readBytes(env, this, bytes, off, len, fis_fd); }jintreadBytes(JNIEnv *env, jobject this, jbyteArray bytes, jint off, jint len, jfieldID fid){ jint nread; char stackBuf[BUF_SIZE]; char *buf = NULL; FD fd; if (IS_NULL(bytes)) { JNU_ThrowNullPointerException(env, NULL); return -1; } if (outOfBounds(env, off, len, bytes)) { JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL); return -1; } if (len == 0) { return 0; } else if (len > BUF_SIZE) { buf = malloc(len);// buf的分配 if (buf == NULL) { JNU_ThrowOutOfMemoryError(env, NULL); return 0; } } else { buf = stackBuf; } fd = GET_FD(this, fid); if (fd == -1) { JNU_ThrowIOException(env, "Stream Closed"); nread = -1; } else { nread = IO_Read(fd, buf, len);// buf是使用malloc分配的直接缓冲区,也就是堆外内存 if (nread > 0) { (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);// 将直接缓冲区的内容copy到bytes数组中 } else if (nread == JVM_IO_ERR) { JNU_ThrowIOExceptionWithLastError(env, "Read error"); } else if (nread == JVM_IO_INTR) { JNU_ThrowByName(env, "java/io/InterruptedIOException", NULL); } else { /* EOF */ nread = -1; } } if (buf != stackBuf) { free(buf); } return nread;} 上述代码中的fis_fd是不是很眼熟?他就是FileInputStream.fd字段的内存偏移量。注意到fd = GET_FD(this, fid);这个方法,获取到其对应的文件描述符,然后使用该文件描述符读取文件内容,填充缓冲区。由此可见,java底层读取文件都是通过文件描述符来进行的。比如: 文章开始提到每个进程刚刚启动的时候,文件描述符0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3,FileDescriptor中的fd为0,1,2时也表示同样的意义。 12FileOutputStream fileOutputStream = new FileOutputStream(FileDescriptor.out);fileOutputStream.write('hello world');// 控制台打印 hello world,因为fileOutputStream使用了标准输出的文件描述符 参考linux 文件描述符表 打开文件表 inode vnode linux中文件描述符fd和文件指针flip的理解 JNI探秘–FileDescriptor、FileInputStream 解惑]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>NIO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java-NIO-MappedByteBuffer]]></title>
<url>%2F2017%2F07%2F04%2FJava-NIO-MappedByteBuffer%2F</url>
<content type="text"><![CDATA[java nio 学习第二篇–内存映射文件 内核空间与用户空间Kernel space 是 Linux 内核的运行空间,User space 是用户程序的运行空间。为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。 内核空间中存放的是内核代码和数据。内核空间是操作系统所在区域。内核代码有特别的权力:它能与设备控制器通讯,控制着用户区域进程的运行状态,等等。最重要的是,所有 I/O 都直接或间接通过内核空间。 用户空间是常规进程所在区域,进程的用户空间中存放的是用户程序的代码和数据。 Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。 当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行,CPU可执行任何指令。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。 32位Linux的虚拟地址空间为0~4G。Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址 0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间)。每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。 12345str = "my string" // 用户空间x = x + 2file.write(str) // 切换到内核空间 y = x + 4 // 切换回用户空间 上面代码中,第一行和第二行都是简单的赋值运算,在 User space 执行。第三行需要写入文件,就要切换到 Kernel space,因为用户不能直接写文件,必须通过内核安排。第四行又是赋值运算,就切换回 User space。 分页存储操作系统在运行程序时,需要为每一个进程分配内存。比如A进程需要200m,B进程需要300m,c进程需要100m。那么操作系统应该如何为他们分配这些内存呢? 一种想法是直接分配连续的内存。操作系统维护一个内存列表,每次申请内存时就去这个列表中寻找合适的连续内存块,分配给用户进程。这样会带来一个问题,那就是内存碎片化。由于程序申请内存的大小是不规律的,在经过多次分配之后,内存空间就会变得零碎,产生很多不连续的小的内存碎片,这些碎片无法被程序使用(因为碎片化的内存不是连续的,也不够大)。 可以通过‘紧凑’的方法将这些碎片拼接成可用的大块内存空间,但是必须要付出很大的开销。因此产生了离散化的分配方式:允许直接将一个紧凑直接分散的装入到许多不相邻的内存块当中。就可以充分的利用内存空间。 离散分配其中之一的分配方式就是分页:将用户程序的地址空间分为若干个固定大小的区域,称为页。比如,每个页为1kb。相应的将内存空间也分为若干个物理块,和页的大小相同。这样就可以将用户程序的任一页放入任一物理块当中,实现了离散分配。 在分页系统中,允许将进程的各个页离散的存储在内存的任一物理块当中,为了保证进程能够正确运行,即能够在内存中找到每个页面所对应的物理块,系统为每一个进程建立了一张页面映像表,简称页表。在进程地址空间内的所有页,依次在页表中有一页表项,其中记录了相应页在内存中的物理块号。 在配置了页表之后,进程执行时,通过查找该表,即可找到每页在内存中的物理块号。可见,页表的作用是实现从页号到物理块号的地址映射。 虚拟内存所有现代操作系统都使用虚拟内存。虚拟内存意为使用虚假(或虚拟)地址取代物理(硬件RAM)内存地址。这样做好处颇多,总结起来可分为两大类: 一个以上的虚拟地址可指向同一个物理内存地址。 虚拟内存空间可大于实际可用的硬件内存。 那么,这是如何做到的呢? 我们会同时运行多个进程,而每个进程占用的内存大小不固定,但是这些进程所需要的内存大小加起来却会超过我们实际的物理内存(比如4g内存),用户感觉到的内存容量会比实际内存容量大的多。这是因为: 应用程序在运行之前没有必要将之全部装入内存,而仅需将那些当前要运行的少数页面装入内存便可运行,其余部分暂留在磁盘上。程序在运行时,如果他要访问的页已经调入内存,便可继续执行下去;但如果程序所要访问的页面尚未调入内存(缺页),便发出缺页请求(页错误),此时操作系统将利用请求调页功能将他们调入内存,以便程序能够继续执行下去。如果此时内存已满,无法再装入新的页,操作系统还需再利用页的置换功能,将内存中暂时不用的页调到磁盘上,腾出足够的内存空间后,再将要访问的页调入内存,使程序继续执行下去。这样,可以使一个或多个大的用户程序在较小的内存空间中运行。 联想一下Linux系统在硬盘分区时需要让我们选择一个swap分区,结合上面的知识,可知这个swap分区就是上面置换时提到的磁盘。摘抄一段百度百科对swap的定义: Swap分区在系统的物理内存不够用的时候,把硬盘空间中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap分区中,等到那些程序要运行时,再从Swap分区中恢复保存的数据到内存中。 因此,虚拟内存的实现利用了上面提到的分页存储的方法,同时,需要存储系统需要增加页面置换和页面调度功能。 我们知道页表的基本作用就是将用户地址空间中的逻辑地址映射为内存空间中的物理地址,为了满足页面的换进换出功能,在页表中增加几个字段: 对上面字段的解释: 状态位P: 由于在请求分页系统中,只将应用程序的一部分调入内存,还有一部分在磁盘上,所以需要在页表中增加一个存在位字段,指示该夜是否已调入内存,供应用程序参考。 访问字段A:用于记录本页在一段时间内的访问次数,或已有多长时间未被访问,提供给置换算法在选择换出页面时参考。 修改位M:标识该页在调入内存后是否被修改过。由于内存中的每一页都在外存上保留一个副本,因此,在置换该页时,若未被修改,就不需要将该页再写回到外存,减少磁盘交互的次数;若已被修改,则必须将该页重写到外存上,保证外存中所保留的副本是最新的。 外存地址:指出该页在外存上的地址,通常是物理块号,供调入该页时参考。 回想一下,在前面 **内核空间与用户空间 这一节当中,提到了 Linux的虚拟地址空间为0~4G,从0x00000000到0xFFFFFFFF。这里的虚拟地址,经过MMU的转换,可以映射为物理页号。每一个进程都维护自己的虚拟地址,从虚拟地址中分配内存,实际上底层将这些虚拟地址,通过查询页表映射到物理块号,然后进行相应的置换或者读入。实际上,是所有的进程共享这些物理内存,此时的物理内存相当于一个池(联想 线程池?)。 IO原理有了上面的基础,我们再来看一下操作系统中的IO: 进程使用read()系统调用,要求其缓冲区被填满。内核随即向磁盘控制硬件发出命令,要求其从磁盘读取数据。磁盘控制器把数据直接写入内核内存缓冲区,这一步通过 DMA 完成,无需主CPU协助。一旦磁盘控制器把缓冲区装满,内核即把数据从内核空间的临时缓冲区拷贝到进程执行read()调用时指定的缓冲区。 我们可能会觉得,把数据从内核空间拷贝到用户空间似乎有些多余。为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢?这样做有几个问题。首先,硬件通常不能直接访问用户空间。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作,因此充当着中间人的角色。 采用分页技术的操作系统执行 I/O 的全过程可总结为以下几步: 确定请求的数据分布在文件系统的哪些页(磁盘扇区组)。磁盘上的文件内容和元数据可能跨越多个文件系统页,而且这些页可能也不连续。 在内核空间分配足够数量的内存页,以容纳得到确定的文件系统页。 在内存页与磁盘上的文件系统页之间建立映射。 为每一个内存页产生页错误。 虚拟内存系统俘获页错误,安排页面调入,从磁盘上读取页内容,使页有效。 一旦页面调入操作完成,文件系统即对原始数据进行解析,取得所需文件内容或属性信息。 内存映射文件传统的文件 I/O 是通过用户进程发布read()和write()系统调用来传输数据的。比如FileInputStream.read(byte b[]),实际上是调用了read()系统调用完成数据的读取。回想上一篇文章,FileInputStream.read(byte b[])会造成几次数据拷贝呢? 从磁盘到内核缓冲区的拷贝 内核缓冲区到JVM进程直接缓冲区的拷贝 JVM直接缓冲区到FileInputStream.read(byte b[])中byte数组b指向的堆内存的拷贝 可见,传统的IO要经历至少三次数据拷贝才可以把数据读出来,即使是使用直接缓冲区DirectBuffer,也需要至少两次拷贝过程。 我们知道,设备控制器不能通过 DMA 直接存储到用户空间,但是利用虚拟内存一个以上的虚拟地址可指向同一个物理内存地址这个特点,则可以把内核空间地址与用户空间的虚拟地址映射到同一个物理地址,这样,DMA 硬件(只能访问物理内存地址)就可以填充对内核与用户空间进程同时可见的缓冲区。 这样的话,就省去了内核与用户空间的往来拷贝,但前提条件是,内核与用户缓冲区必须使用相同的页对齐,缓冲区的大小还必须是磁盘控制器块大小的倍数。 内存映射 I/O 使用文件系统建立从用户空间直到可用文件系统页的虚拟内存映射。这样做有几个好处: 用户进程把文件数据当作内存,所以无需发布read()或write()系统调用。 当用户进程碰触到映射内存空间,页错误会自动产生,从而将文件数据从磁盘读进内存。如果用户修改了映射内存空间,相关页会自动标记为脏,随后刷新到磁盘,文件得到更新。 操作系统的虚拟内存子系统会对页进行智能高速缓存,自动根据系统负载进行内存管理。 数据总是按页对齐的,无需执行缓冲区拷贝。 大型文件使用映射,无需耗费大量内存,即可进行数据拷贝。 MappedByteBuffer了解了上面的内容,我们知道在操作系统和硬件层面实际上是为我们提供了内存映射文件这样的机制的。在java1.4之后,java也提供了对应的接口,可以让我们利用操作系统这一特性,提高文件读写性能,那就是MappedByteBuffer。 MappedByteBuffer继承自ByteBuffer,MappedByteBuffer被abstract修饰,所以他不能被实例化。我们可以调用FileChannel.map()方法获取一个MappedByteBuffer: 123FileInputStream inputStream = new FileInputStream(file);FileChannel channel = inputStream.getChannel();MappedByteBuffer map = channel.map(MapMode.READ_WRITE, 0, file.length()); 这个MappedByteBuffer实际上是其子类DirectByteBuffer实例的引用。也就是说,我们获得的MappedByteBuffer实际上是DirectBuffer类型的缓冲区。也就是说,使用MappedByteBuffer并不会消耗Java虚拟机内存堆。 12345678910public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // 这里仅列出部分API public abstract MappedByteBuffer map(MapMode mode, long position, long size) public static class MapMode { public static final MapMode READ_ONLY public static final MapMode READ_WRITE public static final MapMode PRIVATE }} 我们可以创建一个MappedByteBuffer来代表一个文件中字节的某个子范围。例如,要映射100到299(包含299)位置的字节,可以使用下面的代码:buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 100, 200); 如果要映射整个文件则使用:buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); 文件映射可以是可写的或只读的。前两种映射模式MapMode.READ_ONLY和MapMode.READ_WRITE意义是很明显的,它们表示希望获取的映射只读还是允许修改映射的文件。请求的映射模式将受被调用map()方法的FileChannel对象的访问权限所限制。如果通道是以只读的权限打开的却请求MapMode.READ_WRITE模式,那么map()方法会抛出一个NonWritableChannelException异常;如果在一个没有读权限的通道上请求MapMode.READ_ONLY映射模式,那么将产生NonReadableChannelException异常。 第三种模式MapMode.PRIVATE表示想要一个写时拷贝(copy-on-write)的映射。这意味着通过put()方法所做的任何修改都会导致产生一个私有的数据副本并且该副本中的数据只有MappedByteBuffer实例可以看到。该过程不会对底层文件做任何修改。尽管写时拷贝的映射可以防止底层文件被修改,但也必须以read/write权限来打开文件以建立MapMode.PRIVATE映射。只有这样,返回的MappedByteBuffer对象才能允许使用put()方法。 一个映射一旦建立之后将保持有效,直到MappedByteBuffer对象被施以垃圾收集动作为止。关闭相关联的FileChannel不会破坏映射,只有丢弃缓冲区对象本身才会破坏该映射。 MappedByteBuffer主要用在对大文件的读写或对实时性要求比较高的程序当中。 For most operating systems, mapping a file into memory is more expensive than reading or writing a few tens of kilobytes of data via the usual read and write methods. From the standpoint of performance it is generally only worth mapping relatively large files into memory. 参考java doc FileChannel.map 参考Java nio入门教程详解(三) Java nio入门教程详解(二十一) 《计算机操作系统(第四版)》 西安电子科技大学出版社]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>NIO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java-NIO-Buffer]]></title>
<url>%2F2017%2F06%2F28%2Fjava-NIO-Buffer%2F</url>
<content type="text"><![CDATA[最近在看java nio方面的知识,打算写几篇博客总结一下,就从Buffer开始吧 Bufferjava NIO库是在jdk1.4中引入的,NIO与IO之间的第一个区别在于,IO是面向流的,而NIO是面向块的。 所谓的面向流是指:系统一次一个字节的处理数据,一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。 所谓的面向块是指:以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。 按块的方式处理数据要比按流的方式处理数据快,因为按块的方式读取或写入数据所执行的系统调用要远少于一次一个字节的方式,类似于BufferedInputStream的方式。 上面所说的块,在NIO中就是Buffer对象。 一个 Buffer(缓冲区) 实质上是一个容器对象,它包含一些要写入或者刚读出的数据。在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 举例来说,ByteBuffer实质上是对byte数组进行了封装,其内部是一个byte数组,ByteBuffer对象提供了一些实用的API供我们去操作这个数组,完成一些读取或写入的功能。我们所要学习的,就是理解在调用这些API的时候,Buffer处理数组的方式。 除了boolean类型之外,java为每种基本类型都封装了对应的Buffer对象。 状态变量Buffer使用四个值指定了缓冲区在某个时刻的状态: 容量(Capacity):缓冲区能够容纳的数据元素的最大数量 实际上,这个值指定了底层数组的大小。这一值在缓冲区创建时被设定,并且永远不能被改变。 位置(Position):下一个要被读或写的元素的索引 position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。比如,从通道中读三个字节到缓冲区中,那么缓冲区的 position 将会设置为3,指向数组中第四个元素。 初始的position值为0。 边界(Limit):缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。 在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。 标记(Mark):一个备忘位置。 调用 mark()来设定 mark = postion。调用 reset()设定 position = mark。 初始的mark值为-1。 上面四个属性遵循以下的关系: 0 <= mark <= position <= limit <= capacity API 创建 在了解这些api之前,首先需要知道如何创建一个Buffer对象: 在上一个小节中提到的7种缓冲区类没有一种是可以直接实例化的,他们都是抽象类,但都包含了静态工厂方法创建相应的实例。以ByteBuffer为例:(对于其他六中缓冲区类也适用) ByteBuffer buffer = ByteBuffer.allocate(1024); allocate方法分配了一个具有指定大小底层数组的缓冲区对象,这个大小也就是上面提到的Capacity。 我们也可以使用已经存在的数组来作为缓冲区对象的底层数组: 12byte array[] = new byte[1024];ByteBuffer buffer = ByteBuffer.wrap(array); 此时,buffer对象的底层数组指向了array,这意味着直接修改array数组也会使buffer对象读取的数据产生变化。 123byte[] bs = new byte[10];ByteBuffer buffer = ByteBuffer.wrap(bs);System.out.println(buffer.toString()); 打印如下: 1java.nio.HeapByteBuffer[pos=0 lim=10 cap=10] 可见,新初始化的Buffer实例中,position = 0,limit=capacity=10 存取 注意到Buffer类中并没有提供get或者put函数。实际上每一个Buffer对象都有这两个函数,但它们所采用的参数类型,以及它们返回的数据类型,对每个子类来说都是唯一的,所以它们不能在顶层Buffer类中被抽象地声明。这些存取方法被定义在Buffer类的子类当中,我们一ByteBuffer为例: 1234public abstract byte get();public abstract byte get (int index);public abstract ByteBuffer put (byte b);public abstract ByteBuffer put (int index, byte b); ByteBuffer实际上还提供了 get(byte[] dst, int offset, int length)这样的接口,其内部实现也是循环调用了get()方法。 get和put可以是相对的或者是绝对的。 相对方案是不带有索引参数的函数。当相对函数被调用时,位置在返回时前进一。 绝对存取不会影响缓冲区的位置属性(Position、Limit、Capacity、Mark)。 123456789101112131415161718buffer.put((byte)'h').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o');print(buffer, bs);buffer.put(0, (byte)'y').put((byte)'y');print(buffer, bs);//观察Buffer底层存储情况public static void print(Buffer buffer, byte[] bs) { System.out.println(buffer.toString()); for (int i = 0; i < bs.length; i++) { if (bs[i] != 0) { char c = (char)bs[i]; System.out.print(c); } else { System.out.print("$"); } } System.out.println(""); } 打印如下: 1234java.nio.HeapByteBuffer[pos=5 lim=10 cap=10]hello$$$$$java.nio.HeapByteBuffer[pos=6 lim=10 cap=10]yelloy$$$$ 可以看到,存入5个字节之后,position增加为5,limit与capacity不变。调用buffer.put(0, (byte)’y’),将bs[0]的数据改写为(byte)’y’,position并没有改变。 Buffer.flip() 我们想要将刚刚写入的数据读出的话应该怎么做?应该将position设为0:buffer.position(0),就可以从正确的位置开始获取数据。但是它是怎样知道何时到达我们所插入数据末端的呢?这就是边界属性被引入的目的。边界属性指明了缓冲区有效内容的末端。我们需要将limit设置为当前位置:buffer.limit(buffer.position())。 buffer.limit(buffer.position()).position(0); Buffer已经提供了一个方法封装了这些操作: 123456public final Buffer flip() { limit = position; position = 0; mark = -1; return this;} 12buffer.flip();print(buffer, bs); 打印如下: 12java.nio.HeapByteBuffer[pos=0 lim=6 cap=10]yelloy$$$$ 调用buffer.flip()后,limit设置为当前position值,position重置为0. Buffer.rewind() 紧接着上面的程序: 123456System.out.println((char)buffer.get());System.out.println((char)buffer.get(3));print(buffer, bs); buffer.rewind();print(buffer, bs); 打印如下: 123456yljava.nio.HeapByteBuffer[pos=1 lim=6 cap=10]yelloy$$$$java.nio.HeapByteBuffer[pos=0 lim=6 cap=10]yelloy$$$$ 可以看到,rewind()方法与filp()相似,但是不影响limit,他只是将position设为0,这样就可以从新读取已经读过的数据了。 12345public final Buffer rewind() { position = 0; mark = -1; return this;} Buffer.mark()、Buffer.reset() 123456789101112public final Buffer mark() { mark = position; return this;}public final Buffer reset() { int m = mark; if (m < 0) throw new InvalidMarkException(); position = m; return this;} Buffer.mark(),使缓冲区能够记住一个位置并在之后将其返回。 缓冲区的标记在mark()函数被调用之前是未定义的,调用时标记被设为当前位置的值。reset()函数将位置设为当前的标记值。如果标记值未定义,调用reset()将导致InvalidMarkException异常。 1234567buffer.position(2);buffer.mark();print(buffer, bs);buffer.position(4);print(buffer, bs);buffer.reset();print(buffer, bs); 打印如下: 123456java.nio.HeapByteBuffer[pos=2 lim=6 cap=10]yelloy$$$$java.nio.HeapByteBuffer[pos=4 lim=6 cap=10]yelloy$$$$java.nio.HeapByteBuffer[pos=2 lim=6 cap=10]yelloy$$$$ Buffer.remaining()、Buffer.hasRemaining() remaining()函数将返回从当前位置到上界还剩余的元素数目。 hasRemaining()会返回是否已经达到缓冲区的边界。 1234567public final int remaining() { return limit - position;}public final boolean hasRemaining() { return position < limit;} 有两种方法读取缓冲区的所有剩余数据: 12345678910// 第一种for (int i = 0; buffer.hasRemaining(), i++) { myByteArray [i] = buffer.get();}// 第二种int count = buffer.remaining();for (int i = 0; i < count, i++) { myByteArray [i] = buffer.get();} Buffer.clear() clear()函数将缓冲区重置为空状态。它并不改变缓冲区中的任何数据元素,而是仅仅将上界设为容量的值,并把位置设回 0。 123456public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this;} ByteBuffer.compact() compact()方法并不是Buffer接口中定义的,而是属于ByteBuffer。 如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么使用compact()方法。 compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。 1234print(buffer, bs);System.out.println(buffer.remaining());buffer.compact();print(buffer, bs); 打印如下: 12345java.nio.HeapByteBuffer[pos=2 lim=6 cap=10]yelloy$$$$4java.nio.HeapByteBuffer[pos=4 lim=10 cap=10]lloyoy$$$$ ByteBuffer.equals()、ByteBuffer.compareTo() 可以使用equals()和compareTo()方法两个Buffer。 下面提到的剩余元素是从 position到limit之间的元素。 equals() 当满足下列条件时,表示两个Buffer相等: 有相同的类型(byte、char、int等)。 Buffer中剩余的byte、char等的个数相等。 Buffer中所有剩余的byte、char等都相同。 在每个缓冲区中应被get()函数返回的剩余数据元素序列必须一致。 equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。 compareTo() compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer: 第一个不相等的元素小于另一个Buffer中对应的元素 。 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。 只读缓冲区可以使用asReadOnlyBuffer()函数来生成一个只读的缓冲区视图。 这个新的缓冲区不允许使用put(),并且其isReadOnly()函数将会返回true。对这一只读缓冲区的put()函数的调用尝试会导致抛出ReadOnlyBufferException异常。 两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的位置,上界和标记属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。 复制缓冲区duplicate()函数创建了一个与原始缓冲区相似的新缓冲区。两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的位置,上界和标记属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。 复制一个缓冲区会创建一个新的Buffer对象,但并不复制数据。原始缓冲区和副本都会操作同样的数据元素。 直接缓冲区直接ByteBuffer是通过调用ByteBuffer.allocateDirect(int capacity)函数来创建的。 什么是直接缓冲区(DirectByteBuffer)呢?直接缓冲区意味着所分配的这段内存是堆外内存,而我们通过ByteBuffer.allocate(int capacity)或者ByteBuffer.wrap(byte[] array)分配的内存是堆内存,其返回的实例为HeapByteBuffer,HeapByteBuffer中持有一个byte数组,这个数组所占有的内存是堆内内存。 Netty之Java堆外内存扫盲贴了解java堆外内存。 sun.nio.ch.FileChannelImpl.read(ByteBuffer dst) sun.nio.ch.IOUtil.read(FileDescriptor fd, ByteBuffer dst, long position,NativeDispatcher nd, Object lock) 观察上面两段代码发现,我们通过一个文件通道去填充一个ByteBuffer时,先执行sun.nio.ch.FileChannelImpl.read(ByteBuffer dst)方法,其中调用了sun.nio.ch.IOUtil.read(FileDescriptor fd, ByteBuffer dst, long position,NativeDispatcher nd, Object lock)方法,观察这个方法,发现其中会做一个判断:如果是直接缓冲区(DirectBuffer),直接调用readIntoNativeBuffer(fd, dst, position, nd, lock)并返回;如果是非直接缓冲区(HeapByteBuffer),先获取一个直接缓冲区,然后使用该直接缓冲区作为参数调用readIntoNativeBuffer(fd, dst, position, nd, lock),然后将填充完毕的DirectBuffer的内容复制到HeapByteBuffer当中,然后返回。 直接缓冲区的内存分配调用了sun.misc.Unsafe.allocateMemory(size),返回了内存基地址,实际上就是malloc。 看一下java doc对DirectBuffer的说明: A byte buffer is either direct or non-direct. Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer’s content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system’s native I/O operations. 给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。 结合上面的代码,就可以理解这段话的含义。 那么,为什么需要直接缓冲区,也就是堆外内存来执行IO呢? 以读操作为例,数据从底层硬件读到内核缓冲区之后,操作系统会从内核空间复制数据到用户空间,此时的用户进程空间就是jvm,这意味着 I/O 操作的目标内存区域必须是连续的字节序列。在 Java 中,数组是对象,在 JVM 中,字节数组可能不会在内存中连续存储。因此,这个连续的字节序列就是直接缓冲区中分配的内存空间。需要直接缓冲区来当一个中间人,完成数据的写入或者读取。 其实,在传统BIO中,也是这么做的,同样需要一个堆外内存来充当这个中间人:比如FileInputStream.read(byte b[], int off, int len): FileInputStream.read(byte b[], int off, int len)调用了readBytes(byte b[], int off, int len)方法,这个方法是一个本地方法: 12345JNIEXPORT jint JNICALL Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this, jbyteArray bytes, jint off, jint len) {//除了前两个参数,后三个就是readBytes方法传递进来的,字节数组、起始位置、长度三个参数 return readBytes(env, this, bytes, off, len, fis_fd); } 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253jintreadBytes(JNIEnv *env, jobject this, jbyteArray bytes, jint off, jint len, jfieldID fid){ jint nread; char stackBuf[BUF_SIZE]; char *buf = NULL; FD fd; if (IS_NULL(bytes)) { JNU_ThrowNullPointerException(env, NULL); return -1; } if (outOfBounds(env, off, len, bytes)) { JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL); return -1; } if (len == 0) { return 0; } else if (len > BUF_SIZE) { buf = malloc(len);// buf的分配 if (buf == NULL) { JNU_ThrowOutOfMemoryError(env, NULL); return 0; } } else { buf = stackBuf; } fd = GET_FD(this, fid); if (fd == -1) { JNU_ThrowIOException(env, "Stream Closed"); nread = -1; } else { nread = IO_Read(fd, buf, len);// buf是使用malloc分配的直接缓冲区,也就是堆外内存 if (nread > 0) { (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);// 将直接缓冲区的内容copy到bytes数组中 } else if (nread == JVM_IO_ERR) { JNU_ThrowIOExceptionWithLastError(env, "Read error"); } else if (nread == JVM_IO_INTR) { JNU_ThrowByName(env, "java/io/InterruptedIOException", NULL); } else { /* EOF */ nread = -1; } } if (buf != stackBuf) { free(buf); } return nread;} 可以看到,这个方法其实最关键的就是IO_Read这个宏定义的处理,而IO_Read其实只是代表了一个方法名称叫handleRead,我们去看一下handleRead的源码。 123456789101112131415161718192021222324JNIEXPORT size_t handleRead(jlong fd, void *buf, jint len) { DWORD read = 0; BOOL result = 0; HANDLE h = (HANDLE)fd; if (h == INVALID_HANDLE_VALUE) { return -1; } result = ReadFile(h, buf, len, &read, NULL); if (result == 0) { int error = GetLastError(); if (error == ERROR_BROKEN_PIPE) { return 0; } return -1; } return read; } 通过上面的代码可以发现,传统的BIO也是把操作系统返回的数据放到直接缓冲区当中,然后在copy回我们传入的byte数组当中。 所有的缓冲区都提供了一个叫做isDirect()的boolean函数,来测试特定缓冲区是否为直接缓冲区。 A direct byte buffer may be created by invoking the allocateDirect factory method of this class. The buffers returned by this method typically have somewhat higher allocation and deallocation costs than non-direct buffers. The contents of direct buffers may reside outside of the normal garbage-collected heap, and so their impact upon the memory footprint of an application might not be obvious. It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system’s native I/O operations. In general it is best to allocate direct buffers only when they yield a measureable gain in program performance. 直接缓冲区虽然避免了复制内存带来的消耗,但直接缓冲区使用的内存是通过调用本地操作系统方面的代码分配的,绕过了标准 JVM 堆栈。建立和销毁直接缓冲区会明显比具有堆栈的缓冲区更加破费,并且可能带来不易察觉的内存泄漏,或oom问题。所以,如果对于性能要求不是很严格,一般情况下,使用非直接缓冲区就足够了。 缓冲区分片slice() 方法根据现有的缓冲区创建一种 子缓冲区 。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。 现在我们对这个缓冲区 分片 ,以创建一个包含槽 3 到槽 6 的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个 窗口。 窗口的起始和结束位置通过设置 position 和 limit 值来指定,然后调用 Buffer 的 slice() 方法: 1234print(buffer, bs);buffer.position( 3 ).limit( 7 );ByteBuffer slice = buffer.slice();print(slice, slice.array()); 打印如下: 1234java.nio.HeapByteBuffer[pos=4 lim=10 cap=10]lloyoy$$$$java.nio.HeapByteBuffer[pos=0 lim=4 cap=4]lloyoy$$$$ slice 是缓冲区的 子缓冲区 。不过, slice 和 buffer 共享同一个底层数据数组。 类型视图缓冲区我们知道,Buffer可以作为通道执行IO的源头或者目标,但是通道只接受ByteBuffer类型的参数。比如read(ByteBuffer dst)。 我们在进行IO操作时,可能会使用各种ByteBuffer类去读取文件内容,接收来自网络连接的数据使用各种ByteBuffer类去读取文件内容,接收来自网络连接的数据等。一旦数据到达了您的 ByteBuffer,我们需要对他进行一些操作。ByteBuffer类允许创建视图来将byte型缓冲区字节数据映射为其它的原始数据类型。例如,asLongBuffer()函数创建一个将八个字节型数据当成一个 long 型数据来存取的视图缓冲区。 123456789public abstract class ByteBuffer extends Buffer implements Comparable{ // 这里仅列出部分API public abstract CharBuffer asCharBuffer(); public abstract ShortBuffer asShortBuffer(); public abstract IntBuffer asIntBuffer(); public abstract LongBuffer asLongBuffer(); public abstract FloatBuffer asFloatBuffer(); public abstract DoubleBuffer asDoubleBuffer();} 12345678910111213buffer.clear();buffer.order(ByteOrder.BIG_ENDIAN);//指定字节序buffer.put (0, (byte)0);buffer.put (1, (byte)'H');buffer.put (2, (byte)0);buffer.put (3, (byte)'i');buffer.put (4, (byte)0);buffer.put (5, (byte)'!');buffer.put (6, (byte)0);CharBuffer charBuffer = buffer.asCharBuffer();System.out.println("pos=" + charBuffer.position() + " limit=" + charBuffer.limit() + " cap=" + charBuffer.capacity());;print(charBuffer, bs); 打印如下: 123pos=0 limit=5 cap=5Hi! $H$i$!$$$$ 新的缓冲区的容量是字节缓冲区中存在的元素数量除以视图类型中组成一个数据类型的字节数。视图缓冲区的第一个元素从创建它的ByteBuffer对象的位置开始(positon()函数的返回值)。 测试代码前面提到的测试代码汇总: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475import java.nio.Buffer;import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.nio.CharBuffer;public class Test { public static void main(String[] args) { byte[] bs = new byte[10]; ByteBuffer buffer = ByteBuffer.wrap(bs); System.out.println(buffer.toString()); // put buffer.put((byte)'h').put((byte)'e').put((byte)'l').put((byte)'l').put((byte)'o'); print(buffer, bs); buffer.put(0, (byte)'y').put((byte)'y'); print(buffer, bs); //flip buffer.flip(); print(buffer, bs); // rewind System.out.println((char)buffer.get()); System.out.println((char)buffer.get(3)); print(buffer, bs); buffer.rewind(); print(buffer, bs); // mark reset buffer.position(2); buffer.mark(); print(buffer, bs); buffer.position(4); print(buffer, bs); buffer.reset(); print(buffer, bs); // compact System.out.println(buffer.remaining()); buffer.compact(); print(buffer, bs); // slice buffer.position( 3 ).limit( 7 ); ByteBuffer slice = buffer.slice(); print(slice, slice.array()); // asCharBuffer buffer.clear(); buffer.order(ByteOrder.BIG_ENDIAN); buffer.put (0, (byte)0); buffer.put (1, (byte)'H'); buffer.put (2, (byte)0); buffer.put (3, (byte)'i'); buffer.put (4, (byte)0); buffer.put (5, (byte)'!'); buffer.put (6, (byte)0); CharBuffer charBuffer = buffer.asCharBuffer(); System.out.println("pos=" + charBuffer.position() + " limit=" + charBuffer.limit() + " cap=" + charBuffer.capacity());; print(charBuffer, bs); } public static void print(Buffer buffer, byte[] bs) { System.out.println(buffer.toString()); for (int i = 0; i < bs.length; i++) { if (bs[i] != 0) { char c = (char)bs[i]; System.out.print(c); } else { System.out.print("$"); } } System.out.println(""); }} 参考Why is Traditional Java I/O Uninterruptable? JNI探秘—–FileInputStream的read方法详解 NIO 入门 Java NIO Buffer]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>NIO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[并发场景下缓存的创建]]></title>
<url>%2F2017%2F06%2F24%2F%E5%B9%B6%E5%8F%91%E5%9C%BA%E6%99%AF%E4%B8%8B%E7%BC%93%E5%AD%98%E7%9A%84%E5%88%9B%E5%BB%BA%2F</url>
<content type="text"><![CDATA[问题背景:现场提了一个新的需求:jdbc需要提供一个新的接口,用于查询session的执行进度。后台提供了查询视图,jdbc要做的只是在这个接口中查询这个视图,获得当前session的执行进度返回给客户。查询当前session的执行进度,说明当前session很有可能正在执行某条sql,是阻塞的。所以需要通过创建新的session并以当前session的sessionID作为条件在视图中查找当前session的执行情况。这个接口的调用在某些特定场景下是比较频繁的,比如用户每隔5秒就需要调用一次,那么每次都创建新的session去查询是不是显得太low?是不是可以在第一次查询时创建新的session,然后缓存起来,下次可以直接用?于是涉及到了缓存session的问题。(其实完全可以使用连接池来做到缓存,这次讨论的是并发场景下缓存的创建问题~~思路主要来自于《java并发编程实战》) 使用HashMap建立缓存123456789101112131415161718192021public class ConnCache { private final Map<String, Connection> cache; private final ConnFactory factory; public ConnCache(ConnFactory factory) { cache = new HashMap<>(); this.factory = factory; } public synchronized Connection get(String key) throws SQLException { Connection connection = cache.get(key); if (connection == null) { connection = factory.getConn(); cache.put(key, connection); } return connection; }}interface ConnFactory { Connection getConn() throws SQLException;} 上面的ConnFactory.getConn()创建新的session,是一个相对耗时的操作。用户需要获得session时调用ConnCache.get()方法,先从map中查找是否有对应的session(key可以设计为连接串+用户名映射到指定的session),如果没有,那么创建一个新的session,放到map里面,然后返回。 注意到整个get方法是被synchronized修饰的,因为HashMap不是线程安全的,如果有多个线程同时访问HashMap会出现并发问题。synchronized确保了两个线程不会同时访问HashMap。但是这么做也有一个问题,对整个get方法同步会使访问同一ConnCache对象get方法的线程串行化,如果一个线程正在调用这个方法,那么其他想要调用get方法的线程需要排队等候,很有可能被阻塞很长时间(创建session是个耗时的动作)。这种情况是由于锁的粒度较大带来的伸缩性问题。 使用ConcurrentHashMap建立缓存我们很容易想到使用ConcurrentHashMap来代替HashMap,ConcurrentHashMap本身就是线程安全的,采用了分段锁的技术,并发性能相对于加锁的HashMap要好上很多。使用ConcurrentHashMap后,我们就不需要在访问底层的Map时进行同步了。 123456789101112131415161718192021public class ConnCache { private final Map<String, Connection> cache; private final ConnFactory factory; public ConnCache(ConnFactory factory) { cache = new ConcurrentHashMap<>(); this.factory = factory; } public Connection get(String key) throws SQLException { Connection connection = cache.get(key); if (connection == null) { connection = factory.getConn(); cache.put(key, connection); } return connection; }}interface ConnFactory { Connection getConn() throws SQLException;} 上面这种方法相对于第一种方法,减小了锁的粒度,有着更好的并发性能。但是他也有一个严重的问题:如果一个线程在调用get方法时没有命中缓存,那么他会去创建一个新的session,然后放到map里面。如果在创建session的过程中,另一个线程也调用了get方法传入同样的key,那么就会导致重复创建的问题(这种情况很有可能出现,因为创建session是个耗时的操作)。 所以,我们需要某种方法来知道当前是否有其他线程在创建指定的session,如果有,则等待这个线程创建完毕,然后直接获取创建好的session。这样就能避免一次session多余的创建。 这时,我们就需要FutureTask来实现这个功能。FutureTask表示一个计算过程,这个过程可能计算完成,也可能正在运行。如果计算完毕,那么调用FutureTask.get()就会立即返回结果,否则,该方法会一直阻塞,直到有结果可用。categories: 生活tags: 食物 基于FutureTask建立缓存12345678910111213141516171819202122232425262728293031323334353637public class ConnCache { private final Map<String, Future<Connection>> cache; private final ConnFactory factory; public ConnCache(ConnFactory factory) { cache = new ConcurrentHashMap<>(); this.factory = factory; } public Connection get(String key) throws SQLException { Future<Connection> future = cache.get(key); if (future == null) { Callable<Connection> eval = new Callable<Connection>() { @Override public Connection call() throws Exception { return factory.getConn(); } }; FutureTask<Connection> task = new FutureTask<>(eval); future = task; cache.put(key, task); task.run(); } try { return future.get(); } catch (InterruptedException e) { throw new SQLException(e); } catch (ExecutionException e) { throw new SQLException(e); } }}interface ConnFactory {categories: 生活tags: 食物 Connection getConn() throws SQLException;} 与第二种方法相反,上面的方法是先检查创建session的动作是否开始(第二种方法是检查session创建是否完成),如果已经有线程在创建指定的session,就等待其创建完毕,然后获取结果。 看起来已经很完美了,但是还有一个并发缺陷: if代码块中不是原子的先检查再执行操作,两个线程很有可能同时检查到缓存为空,然后重复创建了session。 解决这个问题的方法有一种思路:把创建好的FutureTask放入到Map这一步需要是一个原子操作,如果对应的FutureTask已经存在了,调用已存在的FutureTask.get()方法即可。 最终的实现ConcurrentHashMap提供了一个同步方法:putIfAbsent() 12345678910111213141516171819202122232425262728293031323334353637383940public class ConnCache { private final Map<String, Future<Connection>> cache; private final ConnFactory factory; public ConnCache(ConnFactory factory) { cache = new ConcurrentHashMap<>(); this.factory = factory; } public Connection get(String key) throws SQLException { Future<Connection> future = cache.get(key); if (future == null) { Callable<Connection> eval = new Callable<Connection>() { @Override public Connection call() throws Exception { return factory.getConn(); } }; FutureTask<Connection> task = new FutureTask<>(eval); future = cache.putIfAbsent(key, task); if (future == null) { future = task; task.run(); } } try { return future.get(); } catch (InterruptedException e) { cache.remove(future); throw new SQLException(e); } catch (ExecutionException e) { cache.remove(future); throw new SQLException(e); } }}interface ConnFactory { Connection getConn() throws SQLException;} 上面的演示是对并发场景的一个思考。实际的缓存在使用中还要考虑缓存过期时间(可以在FutureTask的子类中实现),缓存清理算法等问题。我们也可以通过泛型将上面的代码设计为一个通用的缓存框架: 1234567891011121314151617181920212223242526272829303132333435363738public class Cache<K, V> implements Factory<K, V>{ private final Map<K, Future<V>> cache; private final Factory<K,V> factory; public Cache(Factory<K, V> factory) { cache = new ConcurrentHashMap<>(); this.factory = factory; } public V get(K key) throws InterruptedException { Future<V> future = cache.get(key); if (future == null) { Callable<V> eval = new Callable<V>() { @Override public V call() throws Exception { return factory.get(key); } }; FutureTask<V> task = new FutureTask<>(eval); future = cache.putIfAbsent(key, task); if (future == null) { future = task; task.run(); } } try { return future.get(); } catch (ExecutionException e) { cache.remove(future); throw new IllegalStateException(e); } }}interface Factory<K, V> { V get(K key) throws InterruptedException;}]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何从外网访问家里的电脑]]></title>
<url>%2F2017%2F06%2F05%2F%E5%A6%82%E4%BD%95%E4%BB%8E%E5%A4%96%E7%BD%91%E8%AE%BF%E9%97%AE%E5%AE%B6%E9%87%8C%E7%9A%84%E7%94%B5%E8%84%91%2F</url>
<content type="text"><![CDATA[今天逛论坛的时候偶然间看到了电信用户可以轻松获得公网IP的一条内容,于是看了下自己的(本人用电信)路由器ip,惊喜的发现竟然是公网ip…之前都没有注意到….有了公网IP,就可以做一些好玩的事了。最直接的就是可以把自己的主机当服务器来用了,省去了购买VPS的费用,在公司或者其他地方也可以连到家里的主机,传东西什么的也很方便… 判断是否是公网IP首先要判断运营商分配给自己的IP是否是公网IP,如果没有公网IP,一切都是白搭…移动和联通的不太清楚,电信用户是可以直接获取到公网IP的。 我的路由器是TP-LINK,在电脑地址栏输入路由器地址192.168.1.1,进入路由器web界面 在路由设置->上网设置界面,可以看到WAN口的IP地址。 此时,打开google,查询一下外网IP(或者直接打开http://www.ip138.com/)。 如果路由器WAN口的IP地址和查到的外网IP地址相同,可以确定这个ip地址就是运营商分配给你的公网IP,可以拿来使用。 PS:我们在自己的机器上用ipconfig查询得到的IP地址,往往是路由器分配给我们主机的私有IP地址,有以下几类: 10.0.0.0~10.255.255.255 172.16.0.0~172.31.255.255 192.168.0.0~192.168.255.255 这样的IP地址是属于本地局域网的,因特网中的所有路由器,对于目的地址是上面几类的数据报一律不进行转发。所以,因特网上的机器是无法访问我们的主机的。 当然,如果你没有使用路由器,而是直接把应当插在路由器WAN口的网线插在电脑上的话,ping出来的地址就直接就是公网IP… 动态域名解析实际上,运营商每次分配给我们的公网IP是不一样的,都是从IP池里随机取的。可以重启路由器观察一下,每当你重启一次路由器之后,公网IP就会切换。这样就会造成一个问题:假如我们要通过公网IP来访问我们在家里的主机,但是这个IP不是固定的,随时有可能切换,我们不可能在他切换的时候感知到(其实是可以的),即使可以,每次还要确认这个公网IP,是不是太low了? 这就需要动态域名解析来解决。简单来说就是将这些公网IP映射到一个域名上,无论IP怎么切换,我们只要通过这个域名就能得到IP,并进行访问,至于域名和动态IP怎么映射,我们不必关心,只要记住这个域名就好了。 TP-LINK本身有自己动态域名解析服务,还支持花生壳的动态域名解析服务。在路由器应用管理->DDNS 界面可以选择动态域名解析服务提供方,以及免费域名。我这里就选择了TP-LINK的域名解析服务,简单快捷。 填写好域名信息并保存之后,打开控制台,ping一下这个域名。如果ping返回的响应结果ip地址就是我们的公网IP,那么动态域名配置成功。 端口映射试想一下,连接到路由器上的设备往往不止一个,有电脑,平板,手机…路由器会为每个设备分配一个私有地址,而这些设备共享一个公网IP,大家轮换使用(NAT映射)。那么,我们要从外网访问家里的主机时,只有公网IP,如何在这众多的设备中选择出我们的主机呢? 端口映射解决了这个问题。先看看怎么配置端口映射。在应用管理->虚拟服务器 中添加一行映射。 上图中,外部端口是在外网访问我们局域网主机提供的服务时指定的端口,内部端口是局域网主机提供服务的真实端口,IP地址是要访问的局域网主机的私有IP地址,可以通过ipconfig获得,协议类型是传输层协议,一般选ALL即可。 举个例子,我们想要访问局域网主机的ssh服务,ssh:yukai@debiao.tpddns.cn:8888, 8888是外部端口,路由器拿到这个端口,去查端口映射表,将请求转发到192.168.1.105的22端口,也就是内部端口,也是我们主机ssh服务的端口。这样便完成了外网与局域网主机的通信。 还有一个问题,如果路由器采用DHCP的方式为局域网内的设备分配私有IP,那么这个IP往往是有时效性的,这一次你的主机是192.168.1.105,说不定过一会就自动切换到其他IP了。这样的话对我们的端口映射会有影响。可以修改路由器设置,为局域网主机分配固定的私有IP地址。 进入应用管理->IP与MAC绑定,在IP与MAC映射表中,选择要绑定的IP与主机。主机的mac地址可以使用ipconfig查询。 效果 上图是在自己的电脑上开了一个web服务,可以看到,通过域名+端口可以访问到自己主机提供的这个服务。 ssh的22端口映射一到8888,在外网通过域名+端口8888可以连接到主机的ssh。 最后计划买个树莓派研究研究,在上面搭建博客或者爬虫什么的,通过上面的设置就可以直接访问树莓派啦~~美滋滋]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[建造者模式的使用]]></title>
<url>%2F2017%2F05%2F29%2F%E5%BB%BA%E9%80%A0%E8%80%85%E6%A8%A1%E5%BC%8F%E7%9A%84%E4%BD%BF%E7%94%A8%2F</url>
<content type="text"><![CDATA[因为一个新项目的原因,去北京呆了半个多月,所以有好长时间没有写博客了。这段时间在写代码的过程中,学习到了建造者模式的一种用法,记录下来(设计模式在平时也会用到,网上文章多如牛毛,所以没怎么写关于设计模式这方面的日志) 建造者模式建造者模式用来创建一个内部结构复杂的对象,拥有多个组成部分。建造者模式可以使用户无需知道这些组件的装配细节,只需要指定复杂对象的类型就可以得到该对象。 有的复杂对象的组装还可能有一些限制条件,比如,某些属性必须存在,某些属性的初始化需要遵从一定的顺序等等。举一个修两层楼的比方:必须打好地基才能盖第一层楼,第一层盖好后才能盖第二层。 复杂对象的创建过程被封装到一个建造者对象里面,用户可以直接使用建造者建造完毕的对象,而不必关心建造过程。 建造者模式包含上面几个对象: Builder:抽象建造者 ConcreteBuilder:具体建造者 Director:指挥者 Product:产品角色 还是用修楼那个例子来分析: Client:1234567891011package space.kyu.mode.builder;public class Client { public static void main(String[] args) { Builder builder = new OneBuilder(); Director director = new Director(); director.build(builder); Building building = builder.getResult(); System.out.println(building); }} Director:123456789package space.kyu.mode.builder;public class Director { public void build(Builder builder) { builder.buildFoundation(); builder.buildLayer(); builder.buildSecondLayer(); }} Building:12345678910111213141516171819202122232425262728293031package space.kyu.mode.builder;public class Building { private int foundation;//地基深度 private int layer;//一层高度 private int secondLayer;//二层高度 public int getFoundation() { return foundation; } public void setFoundation(int foundation) { this.foundation = foundation; } public int getLayer() { return layer; } public void setLayer(int layer) { this.layer = layer; } public int getSecondLayer() { return secondLayer; } public void setSecondLayer(int secondLayer) { this.secondLayer = secondLayer; } @Override public String toString() { return "Building [foundation=" + foundation + ", layer=" + layer + ", secondLayer=" + secondLayer + "]"; } } Builder:12345678package space.kyu.mode.builder;interface Builder { void buildFoundation(); void buildLayer(); void buildSecondLayer(); Building getResult();} OneBuilder:12345678910111213141516171819202122232425package space.kyu.mode.builder;public class OneBuilder implements Builder { Building building = new Building(); @Override public void buildFoundation() { building.setFoundation(5);对象 } @Override public void buildLayer() { building.setLayer(3); } @Override public void buildSecondLayer() { building.setSecondLayer(3); } @Override public Building getResult() { return building; }} TwoBuilder:12345678910111213141516171819202122232425package space.kyu.mode.builder;public class TwoBuilder implements Builder { Building building = new Building(); @Override public void buildFoundation() { building.setFoundation(4); } @Override public void buildLayer() { building.setLayer(2); } @Override public void buildSecondLayer() { building.setSecondLayer(2); } @Override public Building getResult() { return building; }} 上面的例子中: 类Client就是用户 类Director就是指挥者Director 类Builder就是抽象建造者Builder 类Building就是产品Product 类OneBuilder、TwoBuilder就是实际建造者 可以看到,我们在客户端中只需要指定实际建造者,指挥类就可以为我们生成想要的产品。我们完全不需要知道这个二层小楼的建造过程,使用者与建造过程顺利解藕。并且,如果需要不同设计方案的二层小楼,我们只需要再实现一个实际建造者,并在客户端中指定即可。而且,修改一个实际建造者不会影响到其他的实现。 建造者模式与工厂方法模式很类似,如果把上面的指挥类当作客户端的话,那么他基本上就是一个工厂方法模式了。 建造者模式 与工厂方法模式的区别就在于增加了这个指挥类,因此建造者模式适用于创建过程更加复杂的对象。 扩展其实这个扩展才是这篇日志的重点,是建造者模式的一种延伸,也是我在这段时间里面学习到的一种用法。 这个用法一开始是在使用org.apache.http.client.utils.URIBuilder的时候学到的,当时用到的时候就觉得眼前一亮,马上想到了建造者模式的Builder,而且使用了流式接口,用起来很顺畅的感觉… 后来看到了网上的一篇文章设计模式(十)——建造者模式的实践讲的就是这个用法,也是翻译了老外的一篇博客。原文已经分析的很好了,所以直接转载过来了。(作者的其他博客也是很通俗易懂的,值的学习) 以下内容引用自设计模式(十)——建造者模式的实践 我不打算深入介绍设计模式的细节内容,因为有很多这方面的文章和书籍可供参考。 本文主要关注于告诉你为什么以及在什么情况下你应该考虑使用建造者模式。 然而,值得一提的是本文中的模式和GOF中的提出的有点不一样。那种原生的模式主要侧重于抽象构造的过程以达到通过修改builder的实现来得到不同的结果的目的。本文中主要介绍的这种模式并没有那么复杂,因为我删除了不必要的多个构造函数、多个可选参数以及大量的setter/getter方法。 假设你有一个类,其中包含大量属性。就像下面的User类一样。假设你想让这个类是不可变的。 12345678public class User { private final String firstName; //required private final String lastName; //required private final int age; //optional private final String phone; //optional private final String address; //optional ...} 在这样的类中,有一些属性是必须的(required)而另外一些是可选的(optional)。如果你想要构造这个类的实例,你会怎么做?把所有属性都设置成final类型,然后使用构造函数初始化他们嘛?但是,如果你想让这个类的调用者可以从众多的可选参数中选择自己想要的进行设置怎么办? 第一个可想到的方案可能是重载多个构造函数,其中有一个只初始化必要的参数,还有一个会在初始化必要的参数同时初始化所有的可选参数,还有一些其他的构造函数介于两者之间,就是一次多初始化一个可选参数。就像下面的代码: 12345678910111213141516171819public User(String firstName, String lastName) { this(firstName, lastName, 0);}public User(String firstName, String lastName, int age) { this(firstName, lastName, age, "");}public User(String firstName, String lastName, int age, String phone) { this(firstName, lastName, age, phone, "");}public User(String firstName, String lastName, int age, String phone, String address) { this.firstName = firstName; this.lastName = lastName; this.age = age; this.phone = phone; this.address = address;} 首先可以肯定的是,这样做是可以满足要求的。 当然,这种方式的缺点也是很明显的。当一个类中只有几个参数的时候还好,如果一旦类中的参数逐渐增大,那么这个类就会变得很难阅读和维护。更重要的是,这样的一个类,调用者会很难使用。我到底应该使用哪个构造方法?是包含两个参数的还是包含三个参数的?如果我没有传递值的话那些属性的默认值是什么?如果我只想对address赋值而不对age和phone赋值怎么办?遇到这种情况可能我只能调用那个参数最全的构造函数,然后对于我不想要的参数值传递一个默认值。 此外,如果多个参数的类型都相同那就很容易让人困惑,第一个String类型的参数到底是number还是address呢? 还有没有其他方案可选择呢?我们可以遵循JaveBean规范,定义一个只包含无参数的构造方法和getter、setter方法的JavaBean。 1234567891011121314151617181920212223242526272829303132333435363738public class User { private String firstName; // required private String lastName; // required private int age; // optional private String phone; // optional private String address; //optional public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; }} 这种方式看上去很容易阅读和维护。对于调用者来说,我只需要创建一个空的对象,然后对于我想设置的参数调用setter方法设置就好了。这难道还有什么问题吗?其实存在两个问题。第一个问题是该类的实例状态不固定。如果你想创建一个User对象,该对象的5个属性都要赋值,那么直到所有的setXX方法都被调用之前,该对象都没有一个完整的状态。这意味着在该对象状态还不完整的时候,一部分客户端程序可能看见这个对象并且以为该对象已经构造完成。 这种方法的第二个不足是User类是易变的(因为没有属性是final的)。你将会失去不可变对象带来的所有优点。 幸运的是应对这种场景我们有第三种选择,建造者模式。解决方案类似如下所示: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768public class User { private final String firstName; // required private final String lastName; // required private final int age; // optional private final String phone; // optional private final String address; // optional private User(UserBuilder builder) { this.firstName = builder.firstName; this.lastName = builder.lastName; this.age = builder.age; this.phone = builder.phone; this.address = builder.address; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public String getPhone() { return phone; } public String getAddress() { return address; } public static class UserBuilder { private final String firstName; private final String lastName; private int age; private String phone; private String address; public UserBuilder(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public UserBuilder age(int age) { this.age = age; return this; } public UserBuilder phone(String phone) { this.phone = phone; return this; } public UserBuilder address(String address) { this.address = address; return this; } public User build() { return new User(this); } }} 值得注意的几个要点: User类的构造函数是私有的,这意味着你不能在外面直接创建这个类的对象。 该类是不可变的。所有属性都是final类型的,在构造方法里面被赋值。另外,我们只为它们提供了getter方法。 builder类使用流式接口风格,让客户端代码阅读起来更容易(我们马上就会看到一个它的例子) builder的构造方法只接收必要的参数,为了确保这些属性在构造方法里赋值,只有这些属性被定义成final类型。 使用建造者模式有在本文开始时提到的两种方法的所有优点,并且没有它们的缺点。客户端代码写起来更简单,更重要的是,更易读。我听过的关于该模式的唯一批判是你必须在builder类里面复制类的属性。然而,考虑到这个事实,builder类通常是需要建造的类的一个静态类成员,它们一起扩展起来相当容易。(译者表示没明白为设定为静态成员扩展起来就容易了。设为静态成员我认为有一个好处就是可以避免出现is not an enclosing class的编译问题,创建对象时候更加方便) 现在,试图创建一个新的User对象的客户端代码看起来如何那?让我们来看一下: 12345678public User getUser() { return new User.UserBuilder("Jhon", "Doe") .age(30) .phone("1234567") .address("Fake address 1234") .build();} 译者注:如果UserBuilder没有设置为static的,以上代码会有编译错误。错误提示:User is not an enclosing class 以上代码看上去相当整洁。我们可以只通过一行代码就可以创建一个User对象,并且这行代码也很容易读懂。除此之外,这样还能确保无论何时你想获取该类的对象都不会是不完整的(译者注:因为创建对象的过程是一气呵成的,一旦对象创建完成之后就不可修改了)。 这种模式非常灵活,一个单独的builder类可以通过在调用build方法之前改变builder的属性来创建多个对象。builder类甚至可以在每次调用之间自动补全一些生成的字段,例如一个id或者序列号。 值得注意的是,像构造函数一样,builder可以对参数的合法性进行检查,一旦发现参数不合法可以抛出IllegalStateException异常。 但是,很重要的一点是,如果要检查参数的合法性,一定要先把参数传递给对象,然后在检查对象中的参数是否合法。其原因是因为builder并不是线程安全的。如果我们在创建真正的对象之前验证参数,参数值可能被另一个线程在参数验证完和参数被拷贝完成之间的时间修改。这段时间周期被称作“脆弱之窗”。我们的例子中情况如下: 1234567public User build() { User user = new user(this); if (user.getAge() > 120) { throw new IllegalStateException(“Age out of range”); // thread-safe } return user;} 上一个代码版本是线程安全的因为我们首先创建user对象,然后在不可变对象上验证条件约束。下面的代码在功能上看起来一样但是它不是线程安全的,你应该避免这么做: 1234567public User build() { if (age > 120) { throw new IllegalStateException(“Age out of range”); // bad, not thread-safe } // This is the window of opportunity for a second thread to modify the value of age return new User(this);} 建造者模式最后的一个优点是builder可以作为参数传递给一个方法,让该方法有为客户端创建一个或者多个对象的能力,而不需要知道创建对象的任何细节。为了这么做你可能通常需要一个如下所示的简单接口:123public interface Builder { T build();} 借用之前的User例子,UserBuilder类可以实现Builder。如此,我们可以有如下的代码: UserCollection buildUserCollection(Builder userBuilder){…} 译者注:关于这这最后一个优点的部分内容并没太看懂,希望有理解的人能过不吝赐教。]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java线程池的使用]]></title>
<url>%2F2017%2F05%2F08%2Fjava%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E4%BD%BF%E7%94%A8%2F</url>
<content type="text"><![CDATA[在并发应用程序中,线程池是很重要的一块。读完《java并发编程实战》以及研究了一遍jdk源代码之后,总结一下线程池方面的知识~ 使用线程池的原因 无线创建线程的不足 在生产环境中,为每一个任务都分配一个线程这种方法存在一些缺陷: 线程生命周期的开销:线程的创建与销毁都会消耗大量资源,频繁创建与销毁线程会带来很大的资源开销 资源消耗:活跃的线程会消耗系统资源。如果可运行的线程数量大于可用的处理器数量,闲置的线程会占用许多内存,并且频繁的线程上下文切换也会带来很大的性能开销 稳定性:操作系统在可创建的线程数量上有一个限制。在高负载情况下,应用程序很有可能突破这个限制,资源耗尽后很可能抛出OutOfMemoryError异常 提高响应速度 任务到达时,不再需要创建线程就可以立即执行 线程池提供了管理线程的功能 比如,可以统计任务的完成情况,统计活跃线程与闲置线程的数量等 使用场景 不适用场合 依赖性任务 在线程池中,如果任务依赖于其他任务,那么可能产生死锁。比如,在单线程的Executor中,如果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交任务的结果,那么通常会引发死锁 使用ThreadLocal的任务 ThreadLocal可以存储线程级变量,将变量封闭到特定的线程当中。然而使用线程池时,这些线程都会被自由的重用,在线程池的线程中不应该使用ThreadLocal在任务之间传递值。 当线程本地值的生命周期受限于任务的生命周期时,可以在线程池的线程中使用ThreadLocal,任务结束后调用ThreadLocal.remove方法将已存储的值清除。 使用线程封闭机制的任务 在单线程应用程序中,不用考虑对象的并发安全问题,他们都被很好的封闭在单个线程当中。如果将单线程的环境换成线程池,那么这些对象有可能造成并发安全问题,失去线程安全性 不同类型或运行时长差异较大的任务 不同类型任务之间很可能存在依赖,并且他们执行的时长也不相同,在线程池中运行时很有可能造成拥塞,甚至死锁 适用场合 当任务是同类型且相互独立时,线程池的性能可以达到最佳 网页服务器、文件服务器、邮件服务器,他们的请求往往是同类型且相互独立的 架构在线程池异常处理方案这篇博客中已经提到了线程池的架构,如图: Executor:异步任务执行框架的基础 123public interface Executor { void execute(Runnable command);} 通过使用Executor,将请求处理任务的提交与任务的实际执行解耦,只需要采用另一种不同的Executor实现,就可以改变服务器的行为。比如: 1234567891011121314// 为每个任务分配一个线程public class ThreadPerTaskExecutor implements Executor { @Override public void execute(Runnable r) { new Thread(r).start(); }}// 以同步的方式执行每个任务public class WithinThreadExecutor implements Executor{ @Override public void execute(Runnable r) { r.run(); }} ExecutorService:ExecutorService扩展了Executor接口,添加了一些用于管理生命周期和任务提交的方法 12345678910111213141516171819202122232425262728293031323334public interface ExecutorService extends Executor { // 生命周期管理 void shutdown(); List<Runnable> shutdownNow(); boolean isShutdown(); boolean isTerminated(); boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; // 任务提交 <T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task); <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException; <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException; <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException; <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;} ExecutorService的生命周期有3中状态:运行、关闭、终止。ExecutorService在初始创建时处于运行状态。shutdown方法执行平缓的关闭过程:不再接受新任务,同时等待已提交的任务执行完成,包括在任务队列中尚未开始的任务。shutdownNow方法将尝试取消所有运行中的任务,并不再启动队列中尚未执行的任务。 所有任务完成后,ExecutorService将转入终止状态。可以调用awaitTermination来等待ExecutorService到达终止状态,或者通过轮询isTerminated来判断ExecutorService是否终止。 AbstractExecutorService ThreadPoolExecutor ScheduledThreadPoolExecutor: 线程池的实现 ThreadPoolExecutor扩展了ExecutorService接口,是线程池的具体实现。ScheduledThreadPoolExecutor支持定时以及周期性任务的执行。 ThreadPoolExecutor支持两种方式的任务提交:exec.execute(Runnable r)以及exec.submit(Runnable r)。关于任务的这两种提交方式在线程池异常处理方案已经提到过了,不再赘述。 定制线程池先来了解一下线程池的创建: 1234567ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 以上是ThreadPoolExecutor的构造函数,看一下每个参数的含义: corePoolSize corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。 runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。 ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。 LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。 SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。 PriorityBlockingQueue:一个具有优先级的无限阻塞队列。 maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。 ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。 RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。下面会有介绍几种饱和策略。 keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。 TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。 设置线程池的大小线程池过大,会导致大量的线程在很少的cpu和内存资源上发生竞争,频繁的线程上下文切换也会带来额外的性能开销。线程池过小,导致许多空闲的处理器无法执行工作,降低吞吐率。 cpu密集型 对于计算密集型的任务,当系统拥有n个处理器时,将线程池大小设置为n+1通常可以实现最优利用率。 io密集型 对于包含io操作或其他阻塞操作的任务,由于线程不会一直执行,因此线程池的规模应该更大。有这么一个简单的公式: N[threads] = N[cpu] * U[cpu] * (1 + W/C) 其中,N[threads]是线程池的大小,U[cpu]是cpu的利用率,W/C是任务等待时间与任务执行时间的比值。 可以通过一些监控工具获得cpu利用率等,Runtime.getRuntime().availableProcessors()返回cpu的数目 资源依赖 如果任务还依赖一些其他的有限资源,比如数据库连接,文件句柄等,那么这些资源也会影响线程池的大小:计算每个任务对该资源的需求量,用该资源的可用总量除以每个任务的需求量,所得的结果就是线程池大小的上限。 ExecutorsExecutors提供了许多静态工厂方法来创建一个线程池: 12345678newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。 具体情况可以结合Executors源码和ThreadPoolExecutor的构造函数查看。我们也可以模仿Executors的这几个工厂方法来定制自己的线程池执行策略。 扩展ThreadPoolExecutor 在线程池异常处理方案这篇总结曾经提到重写ThreadPoolExecutor的afterExecute方法来处理未检测异常,这就是扩展ThreadPoolExecutor的一个例子。除此之外,还可以在这些方法中添加日志、计时、监视等功能。 线程池完成关闭操作后会调用方法terminated。terminated可以用来释放Executor在其生命周期中分配的各种资源,以及执行发送通知、记录日志等操作。 下面编写一个利用beforeExecute、afterExecute和terminated添加日志记录和统计信息收集的扩展ThreadPoolExecutor。 123456789101112131415161718192021222324252627282930313233343536373839404142public class TimingThreadPool extends ThreadPoolExecutor{ // 使用ThreadLocal存储任务起始时间,在beforeExecute设置起始时间,在afterExecute中可以看到这个值 private final ThreadLocal<Long> startTime = new ThreadLocal<>(); private final Logger logger = Logger.getLogger(TimingThreadPool.class.getName()); private final AtomicLong numTasks = new AtomicLong(); private final AtomicLong totalTime = new AtomicLong(); public TimingThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); } @Override protected void beforeExecute(Thread t, Runnable r) { super.beforeExecute(t, r); logger.fine(String.format("Thread %s: start %s", t, r)); startTime.set(System.nanoTime()); } @Override protected void afterExecute(Runnable r, Throwable t) { try { long endTime = System.nanoTime(); long taskTime = endTime - startTime.get(); numTasks.incrementAndGet(); totalTime.addAndGet(taskTime); logger.fine(String.format("Thread %s: end %s, time=%dns", t, r, taskTime)); } finally { super.afterExecute(r, t); } } @Override protected void terminated() { try { logger.fine(String.format("Terminated: avg time=%dns", totalTime.get()/numTasks.get())); } finally { super.terminated(); } }} 扩展ThreadPoolExecutor的newTaskFor方法可以修改通过submit方法返回的默认Future实现FutureTask为自己的实现。在我们自己实现Future的类中可以针对任务做一些操作,比如定制任务的取消行为: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748public class CacellingExecutor extends ThreadPoolExecutor { public CacellingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); } @Override protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) { if (callable instanceof CancellableTask) { return ((CancellableTask<T>)callable).newTask(); } return super.newTaskFor(callable); }}interface CancellableTask<T> extends Callable<T> { void cancel(); RunnableFuture<T> newTask();}abstract class SocketUsingTask<T> implements CancellableTask<T> { private Socket socket; public SocketUsingTask(Socket socket) { this.socket = socket; } @Override public void cancel() {在并发应用程序中,线程池是很重要的一块。读完《java并发编程实战》以及研究了一遍jdk源代码之后,总结一下线程池方面的知识~ try { this.socket.close(); } catch (IOException e) { e.printStackTrace(); } } @Override public RunnableFuture<T> newTask() { return new FutureTask<T>(this){ @Override public boolean cancel(boolean mayInterruptIfRunning) { try { SocketUsingTask.this.cancel(); } finally { return super.cancel(mayInterruptIfRunning); } } }; }} 异常处理异常处理这部分,在前面的博客中已经总结过了:线程池异常处理方案 饱和策略当线程池达到饱和以后(maximumPoolSzie),饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过setRejectedExecutionHandler来修改。当某个任务被提交到一个已经关闭的Executor时,也会用到饱和策略。jdk提供了几种不同的RejectedExecutionHandler实现: AbortPolicy 12345678public static class AbortPolicy implements RejectedExecutionHandler { public AbortPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); } } AbortPolicy是默认的饱和策略,该饱和策略将抛出未检查的RejectedExecutionException。调用者可以处理这个异常。 CallerRunsPolicy 12345678public static class CallerRunsPolicy implements RejectedExecutionHandler { public CallerRunsPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } } } CallerRunsPolicy将任务回退到调用者,他不会在线程池的某个线程中提交任务,而是在调用execute的线程中运行,从而降低新任务的流量。 DiscardPolicy 12345public static class DiscardPolicy implements RejectedExecutionHandler { public DiscardPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { } } DiscardPolicy会悄悄抛弃任务,什么也不做。 DiscardOldestPolicy 123456789public static class DiscardOldestPolicy implements RejectedExecutionHandler { public DiscardOldestPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } } } DiscardOldestPolicy会抛弃下一个将被执行的任务,然后重新尝试提交任务。 其他 CompletionService 如果向Executor提交了一组计算任务,并希望在计算完成后获取结果,那么可以保留与每个任务关联的Future,然后轮询这些future的get方法,判断任务是否完成。这种方法虽然可行,但是有些繁琐。 CompletionService将Executor和BlockingQueue的功能融合在一起,可以将任务提交给他执行,然后使用类似于队列的take或poll方法获取已完成结果。 ExecutorCompletionService 实现了CompletionService,他的实现很简单,在构造函数中创建一个BlockingQueue来保存计算完成的结果。当提交某个任务时,该任务首先包装成为一个QueueingFuture,这是FutureTask的一个子类,他改写了done方法,将结果放入BlockingQueue中。ExecutorCompletionService的take和poll方法委托给了BlockingQueue。 ScheduledThreadPoolExecutor ScheduledThreadPoolExecutor以延迟或定时的方式执行任务,类似于Timer。由于Timer的一些缺陷,可以使用ScheduledThreadPoolExecutor来代替Timer。 Timer在执行所有的定时任务时只会创建一个线程,如果某个任务执行时间过长,就会破坏其他TimerTask的定时准确性。TimerTask抛出异常后,Timer线程也不会捕获这个异常,从而终止定时线程。尚未执行的TimerTask不会再执行,新的任务也不会被调度。 参考java并发编程实战 聊聊并发(三)——JAVA线程池的分析和使用]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何炒肉]]></title>
<url>%2F2017%2F05%2F07%2F%E5%A6%82%E4%BD%95%E7%82%92%E8%82%89%2F</url>
<content type="text"><![CDATA[这个炒肉是跟我妈学的,炒菜或者调面均可,很好吃~ 图比较丑,就不上了… 原料里脊肉、醋、生姜、葱、蒜、酱油、花椒面、盐、味精、清水 做法 将里脊肉洗净,切成小块或肉片均可 生姜切片、葱切段、蒜切片备用 锅中倒油,放入备用的里脊肉翻炒 倒入少许醋,继续翻炒 片刻后加入备用的生姜、葱蒜,花椒面(花椒水更好)、味精以及少许盐继续翻炒 倒入适量酱油,翻炒片刻后加入适量清水 小火,待水被熬的差不多时起锅,倒入碗中,完成]]></content>
<categories>
<category>生活</category>
<category>食物</category>
</categories>
<tags>
<tag>食物</tag>
</tags>
</entry>
<entry>
<title><![CDATA[凉拌黄瓜]]></title>
<url>%2F2017%2F05%2F07%2F%E5%87%89%E6%8B%8C%E9%BB%84%E7%93%9C%2F</url>
<content type="text"><![CDATA[天气热起来了,突然想吃黄瓜了,打电话给我妈问了凉拌黄瓜的做法,吃起来不错~ 忘记拍照了… 原料黄瓜、拉皮(粉皮)、醋、生抽、葱、蒜、盐、味精、辣椒面、香油 做法 黄瓜洗净切片,葱切片,蒜切末(或蒜水) 黄瓜,拉皮(粉皮)放到碗中,将葱、蒜末倒入(蒜水更佳) 放入盐适量,香油适量,少许味精,生抽适量 加入香醋(根据个人口味,喜欢吃醋多加),喜欢吃辣的可以放入适量辣椒面 调匀,品尝口味后可以再适当的加入上述材料,大功告成]]></content>
<categories>
<category>生活</category>
<category>食物</category>
</categories>
<tags>
<tag>食物</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java任务取消]]></title>
<url>%2F2017%2F05%2F02%2Fjava%E4%BB%BB%E5%8A%A1%E5%8F%96%E6%B6%88%2F</url>
<content type="text"><![CDATA[最近在用思维导图做读书笔记,虽然感觉没有网上所说的那么神奇,但是用来做笔记还是可以的。总结梳理了一下java取消任务执方面的一些内容,也是《java并发编程实战》的读书笔记。 取消原因取消一个任务执行的理由有很多,通常有以下几个 用户请求取消 通常用户点击“取消”按钮发出取消命令 有时间限制的操作 计时任务,超时时就会取消任务执行并返回 应用程序逻辑 比如有多个任务对一个问题进行分解和搜索解决方案,如果其中某个任务找到解决方案,其他并行的任务就可以取消了 发生错误 比如爬虫程序下载网页到本地硬盘,如果盘满了之后爬取任务应该被取消 关闭 程序或服务被关闭,则正在执行的任务也应该取消,而不是继续执行 取消线程执行任务的取消执行,其实最后都会落到线程的终止上(任务都是由线程来执行)。在java中没有一种安全的抢占式方法来终止线程(Thread.stop 是不安全的终止线程执行的方法,已经废弃掉了),所以需要一种很好的协作机制来平滑的关闭任务。 自然结束中断线程的最好方法是让代码自然执行到结束,而不是从外部强制打断他。为此可以设置一个“任务取消标志”,任务代码会定期的查看这个标志,如果发现标志被设定了,则任务提前结束。 12345678910111213141516171819202122public class SomeJob { private List<String> list = new ArrayList<>(); private volatile boolean canceled = false; public void run() { while (!canceled) { String res = getResult(); synchronized (this) { list.add(res); } } } private String getResult() { // do something... return ""; } public void cancel() { this.canceled = true; }} 上面的代码中,设置了一个volatile类型的变量canceled,所以其他线程对这个变量的修改对所有线程都是可见的(可见性)。每次循环执行某个操作之前都会检查这个变量是否被其他线程设置为true,如果为true则提前退出。 这是很常见的一种取消任务执行的手段,但是也有他的弊端,比如: 1234567891011121314151617181920212223242526272829import java.util.concurrent.BlockingQueue;import java.util.concurrent.LinkedBlockingQueue;public class SomeJob { private BlockingQueue<String> list = new LinkedBlockingQueue<>(100); private volatile boolean canceled = false; public void run() { try { while (!canceled) { String res = getResult(); synchronized (this) { list.put(res); } } } catch (InterruptedException e) { e.printStackTrace(); } } private String getResult() { // do something... return ""; } public void cancel() { this.canceled = true; }} 上面将list替换为支持阻塞的BlockingQueue,他是一个有界队列,当调用list的put操作时,如果队列已经填满,那么将会一直阻塞直到队列有空余位置为止。如果恰好执行put操作是阻塞了,此时我们调用了cancel方法,那么什么时候检查canceled标志是不确定的,响应性很差,极端情况下,有可能永远也不会去再下一次轮询中检查canceled标志,试想我们执行了取消后,消费队列的线程已经停止,此时put操作又阻塞,那么将会一直阻塞下去,这个线程失去响应。 线程中断通过线程自己的中断机制,可以解决上述问题。 每个线程都有一个boolean类型的变量,表示中断状态。当中断线程时,这个线程的中断状态将被设置为true。在Thread中有三个方法可以设置或访问这个变量: Thread.interrupt: 中断目标线程 Thread.isInterrupted: 返回线程的中断状态 Thread.interrupted: 清除线程的中断状态,并返回之前的值 调用interrupt并不意味着立即停止目标线程正在进行的任务,而只是将中断状态设置为true:他并不会正真中断一个正在运行的线程,而只是发出了一种中断请求,线程可以看到这个中断状态,然后在合适的时刻处理。 中断请求响应中断阻塞上面提到的中断请求,有些方法会处理这些请求,从而结束现在正在进行的任务。像上面代码中的BlockingQueue.put方法,当他在阻塞状态时,依然能够发现中断请求并提前返回,所以解决上面代码中的问题只需要对执行代码的线程thread调用thread.interrupt方法,BlockingQueue.put就可以从阻塞状态中恢复回来,从而完成取消。类似这样的支持中断的阻塞就叫做响应中断阻塞,主要有以下几个: Thread.sleep Object.wait Thread.join 这些支持中断的阻塞在响应中断时执行的操作包括: 清除中断状态 抛出InterruptedException,表示阻塞操作由于中断而提前结束 jvm并不能保证这些阻塞方法检测到中断的速度,但在实际情况中响应速度还是很快的。 利用线程本身的中断状态作为取消机制,我们可以将上面的代码再改造一下: 12345678910111213141516171819202122232425public class SomeJob { private BlockingQueue<String> list = new LinkedBlockingQueue<>(); public void run() { try { while (!Thread.currentThread().isInterrupted()) { String res = getResult(); synchronized (this) { list.put(res); } } } catch (InterruptedException e) { System.out.println("任务被取消..."); } } private String getResult() { // do something... return ""; } public void cancel(Thread thread) { thread.interrupt(); }} 任务代码在每次轮询操作前检查当前线程的状态,如果被中断了就退出。cancel方法是对当前执行任务的线程进行中断。 注意,调用cancel方法的是另一线程,传入的线程实例则是执行run方法的工作者线程,故在执行cancel方法后run方法可以检测到中断。 不响应中断阻塞并非所有的阻塞方法和阻塞机制都能够响应中断请求,比如正在read或write上阻塞的socket就不会响应中断,调用线程的interrupt方法只能设置线程的中断状态,除此以外没有任何作用,因为这些阻塞方法并不会去检查线程中断状态,也不会处理中断。这些阻塞就是不响应中断阻塞。主要有以下几个: java.io包中的同步socket io: 从socket中获取的InputStream和OutputStream中的read或write方法都不会响应中断,解决办法是关闭这个socket,使得正在执行read或write方法而被阻塞的线程抛出一个SocketException java.io包中的同步IO: 当中断一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路。 Selector的异步IO: 如果一个线程在调用Selector.select方法时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并返回 获得某个锁: 如果一个线程由于等待某个内置锁而阻塞,将无法响应中断。Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。(BlockingQueue.put可以响应中断缘于此) 一个简单的例子,取消socket任务: 12345678910111213141516171819202122232425262728293031323334353637public class CanceledThread extends Thread { private final Socket socket; private final InputStream stream; public CanceledThread(Socket socket) throws IOException { this.socket = socket; this.stream = socket.getInputStream(); } @Override public void interrupt() { try { socket.close(); } catch (Exception e) { // do nothing } finally { super.interrupt(); } } @Override public void run() { try { byte[] bytes = new byte[1024]; while (true) { int count = stream.read(bytes); if (count < 0) { break; } else if (count > 0) { // 处理读到 bytes } } } catch (Exception e) { // 可能捕捉到InterruptedException 或 SocketException // 线程退出 } }} 在上面的代码中,即使socket的stream在read过程中阻塞了,也可以中断阻塞并返回。 中断处理上文提到,当调用可中断的阻塞库函数时,会抛出InterruptedException,这个异常会出现在我们的任务代码中(任务代码调用了这些阻塞方法),有三种方法处理这个异常: 不处理,或者在捕捉到异常后打印日志以及做一些资源回收工作 确定我们的任务代码可以这么做时才这么做。这意味这这个任务完全可以在这个线程中取消,不必再向上层报告或需要更上层的代码处理。 传递异常,从而使你的方法也成为可中断的阻塞方法 简单的将异常抛出,让上层代码处理,这意味着需要上层代码再做一些资源回收等工作。 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理 如果不想或无法(Runnable中)传递InterruptedException时,可以通过再次调用interrupt来恢复中断状态。此时上层代码就可以捕捉到这个中断,从而作出处理。 ThreadPoolExcutor就是处理中断的一个例子:当其拥有的工作者线程检测到中断时,他会检查线程池是否正在关闭。如果是,他会在结束前执行一些线程清理工作,否则他可能创建一个新线程将线程池恢复到合理的规模。 取消任务终止线程池线程池的生命周期是由ExcutorService控制的。ExcutorService提供了两种关闭线程池的方法: shutdownNow 强行关闭线程池,首先关闭当前正在执行的任务,然后返回所有尚未启动的任务清单(在任务队列当中的) 关闭速度快,但是有风险,正在执行中的任务可能在执行一半的时候被结束 shutdown 正常关闭线程池,一直等到队列中的所有任务都执行完后才关闭,在此期间不接受新任务 关闭速度慢,却更加安全 终止基于线程的服务在写程序时往往会用到日志,在代码中插入println也是一种日志行为。为了避免由于日志为服务带来性能损耗和并发风险(多个线程同时打印日志有可能引发并发问题),我们往往将打印日志任务放到某个队列中,由专门的线程从队列中取出任务进行打印。下面设计这样一个日志服务: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657public class LogService { private final BlockingQueue<String> queue; private final PrintWriter writer; private final LoggerThread thread; private boolean isShutDown = false; private int reservations = 0; public LogService(PrintWriter writer) { this.writer = writer; thread = new LoggerThread(); queue = new LinkedBlockingQueue<>(); } public void shutdown() { synchronized (this) { isShutDown = true; } thread.interrupt(); } public void log(String msg) throws InterruptedException { synchronized (this) { if (isShutDown) { throw new IllegalStateException("日志服务已经关闭..."); } reservations ++; } queue.put(msg); } class LoggerThread extends Thread { @Override public void run() { try { while (true) { try { synchronized (LogService.this) { if (isShutDown && reservations == 0) { break; } String msg = queue.take(); synchronized (LogService.this) { reservations--; } writer.println(msg); } } catch (InterruptedException e) { // retry } } } finally { writer.close(); } } }} 当关闭日志服务时,日志服务不再会接收新的日志打印请求,并且会将队列中剩余的所有打印任务执行完毕,最后结束。如果此时日志打印线程恰好在queue.take方法中阻塞了,关闭日志服务时也能很好的从阻塞中恢复过来,结束服务。]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[线程池异常处理方案]]></title>
<url>%2F2017%2F04%2F26%2F%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E6%96%B9%E6%A1%88%2F</url>
<content type="text"><![CDATA[最近一直在看《java并发编程实战》这本书,之前看过一点,放弃了,现在重新拿过来学习一下。这本书是老外写的,我买的是翻译版。不得不吐槽这本书的翻译,句子晦涩难懂,而且已经读到有好几处错误,差不多跟谷歌翻译一个水平…但是没办法,谁让咱英语差呢?多读几遍,结合jdk源码也能看个大概… 执行多线程并发任务的时候,如果任务类型相同,一般会考虑使用线程池,一方面利用了并发的优势,一方面避免创建大量线程得不偿失。使用线程池执行的任务一般是我们自己的代码,或者第三方的代码,有没有想过,如果这些代码抛出异常时,线程池会怎么处理呢?如果不处理又会有什么影响? 异常的影响Java 理论与实践: 嗨,我的线程到哪里去了?这篇文章列举了一个由于RuntimeException引发的线程泄漏问题: 考虑这样一个假设的中间件服务器应用程序,它聚合来自各种输入源的消息,然后将它们提交到外部服务器应用程序,从外部应用程序接收响应并将响应路由回适当的输入源。对于每个输入源,都有一个以其自己的方式接受其输入消息的插件(通过扫描文件目录、等待套接字连接、轮询数据库表等)。插件可以由第三方编写,即使它们是在服务器 JVM 上运行的。这个应用程序拥有(至少)两个内部工作队列 ― 从插件处接收的正在等待被发送到服务器的消息(“出站消息”队列),以及从服务器接收的正在等待被传递到适当插件的响应(“入站响应”队列)。通过调用插件对象上的服务例程 incomingResponse() ,消息被路由到最初发出请求的插件。 从插件接收消息后,就被排列到出站消息队列中。由一个或多个从队列读取消息的线程处理出站消息队列中的消息、记录其来源并将它提交给远程服务器应用程序(假定通过 Web 服务接口)。远程应用程序最终通过 Web 服务接口返回响应,然后我们的服务器将接收的响应排列到入站响应队列中。一个或多个响应线程从入站响应队列读取消息并将其路由到适当的插件,从而完成往返“旅程”。在这个应用程序中,有两个消息队列,分别用于出站请求和入站响应,不同的插件内可能也有另外的队列。我们还有几种服务线程,一个从出站消息队列读取请求并将其提交给外部服务器,一个从入站响应队列读取响应并将其路由到插件,在用于向套接字或其它外部请求源提供服务的插件中可能也有一些线程。 如果这些线程中的一个(如响应分派线程)消失了,将会发生什么?因为插件仍能够提交新消息,所以它们可能不会立即注意到某些方面出错了。消息仍将通过各种输入源到达,并通过我们的应用程序提交到外部服务。因为插件并不期待立即获得其响应,因此它仍没有意识到出了问题。最后,接收的响应将排满队列。如果它们存储在内存中,那么最终将耗尽内存。即使不耗尽内存,也会有人在某个时刻发现响应得不到传递 ― 但这可能需要一些时间,因为系统的其它方面仍能正常发挥作用。 当主要的任务处理方面由线程池而不是单个线程来处理时,对于偶然的线程泄漏的后果有一定程度的保护,因为一个执行得很好的八线程的线程池,用七个线程完成其工作的效率可能仍可以接受。起初,可能没有任何显著的差异。但是,系统性能最终将下降,虽然这种下降的方式不易被察觉。 服务器应用程序中的线程泄漏问题在于不是总是容易从外部检测它。因为大多数线程只处理服务器的部分工作负载,或可能仅处理特定类型的后台任务,所以当程序实际上遭遇严重故障时,在用户看来它仍在正常工作。这一点,再加上引起线程泄漏的因素并不总是留下明显痕迹,就会引起令人惊讶甚或使人迷惑的应用程序行为。 我们在使用线程池处理并行任务时,在线程池的生命周期当中,将通过某种抽象机制(Runnable)调用许多未知的代码,这些代码有可能是我们自己写的,也有可能来自第三方。任何代码都有可能抛出一个RuntimeException,如果这些提交的Runnable抛出了RuntimeException,线程池可以捕获他,线程池有可能会创建一个新的线程来代替这个因为抛出异常而结束的线程,也有可能什么也不做(这要看线程池的策略)。即使不会造成线程泄漏,我们也会丢失这个任务的执行情况,无法感知任务执行出现了异常。 所以,有必要处理提交到线程池运行的代码抛出的异常。 如何处理异常简单了解线程池 上面是我画的思维导图 先介绍一下jdk中线程池的实现: Executor定义了一个通用的并发任务框架,即通过execute方法执行一个任务。 ExecutorService定义了并发框架(线程池)的生命周期。 AbstractExecutorService、ThreadPoolExecutor、ScheduledThreadPoolExecutor实现了并发任务框架(线程池)。其中ScheduledThreadPoolExecutor支持定时及周期性任务的执行。 Executors相当于一个线程池工厂类,返回了不同执行策略的线程池对象。 我们一般使用Executors.new…方法来得到某种线程池: 1234567891011newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。 其中,前三者返回ExecutorService实例,他们的实现为ThreadPoolExecutor或其包装类;newScheduledThreadPool返回的是ScheduledExecutorService实例,他的实现为ScheduledThreadPoolExecutor或其包装类。 1ExecutorService exec = Executors.newFixedThreadPool(8); 以上述代码为例,得到ExecutorService实例后,我们可以通过两种方式提交任务(Runnable): exec.execute(runnable) exec.submit(runnable) 对于这两种不同的任务提交方式,我们有不同的异常处理办法。 exec.submit(runnable) 使用exec.submit(runnable)这种方式提交任务时,submit方法会将我们的Runnable包装为一个RunnableFuture对象,这个对象实际上是FutureTask实例,然后将这个FutureTask交给execute方法执行。 Future用来管理任务的生命周期,将Future实例提交给异步线程执行后,可以调用Future.get方法获取任务执行的结果。我们知道Runnable执行是没有返回结果的,那么这个结果是怎么来的? 可以看到,在FutureTask的构造方法中,将Runnable包装成了一个Callable类型的对象。 FutureTask的run方法中,调用了callable对象的call方法,也就调用了我们传入的Runnable对象的run方法。可以看到,如果代码(Runnable)抛出异常,会被捕获并且把这个异常保存下来。 可以看到,在调用get方法时,会将保存的异常重新抛出。所以,我们在使用submit方法提交任务的时候,利用返回的Future对象,通过他的get方法可以得到任务运行中抛出的异常,然后针对异常做一些处理。 由于我们在调用submit时并没有给Runnable指定返回结果,所以在将Runnable包装为Callable的时候,会传入一个null,故get方法返回一个null. 当然,我们也可以直接传入Callable类型的任务,这样就可以获取任务执行返回结果,并且得到任务执行抛出的异常。 这就是使用线程池时处理任务中抛出异常的第一种方法:使用ExecutorService.submit执行任务,利用返回的Future对象的get方法接收抛出的异常,然后进行处理 exec.execute(runnable)利用Future.get得到任务抛出的异常的缺点在于,我们需要显式的遍历Future,调用get方法获取每个任务执行抛出的异常,然后处理。 很多时候我们仅仅是使用exec.execute(runnable)这种方法来提交我们的任务。这种情况下任务抛出的异常如何处理呢? 在使用exec.execute(runnable)提交任务的时候(submit其实也是调用execute方法执行),我们的任务最终会被一个Worker对象执行。这个Worker内部封装了一个Thread对象,这个Thread就是线程池的工作者线程。工作者线程会调用runWorker方法来执行我们提交的任务:(代码比较长,就直接粘过来了) 12345678910111213141516171819202122232425262728293031323334353637383940414243final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock(); // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; try { task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); }} 上面代码的基本意思就是不停的从任务队列中取出任务执行,如果任务代码(task.run)抛出异常,会被最内层的try--catch块捕获,然后重新抛出。注意到最里面的finally块,在重新抛出异常之前,要先执行afterExecute方法,这个方法的默认实现为空,即什么也不做。我们可以在这个方法上做点文章,这就是我们的第二种方法,重写ThreadPoolExecutor.afterExecute方法,处理传递到afterExecute方法中的异常: 12345678910111213141516171819class ExtendedExecutor extends ThreadPoolExecutor { // ... protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); if (t == null && r instanceof Future<?>) { try { Object result = ((Future<?>) r).get(); } catch (CancellationException ce) { t = ce; } catch (ExecutionException ee) { t = ee.getCause(); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); // ignore/reset } } if (t != null) System.out.println(t); } } When actions are enclosed in tasks (such as FutureTask) either explicitly or via methods such as submit, these task objects catch and maintain computational exceptions, and so they do not cause abrupt termination, and the internal exceptions are not passed to this method. If you would like to trap both kinds of failures in this method, you can further probe for such cases, as in this sample subclass that prints either the direct cause or the underlying exception if a task has been aborted: 上面是java doc给出的建议。可以看到,代码中还处理了task是FutureTask的情况。回想一下submit方式提交任务的情况: 在submit方法中,我们传入的Runnable/Callable(要执行的任务)被封装为FutureTask对象,交给execute方法执行 经过一系列操作,提交的FutureTask对象被Worker对象中的工作者线程所执行,也就是runWorker方法 此时的代码运行情况:runWorker->submit方法封装的FutureTask的run方法->我们提交的Runnable的run方法 此时从我们提交的Runnable的run方法中抛出了一个未检测异常RunnableException,被FutureTask的run方法捕获 FutureTask的run方法捕获异常后保存,不再重新抛出。同时意味着run方法执行结束。 runWorker方法没有检测到异常,task.run当作正常运行结束。但是还是会执行afterExecute方法。 经过这样的梳理,上面的代码为什么这么写就一目了然了。 上面已经提到了两种解决任务代码抛出未检测异常的方案。接下来是第三种: 当一个线程因为未捕获的异常而退出时,JVM会把这个事件报告给应用提供的UncaughtExceptionHandler异常处理器,如果没有提供任何的异常处理器,那么默认的行为就是将堆栈信息输送到System.err。 看一下上面的runWorker方法,如果task.run(任务代码)抛出了异常,异常会层层抛出,最终导致这个线程退出。此时这个抛出的异常就会传递到UncaughtExceptionHandler实例当中,由uncaughtException(Thread t,Throwable e)这个方法处理。 于是就有了第三种解决任务代码抛出异常的方案:为工作者线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常 注意,这个方案不适用与使用submit方式提交任务的情况,原因上面也提到了,FutureTask的run方法捕获异常后保存,不再重新抛出,意味着runWorker方法并不会捕获到抛出的异常,线程也就不会退出,也不会执行我们设置的UncaughtExceptionHandler。 如何为工作者线程设置UncaughtExceptionHandler呢?ThreadPoolExecutor的构造函数提供一个ThreadFactory,可以在其中设置我们自定义的UncaughtExceptionHandler,这里不再赘述。 至于第四中方案,就很简单了:在我们提供的Runnable的run方法中捕获任务代码可能抛出的所有异常,包括未检测异常。这种方法比较简单,也有他的局限性,不够灵活,我们的处理被局限在了线程代码边界之内。 总结通过上面的分析我们得到了四种解决任务代码抛异常的方案: 在我们提供的Runnable的run方法中捕获任务代码可能抛出的所有异常,包括未检测异常 使用ExecutorService.submit执行任务,利用返回的Future对象的get方法接收抛出的异常,然后进行处理 重写ThreadPoolExecutor.afterExecute方法,处理传递到afterExecute方法中的异常 为工作者线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常 要注意的是,使用最后一种方案时,无法处理以submit的方式提交的任务。]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[土豆烧肉]]></title>
<url>%2F2017%2F04%2F19%2F%E5%9C%9F%E8%B1%86%E7%83%A7%E8%82%89%2F</url>
<content type="text"><![CDATA[原料土豆、五花肉、姜片、葱、酱油、料酒、盐、青椒、白糖、胡椒面 做法 五花肉切块洗净,土豆洗净、去皮,然后切块放到水中备用。青椒切片、葱切段、姜切片备用。 锅中水烧开,放两片姜片,倒入洗净的肉块,汆烫两分钟(注意时间不要太长),捞出肉块沥干备用 锅中倒油,油七成热后加入加入姜片、葱段、胡椒面爆香,然后倒入肉块煸炒(注意时间不要太长,否则肉会变老) 倒入酱油、精盐、料酒,倒入土豆青椒和肉块一起翻炒几下 加入清水(高汤)刚好没过肉块,加入两小勺盐,适当的撒一点白糖,煮沸后转小火,盖上锅盖烧20分钟 汤锅中汤汁差不多的时候,大火收汁,撒点葱末装盘 成品]]></content>
<categories>
<category>生活</category>
<category>食物</category>
</categories>
<tags>
<tag>食物</tag>
</tags>
</entry>
<entry>
<title><![CDATA[maven学习笔记(二)]]></title>
<url>%2F2017%2F04%2F19%2Fmaven%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B02%2F</url>
<content type="text"><![CDATA[依赖dependency标签可以声明以下一些元素: 1234567891011<dependency> <groupId>...</groupId> <artifactId>...</artifactId> <version>...</version> <scope>...</scope> <type>...</type> <optional>...</optional> <exclusions> <exclusion>...</exclusion> </exclusions></dependency> groupId、artifactId、version:声明了依赖的基本坐标 type: 依赖的类型,对应于项目坐标定义中的packaging,比如说jar scope:依赖的范围 optional:是否是可选依赖 exclusions: 排除传递性依赖 依赖范围(scope) Maven有三套classpath,编译项目主代码、编译测试代码、实际运行。依赖范围就是用来控制依赖与这三种classpath的关系.有以下几个选项: 依赖范围 对主代码classpath有效 对测试classpath有效 对运行时classpath有效 compile Y Y Y test N Y N provided Y Y N runtime N Y Y system Y Y N 传递性依赖 我们在项目的pom.xml文件中声明了直接依赖,如果声明的这些依赖还依赖于其他第三方组件,在maven中,我们不用考虑这些间接依赖,也不用担心引入多余的依赖。Maven会解析各个直接依赖的pom,将那些必要的间接依赖以传递性依赖的方式引入到当前项目的classpath中。 依赖范围不仅能够控制依赖与三种classpath的关系,还会对传递性依赖产生影响。比如设A依赖于B,B依赖于C,A对于B是第一直接依赖,B对于C是第二直接依赖,A对与C是传递性依赖。第一直接依赖与第二直接依赖的依赖范围决定了传递性依赖的依赖范围。如下图,最左边第一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间交叉单元格表示传递性依赖范围 compile test provided runtime compile compile runtime test test test provided provided provided provided runtime runtime runtime 比如,account-email 项目有一个com.icegree:greemail:1.3.1b的直接依赖,这是第一直接依赖,其依赖范围是test;而greemail又有一个javax.mail:mail:1.4的直接依赖,这是第二直接依赖,其依赖的范围是compile。根据上面的表格可以推断,javax.mail:mail:1.4是account-email的一个范围是test的传递依赖。 依赖调解 对于一个构件,存在于不同依赖路径上。选择哪个路径上的构件就是依赖调解要解决的问题,有两种策略: 路径近者优先。 比如,项目A有这样的依赖关系:A->B->C->x(1.0)、A->D->X(2.0),对于两个版本的X,如果都引入就会造成依赖重复。根据路径近者优先的策略,X(2.0)会被引入; 第一声明者优先。 路径近者优先的策略不能解决所有问题,如果出现路径长度相同的情况,那么maven就会选择依赖声明在前的那个路径上的版本。 可选依赖(optional) 假设有这样的依赖关系:项目A依赖于项目B,项目B依赖于项目X,Y,B对于X和Y都是可选依赖:A-B、B->X(可选)、B->Y(可选)。那么此时,由于X、Y都是可选依赖,依赖性将不会传递,也就是说,A中不会引入X和Y。 有这样一种情况符合可选依赖的场景:项目B是一个持久层的工具包,支持多种数据库,X、Y就是其依赖的数据库驱动程序,但是我们的项目A在使用这个工具包B的时候,只依赖一种数据库,故我们不需要将X和Y全部引入。这种情况下需要我们在项目A中声明实际使用的数据库驱动依赖。 排除依赖(exclusions) 传递性依赖为我们的项目隐式的引入了很多依赖,如果我们不想引入某个传递性依赖(自己选择依赖版本),就可以使用排除依赖。 12345678910111213141516<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.7</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>1.7.7</version><exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion></exclusions></dependency> 上面的代码声明了一个排除依赖。我们的项目所依赖的slf4j-log4j12会引入slf4j-api这个传递性依赖。出于版本或者其他考虑,现在不想引入这个依赖,而是由我们显示的声明,那么就可以像上面的代码那样做。 查看依赖 通过执行mvn dependency:list可以查看项目已经解析的依赖;通过执行mvn dependency:tree可以查看项目的依赖树;通过执行mvn dependency:analyze可以查看项目中没有使用,却显示声明的依赖和项目中显示使用了却没有显示声明的依赖。 在Eclispe中,可以双击pom.xml,在Dependencies和Dependency Hierarchy选项卡查看项目的依赖情况。 仓库 仓库分类 对于maven来说,仓库分为两类:本地仓库和远程仓库。本地仓库即存在于我们本地机器上的构件仓库,远程仓库就是远程机器上的构件仓库。我们的依赖(jar)都是从仓库当中下载得到的。 当maven对我们的项目执行编译或者测试时,如果需要使用依赖文件,他总是基于坐标使用本地仓库的依赖文件。如果本地仓库存在此构件,则直接使用;如果本地仓库不存在此构件,或者需要更新的版本,maven会去远程仓库查找,发现需要的构件之后,下载到本地仓库再使用。如果本地仓库和远程仓库都没有需要的构件,则报错。 中央仓库是Maven核心自带的远程仓库,包含了绝大部分开源构件。默认情况下,当本地仓库没有maven需要的构件时,他就会从中央仓库下载。 仓库配置 在linux上本地仓库的默认路径为:~/.m2/repository/ 如果想要自定义本地仓库地址,可以编辑 ~/.m2/setting.xml, 设置localRepository元素值为想要的地址: 1<localRepository>/path/to/local/repo</localRepository> 某些情况下,中央仓库无法满足项目需求,项目需要的构件可能存在于另一个远程仓库上,这是,可以在pom中配置该仓库: 12345678910111213 <repositories> <repository> <id>JBOSS</id> <name>Jboss</name> <url>http://repository.jnoss.com/maven2</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository></repositories> 上面的例子中声明了一个id为JBOSS的远程仓库,任何一个仓库声明的id都必须是唯一的。release的enable值为true,表示开启Jboss仓库发布版本的下载支持;snapshots的enable值为false,表示关闭Jboss仓库快照版本的下载支持。 可以声明多个远程仓库,maven会遍历这些仓库去查找所需的构件。 聚合与继承 聚合 一个项目中往往不止一个模块,比如有core、util等等模块的划分。每个模块是一个独立的工程,提供了对外的接口供调用,各个模块之间有相互依赖的关系。那么,如果我们想要一次性构建项目中的两个两个模块,而不是到两个模块各自的目录下面执行mvn命令,这时候就需要maven的聚合。 为了能够使用一条命令就构建core和util两个模块,我们需要额外创建一个名为aggregator的模块,然后通过该模块构建整个项目的所有模块。aggregator本身作为一个maven项目,必须有自己的pom: 123456789101112 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.test.app</groupId><artifactId>app-aggregator</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><modules> <module>app-core</module> <module>app-util</module></modules> </project> 对于聚合模块来说,packaging的值必须为pom,否则无法构建。用户可以通过在一个打包为pom的Maven项目中声明任意数量的module元素来实现模块的聚合。 这里每一个module的值都是一个当前pom的相对目录。比如app-aggregator的pom路径为~/app/app-aggregator/pom.xml,那么app-core就对应目录~/app/app-aggregator/app-core/,app-util就对应目录~/app/app-aggregator/app-util/,这两个目录各自包含pom.xml、src/main/java等内容,可以独立构建。 从聚合模块运行mvn命令,maven就会解析聚合模块的pom,分析要构建的模块,并计算出一个构建顺序,然后根据这个顺序依次构建各个模块。 继承 在面向对象的设计中,可以在父类中声明一些字段,由子类继承使用。类似的,pom也可以声明这样一种父子结构。 我们继续在~/app/app-aggregator/目录下创建一个名为app-parent的子目录,在该子目录中声明一个pom: 123456789 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.test.app</groupId><artifactId>app-parent</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><name>App parent</name> </project> 修改app-core继承app-parent 12345678910111213 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent> <groupId>com.test.app</groupId> <artifactId>app-parent</artifactId> <version>1.0-SNAPSHOT</version> <relativePath>../app-parent/pom.xml</relativePath></parent><artifactId>app-core</artifactId><packaging>pom</packaging><name>App core</name> </project> 上述pom中parent元素声明父模块,groupId、artifactId、version指定了父模块的坐标。relativePath指定了父模块pom的相对路径。 app-core中没有声明version和groupId,他隐式的从父模块继承了这两个元素。 下面是可继承的pom元素: 12345678910111213141516171819groupId :项目组 ID ,项目坐标的核心元素; version :项目版本,项目坐标的核心元素; description :项目的描述信息; organization :项目的组织信息; inceptionYear :项目的创始年份; url :项目的 url 地址 develoers :项目的开发者信息; contributors :项目的贡献者信息; distributionManagerment :项目的部署信息; issueManagement :缺陷跟踪系统信息; ciManagement :项目的持续继承信息; scm :项目的版本控制信息; mailingListserv :项目的邮件列表信息; properties :自定义的 Maven 属性; dependencies :项目的依赖配置; dependencyManagement :醒目的依赖管理配置; repositories :项目的仓库配置; build :包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等; reporting :包括项目的报告输出目录配置、报告插件配置等。 依赖管理与插件管理 上面的列表中包含了dependencies元素,表示依赖会被继承。因此,我们可以将模块的公有依赖配置到父模块pom中,子模块就可以移除这些依赖,但这样带来一个问题,如果我们新增了模块也继承父模块的话,新增的子模块也就有可能引入了他不需要的依赖。 为了解决这个问题,maven提供了dependencyManagement元素。在dependencyManagement元素下声明的依赖不会被实际引入:app-parent/pom.xml 12345678910111213141516171819 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.test.app</groupId><artifactId>app-parent</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><name>App parent</name><dependencyManagement> <dependencies> <dependency> <groupId>com.googlecode.java-diff-utils</groupId> <artifactId>diffutils</artifactId> <version>1.2.1</version> </dependency> </dependencies></dependencyManagement> </project> 使用dependencyManagement声明的依赖既不会给app-parent引入依赖,也不会给他的子模块引入依赖,不过这段配置会被继承:app-core/pom.xml 1234567891011121314151617181920 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent> <groupId>com.test.app</groupId> <artifactId>app-parent</artifactId> <version>1.0-SNAPSHOT</version> <relativePath>../app-parent/pom.xml</relativePath></parent><artifactId>app-core</artifactId><packaging>pom</packaging><name>App core</name><dependencies> <dependency> <groupId>com.googlecode.java-diff-utils</groupId> <artifactId>diffutils</artifactId> </dependency></dependencies> </project> 可以看到子模块只需要声明dependency的groupId、artifactId,不用声明version,虽然只省去了一行配置,但是统一了项目依赖的版本,降低依赖冲突的机会。 如果子模块没有声明diffutils,diffutils就不会被引入。 maven还提供了pluginManagement元素帮助管理插件。与dependencyManagement的原理相同。 超级pom 实际上,我们声明的pom都隐式的继承自超级pom,位于MAVEN_HOME/lib/maven-model-builder-x.jar中的org/apache/maven/model/pom-4.0.0.xml。 在超级pom中定义了仓库和插件仓库,都是中央仓库的地址,定义了项目的主代码目录,测试目录等等项目的结构。 我们可以模仿超级pom在我们自己的pom中声明这些元素,从而自定义项目结构。 属性与Profile属性属性有点类似于java中的变量。我们可以在pom中使用${属性名}的方式引用属性的值,从而消除重复,也能降低错误发生的概率。 maven属性有6类: 内置属性 主要有${basedir}表示项目根目录,即包含pom.xml的目录;${version}表示项目的版本 pom属性 ${project.build.directory}表示主源码路径; ${project.build.sourceEncoding}表示主源码的编码格式; ${project.build.sourceDirectory}表示主源码路径; ${project.build.finalName}表示输出文件名称; ${project.version}表示项目版本,与${version}相同; 自定义属性 123<properties><project.my>hello</project.build.sourceEncoding></properties> 在pom中的其他地方使用${project.my}就会被替换成hello settings属性 与pom属性同理,用户以settings开头的属性引用settings.xml文件中的xml元素值,如${settings.localRepository}指向本地仓库地址 java系统属性 所有java系统属性都可以使用maven属性引用。如${user.home}指向用户目录 环境变量属性 所有环境变量都可以用以env开头的属性引用。如${env.JAVA_HOME} 资源过滤一般情况下,我们习惯于在src/main/resources/目录下放置配置文件,在配置文件中,我们可能配置数据库的url,用户名密码等信息。但是在不同的环境中,这些数据库的配置常常会变动,比如在测试环境或者运行环境中。比较原始的做法手动更改这些配置,但是这样的方法比较低下也容易出错。maven可以在构建过程中针对不同的环境激活不同的配置。 首先需要使用maven属性将会发生变化的部分提取出来:在数据库配置文件中 12db.jdbc.driver=${db.driver}db.jdbc.url=${db.url} 这里定义了两个属性:db.driver、db.url 既然定义了maven属性,我们需要在某个地方为其赋值。与自定义属性不同的是,这里需要做的是使用profile将其包裹: 123456789<profiles> <profile> <id>pro_A</id> <properties> <db.driver>com.mysql.jdbc.Driver</db.driver> <db.url>jdbc:mysql://localhost:3306</db.url> </properties> </profile></profiles> 那么这个profile是在哪里声明的呢?有三个地方: pom.xml: pom中的profile只对当前项目有效 用户settings.xml: 用户目录下.m2/settings.xml中的profile对该用户的所有maven项目有效 全局settings.xml: maven安装目录下conf/settings.xml中的profile对本机上所有maven项目有效 在配置文件中定义了maven属性,也在profile中为其赋值了,此时要做的是打开资源过滤: 资源文件的处理实际上是maven-resources-plugin插件所做的事情,他的默认行为只是将项目主资源文件复制到主代码编译输出目录中,将测试资源文件复制到测试代码编译输出目录中。我们需要一些配置,使得该插件能够解析资源文件中的maven属性,开启资源过滤。 maven默认的资源目录是在超级pom中定义的,要开启资源目录过滤,需要如下配置: 12345678<build> <resources> <resource> <directory>${project.basedir}/src/main/resources</directory> <filtering>true</filtering> </resource> </resources></build> 上述代码为主资源目录开启了资源过滤,类似的我们可以为多个资源目录提供过滤配置:123456789101112<build> <resources> <resource> <directory>${project.basedir}/src/main/resources</directory> <filtering>true</filtering> </resource> <resource> <directory>${project.basedir}/src/main/sql</directory> <filtering>true</filtering> </resource> </resources></build> 在配置文件中定义了maven属性,在profile中为属性赋值,并且为资源目录开启了资源过滤,接下来只需要在命令行激活profile:mvn clean test -Ppro_A. mvn命令中的-P参数激活了一个名为pro_A的profile。maven在构建项目的时候就会使用profile中的属性值替换在配置文件中的属性定义,然后将其复制到编译输出目录当中。 profile我们可以想到,针对不同的环境定义不同的profile,然后在不同的环境中通过命令行激活对应的profile,就能达到灵活切换配置的目的。除了命令行手动激活profile以外,还有下面几种方式能够激活profile: 默认激活 123456789101112 <profiles> <profile> <id>pro_A</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <db.driver>com.mysql.jdbc.Driver</db.driver> <db.url>jdbc:mysql://localhost:3306</db.url> </properties> </profile></profiles> activeByDefault指定profile自动激活。但是如果pom中的任意一个profile通过其他方式被激活了,那么默认的激活配置失效。 属性激活 123456789101112131415 <profiles> <profile> <id>pro_A</id> <activation> <property> <name>test</name> <value>x</value> </property> </activation> <properties> <db.driver>com.mysql.jdbc.Driver</db.driver> <db.url>jdbc:mysql://localhost:3306</db.url> </properties> </profile></profiles> 属性test存在且值为x时激活该profile。利用这个特性可以在命令行同时激活多个profile:mvn clean test -Dtest=x settings文件激活 12345<settings> <activeProfiles> <activeProfile>pro_x</activeProfile> </activeProfiles></settings> 在settings.xml文件中配置,表示其配置的profile对所有项目处于激活状态。 在profile中不仅可以添加或者修改maven属性,还可以对其他maven元素进行设置。 pom中的profile可以使用的元素: 123456789101112131415 <repositories></repositories><pluginRepositories></pluginRepositories><distributionManagement></distributionManagement><dependencies></dependencies><dependencyManagement></dependencyManagement><modules></modules><properties></properties><reporting></reporting><build> <plugins></plugins> <defaultGoal></defaultGoal> <resources></resources> <testResources></testResources> <finalName></finalName></build> 其他profile可以使用的元素 123<repositories></repositories><pluginRepositories></pluginRepositories><properties></properties>]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>java</tag>
<tag>maven</tag>
</tags>
</entry>
<entry>
<title><![CDATA[maven学习笔记(一)]]></title>
<url>%2F2017%2F04%2F17%2Fmaven%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[上周末两天时间基本上将《Maven实战》这本书看完了。《Maven实战》是很棒的一本介绍maven相关知识的书籍。读完之后,对学到的maven相关的内容做一个梳理总结。 Maven基础有关Maven如何安装和设置,不在这里啰嗦了,可以到官网下载最新版本然后安装。 Maven是一个异常强大的构建工具,能够帮助我们自动化构建过程,从清理、编译、测试到生成报告,打包和部署。我们要做的只是使用Maven配置好项目,然后输入简单的命令,Maven会帮我们处理这些任务。 Maven项目的核心是pom.xml。Pom定义了项目的基本信息,用于描述项目如何构建,声明项目依赖等。下面是一个例子:123456789101112131415161718192021222324252627282930313233343536373839404142434445<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>space.yukai.mergetool</groupId> <artifactId>mergetool-core</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>MergeTool</name> <name>space.yukai</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.4</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.5</version> </dependency> </dependencies></project> Maven不仅是一个构建工具,还是一个依赖管理工具和项目信息管理工具。我们的java项目或多或少都会依赖一些第三方的类库,而maven提供了中央仓库,能够帮助我们自动下载构件。 Maven通过一个坐标系统准确的定位每一个构件(artifact),也就是通过一组坐标Maven能够找到任何一个java类库(如jar包)。 在Maven的世界中,任何的jar、pom、或war都是通过这些坐标进行区分的。上面代码中开头的groupId、artifactId、version定义了这个项目的基本坐标。 groupId定义了项目属于哪个组,这个组往往和项目所在的组织有关联。比如Googlecode上面建立了一个项目app,那么groupId就应该为com.googlecode.app artifactId定义了当前项目在组中的唯一Id。比如为app中不同的子项目分配artifactId,如app-core、app-util、app-web version指定了项目当前的版本。SNAPSHOT表示项目还处于开发中,是不稳定的版本 packaging指定了项目的打包方式,默认为jar name定义了项目名称 上面dependency标签中的内容就是本项目声明的依赖。可以看到dependency标签中声明了groupId、artifactId、version这三个属性,比如commons-io,Maven解析到这个依赖时,就会根据这个坐标去本地仓库查找这个坐标下的依赖是否已经被下载,如果没有下载,那么到中央仓库去下载依赖的jar包到本地仓库,然后把下载好的jar包路径添加到classpath当中。这些工作都是自动进行的,我们要做的,只是在pom.xml中声明这个依赖即可。 Maven在项目构建过程和过程的各个阶段的工作都是由插件实现的,并且大部分的插件都是现成的。我们只需要声明项目的基本元素,Maven就会执行内置的构建过程。上面代码中plugin标签中的内容,是对maven-compiler-plugin这个插件进行的配置。 ArchetypeMaven提倡 “约定优于配置”。遵循Maven的一些约定,我们可以快速的创建项目并完成构建。对于遵循约定的Maven项目,我们可以快速的了解其结构,减轻了我们的学习成本。 比如:在项目的根目录放置pom.xml,在src/main/java目录放置项目主代码,在src/test/java放置项目测试代码。我们称这些基本的目录结构为项目骨架。通过Archetype插件可以帮助我们快速的创建出项目骨架。 运行mvn archetype:generate命令,选择我们的项目骨架。 1234567891011121314151617181920212223242526272829303132C:\Users\kyu\Desktop\test>mvn archetype:generate[INFO] Scanning for projects...[INFO][INFO] ------------------------------------------------------------------------[INFO] Building Maven Stub Project (No POM) 1[INFO] ------------------------------------------------------------------------[INFO][INFO] >>> maven-archetype-plugin:3.0.1:generate (default-cli) @ standalone-pom >>>[INFO][INFO] <<< maven-archetype-plugin:3.0.1:generate (default-cli) @ standalone-pom <<<[INFO][INFO] --- maven-archetype-plugin:3.0.1:generate (default-cli) @ standalone-pom ---[INFO] Generating project in Interactive mode[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)Choose archetype:1: remote -> am.ik.archetype:maven-reactjs-blank-archetype (Blank Project for React.js)2: remote -> am.ik.archetype:msgpack-rpc-jersey-blank-archetype (Blank Project for Spring Boot + Jersey)3: remote -> am.ik.archetype:mvc-1.0-blank-archetype (MVC 1.0 Blank Project)4: remote -> am.ik.archetype:spring-boot-blank-archetype (Blank Project for Spring Boot)5: remote -> am.ik.archetype:spring-boot-docker-blank-archetype (Docker Blank Project for Spring Boot)6: remote -> am.ik.archetype:spring-boot-gae-blank-archetype (GAE Blank Project for Spring Boot)7: remote -> am.ik.archetype:spring-boot-jersey-blank-archetype (Blank Project for Spring Boot + Jersey)8: remote -> at.chrl.archetypes:chrl-spring-sample (Archetype for Spring Vaadin Webapps)9: remote -> br.com.address.archetypes:struts2-archetype (an archetype web 3.0 + struts2 (bootstrap + jquery) + JPA 2.1 with struts2 login system)10: remote -> br.com.address.archetypes:struts2-base-archetype (An Archetype with JPA 2.1; Struts2 core 2.3.28.1; Jquery struts plugin; Struts BootStrap plugin; json Struts plugin;...1813: remote -> us.fatehi:schemacrawler-archetype-plugin-command (-)1814: remote -> us.fatehi:schemacrawler-archetype-plugin-dbconnector (-)1815: remote -> us.fatehi:schemacrawler-archetype-plugin-lint (-)Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): 955: 默认为maven-archetype-quickstart(955)。选择该Archetype,Maven会提示输入创建项目的groupId、artifactId、version及包名package。 在当前目录下,Archetype插件会创建以artifactId命名的子目录: 可以看出,使用约定俗成的Archetype,不仅使用Maven插件代替了原本需要手工处理的劳动,同时节省了时间,降低了错误发生的概率。 也可以定义自己的项目骨架,在创建项目的时候,就可以直接使用该Archetype。 生命周期Maven的生命周期对所有的构建过程进行了抽象和统一。这个生命周期包括项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等所有的构建步骤。所有的项目构建,都可以映射到这样一个生命周期上。 Maven只是抽象出了生命周期,生命周期中实际的任务都是交给插件完成的,类似于设计模式中的模板方法模式。 Maven拥有三套独立的生命周期:clean、default、site。每个生命周期包含一些阶段: clean pre-clean 执行一些清理前需要完成的工作 clean 清理上一次构建生成的文件 post-clean 执行一些清理后需要完成的工作 default validate 检查工程配置是否正确,完成构建过程的所有必要信息是否能够获取到。 initialize 初始化构建状态,例如设置属性。 generate-sources 生成编译阶段需要包含的任何源码文件。 process-sources 处理源代码,例如,过滤任何值(filter any value)。 generate-resources 生成工程包中需要包含的资源文件。 process-resources 拷贝和处理资源文件到目的目录中,为打包阶段做准备。 compile 编译工程源码。 process-classes 处理编译生成的文件,例如 Java Class 字节码的加强和优化。 generate-test-sources 生成编译阶段需要包含的任何测试源代码。 process-test-sources 处理测试源代码,例如,过滤任何值(filter any values)。 test-compile 编译测试源代码到测试目的目录。 process-test-classes 处理测试代码文件编译后生成的文件。 test 使用适当的单元测试框架(例如JUnit)运行测试。 prepare-package 在真正打包之前,为准备打包执行任何必要的操作。 package 获取编译后的代码,并按照可发布的格式进行打包,例如 JAR、WAR 或者 EAR 文件。 pre-integration-test 在集成测试执行之前,执行所需的操作。例如,设置所需的环境变量。 integration-test 处理和部署必须的工程包到集成测试能够运行的环境中。 post-integration-test 在集成测试被执行后执行必要的操作。例如,清理环境。 verify 运行检查操作来验证工程包是有效的,并满足质量要求。 install 安装工程包到本地仓库中,该仓库可以作为本地其他工程的依赖。 deploy 拷贝最终的工程包到远程仓库中,以共享给其他开发人员和工程。 site pre-site 执行一些生成项目站点前需要完成的工作 site 生成项目站点文档 post-site 执行一些生成项目站点前需要完成的工作 site-deploy 将生成的站点发布到服务器 每个生命周期的阶段是有顺序的,后面的阶段依赖前面的阶段。使用maven最直接的交互方式就是调用这些生命周期的阶段。比如,当用户调用pre-clean时,只有pre-clean阶段得以执行,当调用clean时,pre-clean和clean依次执行,当调用post-clean时,pre-clean、clean和post-clean依次执行。 虽然每个生命周期内的阶段是有顺序和前后依赖的,但是三套生命周期之间是互相独立的。比如,当用户调用clean生命周期的clean阶段时,不会触发default生命周期的任何阶段,反之,当用户调用default生命周期的compile阶段时,也不会触发clean生命周期的任何阶段。 mvn clean install,该命令调用了clean生命周期clean阶段与default生命周期install 阶段。实际执行的阶段为clean生命周期的pre-clean、clean阶段,以及default生命周期从validate到install所有阶段。 插件目标Maven的核心仅仅定义了抽象的生命周期,具体的任务是由插件来完成的。 每个插件可以完成多个功能,每个功能就是插件的一个目标。比如maven-dependency-plugin可以列出项目依赖树,列出所有已解析的依赖等等。这两个目标对应的命令为: mvn dependency:tree,mvn dependency:list。冒号前面是插件前缀,冒号后面是插件目标。 所以,我们知道了mvn命令有两种调用方式,一种调用其生命周期,一种直接调用插件目标。 插件的生命周期与插件相互绑定,完成实际的构建任务。调用生命周期实际上也是执行了多个插件目标。 内置绑定 Maven核心已经对一些主要的生命周期阶段绑定了很多插件目标,当用户通过命令行调用生命周期的时候,对应的插件目标就会执行相应的任务。 比如,clean生命周期有pre-clean、clean、post-clean三个阶段,clean生命周期阶段与插件目标的绑定关系如下: 生命周期阶段 插件目标 pre-clean clean maven-clean-plugin:clean post-clean 执行命令mvn post-clean,输出如下: 123456789101112[INFO] ------------------------------------------------------------------------[INFO] Building space.yukai 0.0.1-SNAPSHOT[INFO] ------------------------------------------------------------------------[INFO] [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ space.yukai ---[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time: 0.409 s[INFO] Finished at: 2017-04-17T16:37:08+08:00[INFO] Final Memory: 7M/106M[INFO] ------------------------------------------------------------------------ 从输出中可以看到,执行的插件目标仅为maven-clean-plugin:clean 自定义绑定 除了内置绑定之外,用户可以自己选择将某个插件目标绑定到生命周期的某个阶段。 比如,我们可以自行配置创建项目的源码jar包。maven-source-plugin的jar-no-fork目标可以将项目的主代码打包成jar文件。我们将其绑定到clean生命周期的post-clean阶段测试一下: 12345678910111213<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-source-plugin</artifactId><executions> <execution> <id>attach-source</id> <phase>post-clean</phase> goals> <goal>jar-no-fork</goal> </goals> </execution></executions></plugin> 运行mvn post-clean,输出: 12345678910111213141516[INFO] ------------------------------------------------------------------------[INFO] Building space.yukai 0.0.1-SNAPSHOT[INFO] ------------------------------------------------------------------------[INFO] [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ space.yukai ---[INFO] Deleting Y:\code\versionmergetool\VersionMergeTool\target[INFO] [INFO] --- maven-source-plugin:3.0.1:jar-no-fork (attach-source) @ space.yukai ---[INFO] Building jar: Y:\code\versionmergetool\VersionMergeTool\target\space.yukai-0.0.1-SNAPSHOT-sources.jar[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time: 1.082 s[INFO] Finished at: 2017-04-17T16:55:32+08:00[INFO] Final Memory: 7M/111M[INFO] ------------------------------------------------------------------------ 可以看到,当执行post-clean阶段的时候,maven-source-plugin:jar-no-fork 创建了一个以-sources.jar结尾的源码包。 为了了解插件有哪些配置,我们可以使用maven-help-plugin来获取插件信息。 运行命令mvn help:describe -Dplugin=compiler 123456789101112131415161718192021222324252627282930313233343536373839[INFO] Scanning for projects...[INFO][INFO] ------------------------------------------------------------------------[INFO] Building mvntest-test 1.0-SNAPSHOT[INFO] ------------------------------------------------------------------------[INFO][INFO] --- maven-help-plugin:2.2:describe (default-cli) @ mvntest-test ---[INFO] org.apache.maven.plugins:maven-compiler-plugin:3.6.1Name: Apache Maven Compiler PluginDescription: The Compiler Plugin is used to compile the sources of your project.Group Id: org.apache.maven.pluginsArtifact Id: maven-compiler-pluginVersion: 3.6.1Goal Prefix: compilerThis plugin has 3 goals:compiler:compile Description: Compiles application sourcescompiler:help Description: Display help information on maven-compiler-plugin. Call mvn compiler:help -Ddetail=true -Dgoal=<goal-name> to display parameter details.compiler:testCompile Description: Compiles application test sources.For more information, run 'mvn help:describe [...] -Ddetail'[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time: 5.944s[INFO] Finished at: Mon Apr 17 17:02:52 CST 2017[INFO] Final Memory: 11M/110M[INFO] ------------------------------------------------------------------------ 可以得到插件maven-compiler-plugin的信息及相关目标。还可以加上detail参数输出更详细的信息mvn help:describe -Dplugin=compiler -Ddetail 前缀 笔记开头说了,在maven的世界中,所有的构件都是由坐标来确定的,构件包括依赖的jar或者插件。那么我们上面的mvn help:describe -Dplugin=compiler中并没有指定插件的坐标,为什么能够正常的运行呢? 其实mvn help:describe -Dplugin=compiler就等效于mvn org.apache.maven.plugins:maven-help-plugin:2.2:describe -Dplugin=compiler。help就是maven-help-plugin的前缀,maven能够根据这个前缀找到对应的artifactId,从而解析得到groupId和version,所有能够精确的定位某个插件。compiler也是前缀。]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>java</tag>
<tag>maven</tag>
</tags>
</entry>
<entry>
<title><![CDATA[木耳炒山药]]></title>
<url>%2F2017%2F04%2F16%2F%E6%9C%A8%E8%80%B3%E7%82%92%E5%B1%B1%E8%8D%AF%2F</url>
<content type="text"><![CDATA[天气越来越热了,今天炒了一个比较清淡的菜–木耳炒山药。 原料长山药、黑木耳、青椒、食用油、鸡精、盐、淀粉、葱、蒜、胡椒面 做法 山药切片,入锅钞水后捞出备用 黑木耳温水浸泡 少量淀粉和水勾兑 青椒切片(我还把剩下的胡萝卜丝搁一块了),切好葱蒜备用 锅内倒入少量油,油热后放入葱蒜胡椒面炸香 倒入钞好的山药翻炒,随后加入青椒和木耳一起翻炒 加入适量的盐和鸡精,随后将备勾兑好的水淀粉倒入 翻炒片刻,淋少许油出锅 成品]]></content>
<categories>
<category>生活</category>
<category>食物</category>
</categories>
<tags>
<tag>食物</tag>
</tags>
</entry>
<entry>
<title><![CDATA[hexo博客备份方案]]></title>
<url>%2F2017%2F04%2F11%2Fhexo%E5%8D%9A%E5%AE%A2%E5%A4%87%E4%BB%BD%E6%96%B9%E6%A1%88%2F</url>
<content type="text"><![CDATA[周末的时候给博客换了一个主题,现在的博客看起来比之前的要清爽多了。 hexo是把生成的一套html发布到服务器上面的,我使用了github来托管自己的博客,每次发布时只把生成的html等文件发布到github,源代码并不会一同发到上面。 如果是换电脑的话就很不方便了,再加上之前使用hexo generate -d发布博客的时候出了点问题,所以抽空写了一个专门用来发布博客和保存源代码的脚本,在此记录。 脚本12345678910111213141516171819202122232425262728# hexo generate -d 命令失效,将hexo分支推送到了master分支。使用此脚本进行部署# 将此脚步置于与blog同级目录下。# 部署root="/home/yukai/project/blog"folder="$root/blogdeploy"blog="$root/Hikyu.github.io"if [ ! -d "$folder" ]; then echo "初始化..." mkdir "$folder" cp -R "$blog/.git/" "$folder/.git/" cd "$folder" git checkout masterfiecho "博客生成..."cd "$blog"git checkout hexohexo generatecp -R "$blog"/public/* "$folder"echo "博客发布..."cd "$folder"git add --all .git commit -m 'update'git push origin masterecho "备份博客源码到hexo分支..."cd "$blog"git add --all .git commit -m 'update'git push origin hexo 将上述脚本保存为deploy.sh。 使用时需要将变量root设为博客目录的父目录。 如果博客目录还不存在(换电脑),需要使用git clone命令把博客源代码下载下来。 修改博客源码后,执行 sudo ./deploy.sh进行博客发布与备份。 原理 发布博客 hexo generate -d 可以将博客编译后发布到服务器。其原理就是将源代码中public目录下的内容推送到远程分支上面。 我们使用master分支保存发布的博客(github也规定了要发布的博客必须为master分支)。 首先建立推送博客的目录$folder,并且将博客目录$blog下.git文件夹拷贝到$folder; 切换到博客目录$blog,然后生成博客,将生成的public目录的内容拷贝到$folder; 将$folder中的内容推送到远程分支master,完成发布。 源码备份 使用新的分支hexo来保存我们的博客源代码。 切换到博客目录$blog,并切换分支到hexo; 将博客目录$blog中的源代码内容推动到远程分支hexo,完成备份。]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>hexo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[糖醋里脊]]></title>
<url>%2F2017%2F04%2F09%2F%E7%B3%96%E9%86%8B%E9%87%8C%E8%84%8A%2F</url>
<content type="text"><![CDATA[今天给女朋友做了一道菜:糖醋里脊。如图,还算是比较成功的~ 原料猪里脊肉、白糖、醋、料酒、盐、番茄酱、葱蒜、胡椒粉、面粉、淀粉 制作方法 将醋、糖、料酒、盐,少许水和淀粉混合成调味汁儿备用,比例糖4醋3料酒1,水、盐适量。 将里脊切成粗条状,放入碗中。加入盐、胡椒粉、料酒抓匀后腌制20分钟。 将腌制好的里脊肉沥水后倒入碗中,打入一个鸡蛋,抓匀。 将面粉(可以加点淀粉)倒入碗中,依次将里脊肉放入面粉中,保证里脊粘上一层面粉。 锅中加入油,烧至六成热,转中小火,逐个将粘上面粉的肉条放入,炸约一分钟捞出沥油。 将锅里的油继续加热,油温八九成热时,倒入肉条复炸片刻至金黄色酥脆后,捞出沥油。 锅中留少许底油加热,爆香葱蒜末。加入番茄酱炒香。加入调好的糖醋汁煮至浓稠。 下入炸好的里脊条快速翻匀,出锅装盘。 成品]]></content>
<categories>
<category>生活</category>
<category>食物</category>
</categories>
<tags>
<tag>食物</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java类加载]]></title>
<url>%2F2017%2F04%2F06%2Fjava%E7%B1%BB%E5%8A%A0%E8%BD%BD%2F</url>
<content type="text"><![CDATA[本篇笔记的目标是理解类加载器的架构,学会实现类加载器并理解热替换的底层原理。 什么是类加载 类从被加载到虚拟机内存中开始,到卸载出内存为止,包括了以下几个生命周期: 什么时候会触发类加载的第一个阶段(加载)?虚拟机规范没有强制规定,这一点依据不同的虚拟机实现来定。但对于初始化阶段,虚拟机规范规定了有且只有5种>情况必须立即对类进行初始化(加载阶段自然要在此之前开始): 1.使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰的常量字段除外)、调用一个类的静态方法。 2.使用反射方法对类进行调用 3.初始化一个类的时候,发现其父类未初始化,则触发父类的初始化 4.虚拟机启动时,用户需指定一个要执行的主类(包含main的那个类),虚拟机先初始化该类 5.当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke。MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄所对应的类没有进行过初始化,则先触发其初始化(不懂…) –《深入理解jvm虚拟机》 这篇笔记所要学习的内容,仅仅是类加载的第一个阶段:加载。在加载阶段,虚拟机会完成下面三件事: 1.通过一个类的全限定名获取定义此类的二进制字节流 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口 在上面的三个阶段中,通过一个类的全限定名获取定义此类的二进制字节流 是开发人员可以控制的部分,也是我们这篇笔记所要探讨的内容。 虚拟机设计团队将通过一个类的全限定名获取定义此类的二进制字节流这个动作放到java虚拟机外部去实现,以便让应用程序自己决定去如何获取所需要的类。实现这个动作的代码模块被称为”类加载器”。定义此类的二进制字节流可以来自class文件、网络、zip包、或者运行时生成等。 类加载器实现类的加载动作,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使两个类源自于同一份class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类必定不相等。 12345678910111213141516171819202122232425262728293031323334353637public class ClassLocaderTest { public static void main(String[] args) { Object testClassLoader1 = getMyClassLoader1(); System.out.println(testClassLoader1.getClass()); System.out.println(testClassLoader1 instanceof space.kyu.TestClass); } static Object getMyClassLoader1() { Object obj = null; try { MyClassLoader1 loader = new MyClassLoader1(); obj = loader.loadClass("space.kyu.TestClass").newInstance(); } catch (Exception e) { System.out.println(e); } return obj; }}class MyClassLoader1 extends ClassLoader{ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream stream = getClass().getResourceAsStream(fileName); if (stream == null) {// System.out.println("ClassLoader load class" + name); return super.loadClass(name); } byte[] bs = new byte[stream.available()]; stream.read(bs);// System.out.println("MyClassLoader1 load class: " + name); return defineClass(name, bs, 0, bs.length); } catch (IOException e) { throw new ClassNotFoundException(name); } } } 输出: 12class space.kyu.TestClassfalse 在上面的例子中,虚拟机中存在两个space.kyu.TestClass类,一个是由系统应用程序类加载器加载的,一个是由我们自己实现的类加载器加载的。虽然来自同一个class文件,但依然是两个独立的类,故不相等。 类加载器应用于类层次划分、OSGI、热部署、代码加密等方面。 类加载器层次结构从java虚拟机的角度来看,类加载器分为两类: 1.启动类加载器 使用c++实现,是虚拟机自身的一部分 2.其他类加载器 由java语言实现,独立于虚拟机外部,全都继承自抽象类java.lang.ClassLoader 从类加载器的实现来看,类加载器又可分为系统提供的类加载器与我们自己实现的类加载器。系统提供的类加载器主要有三个: 引导类加载器,用来加载java核心类库。主要是放在JAVA_HOME\lib目录中或被-Xbootclasspath所指定的目录。 扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现。负责加载JAVA_HOME\lib\ext目录中,或java.ext.dirs所指定的路径中的类库。 应用程序类加载器,由sun.misc.Launcher$AppClassLoader实现。这个类也是ClassLoader中getSystemClassLoader()方法的返回值。负责加载classpath上指定的类库。 除了系统提供的类加载器以外,我们可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。 除了引导类加载器之外,所有的类加载器都有一个父类加载器。这种父子关系构成了类加载器的层次结构。 对于系统提供的类加载器来说,应用程序类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器。 因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。 这种类加载器之间的层次关系,称为类加载器的双亲委派模型: 注意,上图中的树状结构并不意味着继承关系,而是使用委托实现的。 双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他会首先把这个请求委托给自己的父类加载器去完成,每一层次的加载器都是如此,最后所有的类加载请求最终都会传递到顶层的引导类加载器中去,只有当父类加载器无法完成这个加载请求(所请求加载的类不在 他加载的范围内)时,子类加载器会尝试自己加载。 双亲委派机制保证了java核心类库的安全,如果尝试加载与rt.jar类库中已有的类重名的java类,该类永远无法被加载运行,因为请求被传递到引导类加载器之后,引导类加载器会返回加载到的rt.jar中的类。 我们观察一下双亲委派机制的实现: 首先看一下ClassLoader中的方法: 1234567891011findLoadedClass:每个类加载器都维护有自己的一份已加载类名字空间,其中不能出现两个同名的类。凡是通过该类加载器加载的类,无论是直接的还是间接的,都保存在自己的名字空间中,该方法就是在该名字空间中寻找指定的类是否已存在,如果存在就返回给类的引用,否则就返回 null。这里的直接是指,存在于该类加载器的加载路径上并由该加载器完成加载,间接是指,由该类加载器把类的加载工作委托给其他类加载器完成类的实际加载。getSystemClassLoader:Java2 中新增的方法。该方法返回系统使用的 ClassLoader。可以在自己定制的类加载器中通过该方法把一部分工作转交给系统类加载器去处理。defineClass:该方法是 ClassLoader 中非常重要的一个方法,它接收以字节数组表示的类字节码,并把它转换成 Class 实例,该方法转换一个类的同时,会先要求装载该类的父类以及实现的接口类。loadClass:加载类的入口方法,调用该方法完成类的显式加载。通过对该方法的重新实现,我们可以完全控制和管理类的加载过程。findClass(String name): 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。resolveClass(Class<?> c): 链接指定的 Java 类。 实现双亲委派机制的代码集中在ClassLoader的loadClass方法中。 12345678910111213141516171819202122232425262728293031323334353637protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } 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; } } 先检查是否已经加载过,若没有则调用父类加载器的loadClass方法,若父类加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,则调用自己的findClass方法进行加载。 双亲委托机制的不足双亲委派机制很好的解决了各个类加载器的基础类统一的问题,基础类总是作为被用户代码调用的API(比如rt.jar中的类)。但是如果基础类要调用用户的代码时会发生什么? 首先要搞明白一点:当我们使用 new 关键字或者 Class.forName 来加载类时,所要加载的类都是由调用 new 或者 Class.forName 的类的类加载器进行加载的。比如我们使用JDBC标准接口时,JDBC标准接口存在于rt.jar中,在这个接口中又需要调用各个数据库厂商提供的jdbc驱动程序来达到管理驱动的目的,这些驱动程序的jar包一般置于claspath路径下。问题出现了:JDBC标准接口是由引导类加载器加载的,故在这些接口中调用classpath路径下的jdbc驱动代码时,也会尝试使用引导类加载器进行加载。但是引导类加载器根本不可能认识这些代码(只负责rt.jar)。 为了解决这个问题,引入了线程上下文类加载器。 这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时没有设置,将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置,那么这个类加载器默认就是应用程序类加载器。 使用java.lang.Thread.getContextClassLoader()可以获得线程上下文类加载器,故可以使用这个加载器加载classpath路径下的代码,也就是父类加载器请求子类加载器完成类加载动作,破坏了双亲委托模型。 实现自己的类加载器上面提到的系统提供的类加载器在大多数情况下可以满足我们的需求,但是在某些情况下,我们需要开发自己的类加载器,比如,加载网络传输得到的类字节码、对字节码进行加密解码、加载运行时生成的字节码、实现类的热替换等。这些情况下类的字节码仅仅依靠上述的三种系统类加载器是无法加载的。 我自己实现了一些测试代码,现在将他们贴到这里,顺便对前面的总结做一个印证。下面的几个类都位于包space.kyu下面: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139class MyClassLoader1 extends ClassLoader{ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream stream = getClass().getResourceAsStream(fileName); if (stream == null) {// System.out.println("ClassLoader load class" + name); return super.loadClass(name); } byte[] bs = new byte[stream.available()]; stream.read(bs);// System.out.println("MyClassLoader1 load class: " + name); return defineClass(name, bs, 0, bs.length); } catch (IOException e) { throw new ClassNotFoundException(name); } }}public class MyClassLoader2 extends ClassLoader { public Class<?> loadDirectly(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; InputStream stream = getClass().getResourceAsStream(fileName); if (stream == null) {// System.out.println("ClassLoader load class" + name); return super.loadClass(name); } byte[] bs = new byte[stream.available()]; stream.read(bs);// System.out.println("MyClassLoader2 load class: " + name); return defineClass(name, bs, 0, bs.length); } catch (IOException e) { throw new ClassNotFoundException(name); } }}public interface Operation { void doSomething();}public class Test { public String str; public Test(String str) { this.str = str; } public void test() { System.out.println(str); }}public class TestClass implements Operation{ public Test test; @Override public void doSomething() { System.out.println("hello"); } public Test test(){ test = new Test("haha"); System.out.println(test.str); return test; }}public class ClassLocaderTest { public static void main(String[] args) { Object testClassLoader1 = getMyClassLoader1(); Object testClassLoader2 = getMyClassLoader2(); System.out.println("*****************testClassLoader1*******************"); printClassLoader(testClassLoader1); reflectInvoke(testClassLoader1); interfaceInvoke(testClassLoader1); System.out.println("*****************testClassLoader2*******************"); printClassLoader(testClassLoader2); reflectInvoke(testClassLoader2); interfaceInvoke(testClassLoader2); } static void printClassLoader(Object object) { System.out.println("*********printClassLoader:"); ClassLoader classLoader = object.getClass().getClassLoader(); while (classLoader != null) { System.out.println(classLoader); classLoader = classLoader.getParent(); } } static void reflectInvoke(Object obj) { System.out.println("*********reflectInvoke:"); try { Method test = obj.getClass().getMethod("test", new Class[] {}); test.invoke(obj, new Object[] {}); Method doSomething = obj.getClass().getMethod("doSomething", new Class[] {}); doSomething.invoke(obj, new Object[] {}); } catch (InvocationTargetException e) { Throwable t = e.getTargetException();// 获取目标异常 System.out.println(t); } catch (Exception e) { System.out.println(e); } } static void interfaceInvoke(Object obj) { System.out.println("*********interfaceInvoke:"); try { Operation operation = (Operation) obj; operation.doSomething(); } catch (Exception e) { System.out.println(e); } } static Object getMyClassLoader1() { Object obj = null; try { MyClassLoader1 loader = new MyClassLoader1(); obj = loader.loadClass("space.kyu.TestClass").newInstance(); } catch (Exception e) { System.out.println(e); } return obj; } static Object getMyClassLoader2() { Object obj = null; try { MyClassLoader2 loader = new MyClassLoader2(); obj = loader.loadDirectly("space.kyu.TestClass").newInstance(); } catch (Exception e) { System.out.println(e); } return obj; }} 上述六个类位于space.kyu下不同的类文件当中。ClassLocaderTest运行结果: 1234567891011121314151617181920*****************testClassLoader1****************************printClassLoader:space.kyu.MyClassLoader1@76e2d0absun.misc.Launcher$AppClassLoader@52a53948sun.misc.Launcher$ExtClassLoader@5d53d05b*********reflectInvoke:hahahello*********interfaceInvoke:java.lang.ClassCastException: space.kyu.TestClass cannot be cast to space.kyu.Operation*****************testClassLoader2****************************printClassLoader:space.kyu.MyClassLoader2@6c618821sun.misc.Launcher$AppClassLoader@52a53948sun.misc.Launcher$ExtClassLoader@5d53d05b*********reflectInvoke:hahahello*********interfaceInvoke:hello 一般来说,我们自己开发的类加载器只要继承ClassLoader并覆盖findClass方法即可。这样的话就会自动使用双亲委派机制,我们可以在findClass方法中填写我们自己的加载逻辑:从网络上或者是硬盘上加载一个类的字节码。 上面的例子中并没有使用这个套路,MyClassLoader1直接复写loadClass方法,MyClassLoader2添加了方法loadDirectly,如果不这样做的话,我们在加载space.kyu.TestClass这个类的时候,因为这个类在classpath上,由于双亲委派机制,这个类会被应用程序类加载器先进行加载,达不到测试的效果。 观察上面printClassLoader部分,通过getParent方法打印了类加载器的层次结构。可见虽然我们并未显示指定这两个自定义加载器的父类加载器,但是他们的父类加载器已经被默认设置为sun.misc.Launcher$AppClassLoader,也就是加载这两个个自定义类加载器所使用的加载器。印证上面的结论:对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。 reflectInvoke方法是使用反射机制调用了加载出来类的方法,如果去掉上面自定义类加载器中注掉的System.out方法,就会看到,在反射调用TestClass的test方法的时候,类加载器加载了space.kyu.Test这个类,并且加载他的类加载器正是我们自定义的类加载器,印证了我们上面的结论:当我们使用 new 关键字或者 Class.forName 来加载类时,所要加载的类都是由调用 new 或者 Class.forName 的类的类加载器进行加载的 思考上面的反射用法,为什么不直接将getMyClassLoader1()方法返回的Object对象强转为space.kyu.TestClass呢?比如这样: space.kyu.TestClass testClass = (TestClass)getMyClassLoader1(); 编译并没有问题,但是在运行时就会报错:java.lang.ClassCastException 为什么会出现这样的结果呢?其实从这篇文章的一开始就已经演示过了。space.kyu.TestClass testClass这个类是通过应用程序类加载器加载的,而getMyClassLoader1()方法得到的是我们自定义类加载器加载的类,这两个类是不相等的(虽然名字相同),所以强转失败。 接下来看interfaceInvoke这部分。将自定义类加载器加载得到的对象强转为了接口类型。注意到,MyClassLoader1加载的类对象在强转时抛出异常,而MyClassLoader2可以正常强转并调用接口方法。 MyClassLoader1加载的类为什么强转失败?原因在于,MyClassLoader1在加载TestClass类时,触发其父类接口Operation的加载,此时默认使用MyClassLoader1加载Operation类。在MyClassLoader1中我们覆盖了loadClass方法,故加载Operation时也会调用我们自己实现的loadClass方法进行加载。 同样的,MyClassLoader2在加载TestClass类时,也触发其父类接口Operation的加载,此时默认使用MyClassLoader2加载Operation类。不同之处在于我们并未覆盖loadClass方法,加载Operation时调用了ClassLoader中的loadClass方法,在这个方法的实现中,由应用程序类加载器加载了Operation类。 所以,出现上面结果的原因也就一目了然了。 类加载器与热替换普通的java应用中不能实现类的热替换的原因在于同名类的不同版本的实例不能共存,因为使用了默认的类加载机制后,一个类只会被加载一次,再次请求加载时直接返回之前加载的缓存(findLoadedClass)。故我们重新编译生成的class文件并不会被重新读取并加载。 为了绕过这个加载机制,我们可以通过不同的类加载器来加载该类的不同版本。 在space.kyu包下面新增一个类HotSwapTest:12345678910111213141516171819202122public class HotSwapTest { public static void main(String[] args) { Timer timer = new Timer(false); TimerTask task = new TimerTask() { public void run() { update(); } }; timer.schedule(task, 1000, 2000); } public static void update() { try { MyClassLoader2 loader = new MyClassLoader2(); Object obj = loader.loadDirectly("space.kyu.TestClass").newInstance(); Method doSomething = obj.getClass().getMethod("doSomething", new Class[] {}); doSomething.invoke(obj, new Object[] {}); } catch (Exception e) { e.printStackTrace(); } }} 在HotSwapTest类中,我们模拟了一个定时升级的任务:每隔两秒执行一次升级,实例化一个MyClassLoader2对象并使用该类加载器加载space.kyu.TestClass,反射调用其doSomething方法打印字符串。 编译并运行HotSwapTest,运行过程中,每隔两秒doSomething便打印字符串”hello”,此时修改space.kyu.TestClass源码,将打印字符串替换为”world”,CTRL+S,我们的程序并未停止,但是下一次打印出的字符串已然不同了: 12345678hellohellohellohellohelloworldworldworld 上面就是一个简单的热替换的例子。实际的应用中当然不是通过一个定时任务进行升级的。把新版本类的字节码通过网络传输到服务器上去,然后发送一个升级指令,使用上面类似的方法便可对类进行升级。 参考Java 类的热替换 —— 概念、设计与实现 深入探讨 Java 类加载器]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>类加载</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java注解]]></title>
<url>%2F2017%2F03%2F31%2Fjava%E6%B3%A8%E8%A7%A3%2F</url>
<content type="text"><![CDATA[在写java代码的过程中,经常会遇到注解,但是没有去理解注解背后的原理,也没有实现过注解。网上关于java注解的文章已经有很多了,参考了一些资料,整理一下注解这方面的知识~ 什么是注解注解其实很常见。比如@override、Deprecated等。在使用Junit或Spring boot等一些框架的时候,注解更是无处不在了。那么,到底什么是注解呢? 123Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate. >>[https://docs.oracle.com/javase/tutorial/java/annotations/] 注解是元数据的一种,提供了关于程序的一些描述信息,但这些信息并不属于这个程序本身的一部分。注解并不会直接影响到代码的执行。 翻译起来有点拗口。实际上,注解是那些插入到源代码中用于某种工具进行处理的标签。注解不会改变对编写的程序的编译方式,对于包含和不包含注解的代码,java编译器都会生成相同的虚拟机指令。 一句话来说,注解只是描述代码的标签。注解本身不会做什么事情,为了使注解起到作用来实现一些黑科技,我们还需要用于处理注解的工具(编写代码处理这些注解)。 我们使用注解可以: 生成文档 在编译时进行检查。比如@override 替代配置文件,实现自动配置。比如 Springboot 注解Annotation是在jdk1.5之后引进的,jdk1.8之后又增加了一些新的特性。接下来的讨论基于jdk1.7。 注解的使用在java中,注解是当做一个修饰符(比如public或static之类的关键词)来使用的。注解可以存在于: 包 | 类(包括enum) | 接口(包括注解接口) | 方法 | 构造器 | 成员变量 | 本地变量 | 方法参数 注意: 1.对于包的注解,需要在package-info.java中声明。 2.对于本地变量的注解,只能在源码级别上进行处理。所有的本地变量注解在类编译完之后会被遗弃掉。 假如有这样一个注解:(注解的定义见下文) 12345@interface Result { String name() default ""; int value() default -1; String res() default "";} 我们可以这样使用它: @Result(name="res1",value=1) 括号中元素的顺序无关紧要 @Result(value=1,name="res1")等价于@Result(name="res1",value=1) 如果元素值没有指定,则使用默认值:(没有声明默认值时必须指定元素值) @Result等价于@Result(name="",value=-1,"") 如果元素的名字为特殊值value,那么可以忽略这个元素名和等号: @Result(1)等价于@Result(name="",value=1,"") 如果元素是数组,那么他的值要用括号括起来: @Result(res={"a","b"}) 如果数组是单值,可以忽略这些括号: @Result(res="a") 注解分类根据注解的用途和使用方式,注解可以分为以下几类: 元注解:注解注解的注解。也就是用来描述注解定义的注解 预定义注解:jdk内置的一些注解 自定义注解:我们自己定义的注解 元注解 元注解包含下面几个: @Target: 指定这个注解可以应用于哪些项 12345678910111213141516171819202122232425public enum ElementType { /** Class, interface (including annotation type), or enum declaration */ TYPE, /** Field declaration (includes enum constants) */ FIELD, /** Method declaration */ METHOD, /** Parameter declaration */ PARAMETER, /** Constructor declaration */ CONSTRUCTOR, /** Local variable declaration */ LOCAL_VARIABLE, /** Annotation type declaration */ ANNOTATION_TYPE, /** Package declaration */ PACKAGE} 比如,我们定义了一个注解Bug,该注解只能应用于方法或成员变量: 1234@Target({ElementType.METHOD,ElementType.FIELD})@interface Bug{ int value() default -1;} 注解Bug则只能用于类方法或成员变量,如果注解了其他项比如类或者包,编译则不会通过。 对于一个没有声明@Target的注解,可以应用到任何项上。 @Retention: 指定这个注解可以保留多久 123456789101112131415161718192021public enum RetentionPolicy { /** * Annotations are to be discarded by the compiler. */ SOURCE, /** * Annotations are to be recorded in the class file by the compiler * but need not be retained by the VM at run time. This is the default * behavior. */ CLASS, /** * Annotations are to be recorded in the class file by the compiler and * retained by the VM at run time, so they may be read reflectively. * * @see java.lang.reflect.AnnotatedElement */ RUNTIME} SOURCE:只存在于源代码,编译成.class之后就没了 CLASS: 保留到类文件中,但是虚拟机不会载入 RUNTIME:保留到类文件中,并且虚拟机会载入。这意味着通过反射可以获取到这些注解和注解元素值 默认情况下(没有声明@Retention),注解保留级别为CLASS @Document:指定这个注解应该包含在文档中 文档化的注解意味着像javadoc这样的工具生成的文档中会包含这些注解。比如: 12345@Documented@Retention(RetentionPolicy.RUNTIME)@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})public @interface Deprecated {} @Deprecated是文档化的注解,URLDecoder.decode(String s)方法应用了这个注解: 可以在文档中看到Deprecated的出现。 @Inherited: 指定一个注解,当他应用于一个类的时候,能够自动被其子类继承 @Inherited只能应用于对类的注解。如果一个类具有继承注解,那么他的所有子类都自动具有同样的注解。 比如,定义了一个继承注解@Secret表示一个类是隐私的不可被序列化传输的,那么该类的子类会被自动注解为不可序列化传输的。 1234567@Inherited@interface Secret{}@Secret class A{}class B extends A{} //同样是@Secret的 当注解工具去获取声明了@Secret的对象时,他能够获取到A的对象和B的对象。 预定义注解 常用的有三个:@override、@Deprecated、@SuppressWarnings,具体的作用可以查文档或者源码,不再赘述。 注解的定义上面的讨论中已经涉及到了注解的定义。一个注解是由一个注解接口来定义的: 1234@interface Result { String name(); int value() default -1;} 每个元素的声明有两种形式,有默认值和没有默认值的,就像上面那样。注解的元素可以是下面之一: 基本类型|String|Class类型|enum|注解类型|由前面所述类型构成的数组 123456789@interface BugReport{ enum Status{FIXED,OPEN,NEW,CLOSE}; boolean isIgnore() default false; String id(); Class<?> testCase() default Void.class; Status status() default Status.NEW; Author author() default @Author; String[] reportMsg() default "";} 注意,虽然注解元素可以是另一个注解,但是不能在注解中引入循环依赖,比如@BugReport依赖@Author,而@Author又依赖@BugReport。同时,注解元素也不可以为null,元素的值必须是编译期常量。 我们可以通过在注解的定义前声明之前提到的元注解来定制我们的注解,比如: 123456789@Target({ ElementType.METHOD, ElementType.FIELD })@Inherited@Documented@Retention(RetentionPolicy.RUNTIME)@interface Result { String name() default ""; int value() default -1; String[] reportMsg() default "";} 所有的注解接口隐式的继承自java.lang.annotation.Annotation接口。这是一个正常的接口,而不是注解接口。123456public interface Annotation { boolean equals(Object obj); int hashCode(); String toString(); Class<? extends Annotation> annotationType();} 也就是说,注解接口也是普通接口的一种。注解接口中的元素实际上也是方法的声明,这些方法类似于bean的get、set,我们使用@Result(name=”A”)的形式实际上是调用了set方法给某个变量赋值。 既然是接口,那么就应该有实现(不然怎么用呢?)。我们不需要主动提供实现了注解接口的类,虚拟机会在需要的时候产生一些代理类和对象。下文会提到。 既然可以为注解元素赋值,那么必定有方法去获得这些值。也就是注解的解析。 注解的解析我们定义了注解并且应用了注解,但是仅仅这样的话注解并不会起到什么作用。需要我们提供一种工具去解析声明的注解,然后实现一些自动配置或者生成报告的功能。这就是注解的解析。 源代码中的注解 注解的用处之一就是自动生成一些包含程序额外信息的文件。比如,根据注解生成代码进度报告,或者bug修复报告等。生成的文件可以是属性文件、xml文件、html文档或者shell脚本。也可以生成java源文件。 注解处理器通常通过集成AbstractProcessor类实现了Processor接口,通过process方法实现处理源码中注解的逻辑。通过声明具体的注解类型来指定该处理器处理哪些注解。 12345678910@SupportedAnnotationTypes("space.yukai.annotations.BUG")class AnnotationProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // TODO Auto-generated method stub return false; } } process的两个参数:annotations代表了要处理的注解集,roundEnv是包含有关当前处理循环信息的RoundEnv引用。 Java注解处理器这篇文章以通过注解自动生成工厂类文件为例,详细介绍了如何处理源码级别的注解。(英文原文在这:http://hannesdorfmann.com/annotation-processing/annotationprocessing101) 注意,我们虽然可以通过源码级别的注解处理器生成新的文件,却很难编辑源文件,比如,通过处理注解自动生成get、set方法。字节码级别的处理器是可以的。 字节码中的注解 字节码级别的注解,即存在于class文件中的注解。我们还可以通过BCEL这样的字节码工程类库修改或插入字节码来改变类文件。比如在声明了@LogEntity的方法开始部分插入打印日志信息的字节码。 涉及的不多,不再赘述。 运行时的注解 在运行时处理注解是比较常见的注解处理手段。一般是通过反射API获取到我们的注解信息,从而实现一些功能。 下面是自己写的一个例子,通过解析BugReport注解得到一些测试信息,然后通过动态代理的方式生成代理测试类,最后运行测试自动生成测试报告。(不要在意代码有什么缺陷或者其他问题,仅仅是一个例子而已~) 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111package space.kyu.proxy;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.ArrayList;import java.util.List;public class AnnotationsTest { public static void main(String[] args) { TestBug testBug = new TestBug(true); TestBug testBug1 = new TestBug(false); TestExecutor executor = new TestExecutor(); executor.addTest(testBug); executor.addTest(testBug1); executor.executeTest(); }}class TestExecutor { private List<Test> testCases; public TestExecutor() { testCases = new ArrayList<Test>(); } public <T extends Test> void addTest(T testCase) { Class<? extends Object> cl = testCase.getClass(); Method[] declaredMethods = cl.getDeclaredMethods(); try { for (Method method : declaredMethods) { if (method.isAnnotationPresent(BugReport.class)) { BugReport annotation = method.getAnnotation(BugReport.class); if (annotation != null) { System.out.println(annotation.toString()); System.out.println(annotation.annotationType().getName()); System.out.println(annotation.getClass().getName()); String bugId = annotation.id(); String bugMsg = annotation.msg(); Test obj = (Test) createBugReportHandler(testCase,bugId,bugMsg); testCases.add(obj); } } } } catch (Exception e) { e.printStackTrace(); throw new IllegalStateException(e); } } public void executeTest() { for (Test test : testCases) { test.test(); } } private Object createBugReportHandler(Test testCase, final String bugId, final String bugMsg) { return Proxy.newProxyInstance(testCase.getClass().getClassLoader(), testCase.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { boolean res = (boolean) method.invoke(testCase, args); //也可以输出到文件中形成测试报告 System.out.println("******************************"); System.out.println("bug: " + bugId + "测试结果:"); if (res) {//测试通过 System.out.println("已通过"); } else {//测试不通过 System.out.println("未通过"); } System.out.println("备注信息:" + bugMsg); return res; } }); }}interface Test { /** * 测试方法 2017年4月1日 * @return * true 通过测试 * false 未通过测试 */ boolean test();}class TestBug implements Test { boolean fixed; public TestBug(boolean fixed) { //控制测试成功或失败 this.fixed = fixed; } @BugReport(id = "bug001", msg = "bug注释:这是一条测试bug") @Override public boolean test() { System.out.println("执行测试..."); //假装测试成功或者失败了 return fixed; }}@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@interface BugReport { String id(); String msg();} 运行结果: 12345678910111213141516@space.kyu.proxy.BugReport(id=bug001, msg=bug注释:这是一条测试bug)space.kyu.proxy.BugReportspace.kyu.proxy.$Proxy1@space.kyu.proxy.BugReport(id=bug001, msg=bug注释:这是一条测试bug)space.kyu.proxy.BugReportspace.kyu.proxy.$Proxy1执行测试...******************************bug: bug001测试结果:已通过备注信息:bug注释:这是一条测试bug执行测试...******************************bug: bug001测试结果:未通过备注信息:bug注释:这是一条测试bug 上面的代码很简单,我们要注意的有几点: 1.method.isAnnotationPresent(BugReport.class)和 method.getAnnotation(BugReport.class) 这两个方法来自于接口AnnotatedElement,Method、Field、Package、Constructor、Class这些类都实现了这个接口,使得这些类拥有了提供所声明的注解的功能。 通过method.getAnnotation(BugReport.class)得到了声明在方法上的BugReport注解,获得这个注解的实例之后,我们就可以调用以该注解声明的元素为名称的方法来获取对应的元素值了。 2.annotation.annotationType().getName()和annotation.getClass().getName() annotation.annotationType()方法上面已经提到过了,是Annotation的一个方法,用于描述该注解对象的注解接口。这个方法返回的内容为:space.kyu.proxy.BugReport annotation.getClass()获得了实现了Annotation接口的代理类,通过调用getName()方法可以打印这个代理类的名称:space.kyu.proxy.$Proxy1。从而印证了我们上面所说,确实自动生成了代理类。 上面的例子很简单,说白了,注解就是给代码加了一些额外的信息,这些信息对代码里面的逻辑是没有任何影响的。但是我们可以通过其他手段获得我们在代码中的注解,从而实现一些重复性的工作。这就是注解的作用。]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java代理机制]]></title>
<url>%2F2017%2F03%2F28%2Fjava%E4%BB%A3%E7%90%86%E6%9C%BA%E5%88%B6%2F</url>
<content type="text"><![CDATA[代理模式代理模式,顾名思义,即一个客户不想或者不能直接访问一个对象,需要通过一个称为代理的第三方对象来实现间接引用。代理对象的作用就是客户端和目标对象之间的一个中介,通过代理对象可以隐藏不让用户看到的内容或实现额外的服务。 代理机制应用的场景有很多:比如在代理对象中实现缓存,验证,权限控制等功能,真正的业务逻辑封装在真实对象中。RMI远程方法调用也用到了代理。当你调用一个远程方法的时候,相当于调用这个方法的代理对象,在代理对象中封装了网络请求等部分,真实对象存在于另一个进程上。重构老旧代码的时候也常常会用到代理模式。 代理分两种:静态代理和动态代理 静态代理静态代理即在代码中手动实现代理模式。代理模式涉及到三个角色: 真实对象RealSubject、抽象主题Subject、代理对象Proxy 12345678910111213141516171819202122232425262728293031323334public class ProxyTest { public static void main(String[] args) { new Proxy("hello").request(); }}interface Subject { void request();}class Proxy implements Subject{ String str; RealSubject subject; public Proxy(String string) { str = string; subject = new RealSubject(str); } @Override public void request() { System.out.println("代理对象验证机制...."); subject.request(); } }class RealSubject implements Subject{ String str; public RealSubject(String string) { str = string; } @Override public void request() { System.out.println("真实对象打印str: " + str); } } 输出:12代理对象验证机制....真实对象打印str: hello 上面的代码模拟了一个代理对象实现验证机制的过程。可以看到,代码很简单,代理模式也很好理解。(我们在真实生活中不也有代理么,,比如黄牛,帮你买到你买不到的火车票) JDK动态代理实现动态代理时较为高级的一种代理模式。典型的应用有Spring AOP,RMI。 在上面的静态代理模式中,真实对象是事先存在的,并且作为代理对象的内部成员属性。一个真实的对象必须对应一个代理对象,如果真实对象很多的话会导致类膨胀。 另外,如何在事先不知道真实对象的情况下使用代理代理对象,这都是动态代理需要解决的问题。 比如有n个类需要在执行前打印几行日志,而这n个类是无法通过源代码修改的(从jar包中引入的)。通过静态代理实现的话将会有n个新的代理类产生,而使用动态代理的话,只需一个类即可。 动态代理的实现方式有很多,我们只讨论JDK中的动态代理实现。 12345678910111213141516171819202122232425262728293031323334353637383940import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class DynamicProxyTest { public static void main(String[] args) { Subject subject = (Subject) new DynamicProxy().bind(new RealSubject("hello")); subject.request(); }}interface Subject { void request();}class RealSubject implements Subject{ String str; public RealSubject(String string) { str = string; } @Override public void request() { System.out.println("真实对象打印str: " + str); }}class DynamicProxy implements InvocationHandler { Object object; public Object bind(Object object) { this.object = object; return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理对象验证..."); return method.invoke(object, args); }} 输出:12代理对象验证...真实对象打印str: hello 可以看到,动态代理实现了与静态代理一样的功能,但他的优点在于代理的真实对象不是确定的,可以在运行时指定,增大了灵活性。如果我们有很多的真实对象需要代理访问,并且他们代理对象中的内容都实现了相同的功能,那么我们只需要一个动态代理类即可。 动态代理原理我们通过观察java.lang.reflect.Proxy的源码来了解动态代理的原理。下面的代码截取自openjdk7-b147 (安利一个不错的搜索java源码的网站:http://grepcode.com) 上面的方法截取自Proxy.newProxyInstance,可以看到,调用getProxyClass方法获取到一个代理类class对象,然后使用该class对象通过反射方法实例化一个对象返回。 接下来观察getProxyClass方法。 这部分代码截取自getProxyClass,先从缓存中查询是否已经生成过对应的class,若有,则直接返回该对象,没有,则继续下一步生成class 这部分代码是代理类class对象的生成过程。其中: byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);这行代码调用ProxyGenerator.generateProxyClass返回了代理类class对象的字节码byte序列,proxyClass = defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);这一行则进行了类加载的工作,最终生成了代理类class对象。 generateProxyClass,其中的gen.generateClassFile()方法实现了字节码的生成。 generateClassFile方法的实现。开头调用的三个addProxyMethod方法将object类中的hashcode、equals、toString方法重写,故对这三个方法的调用会传递到InvocationHandler.invoke方法当中。注意,除了上述三个方法之外,调用代理类中Object定义的其他方法不会传递到invoke方法当中,也就是说,调用这些方法会执行Object中的默认实现。 如果想要查看ProxyGenerator.generateProxyClass这个方法在运行时产生的代理类中写了些什么,可以在main方法中加入: 1System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); 运行时会将生成的class文件保存到硬盘当中:$Proxy0.class 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;public final class $Proxy0 extends Proxy implements Subject{ private static Method m1; private static Method m3; private static Method m0; private static Method m2; public $Proxy0(InvocationHandler paramInvocationHandler) { super(paramInvocationHandler); } public final boolean equals(Object paramObject) { try { return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue(); } catch (Error|RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } public final void request() { try { this.h.invoke(this, m3, null); return; } catch (Error|RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } public final int hashCode() { try { return ((Integer)this.h.invoke(this, m0, null)).intValue(); } catch (Error|RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } public final String toString() { try { return (String)this.h.invoke(this, m2, null); } catch (Error|RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") }); m3 = Class.forName("Subject").getMethod("request", new Class[0]); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); return; } catch (NoSuchMethodException localNoSuchMethodException) { throw new NoSuchMethodError(localNoSuchMethodException.getMessage()); } catch (ClassNotFoundException localClassNotFoundException) { throw new NoClassDefFoundError(localClassNotFoundException.getMessage()); } }} 上面的代码很好理解。可以看到equals、hashCode、toString以及我们Subject接口request方法的实现中都是调用了InvocationHandler.invoke方法,而这个InvocationHandler实例就是我们在Proxy.newProxyInstance中传入的对象。 综上,可以看到实现动态代理的几个步骤: 1.实现InvocationHandler 2.获得动态代理类,这一步又涉及到运行时代理类字节码的生成和类加载 3.通过反射机制(getConstructor(InvocationHandler.class))获取代理类的实例并返回该对象 4.调用代理对象的目标方法(也就是request方法,代理类也实现了Subject这个接口),调用转发到InvocationHandler.invoke方法当中,执行invoke的逻辑(我们自己的InvocationHandler实现) 至此,我们就了解了动态代理的运行原理。动态代理的机制也有一些缺陷,比如他代理的必须是接口方法。看一下我们上面生成的$Proxy0.class,可知这个代理类已经默认继承了类Proxy,所以,他只能通过实现我们提供的接口来代理我们的方法。在invoke方法中,我们可以通过对传入的代理类、方法和参数来进行判断,对不同的方法实现不同的业务逻辑。]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java序列化与反序列化]]></title>
<url>%2F2017%2F03%2F22%2Fjava%E5%BA%8F%E5%88%97%E5%8C%96%E4%B8%8E%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%2F</url>
<content type="text"><![CDATA[什么是序列化所谓的序列化,即把java对象以二进制形式保存到内存、文件或者进行网络传输。从二进制的形式恢复成为java对象,即反序列化。 通过序列化可以将对象持久化,或者从一个地方传输到另一个地方。这方面的应用有RMI,远程方法调用。 java中实现序列化有两种方式,实现Serializable接口或者Externalizable接口。这篇总结只讨论Serializable的情况。 123456789101112131415161718public class SerializeTest implements Serializable{ private static final long serialVersionUID = 1L; public String str; public SerializeTest(String str) { this.str = str; } public static void main(String[] args) throws IOException, ClassNotFoundException { SerializeTest test = new SerializeTest("hello"); ByteArrayOutputStream oStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(oStream); objectOutputStream.writeObject(test);//序列化 ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(oStream.toByteArray())); SerializeTest obj = (SerializeTest) inputStream.readObject();//反序列化 System.out.println(obj.str); }} 上面的代码演示了类SerializeTest实现序列化和反序列化的过程。 所有的序列化和反序列化过程都是java默认实现的,你只需要实现接口Serializable,就能得到一个实现了序列化的类。通过ObjectOutputStream和ObjectInputStream分别将序列化对象输出或者写入到某个流当中。流的目的地可以是内存字节数组(上例)、文件、或者网络。 下面研究一下序列化过程中的几个问题: 静态变量如何序列化12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public class SeriaUtil { ByteArrayInputStream bInputStream; ByteArrayOutputStream byOutputStream; ObjectOutputStream outputStream ; ObjectInputStream inputStream; public void seria(Object test) throws IOException { if (byOutputStream == null) { byOutputStream = new ByteArrayOutputStream(); } if (outputStream == null) { outputStream = new ObjectOutputStream(byOutputStream); } outputStream.writeObject(test);// System.out.println(byOutputStream.toByteArray().length); } public Object reSeria() throws IOException, ClassNotFoundException { if (bInputStream == null) { bInputStream = new ByteArrayInputStream(byOutputStream.toByteArray()); } if (inputStream == null) { inputStream = new ObjectInputStream(bInputStream); } Object obj = inputStream.readObject(); return obj; }}public class StaticTest implements Serializable{ private static final long serialVersionUID = 1L; public static int A = 0; public static String B = "hello"; public static void main(String[] args) throws IOException, ClassNotFoundException { //先序列化类,此时 A=0 B = hello SeriaUtil seriaUtil = new SeriaUtil(); StaticTest test = new StaticTest(); seriaUtil.seria(test); //修改 静态变量的值 StaticTest.A = 1; StaticTest.B = "world"; StaticTest obj = (StaticTest) seriaUtil.reSeria(); //输出 A = 1 B = world System.out.println(obj.A); System.out.println(obj.B); }} 上面的代码说明了,静态变量不会被序列化。 序列化StaticTest实例test时,静态变量 A=0 B=”hello”,序列化之后,修改StaticTest类的静态变量值,A=1 B=”world”,此时反序列化得到之前序列化的实例对象赋给obj,发现obj的静态变量变为A=1 B=”world”,说明静态变量并未序列化成功。 事实上,在序列化对象时,会忽略对象中的静态变量。很好理解,静态变量是属于类的,而不是某个对象的状态。我们序列化面向的是对象,是想要将对象的状态保存下来,所以静态变量不会被序列化。反序列化得到的对象中的静态变量的值是当前jvm中静态变量的值。静态变量对于同一个jvm中同一个类加载器加载的类来说,是一样的。对于同一个静态变量,不会存在同一个类的不同实例拥有不同的值。 同一对象序列化两次,反序列化后得到的两个对象是否相等这个问题提到的相等,是指是否为同一对象,即==关系 在某些情况下,确保这种关系是很重要的。比如王经理和李经理拥有同一个办公室,即存在引用关系: 12345678910111213141516public class Manager{ Room room; public Manager(Room r){ room = r; }}public class Room{}public class APP{ public void main(String args[]){ Room room = new Room(); Manager wang = new Manager(room); Manager li = new Manager(room); }} 反序列化之后,wang,li,room的这种引用关系不应该发生变化。通过代码验证一下: 12345678910111213141516171819public class ReferenceTest implements Serializable{ public String a; public ReferenceTest() { a = "hah"; } public static void main(String[] args) throws Exception { System.out.println("构造对象********************"); ReferenceTest test = new ReferenceTest(); System.out.println("序列化**********************"); SeriaUtil util = new SeriaUtil(); util.seria(test); util.seria(test);//第二次序列化该对象 System.out.println("反序列化**********************"); ReferenceTest obj = (ReferenceTest) util.reSeria(); ReferenceTest obj1 = (ReferenceTest) util.reSeria(); System.out.println(obj == obj1);//true System.out.println(obj == test);//false }} 上面的例子证明(System.out.println(obj == obj1);//true),同一对象序列化多次之后,反序列化得到的多个对象相等,即内存地址一致。 使用同一个ObjectOutputStream对象序列化某个实例时,如果该实例还没有被序列化过,则序列化,若之前已经序列化过,则不再进行序列化,只是做一个标记而已。所以在反序列化时,可以保持原有的引用关系。 System.out.println(obj == test);//false 也可以理解,反序列化之后重建了该对象,内存地址必然是新分配的,故obj != test 父类没有实现Serializable,父类中的变量如何序列化12345678910111213141516171819202122232425262728293031323334353637public class SuperTest{ public String superB; public SuperTest() { superB = "hehe"; System.out.println("super 无参构造函数"); } public SuperTest(String b){ System.out.println("super 有参构造函数"); superB = b; } public static void main(String[] args) throws IOException, ClassNotFoundException { System.out.println("构造对象*******************"); SonTest sonTest = new SonTest("son", "super"); System.out.println("序列化*********************"); SeriaUtil seriaUtil = new SeriaUtil(); seriaUtil.seria(sonTest); System.out.println("反序列化******************"); SonTest obj = (SonTest) seriaUtil.reSeria(); System.out.println(obj.sonA); System.out.println(obj.superB); }}class SonTest extends SuperTest implements Serializable{ private static final long serialVersionUID = 1L; public String sonA; public SonTest() { System.out.println("son 无参构造函数"); } public SonTest(String a, String b) { super(b); System.out.println("son 有参构造函数"); sonA = a; }} 输出: 构造对象*super 有参构造函数son 有参构造函数序列化*反序列化**super 无参构造函数sonhehe 通过上面的代码可以看出,父类如果没有实现serializable,反序列化时会调用父类的无参构造函数初始化父类当中的变量。 所以,我们可以通过显示声明父类的无参构造函数,并在其中初始化变量值来控制反序列化后父类变量的值。 transient使用实现了serializable接口的类在序列化时默认会将所有的非静态变量进行序列化。我们可以控制某些字段不被默认的序列化机制序列化。 比如,有些字段是跟当前系统环境相关的或者涉及到隐私的,需要保密的。这些字段是不可以被序列化到文件中或者通过网络传输的。我们可以通过为这些字段声明transient关键字,保证其不被序列化。 被关键字transient声明的变量不会被序列化,反序列化时该变量会被自动填充为null(int 为0)。我们也可以为这些字段实现自己的序列化机制。 123456789101112131415161718192021222324252627public class TransientTest implements Serializable{ private static final long serialVersionUID = 1L; public transient String str; public TransientTest() { str = "hello"; } private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); String encryption = "key" + str; out.writeObject(encryption); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); String encryption = (String) in.readObject(); str = encryption.substring("key".length(), encryption.length()); } public static void main(String[] args) throws IOException, ClassNotFoundException { TransientTest test = new TransientTest(); SeriaUtil util = new SeriaUtil(); util.seria(test); TransientTest reSeria = (TransientTest) util.reSeria(); System.out.println(reSeria.str);//hello }} 通过实现writeObject和readObject实现自己的序列化机制。上面的代码模拟了一个加密的序列化过程。 成员变量没有实现序列化序列化某个实例时,如果这个实例含有对象类型的成员变量,那么同时会触发该变量的序列化机制。这时就要求这个成员变量也实现Serializable接口,如果没有实现该接口,抛出异常。 1234567891011121314151617181920212223public class VariableTest implements Serializable{ Variable variable ; public VariableTest() { variable = new Variable(); } public static void main(String[] args) throws IOException, ClassNotFoundException { System.out.println("构造对象*************"); VariableTest variableTest = new VariableTest(); System.out.println("序列化**************"); SeriaUtil util = new SeriaUtil(); //抛出异常:java.io.NotSerializableException util.seria(variableTest); System.out.println("反序列化****************"); VariableTest obj = (VariableTest) util.reSeria(); System.out.println(obj.variable.a);//抛出异常:Exception in thread "main" java.io.NotSerializableException: space.kyu.Variable }}class Variable { public String a; public Variable(){ a = "hehe"; }} 单例模式下的序列化1234567891011public class SingleTest implements Serializable{ public static SingleTest instance = new SingleTest(); private SingleTest(){} public static void main(String[] args) throws IOException, ClassNotFoundException { SingleTest test = SingleTest.instance; SeriaUtil util = new SeriaUtil(); util.seria(test); SingleTest reSeria = (SingleTest) util.reSeria(); System.out.println(reSeria == SingleTest.instance);//false }} 由上面的代码可以看出,有两个SingleTest实例同时存在,通过反序列化破坏了单例模式。反序列化时会开辟新的内存空间重新实例化对象,所以单例模式被破坏。 为了解决这种问题,可以实现readResolve()方法。 1234567891011121314public class SingleTest implements Serializable{ public static SingleTest instance = new SingleTest(); private SingleTest(){} private Object readResolve() { return SingleTest.instance; } public static void main(String[] args) throws IOException, ClassNotFoundException { SingleTest test = SingleTest.instance; SeriaUtil util = new SeriaUtil(); util.seria(test); SingleTest reSeria = (SingleTest) util.reSeria(); System.out.println(reSeria == SingleTest.instance);//true }} 序列化版本代码是在不断的演化的。1.1版本的类可以读取1.0版本的序列化文件吗?这就涉及到序列化的版本管理。 每个序列化版本都有其唯一的ID,他是数据域类型和方法签名的指纹。当类的定义产生变化时,他的指纹也会跟着产生变化,对象流将拒绝读入具有不同指纹的对象。如果想保持早期版本的兼容,首先要获取这个类早期版本的指纹。 我们可以使用 jdk自带的工具 serialver 获得这个指纹:serialver Test1staic final long serialVersionUID = -1423859403827594712L 然后将1.1版本中Test类的serialVersionUID常量定义为上面的值,即可序列化老版本的代码。 如果一个类具有名为serialVersionUID的常量,那么java就不会再主动计算这个值,而是直接将其作为这个版本类的指纹。没有特殊要求的话,一般都显示的声明serialVersionUID:private static final long serialVersionUID = 1L;来保证兼容性 如果对象流中的对象具有在当前版本中没有的数据域,那么对象流会忽略这些数据;如果当前版本具有对象流中所没有的数据域,那么这些新加的域将被设为默认值。 序列化与克隆反序列化重新构建对象的机制提供了一种克隆对象的简便途径,只要对应的类可序列化即可。 做法很简单:直接将对象序列化到输出流当中,然后将其读回。这样产生的对象是对现有对象的一个深拷贝。 12345678910111213141516171819202122232425public class CloneTest implements Serializable, Cloneable { public String str; public CloneTest(String str) { this.str =str; } @Override protected Object clone() throws CloneNotSupportedException { SeriaUtil util = new SeriaUtil(); try { util.seria(this); CloneTest reSeria = (CloneTest) util.reSeria(); return reSeria; } catch (Exception e) { e.printStackTrace(); return null; } } public static void main(String[] args) throws CloneNotSupportedException { CloneTest test = new CloneTest("hi"); CloneTest clone = (CloneTest) test.clone(); System.out.println(clone.str);//hi System.out.println(clone == test);//false }} 这样克隆对象的优点是简单,缺点是比普通的克隆实现要慢的多。]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java I/O总结]]></title>
<url>%2F2017%2F03%2F19%2FI-O%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[在平时维护JDBC驱动的过程中,经常会接触到IO相关的代码。总结梳理一下java中的IO~ IOIO,即Input和Output。流可以理解为字节序列的流动,可以从其中读入字节序列的对象称为输入流,可以向其中写入字节序列的对象称为输出流。这些字节序列的来源和目的地可以是文件,网络,或者内存等。 java中的IO基本可以分为两大类: 1.基于字节操作的 I/O 接口:InputStream 和 OutputStream 2.基于字符操作的 I/O 接口:Writer 和 Reader 不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是我们的程序通常操作的数据都是以字符的形式存在,比如处理网页中的内容或者磁盘文件中的文本。为了操作方便,提供了直接操作字符的接口。操作字符的接口底层还是基于字节的,只不过封装了一些例如编码和解码等操作。 下面是java.io包中的内容:Package java.io java.io 类图: 字节接口 上图给出了InputStream和OutputStream的继承关系(不仅仅是java.io包) 理解InputStream家族,首先要理解装饰者模式: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263Component:定义一个对象接口,可以给这些对象动态地添加职责。public interface Component{ void operation();} Concrete Component:定义一个对象,可以给这个对象添加一些职责。动作的具体实施者。public class ConcreteComponent implements Component{ public void operation() { // Write your code here }} Decorator:维持一个指向Component对象的引用,并定义一个与 Component接口一致的接口。public class Decorator implements Component{ public Decorator(Component component) { this.component = component; } public void operation() { component.operation(); } private Component component;} Concrete Decorator:在Concrete Component的行为之前或之后,加上自己的行为,以“贴上”附加的职责。public class ConcreteDecorator extends Decorator{ public void operation() { //addBehavior也可以在前面 super.operation(); addBehavior(); } private void addBehavior() { //your code }} 使用装饰模式来实现扩展比继承更加灵活,它以对客户透明的方式动态地给一个对象附加更多的责任。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。 InputStream族与装饰者模式的对应关系: Component: InputStream ConcreteComponent: InputStream除FilterINputStream以外的直接子类,如FileInputStream,他们提供了最终的读取字节功能 Decorator: FilterInputStream ConcreteDecorator: FilterINputStream的直接子类,如BufferedInputStream,他们为读取字节附加了一些功能 首先看一下InputStream为我们提供了哪些操作: 123456789101112131415161718int available()返回不阻塞情况下可用的字节数void close()关闭这个输入流void mark(int readlimit)在流的当前位置打一个标记boolean markSupported()如果这个流支持打标记,则返回trueabstract int read()从数据中读取一个字节,并返回该字节int read(byte[] b)读入一个字节数组,并返回读入的字节数int read(byte[] b, int off, int len)读入一个字节数组,并返回读入的字节数。b:数据读入的数组 off:第一个读入的字节在b中的偏移量 len:读入字节的最大数量void reset()返回最后的标记,随后对read的调用将重新读入这些字节long skip(long n)在输入流中跳过n个字节,返回实际跳过数 其中:abstract int read()int read(byte[] b)int read(byte[] b, int off, int len) 这三个方法提供了基本的读入功能。read方法在执行时将被阻塞,直到字节确实被读入。InputStream的子类实现或重写了这些方法的具体的操作,来完成对具体对象的读入功能。 通过一个简单的例子来理解InputStream的组合过滤器功能。 我们要从文件中读入数字,则首先必须有一个具体实现读取文件的FileInputStream实例: FileInputStream fin = new FileInputStream(“test.dat”); 流在默认情况下是不被缓冲区缓存的,也就是说,对于每一次的read的调用都会请求操作系统再分发一个字节。相比之下,一次请求一个数据块将其置于缓冲区显得更加高效。因此,我们为流添加缓冲功能,形成缓冲流: BufferedInputStream bin = new BufferedInputStream(fin); test.dat文件中存储的是十进制数字的二进制序列。此时我们的流仅仅提供了读取字节序列的功能,为了实现将二进制序列转为十进制数字的功能,我们做进一步转换:DataInputStream din = new DataInputStream(bin); 此时我们可以调用 din.readInt()方法依次读取文件中以二进制形式存储的十进制数字了。 注意,我们将DataInputStream置于构造链的最后,这是因为我们最终希望使用DataInputStream的方法来读取十进制数字。 观察一下上面提到的几个类的实现,就会对这种装饰者模式的工作机制有更加深刻的理解。理解了装饰者模式之后,再结合上面的类图,使用IO流就会更加得心应手。 对于OutputStream,实现原理与InputStream是一样的,不再赘述。 记录几个常用的stream: DataOutputStream: 将基本类型的数据以二进制流的形式写出 DataInputStream: 将二进制流读入为基本类型数据 ObjectInputStream: 将Java对象以二进制流的形式写出 (序列化使用) ObjectOutputStream: 将二进制流读入为java对象 (序列化时使用) PipedOutputStream和PipedInputStream分别是管道输出流和管道输入流,让多线程可以通过管道进行线程间的通讯 ZipOutputStream和ZipInputStream: 文件压缩与解压缩 PushBackInputStream:回退流 字符接口 在保存数据时,可以选择二进制格式保存或者文本格式保存。比如,整数12234可以存储成二进制,是由00 00 04 D2构成的字节序列。而存储成文本格式,则存储成为字符串“1234”。二进制格式的存储高效且节省空间,但是文本格式的存储方式更适宜人类阅读,应用也很广泛。 与字节接口类似,字符接口族也是采用了装饰者模式的架构。 在存储或读取文本字符串时,可以选择编码。比如: InputStreamReader reader = new InputStreamReader(new FileInputStream(“test.dat”),”UTF-8”); reader将使用GBK编码读取文本test.dat的内容。如果构造器没有显示指定编码,将使用主机系统所使用的默认文字编码方式。 与DataOutputStream对应,PrintWriter用来以文本的格式打印字符串和数字。 与DataInputStream对应,可以使用Scanner类来读取文本格式的数据。 字符集字符集规定了某个字符对应的二进制数字存放方式(编码)和某串二进制数值代表了哪个字符(解码)的映射关系。 JavaSE-1.4的java.nio包中引入了类Charset统一了对字符集的转换。 通过观察InputStreamReader的源码(1.7),InputStreamReader 将字符的读取与解码委托给了类StreamDecoder实现。而在StreamDecoder中,又是通过传入的InputStream与指定的Charset配合完成了字节序列的读取和解码工作。 可以通过调用静态的forName方法获取一个Charset:1Charset charset = Charset.forName("UTF-8"); 其中,传入的参数是某个字符集的官方名或者别名。 Set alias = charset.aliases(); //获取某个Charset的所有可用别名Map charsets = Charset.availableCharsets(); //获取所用可用字符集的名字 有了字符集Charset,就可以通过他将字节序列解码为字符序列或者将字符序列编码为字节序列: 12345678910//编码String str = "hello";ByteBuffer bb = charset.encode(str);byte[] bytes = bb.array();//解码byte[] bytes = ....ByteBuffer bb = ByteBuffer.wrap(bytes, 0, bytes.length);CharBuffer cb = Charset.decode(bb);String str = cb.toString(); 实际上,通过观察源码,得知InputStreamReader也是这么做的(还有String)。 文件操作Stream关心的是文件的内容,File类关心的是文件的存储。 关于File的使用,网上有很多介绍,可以参考官网Class File,不再赘述。 要注意一个类RandomAccessFile,可以在文件的任何位置查找或者写入数据,RandomAccessFile同时实现了DataInput和DataOutput接口。 我们可以使用RandomAccessFile随机读写的特性来完成大文件的上传或者下载。把文件分为n份,开启n个线程同时对这n个部分进行读写操作,提高了读写的效率。(让我想起了ConcurrentHashMap,分段锁的原理)同时,还具有了断点续传的功能。]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>NIO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[webservice相关]]></title>
<url>%2F2017%2F03%2F15%2Fwebservice%E7%9B%B8%E5%85%B3%2F</url>
<content type="text"><![CDATA[上周最后三天去北京培训了Hadoop,了解了一些目前流行的分布式组件。在谈到这些组件的交互过程中,经常会提到RPC,webservice等关键词,所以简单了解了一下这些关键词的含义~ WebServicewebservice,拆开来看就是 web(网络)和service(服务),先了解一下什么是服务: 计算机的后台进程提供了某种功能,我们把提供了这种功能的进程称为守护进程(Daemon),提供的功能称为服务。比如,我们启动了数据库,数据库进程就会一直运行在后台监听连接数据库的动作,这种监听连接的功能就是一种服务。 服务分为本地服务和网络服务。使用同一台机器上提供的服务就是本地服务,通过网络连接到另一台计算机使用它提供的服务就是网络服务。 所以,webservice 就是通过网络使用了其他服务器提供的某种功能或获取了某些资源。 这样一来,webservice就很常见了。比如我们做了一个显示天气情况的app,使用了百度地图提供的定位功能,使用了其他服务商提供的天气数据,这些都属于webservice。 webservice可以包含以下几个实现: RPC:面向过程 RMI:面向对象 REST:面向资源 Web Service tutorial RPC概念 远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用。 上面的解释摘自维基百科 RPC属于webservice的一种,就是在一个进程中调用另一个进程中的服务。就像调用本地方法一样调用远程服务器的方法。 RPC有很多实现,包括XML-RPC、JSON-RPC、JAX-RPC等等。 一次RPC调用过程就是向服务器发送一个过程调用的方法和参数,得到服务器返回的方法执行结果。RPC的本质就是一次远程调用,但更强调透明调用。RPC是跨语言的。 使用以Json-Rpc 为例,看一下RPC是如何工作的: 使用java中的json-rpc实现jsonrpc4j:(也有其他语言的实现) Create your service interface:1234567package com.mycompany;public interface UserService { User createUser(String userName, String firstName, String password); User createUser(String userName, String password); User findUserByUserName(String userName); int getUserCount();} Implement it:1234567891011121314151617181920212223242526package com.mycompany;public class UserServiceImpl implements UserService { public User createUser(String userName, String firstName, String password) { User user = new User(); user.setUserName(userName); user.setFirstName(firstName); user.setPassword(password); database.saveUser(user) return user; } public User createUser(String userName, String password) { return this.createUser(userName, null, password); } public User findUserByUserName(String userName) { return database.findUserByUserName(userName); } public int getUserCount() { return database.getUserCount(); }} Server12345678910111213141516class UserServiceServlet extends HttpServlet { private UserService userService; private JsonRpcServer jsonRpcServer; protected void doPost(HttpServletRequest req, HttpServletResponse resp) { jsonRpcServer.handle(req, resp); } public void init(ServletConfig config) { //this.userService = ... this.jsonRpcServer = new JsonRpcServer(this.userService, UserService.class); }} Client123456789JsonRpcHttpClient client = new JsonRpcHttpClient( new URL("http://example.com/UserService.json"));UserService userService = ProxyUtil.createClientProxy( getClass().getClassLoader(), UserService.class, client);User user = userService.createUser("bob", "the builder"); 上面的例子只是最简单的访问方式,在分布式环境中,RPC还涉及到服务寻址,负载均衡等等问题。 更加详细的RPC介绍,可以参考RPC 是什么 为什么是RPC在网上查看RPC资料的时候,就想到一个问题,既然RPC这么复杂,为什么不使用HTTP接口调用的方式来进行网络通信呢?以前做一些小demo的时候使用HttpClient就完全OK了。 看到一个很有意思的讨论:为什么需要RPC,而不是简单的HTTP接口 RPC调用简单,适用于业务逻辑比较复杂的情况,比如分布式环境当中。RPC强调透明调用,也利于解耦。 原理QiuRPC:一个通用的网络RPC框架 你应该知道的 RPC 原理 SOAPXML-RPC只能使用有限的数据类型种类和一些简单的数据结构。于是就出现了SOAP(Simple Object Access Protocol)。 SOAP (Simple Object Access Protocol) 顾名思义,是一个严格定义的信息交换协议,用于在Web Service中把远程调用和返回封装成机器可读的格式化数据。 事实上SOAP数据使用XML数据格式,定义了一整套复杂的标签,以描述调用的远程过程、参数、返回值和出错信息等等。而且随着需要的增长,又不得增加协议以支持安全性,这使SOAP变得异常庞大,背离了简单的初衷。另一方面,各个服务器都可以基于这个协议推出自己的API,即使它们提供的服务及其相似,定义的API也不尽相同,这又导致了WSDL的诞生。 WSDL (Web Service Description Language) 也遵循XML格式,用来描述哪个服务器提供什么服务,怎样找到它,以及该服务使用怎样的接口规范,简言之,服务发现。 现在,使用Web Service的过程变成,获得该服务的WSDL描述,根据WSDL构造一条格式化的SOAP请求发送给服务器,然后接收一条同样SOAP格式的应答,最后根据先前的WSDL解码数据。绝大多数情况下,请求和应答使用HTTP协议传输,那么发送请求就使用HTTP的POST方法。 Working Soap client example SOAP Messaging Models and Examples 通过soap demo体会soap与rpc的区别 RMIRMI其实也属于RPC的一种,是面向对象的。RMI只能在java里玩,不支持跨语言。与RPC不同的是,RMI可以返回java对象以及基本的数据类型,RPC不允许返回对象,RPC服务的信息由外部数据表示。RMI的优势在于依靠Java序列化机制,对开发人员屏蔽了数据编排和解排的细节,要做的事情非常少。 用代码说话: RMI采用的是典型的客户端-服务器端架构。首先需要定义的是服务器端的远程接口,这一步是设计好服务器端需要提供什么样的服务。对远程接口的要求很简单,只需要继承自RMI中的Remote接口即可。Remote和Serializable一样,也是标记接口。远程接口中的方法需要抛出RemoteException。定义好远程接口之后,实现该接口即可。如下面的Calculator是一个简单的远程接口。123public interface Calculator extends Remote { String calculate(String expr) throws RemoteException;} 实现了远程接口的类的实例称为远程对象。创建出远程对象之后,需要把它注册到一个注册表之中。这是为了客户端能够找到该远程对象并调用。12345678910public class CalculatorServer implements Calculator { public String calculate(String expr) throws RemoteException { return expr; } public void start() throws RemoteException, AlreadyBoundException { Calculator stub = (Calculator) UnicastRemoteObject.exportObject(this, 0); Registry registry = LocateRegistry.getRegistry(); registry.rebind("Calculator", stub); }} CalculatorServer是远程对象的Java类。在它的start方法中通过UnicastRemoteObject的exportObject把当前对象暴露出来,使得它可以接收来自客户端的调用请求。再通过Registry的rebind方法进行注册,使得客户端可以查找到。 客户端的实现就是首先从注册表中查找到远程接口的实现对象,再调用相应的方法即可。实际的调用虽然是在服务器端完成的,但是在客户端看来,这个接口中的方法就好像是在当前JVM中一样。这就是RMI的强大之处。123456789101112public class CalculatorClient { public void calculate(String expr) { try { Registry registry = LocateRegistry.getRegistry("localhost"); Calculator calculator = (Calculator) registry.lookup("Calculator"); String result = calculator.calculate(expr); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } }} 在运行的时候,需要首先通过rmiregistry命令来启动RMI中用到的注册表服务器。 为了通过Java的序列化机制来进行传输,远程接口中的方法的参数和返回值,要么是Java的基本类型,要么是远程对象,要么是实现了 Serializable接口的Java类。当客户端通过RMI注册表找到一个远程接口的时候,所得到的其实是远程接口的一个动态代理对象。当客户端调用其中的方法的时候,方法的参数对象会在序列化之后,传输到服务器端。服务器端接收到之后,进行反序列化得到参数对象。并使用这些参数对象,在服务器端调用实际的方法。调用的返回值Java对象经过序列化之后,再发送回客户端。客户端再经过反序列化之后得到Java对象,返回给调用者。这中间的序列化过程对于使用者来说是透明的,由动态代理对象自动完成。除了序列化之外,RMI还使用了动态类加载技术。当需要进行反序列化的时候,如果该对象的类定义在当前JVM中没有找到,RMI会尝试从远端下载所需的类文件定义。可以在RMI程序启动的时候,通过JVM参数java.rmi.server.codebase来指定动态下载Java类文件的URL。 RESTREST只是一种软件架构的风格,而不是一种协议或者其他。 REST基于HTTP协议,一般使用JSON传输数据。REST是面向资源的,资源由URI来表示。 以下解释参考自理解RESTful架构 要理解RESTful架构,最好的方法就是去理解Representational State Transfer这个词组到底是什么意思,它的每一个词代表了什么涵义。如果你把这个名称搞懂了,也就不难体会REST是一种什么样的设计。 资源(Resources) REST的名称”表现层状态转化”中,省略了主语。”表现层”其实指的是”资源”(Resources)的”表现层”。 所谓”资源”,就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或独一无二的识别符。 所谓”上网”,就是与互联网上一系列的”资源”互动,调用它的URI。 表现层(Representation) “资源”是一种信息实体,它可以有多种外在表现形式。我们把”资源”具体呈现出来的形式,叫做它的”表现层”(Representation)。 比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式;图片可以用JPG格式表现,也可以用PNG格式表现。 URI只代表资源的实体,不代表它的形式。严格地说,有些网址最后的”.html”后缀名是不必要的,因为这个后缀名表示格式,属于”表现层”范畴,而URI应该只代表”资源”的位置。它的具体表现形式,应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对”表现层”的描述。 状态转化(State Transfer) 访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。 互联网通信协议HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生”状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是”表现层状态转化”。 客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。 综述 综合上面的解释,我们总结一下什么是RESTful架构:(1)每一个URI代表一种资源;(2)客户端和服务器之间,传递这种资源的某种表现层;(3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。 参考RESTful API 设计指南设计Rest API github的API设计就是REST风格的。 网上关于REST和PRC的争论有很多,总的来说有以下几个: 安全性上:SOAP安全性高于REST 成熟度上:SOAP在成熟度上优于REST 效率和易用性上:REST更胜一筹 参考你应该知道的 RPC 原理 RPC 是什么 Web service是什么? Java深度历险(十)——Java对象序列化与RMI]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>web</tag>
</tags>
</entry>
<entry>
<title><![CDATA[linux分区]]></title>
<url>%2F2017%2F03%2F04%2Flinux%E5%88%86%E5%8C%BA%2F</url>
<content type="text"><![CDATA[每次安装linux的时候,都会选择分区和挂载点。但是基本上没怎么研究过linux分区的细节,今天回顾了一下《鸟哥的linux私房菜》,总结一下分区的相关知识。 磁盘的组成 磁道(Track):当磁盘旋转时,磁头若保持在一个位置上,则每个磁头都会在磁盘表面划出一个圆形轨迹,这些圆形轨迹就叫做磁道(Track)。 柱面(Cylinder):在有多个盘片构成的盘组中,由不同盘片的面,但处于同一半径圆的多个磁道组成的一个圆柱面(Cylinder)。 扇区(Sector):磁盘上的每个磁道被等分为若干个弧段,这些弧段便是硬盘的扇区(Sector)。 磁头(Heads) 盘片(Platters) 每个碟片都有两面,因此也会相对应每碟片有2个磁头。 硬盘的物理结构一般由磁头与碟片、电动机、主控芯片与排线等部件组成;当主电动机带动碟片旋转时,副电动机带动一组(磁头)到相对应的碟片上并确定读取正面还是反面的碟面,磁头悬浮在碟面上画出一个与碟片同心的圆形轨道(磁轨或称柱面),这时由磁头的磁感线圈感应碟面上的磁性与使用硬盘厂商指定的读取时间或数据间隔定位扇区,从而得到该扇区的数据内容; 磁盘的第一个扇区记录了整块磁盘的重要信息,主要有两个: 1.主引导分区:可以安装引导加载程序的地方,有446bytes 2.分区表:记录整块音盘的分区状况,有64bytes 开机流程简单梳理一下开机流程: 计算机开机后,会主动执行BIOS程序,BIOS是写入到主板上面的一个软件程序。接下来BIOS会去分析计算机中有哪些存储设备,然后依据用户的设置获取可以开机的硬盘(比如我们设置从usb启动),然后到读取该硬盘中的第一个扇区MBR位置。MBR中放置着基本的引导加载程序,接下来就是MBR内的引导加载程序加载内核文件。这个引导加载程序是操作系统在安装时提供的。 磁盘分区表分区不难理解,在windows中,就意味着C,D,E等不同的盘,其实这些c盘,d盘往往都是属于同一块磁盘,将同一块磁盘划分开来,这就是分区。那为什么要分区呢? 1.数据安全 很好理解。比如我们在重装win操作系统的时候,往往只需要重装c盘即可,d盘等其他分区里的数据并不受重装系统的影响。 2.性能 将磁盘分区后,提高了数据读取的速度。我们在寻找某个分区的数据时,只需要扫描该分区对应磁盘的位置即可,并不需要全盘扫描。 那么,到底是如何分区呢? 上图中,不同颜色的柱面范围就代表了不同的分区。分区利用了柱面号码的方式来处理。在分区表所在的64bytes中,总共分为四组记录,每组记录该分区的起始与结束柱面号码。 我们将上图中从圆心到周长中间切出一条长方形来看: 上图假设硬盘有400个柱面,分成四个分区。所谓的分区其实就是针对分区表进行设置而已。分区表最多可以容纳四个分区(只能记录四条数据),这四个分区被称为主分区或者扩展分区。系统要写磁盘时,首先会参考磁盘分区表,然后才对某个分区的数据进行处理。 假设这个磁盘在linux中的设备文件名为/dev/hda,那么这四个分区的文件名分别为: p1:/dev/hda1 p2:/dev/hda2 p3:/dev/hda3 p4:/dev/hda4 那么如何可以分得更多的分区呢?装过操作系统的人都知道分区不仅仅可以分四个,我们可以有c,d,e,f,g等等多个磁盘的划分。这都是通过扩展分区来做到的。 扩展分区的原理就是利用额外的扇区来记录更多的分区信息,从而继续分出更多的分区来。由扩展分区分出来的分区叫做逻辑分区。 上图中,我们将磁盘/dev/sdb 分为了六个分区,分别是 三个主分区:/dev/sdb1,/dev/sdb2/,/dev/sdb3 三个逻辑分区:/dev/sdb5,/dev/sdb6/,/dev/sdb7 为什么没有sdb4呢?那是因为1-4是保留给主分区或者扩展分区使用的。 注意以下几点: 主分区和扩展分区最多只能有四个; 扩展分区只能有一个; 逻辑分区是有扩展分区再切割而来的; 扩展分区不能够格式化,所以无法直接使用,必须分为逻辑分区后才可以访问; 所以说,如果我们想要分出四个以上的分区时,务必要设置一个扩展分区,而不能将四个分区全部划为主分区。 多重引导前面的开机流程中提到,计算机通过读取MBR中的引导加载程序,使用该程序读取内核文件,启动操作系统。如果我们安装了双系统的话,又是如何指定启动哪一个操作系统呢? 引导加载程序主要有下面几个功能: 提供不同的开机选项; 载入内核文件; 将引导加载功能转交给其他引导加载程序; 其中第三点,转交给其他引导加载程序,表明我们可以安装不同的引导加载程序到硬盘上面,但是MBR只有一个,也只能安装一个引导加载程序。其他的引导加载程序,可以安装在不同分区的引导扇区上面。 上图中,我们假设分别在两块分区上安装了windows和linux。当MBR中的引导加载程序开始工作时,会提供两个开机选项供我们选择: 如果我们选择windows,引导加载程序直接加载windows的内核文件开机; 如果我们选择linux,引导加载程序会把工作交给第二个分区的启动扇区中的引导加载程序。第二个引导加载程序启动后,加载该分区内的内核文件开机。 那么,为什么安装双系统时,常常要求先安装windows,后安装linux呢? 那是因为windows在安装的时候,会主动覆盖掉MBR及自己所在分区的启动扇区,并且也没有提供不同的开机选项菜单;而安装linux,可以选择将引导程序安装在MBR或者其他分区的启动扇区,并且也提供了手动设置开机菜单的功能。 如果我们先安装了linux,再安装windows的时候就会把linux在MBR内的引导加载程序覆盖掉,并且也不会提供linux选项,而是直接进入windows系统。 挂载点安装linux的时候,都会让我们选择挂载点。这个挂载点又与分区有什么关系呢? 我们知道linux中所有的数据都以文件的形式存在,而文件数据是放在磁盘的分区当中的。所有的文件都是又根目录/衍生而来,我们想要取得/home/yukai/data.txt这个文件时,系统又根目录开始找,找到home,然后找到yukai,最后找到data.txt这个文件。如何由目录树找到磁盘分区中的数据,就是挂载点的意义。 所谓的挂载就是利用一个目录当作进入点,去访问挂载在这个目录上的分区内的文件,即进入该目录就可以读取该分区,该目录是该分区的入口。我们想要访问一个分区时,必须将该分区挂载到某个目录上面,这个目录就是挂载点。 所以说在安装linux的时候,要选择分区和挂载点,意思就是把不同的数据放置到不同的分区上的意思,比如我们把 /dev/sda1的挂载点设置为/home,就意味着/home下所有的数据都存放在/dev/sda1这个分区上面。]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Linux的五个查找命令]]></title>
<url>%2F2017%2F02%2F26%2FLinux%E7%9A%84%E4%BA%94%E4%B8%AA%E6%9F%A5%E6%89%BE%E5%91%BD%E4%BB%A4%2F</url>
<content type="text"><![CDATA[最近一直在读阮一峰老师的博客,今天读到一篇介绍Linux查找命令的文章。恰好最近在学习linux,所以转载过来,当作一篇备忘录。 原文链接:Linux的五个查找命令 使用电脑的时候,经常需要查找文件。 在Linux中,有很多方法可以做到这一点。国外网站LinuxHaxor总结了五条命令,你可以看看自己知道几条。大多数程序员,可能经常使用其中的2到3条,对这5条命令都很熟悉的人应该是不多的。 findfind是最常见和最强大的查找命令,你可以用它找到任何你想找的文件。 find的使用格式如下:1234 $ find <指定目录> <指定条件> <指定动作> - <指定目录>: 所要搜索的目录及其所有子目录。默认为当前目录。 - <指定条件>: 所要搜索的文件的特征。 - <指定动作>: 对搜索结果进行特定的处理。 如果什么参数也不加,find默认搜索当前目录及其子目录,并且不过滤任何结果(也就是返回所有文件),将它们全都显示在屏幕上。 find的使用实例:123456 $ find . -name 'my*'搜索当前目录(含子目录,以下同)中,所有文件名以my开头的文件。 $ find . -name 'my*' -ls搜索当前目录中,所有文件名以my开头的文件,并显示它们的详细信息。 $ find . -type f -mmin -10搜索当前目录中,所有过去10分钟中更新过的普通文件。如果不加-type f参数,则搜索普通文件+特殊文件+目录。 locatelocate命令其实是”find -name”的另一种写法,但是要比后者快得多,原因在于它不搜索具体目录,而是搜索一个数据库(/var/lib/locatedb),这个数据库中含有本地所有文件信息。Linux系统自动创建这个数据库,并且每天自动更新一次,所以使用locate命令查不到最新变动过的文件。为了避免这种情况,可以在使用locate之前,先使用updatedb命令,手动更新数据库。 locate命令的使用实例:123456 $ locate /etc/sh搜索etc目录下所有以sh开头的文件。 $ locate ~/m搜索用户主目录下,所有以m开头的文件。 $ locate -i ~/m搜索用户主目录下,所有以m开头的文件,并且忽略大小写。 whereiswhereis命令只能用于程序名的搜索,而且只搜索二进制文件(参数-b)、man说明文件(参数-m)和源代码文件(参数-s)。如果省略参数,则返回所有信息。 whereis命令的使用实例:1 $ whereis grep whichwhich命令的作用是,在PATH变量指定的路径中,搜索某个系统命令的位置,并且返回第一个搜索结果。也就是说,使用which命令,就可以看到某个系统命令是否存在,以及执行的到底是哪一个位置的命令。 which命令的使用实例:1 $ which grep typetype命令其实不能算查找命令,它是用来区分某个命令到底是由shell自带的,还是由shell外部的独立二进制文件提供的。如果一个命令是外部命令,那么使用-p参数,会显示该命令的路径,相当于which命令。 type命令的使用实例:123456 $ type cd系统会提示,cd是shell的自带命令(build-in)。 $ type grep系统会提示,grep是一个外部命令,并显示该命令的路径。 $ type -p grep加上-p参数后,就相当于which命令。]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[centos7BCM驱动安装]]></title>
<url>%2F2017%2F02%2F25%2Fcentos7BCM%E9%A9%B1%E5%8A%A8%E5%AE%89%E8%A3%85%2F</url>
<content type="text"><![CDATA[前段时间安装了centos后,一直使用网线上网。今天是周末,安装了一下无线驱动,可以愉快的使用无线wifi上网啦~ 查看无线驱动1iwconfig 如果没有iwconfig命令,则先安装: 1sudo yum install wireless-tools 如上图显示有类似wlp7s0这样的信息,则表示无线驱动已经安装好了。若没有,则全部为no wireless extensions. 没有安装无线驱动,进行下面的操作。 查看网卡型号1lspci |grep -i network 显示: 107:00.0 Network controller: Broadcom Limited BCM43142 802.11b/g/n (rev 01) 表示是BCM的网卡 查看内核信息1uname -r 显示内核信息:13.10.0-514.6.1.el7.x86_64 注意上面的发行版版本为el6,后面64为64位操作系统 编译安装驱动程序下面只针对el7 64的情况,其他配置的系统参考:wl-kmod 1.安装工具12345yum group install 'Development Tools'yum install redhat-lsb kernel-abi-whitelistsyum install kernel-devel-$(uname -r) 2.切换到普通用户,配置构建树123mkdir -p ~/rpmbuild/{BUILD,RPMS,SPECS,SOURCES,SRPMS}echo -e "%_topdir $(echo $HOME)/rpmbuild\n%dist .el$(lsb_release -s -r|cut -d"." -f1).local" >> ~/.rpmmacros 3.下载 wl-kmod*nosrc.rpm 到任意目录,比如 ~/packagehttp://elrepo.org/linux/elrepo/el7/SRPMS/wl-kmod-6_30_223_271-3.el7.elrepo.nosrc.rpm 4.下载合适的驱动http://www.broadcom.com/support/802.11 选择64位的驱动,下载到~/rpmbuild/SOURCES/目录 5.构建kmod-wl1rpmbuild --rebuild --target=`uname -m` --define 'packager yukai' ~/package/wl-kmod*nosrc.rpm 其中,yukai是当前登陆用户,~/package/是第三步下载的rpm文件位置 6.安装kmod-wl 1rpm -Uvh /home/yukai/rpmbuild/RPMS/x86_64/kmod-wl*rpm 第五步构建完成之后,在~/rpmbuild/RPMS/x86_64目录会有kmod-wl*rpm文件生成,安装它即可 7.删除不用的文件 保存/home/yukai/rpmbuild/RPMS/x86_64/kmod-wl*rpm文件,然后删除~/rpmbuild/ 1rm -rf ~/rpmbuild/ 8.重启即可]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ssh的两种用法]]></title>
<url>%2F2017%2F02%2F23%2Fssh%E7%9A%84%E4%B8%A4%E7%A7%8D%E7%94%A8%E6%B3%95%2F</url>
<content type="text"><![CDATA[工作中在远程主机上进行一些操作时,经常使用ssh进行远程登录。后来使用github时,也会用到ssh。其中涉及到了ssh的两种用法,记录一下~ 本文主要参考自阮一峰老师的博客:SSH原理与运用(一):远程登录 ssh是每台linux机器的标准配置。ssh是一种协议,SSH在计算机之间构建网络连接,确保连接双方身份的真实性,同时,它还保证在此连接上传送的数据到达时不会被人更改,不会被他人窃听。 ssh有多种实现,我们一般使用的是openssh,这是一款免费开源的ssh工具。一般在我们使用的linux发行版中,已经预设了这个软件了,所以可以直接使用它。如果是在windows中使用ssh客户端连接远程linux ssh服务,我使用一款名为MobaXterm的工具。 密码登录这是我们使用远程linux机器时最一般的登录方式。使用user这个用户登录到主机host: ssh user@host ssh默认端口是22,如果远程主机ssh服务端口不是22的话,可以通过-p参数修改连接端口: ssh -p 8899 user@host 整个的登录过程: 1.远程主机收到客户端的登录请求,把自己的公钥发给用户。 2.客户端使用这个公钥,将登录密码加密后,发送回来。 3.远程主机用自己的私钥,解密登录密码,如果密码正确,就同意客户端登录。 这样的登录方式有一个问题,那就是如果有人截获了客户端的登录请求(比如使用公共WiFi),然后将自己的公钥发给了客户端,就可以冒充远程主机获取客户端的密码了。Https协议是有CA证书中心作公证的,ssh协议并不存在这样的机构,所以就有中间人攻击的风险。 为了应对这种风险,当客户端第一次登陆远程主机时,会有类似的提示: 123The authenticity of host 'test.linux.org (192.168.1.100)' can't be established. RSA key fingerprint is 46:cf:06:6a:ad:ba:e2:85:cc:d9:c4:8d:15:bb:f3:ec. Are you sure you want to continue connecting (yes/no)? 意思就是要你核对远程主机的公钥指纹,从而证明他的身份。那么我们如何判断这个公钥指纹是不是远程主机的公钥产生的指纹呢?答案是没有好办法,只能由用户自己想办法核对,比如远程主机在网站上贴出了自己的公钥指纹。 当我们输入yes之后,会出现: 1Warning: Permanently added 'test.linux.org,192.168.1.100' (RSA) to the list of known 表示我们已经认可了该主机。 此时要求我们输入登录用户的密码,密码正确则登录。 当远程主机的公钥被接受以后,它就会被保存在文件$HOME/.ssh/known_hosts之中。下次再连接这台主机,系统就会认出它的公钥已经保存在本地了,从而跳过警告部分,直接提示输入密码。 每个SSH用户都有自己的known_hosts文件,此外系统也有一个这样的文件,通常是/etc/ssh/ssh_known_hosts,保存一些对所有用户都可信赖的远程主机的公钥。 公钥登录步骤ssh还提供了另一种登录方式,就是公钥登录,避免了输入密码的麻烦。 所谓”公钥登录”,原理很简单,就是用户将自己的公钥储存在远程主机上。登录的时候,远程主机会向用户发送一段随机字符串,用户用自己的私钥加密后,再发回来。远程主机用事先储存的公钥进行解密,如果成功,就证明用户是可信的,直接允许登录shell,不再要求密码。 这种方法要求用户必须提供自己的公钥。如果没有现成的,可以直接用ssh-keygen生成一个: ssh-keygen 运行结束以后,在$HOME/.ssh/目录下,会新生成两个文件:id_rsa.pub和id_rsa。前者是你的公钥,后者是你的私钥。 这时再输入下面的命令,将公钥传送到远程主机host上面: ssh-copy-id user@host 然后,打开远程主机/etc/ssh/sshd_config这个文件,检查以下几项: RSAAuthentication yesPubkeyAuthentication yesAuthorizedKeysFile .ssh/authorized_keys 然后重启ssh服务就可以了: service sshd restart authorized_keys远程主机将用户的公钥,保存在登录后的用户主目录的$HOME/.ssh/authorized_keys文件中。公钥就是一段字符串,只要把它追加在authorized_keys文件的末尾就行了。 这里不使用上面的ssh-copy-id命令,改用下面的命令,解释公钥的保存过程: ssh user@host ‘mkdir -p .ssh && cat >> .ssh/authorized_keys’ < ~/.ssh/id_rsa.pub 这条命令由多个语句组成,依次分解开来看: “ssh user@host”,表示登录远程主机; 单引号中的mkdir .ssh && cat >> .ssh/authorized_keys,表示登录后在远程shell上执行的命令: “mkdir -p .ssh”的作用是,如果用户主目录中的.ssh目录不存在,就创建一个; ‘cat >> .ssh/authorized_keys’ < ~/.ssh/id_rsa.pub的作用是,将本地的公钥文件~/.ssh/id_rsa.pub,重定向追加到远程文件authorized_keys的末尾。 写入authorized_keys文件后,公钥登录的设置就完成了。 参考簡易 Telnet 與 SSH 主機設定 SSH原理与运用(一):远程登录]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java日志框架的使用]]></title>
<url>%2F2017%2F02%2F21%2Fjava%E6%97%A5%E5%BF%97%E4%BD%BF%E7%94%A8%2F</url>
<content type="text"><![CDATA[之前在web项目中用到了日志,日志在web应用中很重要,特别是对于程序员追查bug而言。但是由于对混乱的日志框架体系不是十分清楚,导致各种jar包冲突和日志不正常输出。今天来总结一下日志框架的使用主要介绍日志门面slf4j结合日志实现log4j。关于其他的日志框架介绍和使用,参考链接里列出了前辈总结的很好的资料,相信读完之后一定会有收获。 日志门面和实际日志框架日志框架有:jdk自带的logging(jul),log4j1、log4j2、logback日志门面有:apache commons-logging、slf4j 日志框架很好理解,就是提供日志api,使我们可以很轻易的,有组织有规范的输出日志。日志门面的作用是在日志记录实现的基础上提供一个封装的 API 层次,对日志记录 API 的使用者提供一个统一的接口,使得可以自由切换不同的日志记录实现。日志门面就好比java中的jdbc规范接口,各个数据库厂家实现的jdbc驱动程序就是实际的日志框架,他们遵循了这个规范,使得我们在编写java程序时不用考虑底层驱动的不同,只需调用jdbc规范接口即可。这是典型的面向对象思想。 slf4j的使用简介 上图来自slf4j官网 slf4j-api.jar包含了slf4j的抽象层API,我们在代码中调用这个jar包中的接口。API层 slf4j-log412.jar、slf4j-jdk14.jar等是slf4j通向具体日志实现框架的桥梁。中间层 log4j.jar等则是具体的日志实现框架。实现层 SLF4J does not rely on any special class loader machinery. In fact, each SLF4J binding is hardwired at compile time to use one and only one specific logging framework. For example, the slf4j-log4j12-1.7.23.jar binding is bound at compile time to use log4j. In your code, in addition to slf4j-api-1.7.23.jar, you simply drop one and only one binding of your choice onto the appropriate class path location. Do not place more than one binding on your class path. Here is a graphical illustration of the general idea. 上面这段话的意思大概是,我们不应该在classpath中绑定多于一个的中间层,否则会导致jar包冲突或者输出混乱。 To switch logging frameworks, just replace slf4j bindings on your class path. For example, to switch from java.util.logging to log4j, just replace slf4j-jdk14-1.7.23.jar with slf4j-log4j12-1.7.23.jar. 当我们需要将日志实现由jul切换到log4j时,仅仅把中间层替换,同时切换实现层即可,并不需要修改代码。这就是日志门面的好处。不过实际应用中,需要切换日志实现的场景貌似不是很多。 slf4j结合log4j依赖需要的jar包:slf4j-api.jar、slf4j-log4j12.jar、log4j.jar 对应的maven依赖: 123456789101112131415<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.23</version></dependency><dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.23</version></dependency><dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version></dependency> 使用编写log4j.properties配置文件,放到类路径下 123456log4j.rootLogger=INFO,stdoutlog4j.appender.stdout=org.apache.log4j.ConsoleAppenderlog4j.appender.stdout.Target=System.outlog4j.appender.stdout.layout=org.apache.log4j.PatternLayoutlog4j.appender.stdout.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n java代码 123456789101112131415161718package space.kyu.LogTest.log4j;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class Test { public static Logger logger = LoggerFactory.getLogger(Test.class); public void test() { logger.info("info"); logger.debug("debug"); logger.warn("warn"); logger.error("error"); } public static void main(String[] args) { new Test().test(); }} 输出 123456[INFO ] 2017-02-21 14:52:28,083 method:space.kyu.LogTest.log4j.Test.test(Test.java:10)info[WARN ] 2017-02-21 14:52:28,085 method:space.kyu.LogTest.log4j.Test.test(Test.java:12)warn[ERROR] 2017-02-21 14:52:28,085 method:space.kyu.LogTest.log4j.Test.test(Test.java:13)error 原理slf4j-api.jar: org.slf4j.LoggerFactory1234public static Logger getLogger(String name) { ILoggerFactory iLoggerFactory = getILoggerFactory(); return iLoggerFactory.getLogger(name);} 可以看到,主要分为两部分,获取ILoggerFactory,使用ILoggerFactory获取logger. slf4j-api.jar: org.slf4j.LoggerFactory1234567891011121314151617181920212223public static ILoggerFactory getILoggerFactory() { if (INITIALIZATION_STATE == UNINITIALIZED) { synchronized (LoggerFactory.class) { if (INITIALIZATION_STATE == UNINITIALIZED) { INITIALIZATION_STATE = ONGOING_INITIALIZATION; performInitialization(); } } } switch (INITIALIZATION_STATE) { case SUCCESSFUL_INITIALIZATION: return StaticLoggerBinder.getSingleton().getLoggerFactory(); case NOP_FALLBACK_INITIALIZATION: return NOP_FALLBACK_FACTORY; case FAILED_INITIALIZATION: throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG); case ONGOING_INITIALIZATION: // support re-entrant behavior. // See also http://jira.qos.ch/browse/SLF4J-97 return SUBST_FACTORY; } throw new IllegalStateException("Unreachable code");} 注意return StaticLoggerBinder.getSingleton().getLoggerFactory();这行,StaticLoggerBinder是slf4j-log4j12.jar中的类 slf4j-log4j12.jar: org.slf4j.impl.StaticLoggerBinder123456789private StaticLoggerBinder() { loggerFactory = new Log4jLoggerFactory(); try { @SuppressWarnings("unused") Level level = Level.TRACE; } catch (NoSuchFieldError nsfe) { Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version"); }} slf4j-log4j12.jar: org.slf4j.impl.Log4jLoggerFactory12345public Log4jLoggerFactory() { loggerMap = new ConcurrentHashMap<String, Logger>(); // force log4j to initialize org.apache.log4j.LogManager.getRootLogger();} 注意org.apache.log4j.LogManager.getRootLogger();初始化了log4j为具体的日志实现 追踪源代码还可以发现,我们在代码中调用的LoggerFactory.getLogger(Test.class);最终返回的是org.apache.log4j.Logger实例,也就是说,日志实现最终托付给了log4j log4j的使用日志组件Loggers:Logger负责捕捉事件并将其发送给合适的Appender。 Appenders:也被称为Handlers,负责将日志事件记录到目标位置。在将日志事件输出之前,Appenders使用Layouts来对事件进行格式化处理。 Layouts:也被称为Formatters,它负责对日志事件中的数据进行转换和格式化。Layouts决定了数据在一条日志记录中的最终形式。 当Logger记录一个事件时,它将事件转发给适当的Appender。然后Appender使用Layout来对日志记录进行格式化,并将其发送给控制台、文件或者其它目标位置。 日志级别每个Logger都被了一个日志级别(log level),用来控制日志信息的输出。日志级别从高到低分为: org.apache.log4j.Level12345678public final static int OFF_INT = Integer.MAX_VALUE;public final static int FATAL_INT = 50000;public final static int ERROR_INT = 40000;public final static int WARN_INT = 30000;public final static int INFO_INT = 20000;public final static int DEBUG_INT = 10000; //public final static int FINE_INT = DEBUG_INT;public final static int ALL_INT = Integer.MIN_VALUE; A:off 最高等级,用于关闭所有日志记录。 B:fatal 指出每个严重的错误事件将会导致应用程序的退出。 C:error 指出虽然发生错误事件,但仍然不影响系统的继续运行。 D:warm 表明会出现潜在的错误情形。 E:info 一般和在粗粒度级别上,强调应用程序的运行全程。 F:debug 一般用于细粒度级别上,对调试应用程序非常有帮助。 G:all 最低等级,用于打开所有日志记录。 我们一般只是用error,warn,info和debug就够了。 配置文件编写了解了组件和日志级别,我们可以编写自己的配置文件,log4j.properties放到类路径下,如果缺少了配置文件,log4j会报错。我们也可以使用PropertyConfigurator.configure ( String configFilename)来指定配置文件 配置logger配置根logger:log4j.rootLogger = [ level ] , appenderName, appenderName, … 比如:log4j.rootLogger=INFO,stdout level为日志级别,表示这个logger只打印级别大于等于level的日志。 appenderName定义了如何处理日志,即把日志输出到哪个地方,如何输出。 可以看出,一个logger可以根据appender同时输出到多个地方,logger与appender是一对多的关系。 我们也可以定义自己的logger:log4j.logger.yukai=DEBUG,stdout 配置appenderappender定义了日志输出的目的地:log4j.appender.appenderName = fully.qualified.name.of.appender.class 其中,Log4j提供的常用的appender: org.apache.log4j.ConsoleAppender(控制台), org.apache.log4j.FileAppender(文件), org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件), org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件), org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方) 我们还可以设置appender的属性,比如针对ConsoleAppender 123456属性 描述layout Appender 使用 Layout 对象和与之关联的模式来格式化日志信息。target 目的地可以是控制台、文件,或依赖于 appender 的对象。level 级别用来控制过滤日志信息。threshold Appender 可脱离于日志级别定义一个阀值级别,Appender 对象会忽略所有级别低于阀值级别的日志。filter Filter 对象可在级别基础之上分析日志信息,来决定 Appender 对象是否处理或忽略一条日志记录。 配置layout一个appender可以关联某一个layout,用来格式化日志的输出。可用的layout有以下几种: org.apache.log4j.HTMLLayout(以HTML表格形式布局), org.apache.log4j.PatternLayout(可以灵活地指定布局模式), org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串), org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息) 可用的格式: %m 输出代码中指定的消息 %p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL %r 输出自应用启动到输出该log信息耗费的毫秒数 %c 输出所属的类目,通常就是所在类的全名 %t 输出产生该日志事件的线程名 %n 输出一个回车换行符,Windows平台为”rn”,Unix平台为”n” %d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921 %l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(Test Log4.java:10) 比如: 12log4j.appender.stdout.layout=org.apache.log4j.PatternLayoutlog4j.appender.stdout.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n 打印的信息: 12[ERROR] 2017-02-21 16:41:25,563 method:space.kyu.LogTest.log4j.Test.test(Test.java:13)info logger的继承关系我们可以定义自己的logger:log4j.logger.yukai=DEBUG,stdout 其中,yukai指定了这个logger的名字,可以在代码中使用它: 1public static Logger logger = LoggerFactory.getLogger("yukai"); 假如我们同时定义这样一个logger:log4j.logger.yukai.child=DEBUG,stdout 就表示yukai.child这个logger继承了yukai这个logger,更java中的包有些类似 在代码中使用: 1public static Logger logger = LoggerFactory.getLogger("yukai.child"); 使用了该logger会默认实现自身的设定和父Logger的设定。比如使用该logger打印了一条debug信息,同样的打印动作会执行两次,因为父logger的打印动作也会实现。 上面提到的rootlogger就是所有looger的根logger。 我们经常在代码中使用 public static Logger logger = LoggerFactory.getLogger(Test.calss);这样的方式,因此我们可以通过指定包名的logger来控制某个包下面所有的logger输出。比如: 我们指定:log4j.logger.space.kyu=DEBUG,stdout,就表示space.kyu包下面的所有logger(以LoggerFactory.getLogger(Test.calss);这种方式获取的)都继承该logger的设定。这在分层的应用或功能性应用中有可以用到。 可以通过配置log4j.additivity.XXX=ture/false来打开或关闭继承功能;若为 false,表示Logger 的 appender 不继承它的父Logger; 若为true,则继承,这样就兼有自身的设定和父Logger的设定。 MDC的使用 在一个高访问量的 Web 应用中,经常要在同一时刻处理大量的用户请求。Web 服务器会为每一个请求分配一个线程,每一个线程都会向日志系统输入一些信息,通常日志系统都是按照时间顺序而不是用户顺序排列这些信息的,这些线程的交替运行会让所有用户的处理信息交错在一起,让人很难分辨出那些记录是同一个用户产生的。另外,高可用性的网站经常会使用负载均衡系统平衡网络流量,这样一个用户的操作记录很可能会分布在多个 Web 服务器上,如果我们没有一种方法来标示一条记录是哪个用户产生的,从这众多的日志信息中筛选出对我们有用的东西将是一项艰巨的工作。 NDC或MDC就是用来解决这个问题的。 MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。 1234567891011121314151617public class App { private static final Logger logger = LoggerFactory.getLogger(App.class); public static void main(String[] args) { new App().log("main thread"); new Thread(){ public void run() { new App().log("sub thread"); }; }.start(); } public void log(String arg) { MDC.put("username", "Yukai"); logger.info("This message from : {}", arg); }} 设置appender: log4j.appender.stdout.layout.ConversionPattern=%X{username} %d{yyyy-MM-dd HH:mm:ss} [%p] %c - %m%n 输出 Yukai 2017-02-21 22:10:54 [INFO] space.kyu.log_test.App - This message from : main threadYukai 2017-02-21 22:10:54 [INFO] space.kyu.log_test.App - This message from : sub thread 参考SLF4J user manual JDK Logging深入分析 jdk-logging、log4j、logback日志介绍及原理 commons-logging与jdk-logging、log4j1、log4j2、logback的集成原理 slf4j与jdk-logging、log4j1、log4j2、logback的集成原理 slf4j、jcl、jul、log4j1、log4j2、logback大总结 Java 日志管理最佳实践]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java国际化]]></title>
<url>%2F2017%2F02%2F19%2Fjava%E5%9B%BD%E9%99%85%E5%8C%96%2F</url>
<content type="text"><![CDATA[使用java编写一个带GUI程序或者其他需要给用户传递文字信息的程序的时候,就很有可能需要用到国际化的知识,来总结一下。 所谓的国际化,就是使编写的程序可以适应不同的语言环境,比如,在中文环境下,可以与用户使用中文交互,在英文环境下则切换为英文。而这个切换过程不需要修改代码或者仅仅修改少量的代码。java给我们提供了这样的实现。 java文件国际化我们通过将与界面显示有关系的资源提取出来到资源文件中,然后读取不同的资源文件来达到国际化的目的。在java中,这些是通过ResourceBundle这个类来实现的。 ResourceBundle分为两种,一种是ListResourceBundle,另一种是PropertyResourceBundle。下面介绍这两种ResourceBundle的使用方法: 首先列出demo工程的代码结构: 1234567891011121314151617181920212223242526272829TestResourceBundle||--src | |--kyu | |--bundle | | | |--ListResourceTranslator.java | | | |--PropertyResourceTranslator.java | | | |--ResourceTranslator.java | |--test | | | |--App.java | |--Errors_en_Us.java | |--Errors_zh_CN.java | |--Errors.java | |--Errors_en_Us.properties | |--Errors_zh_CN.properties | |--Errors.properties PropertyResourceBundle 首先需要建立若干语言的properties文件: 自定义名语言代码国别代码.properties 比如:errors_en_US.properties, errors_zh_CN.properties 其中的语言代码和国别代码,分别是你要国际化的语言。需要几种语言,就添加几个properties文件。 国别代码 语言代码 通过打印java所支持的语言和国家查看: 1234567private static void printLocal() { Locale[] localeList = Locale.getAvailableLocales(); for (int i = 0; i < localeList.length; i++) { System.out.println(localeList[i].getDisplayCountry() + ": " + localeList[i].getCountry()); System.out.println(localeList[i].getDisplayLanguage() + ": " + localeList[i].getLanguage()); }} 建立默认语言的properties文件:自定义名.properties 比如:errors.properties 当所需语言的properties文件不存在的时候就会默认读取这个文件中的内容 注意,资源文件都必须是ISO-8859-1编码,对于中文等非西方语系,可通过JDK自带的工具native2ascii进行处理,在Eclipse中也可以安装插件SimplePropertiesEditor来处理这类文件。 使用 ResourceTranslator.java 1234567891011121314package kyu.bundle;import java.util.Locale;import java.util.ResourceBundle;public abstract class ResourceTranslator { protected ResourceBundle bundle; protected Locale lc; protected static final String PROP_FILE = "kyu.errors"; public String translate(String id) { return bundle.getString(id); }} PropertyResourceTranslator.java 123456789101112131415161718package kyu.bundle;import java.util.Locale;import java.util.PropertyResourceBundle;import java.util.ResourceBundle;public class PropertyResourceTranslator extends ResourceTranslator{ public PropertyResourceTranslator() { lc = Locale.getDefault(); bundle = PropertyResourceBundle.getBundle(PROP_FILE, lc); } public PropertyResourceTranslator(String language, String country) { lc = new Locale(language, country); bundle = PropertyResourceBundle.getBundle(PROP_FILE, lc); }} App.java 12345678910111213141516171819202122232425package kyu.test;import kyu.bundle.ListResourceTranslator;import kyu.bundle.PropertyResourceTranslator;public class App { public static void main(String[] args) { testPropertyResource(); } private static void testPropertyResource() { PropertyResourceTranslator translatorDefault = new PropertyResourceTranslator(); PropertyResourceTranslator translatorZH = new PropertyResourceTranslator("zh", "CN"); PropertyResourceTranslator translatorEN = new PropertyResourceTranslator("en", "US"); String def = translatorDefault.translate("ERROR-001"); String zh = translatorZH.translate("ERROR-001"); String en = translatorEN.translate("ERROR-001"); System.out.println("test PropertyResourceBundle>>>>>>"); System.out.println("default: " + def); System.out.println("zh: " + zh); System.out.println("en: " + en); } } .properties文件内容: 12345678Errors_en_Us.properties:ERROR-001=error passwordErrors.propertiesERROR-001=error passwordErrors_zh_CN.propertiesERROR-001=密码错误 执行App.java的测试结果: 1234test PropertyResourceBundle>>>>>>default: 密码错误zh: 密码错误en: error password 可以看到,通过指定Local构造函数的语言和国别代码,就能自动找到对应的.properties,并匹配其中的内容。 当使用Locale.getDefault()时,自动检测当前的系统环境,从而选择对应的语言。 还有一点要注意的是: PropertyResourceBundle.getBundle(PROP_FILE, lc); 其中PROP_FILE,为properties文件的路径,此路径为properties文件的完整路径,即 所在完整包名.自定义名称 ListResourceBundleListResourceBundle的使用与PropertyResourceBundle的使用大同小异,不过是将properties文件换成了.java文件 ListResourceTranslator.java 12345678910111213141516package kyu.bundle;import java.util.ListResourceBundle;import java.util.Locale;public class ListResourceTranslator extends ResourceTranslator{ public ListResourceTranslator() { lc = Locale.getDefault(); bundle = ListResourceBundle.getBundle(PROP_FILE, lc); } public ListResourceTranslator(String language, String country) { lc = new Locale(language, country); bundle = ListResourceBundle.getBundle(PROP_FILE, lc); }} ERRORS_en_US.java 12345678910111213package kyu;import java.util.ListResourceBundle;public class ERRORS_en_US extends ListResourceBundle{ static final Object[][] contents = new String[][] { { "ERROR-001", "error password" } }; public Object[][] getContents() { return contents; }} ERRORS_zh_CN.java 12345678910111213package kyu;import java.util.ListResourceBundle;public class ERRORS_zh_CN extends ListResourceBundle { static final Object[][] contents = new String[][] { { "ERROR-001", "密码错误" } }; public Object[][] getContents() { return contents; }} 继承了ListResourceBundle的类就相当于前面的properties文件,需要提供一个getContents()方法,返回对应的键值对。 同样的,要注意ListResourceBundle子类的命名规则,与properties文件相同,路径也与properties文件相同。 通过App.java的测试(将对应的bundle替换),可以得到相同的测试结果。 使用Eclipse对java文件进行国际化在需要国际化的类文件上点击右键->Source->Externalize Strings… 出现窗口,Eclipse会自动检测类文件中的字符串,在窗口中可以选择相应的字符串,最后自动生成类似于上面的PropertyResourceTranslator.java和properties文件,完成国际化。 其原理与以上所讲的相同,故不再详细说明。 Eclipse RCP国际化 最近在使用Eclipse RCP这项技术开发程序,其中也有国际化相关的东西,总结下来。 Eclipse RCP and Plugin Internationalization - Tutorial 上面这篇文章详细的说明了Eclipse RCP工程中的国际化问题,可以作为一个备忘录。下面介绍一下eclipse rcp中比较常用到的国际化方式: plugin.xml文件国际化plugin.xml文件中保存了扩展点等相关的信息,当扩展点与界面UI相关时,就需要用到国际化 在工程的根目录下面建立一个plugin.properties资源文件,此文件类似于我们上面提到的errors.properties。当然,也可以建立plugin_zh.properties等文件,这些文件名中的plugin是可以自由定义的。 在 MANIFEST.MF文件中增加代码行:Bundle-Localization: plugin 注意,这个plugin与上面的properties文件名保持一致。 plugin.xml配置文件对资源文件进行引用时, 在引用的key前面加一个%,比如: 12345678<view id="org.jkiss.dbeaver.core.databaseNavigator" category="org.jkiss.dbeaver.core.category" class="org.jkiss.dbeaver.ui.navigator.database.DatabaseNavigatorView" allowMultiple="false" icon="icons/databases.png" name="%view.database.navigator.title"/><view 类文件国际化与前面所介绍的java类文件国际化相同,也可以通过选择类文件点击右键->Source->Externalize Strings… Eclipse IDE会自动帮你完成国际化的一些工作,同样也生成了相关的类和properties文件,但不同的是,生成的类文件内容类似于: 1234567891011121314151617package test;import org.eclipse.osgi.util.NLS;public class Messages extends NLS { private static final String BUNDLE_NAME = "test.messages"; //$NON-NLS-1$ public static String View_0; public static String View_1; static { // initialize resource bundle NLS.initializeMessages(BUNDLE_NAME, Messages.class); } private Messages() { }} 当然,我们也可以手动建立这些文件进行国际化操作~~不再详细说明,可以通过观察IDE的行为进行我们手动的国际化操作~]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[win7与centos7双系统安装]]></title>
<url>%2F2017%2F02%2F18%2Fwin7%E4%B8%8Ecentos7%E5%8F%8C%E7%B3%BB%E7%BB%9F%E5%AE%89%E8%A3%85%2F</url>
<content type="text"><![CDATA[之前会有同学让帮忙重装操作系统,正好计划重新装一下自己电脑的系统,折腾了win7与linux双系统,在此记录。以后就可以给他们看这篇笔记自己去装啦~ ps: 由于是双系统的安装操作,而不是虚拟机,所以并未截图,是纯文字说明。如果不清楚,可以参考最下面的参考链接 安装win7下载win7镜像文件:系统之家win7下载 选择一款进行下载,我选择了雨林木风 GHOST WIN7 SP1 X64 装机旗舰版 V2017.02(64位) 制作U盘启动工具下载启动盘制作工具: 老毛桃u盘启动盘制作工具 下载装机版,进行安装 启动盘制作工具安装完成,且镜像下载完毕之后,启动老毛桃,选择U盘启动->默认模式 插入一个可用的U盘,在选择设备这项中,选择插入的U盘设备,注意不要选错 其他选项默认,点击 开始制作 进行启动盘的制作。 启动盘制作完毕之后,将下载好的iso文件拷入U盘 安装win7插入制作好的启动盘,重启电脑 开机画面出现时,按下F12,boot options(我的电脑是DELL的,其他品牌电脑自行查找) 选择USB启动 老毛桃启动盘正常启动,此时选择第二项,win8pe,点击进入win8pe 首先进行分区,选择分区工具,可以自由设置分区数目和分区大小(我分了3个区,一个用来装win7(C),一个用来做Windows的资料盘(D),另外一个用来安装Linux(E)) 双击win8pe桌面上的“diskgenius分区工具”图标,在弹出的窗口中点击“快速分区”,即可启动分区工具,具体的分区方法参考官网介绍:老毛桃分区工具的使用 设置好分区后,鼠标左键双击打开桌面上的“老毛桃PE装机工具”,选择“还原分区”,映像文件路径选择上一步拷入U盘的镜像,然后选择要装入的分区(C盘),点击确认开始自动装机。 系统自动重启几次之后,就安装好了,可以卸载一些不必要的系统自带软件,此时的操作系统已经是激活的版本了(我下载的镜像是),各种需要的驱动也都安装完毕,简单省事。 安装centos7查看磁盘分区情况右键计算机->管理,打开计算机管理程序。选择磁盘管理,查看分区情况。 选择之前分好的E盘,右键“删除卷”,使之状态变为“可用空间”,以便centos安装程序识别。 制作U盘启动工具下载centos7镜像文件(DVD版本即可) CentOS-7-x86_64-DVD-1611.iso 下载烧录U盘工具 ImageUsb 插入一个可用的U盘 启动 imageusb.exe, step1 选择要写入的U盘; step2 选择 write image to USB driver; step3 选择下载好的centos7镜像文件 step4 点击 write ,开始烧录启动盘 启动盘烧录完毕后,可以开始安装centos 安装centos7插入制作好的启动盘,重启电脑 开机画面出现时,按下F12 (我的电脑是DELL的,其他品牌电脑自行查找) 选择USB启动 选择第一项,Install CentOs 7 ,回车,开始安装CentOS 7 之后就进入了简单的可视化安装界面,有几点需要注意: 1.分区选择 如果是像上面一样分了3个区,两个被Windows使用,还剩一个分区未被使用,那么可以选择自动选择分区,centos安装程序会自动选择好这块未被使用的分区(将“/”目录挂载到这个分区上面) 也可以自定义分区,建议分四个区:/ /boot /home swap(如果有这么些分区的话) 2.在之后的安装信息摘要中,注意“软件”-“软件选择”,默认是最小安装,即不安装桌面环境,若选择此项,则只有命令行界面,应该选择带有UI界面的选项。 之后就是很简单的安装了 配置引导程序再次启动电脑之后,会发现自动进入Linux,没有win7系统的选项,此时需要配置引导程序 首先添加ntfs支持 123wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repoyum update;yum install ntfs-3g 安装完毕后打开终端,运行grub2-mkconfig -o /boot/grub2/grub.cfg 就会重新生成引导项,重启电脑即可 参考老毛桃u盘安装原版win7系统详细教程 老毛桃分区工具的使用 CentOS 7.0系统安装配置图解教程]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Vps使用笔记]]></title>
<url>%2F2017%2F02%2F07%2FVps%E4%BD%BF%E7%94%A8%E7%AC%94%E8%AE%B0%2F</url>
<content type="text"><![CDATA[今天折腾了一天,总算是把翻墙的梯子搭起来了,租了国外的vps,然后在上面搭建了shadowsocks服务,手机和电脑就可以使用shadowsocks客户端的方式科学上网了。Shadowsocks 是一个由很多人参与的开源项目,感谢shadowsocks作者@clowwindy.关于shadowssocks和vpn等等翻墙方式的区别,不再一一赘述,本文只是记录梯子搭建的过程 购买VPSVPS的概念我也不是特别清楚,就把他当成主机好了,类似于阿里云等等。 购买之后,会分配给你root账户密码,你就拥有了一台国外机房的主机。 VPS 可以参考 有哪些便宜稳定,速度也不错的Linux VPS 推荐?,我买了Vultr 日本机房的vps,充了5美刀先试试。具体的购买方法自行谷歌 搭建shadowsocks服务升级内核我选择了Centos7的Linux发型版本作为操作系统。 使用MobaXterm(或者是其他ssh工具)连接到分配给你的主机(root密码购买成功时会给出), 首先升级一下内核:wget -O- https://zhujiwiki.com/usr/uploads/2016/12/install_bbr_centos.sh | bash 完成之后,重启: reboot 重新登录,查看内核版本: uname -r 不出意外已经更新到了最新版本。接着执行: sysctl -a|grep congestion_control 如果输出选项包含:net.ipv4.tcp_congestion_control = bbr 表示安装成功,否则需要手动开启bbr模式: 12345echo "net.core.default_qdisc=fq" >> /etc/sysctl.confecho "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.confsysctl -psysctlnet.ipv4.tcp_available_congestion_control 再次查看: sysctl -a|grep congestion_control 搭建服务搭建服务可以选择使用别人写好的现成的脚本,也可以自己搭建,下面介绍这两种方法: 使用现成脚本执行以下命令: --no-check-certificatelink12chmod +x shadowsocks.sh./shadowsocks.sh 2>&1 | tee shadowsocks.log 首先会提示你输入密码和端口,然后开始安装。安装完成后代表shadowsocks服务已经开启了。此时可以使用shadowsocks客户端去连接这个服务器了。 脚本可用的命令: 启动:/etc/init.d/shadowsocks start 停止:/etc/init.d/shadowsocks stop 重启:/etc/init.d/shadowsocks restart 状态:/etc/init.d/shadowsocks status 卸载:./shadowsocks.sh uninstall 支持多用户的方法:修改配置文件:/etc/shadowsocks.json 123456789101112131415{ "server":"0.0.0.0", "local_address":"127.0.0.1", "local_port":1080, "port_password":{ "8989":"password0", "9001":"password1", "9002":"password2", "9003":"password3", "9004":"password4" }, "timeout":300, "method":"aes-256-cfb", "fast_open": false} 自行搭建首先安装shadowsocks服务: 12yum install python-setuptools && easy_install pippip install shadowsocks 显示 “Successfully installed shadowsocks-2.6.10″。意味着Shadowsocks已经成功安装。 接着,创建一个Shadowsocks配置文件。输入以下命令: vi /etc/shadowsocks.json 然后在该文件中输入: 12345678910{ "server":"your_server_ip", "server_port":8388, "local_address": "127.0.0.1", "local_port":1080, "password":"auooo.com", "timeout":300, "method":"aes-256-cfb", "fast_open": false} 上面各项配置的含义已经很明显了,除了server(vps的ip)、server_port和password之外,其他默认即可。保存该文件。执行: ssserver -c /etc/shadowsocks.json -d start 服务就启动了,如果想要关闭,执行: ssserver -c /etc/shadowsocks.json -d stop 支持多用户的方法与上面的类似 端口问题有时候服务正常启动了,但是发现客户端还是没有办法翻墙。这时要注意检查vps开放的端口: Centos升级到7之后,使用firewalld代替了原来的iptables作为防火墙。 启动:systemctl start firewalld 重启:firewall-cmd –reload 查看状态:systemctl status firewalld 或者 firewall-cmd –state 查看开放的端口:firewall-cmd –list-ports 增加开放端口:firewall-cmd –add-port=9001/tcp –permanent (重启生效) 9001:要开放的端口 tcp:协议 –permanent:永久生效,没有此参数重启后失效其他命令自行查找 锐速加速锐速破解版linux一键自动安装包(8月7日更新) 文章开头的更新内核使用bbr加速或者锐速加速都可以,没试过哪种方式效果更好一些 vps使用ssh密钥登录使用ssh root账户和密码登录的方式是vps一开始会给出的登录主机的方式,这种方式比较简单,但是容易被别人暴力破解登录密码,控制我们的主机。 所以,应该使用ssh密钥登录的方式来增强安全性。 修改root密码第一步应该先把默认的root密码修改为自己的密码,最好复杂一点。 确保自己记住了这个密码。 passwd 创建用户可以先创建一个非root用户: 1useradd yukai 创建账户密码: 1passwd yukai 给予账户sudo权限: 1visudo 在 root ALL=(ALL) ALL 下新增一行: yukai ALL=(ALL) ALL 切换到yukai这个账户: su - yukai 创建密钥1ssh-keygen -t rsa 在 /home/yukai 下生成了一个隐藏目录:.ssh, 执行: 12cd ~/.sshcat id_rsa.pub >> authorized_keyschmod 600 authorized_keys 将 id_rsa文件下载下来保存到本地(使用MobaXterm等工具) 使用密钥认证登录打开MobaXterm工具,新建一个session,选择ssh,在Advanced SSH settings选项中选择 use private keygen复选框,并把上一步下载好的id_rsa选择进来。 以用户yukai登录,提示输入密钥文件的密码,输入生成密钥时所填的密码,如果能够登录,则设置正确了 禁用root用户登录和密码登录确保自己记住了root密码,并且上一步密钥认证方式登录成功。 1sudo vi /etc/ssh/sshd_config 找到以下几项,进行如下设置: 1234RSAAuthentication yesPubkeyAuthentication yesPermitRootLogin noPasswordAuthentication no 重启ssh服务: 1service sshd restart 多台主机管理可以将生成的密钥文件authorized_keys保存到多个主机的相同位置:~/.ssh 然后进行上面的配置。此时就可以使用一份私钥文件id_rsa登录多个主机了。 ss多用户管理前端使用ss-panel,后台使用shadowsocks-manyuser 教程参见: 可能是最好的 ss-panel 部署教程 ShadowSocks多用户管理系统搭建(moeSS+manyuser) 在Linux上使用shadowsocks服务翻墙开启shadowsocks客户端确认安装了shadowsocks服务并从配置好的shadowsocks服务器端获得配置文件: /etc/shadowsocks/config.json 1234567891011{ "server":"remote-shadowsocks-server-ip-addr", "server_port":443, "local_address":"127.0.0.1", "local_port":1080, "password":"your-passwd", "timeout":300, "method":"aes-256-cfb", "fast_open":false, "workers":1} 在config.json所在目录下运行sslocal即可: sslocal -c /etc/shadowsocks/config.json 也可以手动指定参数运行: sslocal -s 服务器地址 -p 服务器端口 -l 本地端端口 -k 密码 -m 加密方法 使用chrome代理在chrome应用商店中查找 Proxy SwitchyOmega,并安装该扩展 新建情景模式,代理协议选择SOCKS5,代理服务器选择 127.0.0.1, 代理端口选择上一步中的local_port,即1080 启用该扩展程序,此时可顺利使用google了 附录安装vimvim编辑器需要安装三个包: 123vim-enhanced-7.0.109-7.el5vim-minimal-7.0.109-7.el5vim-common-7.0.109-7.el5 输入 rpm -qa|grep vim 这个命令,如何vim已经正确安装,则会显示上面三个包的名称 如果缺少了其中某个,比如说: vim-enhanced这个包少了,执行:yum -y install vim-enhanced 命令,它会自动下载安装 如果上面三个包一个都没有显示,则直接输入命令: 1yum -y install vim* 安装net-tools1yum install net-tools 更改文件所有者chown -R www.www ./ 将本路径下所有文件所有者改为www组的www 参考Shadowsocks Python版一键安装脚本 使用 Shadowsocks 自建翻墙服务器,实现全平台 100% 翻墙无障碍 http://shadowsocks.org/ shadowsocks(影梭)不完全指南]]></content>
<categories>
<category>技术</category>
<category>Linux</category>
</categories>
<tags>
<tag>linux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java异常的学习]]></title>
<url>%2F2016%2F12%2F13%2Fjava%E5%BC%82%E5%B8%B8%E7%9A%84%E5%AD%A6%E4%B9%A0%2F</url>
<content type="text"><![CDATA[最近的写代码的过程中,遇到很多异常的处理,以前上大学的时候写代码,遇到异常直接给个try catch了事,只是停留在看懂异常能够找出异常抛出点的水平。真正写代码的时候,不了解java的异常机制给自己编程带来很多不便,基础知识很重要!学习之~~ 异常分类 java异常中的异常大体上可分为两类:Error 与 Exception,他们都继承自Throwable Error:Error表示一些无法恢复的错误,会导致应用程序中断。比如我们喜闻乐见的OutOfMemoryError(内存溢出), StackOverflowError(堆栈溢出)等,就是一种Error类型的异常。面对这种类型的异常在我们的应用程序中一般是无法挽救的,将直接导致程序错误退出。因此我们在代码中一般不必去特别关心这种类型的异常。 Error类型的异常及其子类 Exception:Exception是一般常见的异常,我们的应用程序可以处理这些异常,比如NullPointerException、IndexOutOfBoundsException等等。发生这些异常时,可以选择通过捕获这些异常进行处理,使程序可以继续往下执行。 Exception又可以分为CheckException(检测型异常)与UncheckException(非检测型异常)。 在Exception的子类当中,非检测型异常为RuntimeException及其子类,剩下的异常则为检测型异常。 检测型异常所谓检测型异常,表示其接受编译器的检测,比如 12345678910public void readFile(String filePath){ //编译无法通过 File file = new File(filePath); FileReader fileReader = new FileReader(file);//FileNotFoundException BufferedReader bReader = new BufferedReader(fileReader); String line = null; while ((line = bReader.readLine()) != null) {//IOException System.out.println(line); }} 编译上述代码,编译器会报错,编译无法通过。如果是用Eclipse等ide去写这段代码,ide通常就会告诉你这段代码有错误。原因是上述代码会抛出检测型异常IOException。有以下两种修复错误的方法: 123456789101112131415161718192021222324//第一种public void readFile(String filePath) throws IOException{ File file = new File(filePath); FileReader fileReader = new FileReader(file); BufferedReader bReader = new BufferedReader(fileReader); String line = null; while ((line = bReader.readLine()) != null) { System.out.println(line); }}//第二种public void readFile(String filePath) { File file = new File(filePath); try { FileReader fileReader = new FileReader(file); BufferedReader bReader = new BufferedReader(fileReader); String line = null; while ((line = bReader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { // TODO: handle exception }} 第一种方式通过声明throws关键字将该异常继续向上抛出,这种方式下,该方法就抛出了检测型异常,他的调用者就会遇到一样的问题,调用者可以选择使用这种方式继续向调用链上层抛出,或者采用第二种方式处理异常。 第二种方式通过将抛出异常的代码使用try..catch块包裹起来,处理该异常。这种方式下他的调用者无需再次处理该异常。 上面这种类型的异常就是检测型异常,我们必须显示的去处理他,代表的有SQLException、IOException等等。 非检测型异常与检测型异常相对的,编译阶段不会检测这种类型的异常,当代码中有这种类型的异常抛出时,我们不需要像上面那样显示的处理他,比如: 1234public void test() { //编译可以通过 throw new NullPointerException();} 通过throw关键字抛出了一个NullPointerException异常,该异常是RunTimeException的子类,是一个非检测类型的异常。这种类型的异常一般是由程序逻辑错误引起的,比如空指针或者数组越界等等。 对于非检测类型的异常,我们也可以去使用try..catch捕获他,从而进行一些处理。如果我们没有捕获处理这个异常,系统会把异常一直往上层抛,一直到最上层,如果是多线程就由Thread.run()抛出,如果是单线程就被main()抛出。抛出之后,如果是线程,这个线程也就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了。 Error实际上也是一种非检测型异常。 异常处理关键字了解了异常的分类,我们就可以愉快的处理异常了。记得上大学的时候写的代码,由于不理解java的异常机制,每当遇到检测型异常时,ide会要求在代码中处理这种情况。于是简单的try..catch解决,并在catch块中 e.printStackTrace(),就解决了IDE的报警问题。现在想来,这种代码如果运行在生产环境中,将会多么可怕。 通过上面的总结,我们已经了解了四个关键字:try catch throw throws. throw表示将要抛出一个异常,后面跟一个Throwable的实例,throws置于函数的声明当中,表示该函数将会抛出何种类型的异常。 try catch 一般配合使用,还有一个关键字finally,也是与try catch 配合使用的。有这三种使用方式: try..catch try..finally try..catch..finally catch 可以有多个,try只能有一个,finally可选。 finally用于保证一些资源的释放,因为一般情况下,finally中的语句总会在方法返回之前得到执行。 try catch finally执行顺序try catch finally的执行顺序为 try->catch->finally。 有多个catch时,按照catch块的先后顺序进行匹配,一旦一个异常与一个catch块匹配,则不会再与后面的catch块进行匹配。因此,如果我们使用多个catch块捕获异常时,如果多个catch块捕获的异常具有继承关系,注意把继承链中低层次(也就是子类)的放在前面,把继承链中高层次的(也就是父类)放在后面。这样做的目的很简单,就是尽量使异常被适合的catch所捕获,这样处理起来比较明确。 finally块中的代码一般是在try与catch内的控制转移语句执行之前执行的,用来做一些资源释放的操作。控制转移语句包括:return、throw、break 和 continue。 关于finally块的详细解析,参考关于 Java 中 finally 语句块的深度辨析 另外有一点需要注意:不要在finally中return,因为finally中的return会覆盖已有的返回值,比如try或者catch中的返回值。比如: 1234567891011121314151617181920212223public class TestException { public static void main(String[] args) { String str = new TestException().readFile(); System.out.println(str); } public String readFile() { try { FileInputStream inputStream = new FileInputStream("D:/test.txt"); int ch = inputStream.read(); return "try"; } catch (FileNotFoundException e) { System.out.println("file not found"); return "FileNotFoundException"; }catch (IOException e) { System.out.println("io exception"); return "IOException"; }finally{ System.out.println("finally block"); //return "finally"; } }} D:/test.txt 并不存在,执行结果为:file not found finally block FileNotFoundException去掉finally中的注释,执行结果为:file not found finally block finally 一些建议了解了异常机制的基本原理,不一定能够很好的处理异常。当我们遇到异常需要处理的时候,需要遵循几个原则,才能写出更好的代码: 不要使用空的catch块正如前面所说,上学的时候遇到异常时,直接try..catch了事,只是简单地e.printStackTrace(),把堆栈打印出来了。这样可能对定位异常比较有帮助,但是对异常的处理却没有帮助,既没有处理异常,上层代码也无法得知异常的抛出,程序会继续运行,有可能出现无法预料的结果。当然,如果程序的逻辑容忍异常可以不用处理,那么可以不处理异常,简单的输出到日志记录即可。处理还需视实际情况而定。 在能够处理异常的地方处理异常换句话说,就是在高层代码中处理异常。尽量将异常抛给上层调用者,由上层调用者统一进行处理。这样会使得程序流程比较清晰。 日志打印只在必要的地方打印日志,如只在异常发生的地方输出日志,然后将异常抛到上层。这样比较容易定位异常,避免每次向上抛出异常时都打印日志,反复打印同一个异常会使得日志变得混乱 检测异常与非检测异常的选择为可恢复的条件使用检查型异常,为编程错误使用运行时异常 在web项目中,我们经常把代码层次分为controller,service,dao等几层。在dao层中一搬会抛出SQLException,这就使得他的调用者必须显示的捕获该异常或者继续抛出。这样提高了代码的耦合性,污染了上层代码。在比如接口的声明当中,我们为方法声明了一个检测型异常,那么他的所有实现类都必须作出同样的声明,即使实现类不会抛出异常。而且,所有的调用者都必须显示的捕获异常或者抛出异常,异常就会扩散开来,会使代码变得混乱。所以我们应该正确的选择检测型异常和非检测型异常。个人认为,检测型异常的意义就在于提醒用户进行处理,比如一些资源的释放等等。如果该异常出现的很普遍,需要提醒调用者进行处理,那么就是用检测型异常,否则,就使用非检测型异常。 对于已经抛出的检测型异常,我们可以进行封装处理,比如对于第一种情况,污染上层代码的问题: 12345678910111213141516//处理前public Data getDataById(Long id) throw SQLException { //根据 ID 查询数据库}//处理后public Data getDataById(Long id) { try{ //根据 ID 查询数据库 }catch(SQLException e){ //利用非检测异常封装检测异常,降低层次耦合 throw new RuntimeException(SQLErrorCode, e); }finally{ //关闭连接,清理资源 }} 我们将一个检测型异常封装成了非检测型异常向上抛出。 不要使用Exception捕捉所有潜在的异常针对具体的异常进行处理,而不是使用Exception捕获所有的异常。这样不利于异常情况的处理,并且如果再次向上抛出时可能会丢书原有的异常信息。 抛出与抽象相适应的异常换句话说,一个方法所抛出的异常应该在一个抽象层次上定义,该抽象层次与该方法做什么相一致,而不一定与方法的底层实现细节相一致。例如,一个从文件、数据库或者 JNDI 装载资源的方法在不能找到资源时,应该抛出某种 ResourceNotFound 异常(通常使用异常链来保存隐含的原因),而不是更底层的 IOException 、 SQLException 或者 NamingException 。 我们有时候在捕获一个异常后抛出另一个封装后的异常信息,并且希望将原始的异常信息也保持起来,throw抛出的是一个新的异常信息,这样势必会导致原有的异常信息丢失,如何保持?在Throwable及其子类中的构造器中都可以接受一个cause参数,该参数保存了原有的异常信息,通过getCause()就可以获取该原始异常信息。 多线程中的异常在java多线程程序中,所有线程都不允许抛出未捕获的检测型异常(比如sleep时的InterruptedException),也就是说各个线程需要自己把自己的检测型处理掉。这一点是通过java.lang.Runnable.run()方法声明(因为此方法声明上没有throw exception部分)进行了约束。但是线程依然有可能抛出非检测型异常,当此类异常跑抛出时,线程就会终结,而对于主线程和其他线程完全不受影响,且完全感知不到某个线程抛出的异常(也是说完全无法catch到这个异常)。 如果我们不考虑线程内可能出现的异常而导致线程的终结,那么就有可能造成意想不到的后果。如果是使用线程池的话,就有可能导致线程泄漏,这样的错误可能难以察觉,最终导致程序挂掉或者内存溢出等等意想不到的问题,但是往往不好追踪问题出现的原因。Java 理论与实践: 嗨,我的线程到哪里去了?这篇文章很好的展示了一个多线程发生异常后产生的一系列后果。 对于线程可以自己处理的异常,比较好解决。我们可以在线程内部捕获异常,作一些处理,防止线程退出。比如,Java 理论与实践: 嗨,我的线程到哪里去了?这篇文章中的一个示例: 12345678910111213141516171819202122private class SaferPoolWorker extends Thread { public void run() { IncomingResponse ir; while (true) { ir = (IncomingResponse) queue.getNext(); PlugIn plugIn = findPlugIn(ir.getResponseId()); if (plugIn != null) { try { plugIn.handleMessage(ir.getResponse()); } catch (RuntimeException e) { // Take some sort of action; // - log the exception and move on // - log the exception and restart the worker thread // - log the exception and unload the offending plug-in } } else log("Unknown plug-in for response " + ir.getResponseId()); } }} 但是,当子线程发生异常时,我们需要父线程或者主线程可以感知子线程的异常,也就是得到子线程产生的异常,然后做一些处理。Java线程池异常处理最佳实践这篇文章给出了很好的总结,摘抄其总结如下:1234567处理线程池中的异常有两种思路: 1)提交到线程池中的任务自己捕获异常并处理,不抛给线程池 2)由线程池统一处理对于execute方法提交的线程,有两种处理方式 1)自定义线程池并实现afterExecute方法 2)给线程池中的每个线程指定一个UncaughtExceptionHandler,由handler来统一处理异常。对于submit方法提交的任务,异常处理是通过返回的Future对象进行的。 其他还发现了一个比较有趣的异常处理情况,虽然可能很少碰到,但是碰到了可以参考作者的思路。 技巧:当不能抛出异常时这篇文章介绍了一种不是很常见的情况:即不能处理,也不能抛出异常(包括非检测型异常)时怎么办? 参考Java 异常处理的误区和经验总结 Java异常处理和设计 Java 理论与实践: 关于异常的争论]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java远程调试学习]]></title>
<url>%2F2016%2F12%2F07%2Fjava%E8%BF%9C%E7%A8%8B%E8%B0%83%E8%AF%95%E5%AD%A6%E4%B9%A0%2F</url>
<content type="text"><![CDATA[集群前后台协议需要做一些修改,我负责jdbc这边的修改。按照协议内容修改完代码之后却面临一个测试的问题:修改后的后台又部署在北京,但北京并不是所有的机器都对天津这边开放,只给提供一台机器A,就是集群的服务器。这样的话,就无法创建节点的连接,测试没有办法进行。 一开始用了最简单的办法,把打好的jar包通过远程ssh放到A上面,再通过ssh去跑用例,打印结果看看正确与否。但是这样的效率真的太低了,每做一次修改都要打包、部署、运行、分析log。于是借这个机会学习一下java程序的远程调试。以下为总结。 一些概念JDPA: java平台调试架构 JVMTI: JVM端调试接口 JDI: java端调试接口 JDWP: java调试网络协议 JPDA 定义了一套如何开发调试工具的接口和规范。 JPDA 由三个独立的模块 JVMTI、JDWP、JDI 组成。 调试者通过 JDI 发送接受调试命令。 JDWP 定义调试者和被调试者交流数据的格式。 JVMTI 可以控制当前虚拟机运行状态。 上图中的前端工具就是我们要用到的调试工具。如JDB、Eclipse等等。这些工具实现了JDI接口,通过这些工具我们可以达到在命令行或者图形界面下调试的目的。 关于这部分,只是简单的了解一下概念,更多的关于JDPA的介绍:JDPA体系 使用JDB进行本地调试JDB 是jdk自带的一个调试工具,用于命令行下调试java程序 jdb.exe就位于jdk安装目录的bin目录下,安装好jdk并设置好环境变量之后就可以愉快的使用jdb了。 12345678910111213141516171819202122232425262728293031323334C:\Users\kyu>jdb -help用法: jdb <options> <class> <arguments>其中, 选项包括: -help 输出此消息并退出 -sourcepath <由 ";" 分隔的目录> 要在其中查找源文件的目录 -attach <address> 使用标准连接器附加到指定地址处正在运行的 VM -listen <address> 等待正在运行的 VM 使用标准连接器在指定地址处连接 -listenany 等待正在运行的 VM 使用标准连接器在任何可用地址处连接 -launch 立即启动 VM 而不是等待 'run' 命令 -listconnectors 列出此 VM 中的可用连接器 -connect <connector-name>:<name1>=<value1>,... 使用所列参数值通过指定的连接器连接到目标 VM -dbgtrace [flags] 输出信息供调试jdb -tclient 在 HotSpot(TM) 客户机编译器中运行应用程序 -tserver 在 HotSpot(TM) 服务器编译器中运行应用程序转发到被调试进程的选项: -v -verbose[:class|gc|jni] 启用详细模式 -D<name>=<value> 设置系统属性 -classpath <由 ";" 分隔的目录> 列出要在其中查找类的目录 -X<option> 非标准目标 VM 选项<class> 是要开始调试的类的名称<arguments> 是传递到 <class> 的 main() 方法的参数要获得命令的帮助, 请在jdb提示下键入 'help' 上面是启动JDB的语法说明。 现假设运行程序的工程目录如下: 1234567JDBTest |----bin(编译生成的class文件) | |----*.class |----src(源文件) | |----*.java |----lib(依赖的第三方jar) | |----*.jar 开始启动JDB调试: Y:\project\JavaProject\JDBTest>jdb -classpath ./bin/;./lib/* -sourcepath ./src/ test.JDBTest 注意,如果有多个文件,windows下使用 ";" 分隔每个文件或目录,Linux下使用 ":" 分隔每个文件或目录 -classpath 指定了类路径,-sourcepath 指定了源文件的路径 回车,出现如下信息: 123Y:\project\JavaProject\JDBTest>jdb -classpath ./bin/;./lib/* -sourcepath ./src/ test.JDBTest正在初始化jdb...> 此时,JDB调试器等待用户的输入,输入help,出现如下信息: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100Y:\project\JavaProject\JDBTest>jdb -classpath ./bin/;./lib/* -sourcepath ./src/ test.JDBTest正在初始化jdb...> help** 命令列表 **connectors -- 列出此 VM 中可用的连接器和传输run [class [args]] -- 开始执行应用程序的主类threads [threadgroup] -- 列出线程thread <thread id> -- 设置默认线程suspend [thread id(s)] -- 挂起线程 (默认值: all)resume [thread id(s)] -- 恢复线程 (默认值: all)where [<thread id> | all] -- 转储线程的堆栈wherei [<thread id> | all]-- 转储线程的堆栈, 以及 pc 信息up [n frames] -- 上移线程的堆栈down [n frames] -- 下移线程的堆栈kill <thread id> <expr> -- 终止具有给定的异常错误对象的线程interrupt <thread id> -- 中断线程print <expr> -- 输出表达式的值dump <expr> -- 输出所有对象信息eval <expr> -- 对表达式求值 (与 print 相同)set <lvalue> = <expr> -- 向字段/变量/数组元素分配新值locals -- 输出当前堆栈帧中的所有本地变量classes -- 列出当前已知的类class <class id> -- 显示已命名类的详细资料methods <class id> -- 列出类的方法fields <class id> -- 列出类的字段threadgroups -- 列出线程组threadgroup <name> -- 设置当前线程组stop in <class id>.<method>[(argument_type,...)] -- 在方法中设置断点stop at <class id>:<line> -- 在行中设置断点clear <class id>.<method>[(argument_type,...)] -- 清除方法中的断点clear <class id>:<line> -- 清除行中的断点clear -- 列出断点catch [uncaught|caught|all] <class id>|<class pattern> -- 出现指定的异常错误时中断ignore [uncaught|caught|all] <class id>|<class pattern> -- 对于指定的异常错误, 取消 'catch'watch [access|all] <class id>.<field name> -- 监视对字段的访问/修改unwatch [access|all] <class id>.<field name> -- 停止监视对字段的访问/修改trace [go] methods [thread] -- 跟踪方法进入和退出。 -- 除非指定 'go', 否则挂起所有线程trace [go] method exit | exits [thread] -- 跟踪当前方法的退出, 或者所有方法的退出 -- 除非指定 'go', 否则挂起所有线程untrace [methods] -- 停止跟踪方法进入和/或退出step -- 执行当前行step up -- 一直执行, 直到当前方法返回到其调用方stepi -- 执行当前指令next -- 步进一行 (步过调用)cont -- 从断点处继续执行list [line number|method] -- 输出源代码use (或 sourcepath) [source file path] -- 显示或更改源路径exclude [<class pattern>, ... | "none"] -- 对于指定的类, 不报告步骤或方法事件classpath -- 从目标 VM 输出类路径信息monitor <command> -- 每次程序停止时执行命令monitor -- 列出监视器unmonitor <monitor#> -- 删除监视器read <filename> -- 读取并执行命令文件lock <expr> -- 输出对象的锁信息threadlocks [thread id] -- 输出线程的锁信息pop -- 通过当前帧出栈, 且包含当前帧reenter -- 与 pop 相同, 但重新进入当前帧redefine <class id> <class file name> -- 重新定义类的代码disablegc <expr> -- 禁止对象的垃圾收集enablegc <expr> -- 允许对象的垃圾收集!! -- 重复执行最后一个命令<n> <command> -- 将命令重复执行 n 次# <command> -- 放弃 (无操作)help (或 ?) -- 列出命令version -- 输出版本信息exit (或 quit) -- 退出调试器<class id>: 带有程序包限定符的完整类名<class pattern>: 带有前导或尾随通配符 ('*') 的类名<thread id>: 'threads' 命令中报告的线程编号<expr>: Java(TM) 编程语言表达式。支持大多数常见语法。可以将启动命令置于 "jdb.ini" 或 ".jdbrc" 中位于 user.home 或 user.dir 中> 上面的帮助信息说明了如何进行JDB调试,解释一下其中的几个: step: – 执行当前行 相当于Eclipse中的F5 step up: – 一直执行, 直到当前方法返回到其调用方 相当于Eclipse中的F7 next: – 步进一行 (步过调用) 相当于Eclipse中的F6 cont: – 从断点处继续执行 相当于Eclipse中的F8 此时,继续输入: 1234567891011121314> stop at test.JDBTest:7正在延迟断点test.JDBTest:7。将在加载类后设置。> run运行test.JDBTest设置未捕获的java.lang.Throwable设置延迟的未捕获的java.lang.Throwable>VM 已启动: 设置延迟的断点test.JDBTest:7断点命中: "线程=main", test.JDBTest.main(), 行=7 bci=07 JDBTest jdbTest = new JDBTest();main[1] stop at test.JDBTest:7 表示在这个类文件的第7行处打一个断点 接着,输入run,就开始进入调试步骤了。现在可以输入上面帮助中的语法来了解当前程序的执行情况了。一试便知 注意, 若想要在调试时能够正常输出调试信息如变量值等等,需要在编译java文件时指定 -g 参数,否则无法获得其运行时的调试信息 另外,使用list可以打印当前断点处的源代码,如果没有在启动JDB时指定源代码路径 -sourcepath ./src/ ,那么会提示没有源代码信息,无法输出。此时可以使用命令 use ./src/ 来指定源代码路径,再使用list命令时可以正常打印了。 以上就是使用JDB调试本地程序的方法,具体的使用可根据实际情况参照语法说明去执行。 使用JDB进行远程调试如果程序不是运行在本机,而是在其他机器或者现场的时候,可以使用java提供的远程调试功能。 假设程序现运行在主机 192.168.101.72 这台机器上,该机器为linux环境,且只可以通过ssh作为一个普通用户连接。我们想要在自己的机器上调试运行在192.168.101.72这台机器上的程序。 启动要调试的程序在192.168.101.72这台主机上以下面的方式启动java程序:还是以JDBTest为例 java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8899 -classpath ./bin/:./lib/* test.JDBTest 此时,命令行输出 Listening for transport dt_socket at address: 8899 并处于等待状态 下面是几个参数的解释: -Xdebug 启用调试特性。 -Xrunjdwp: 在目标 VM 中加载 JDWP 实现。它通过传输和 JDWP 协议与独立的调试器应用程序通信。下面介绍一些特定的子选项。从 Java V5 开始,您可以使用 -agentlib:jdwp 选项,而不是 -Xdebug 和 -Xrunjdwp。但如果连接到 V5 以前的 VM,只能选择 -Xdebug 和 -Xrunjdwp。下面简单描述 -Xrunjdwp 子选项。 transport 这里通常使用套接字传输。但是在 Windows 平台上也可以使用共享内存传输。 server 如果值为 y,目标应用程序监听将要连接的调试器应用程序。否则,它将连接到特定地址上的调试器应用程序。 address 这是连接的传输地址。如果服务器为 n,将尝试连接到该地址上的调试器应用程序。否则,将在这个端口监听连接。 suspend 如果值为 y,目标 VM 将暂停,直到调试器应用程序进行连接。 本机连接远程程序并启动调试在本机上命令行下输入: jdb -connect com.sun.jdi.SocketAttach:hostname=192.168.101.72,port=8899 然后就进入了调试界面,你可以像调试本机程序那样使用JDB的一些命令来调试了。当退出调试程序时,远程主机上的程序也就退出了。 使用Eclipse进行远程调试可以使用Eclipse进行远程调试,就上上面使用JDB一样。 启动要调试的程序与JDB远程调试一样,启动远程主机上的程序: java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8899 -classpath ./bin/:./lib/* test.JDBTest 本机启动Eclipse进行调试首先要右键工程->java compiler 上图中的几个选项最好全部打勾,否则调试时会出现无法打断点或者获取不到行号等问题(关于这几个选项的含义在之前的总结中有提到) 接着,右键工程->Debug As->Run Configurations, 在出现的对话框中选择Remote Java Application, 右键->New, 出现如下界面: 在Connect页中,选择对应的java 工程,Connection Type选择 Socket Attach,然后填写远程主机的ip和端口,这里应该填写192.168.101.72和8899。在Source页中可以添加源代码,如用到的第三方jar的源代码或者引用的工程,调试时就可以进入到这部分代码查看。在Common页可以设置编码的配置。 接下来点击Debug按钮,就可以愉快的在本机调试远程程序了,就像调试本地程序那样。只不过可能有一点一点慢,不过比打Log的方式要好很多了。 参考使用 Eclipse 远程调试 Java 应用程序 JDB的简单使用 深入 Java 调试体系: 第 1 部分,JPDA 体系概览]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>java</tag>
<tag>调试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java中的路径]]></title>
<url>%2F2016%2F12%2F04%2Fjava%E4%B8%AD%E7%9A%84%E8%B7%AF%E5%BE%84%2F</url>
<content type="text"><![CDATA[每次写java代码的时候,免不了需要加载一些外部资源,比如配置文件等等。每当需要读取这些文件时,都是去网上谷歌,写过就忘。于是今天来总结一些java中有关路径的一些用法。 绝对路径无论是在linux还是windows中,路径都分为绝对路径和相对路径两种。 绝对路径就是系统中唯一能够定位资源的某个URI,即完整的描述文件的路径就是绝对路径。比如: linux中的绝对路径:/home/yukai/test.txt windows中的绝对路径:E:\yukai\test.txt 绝对路径都是以根目录起始的。 相对路径相对路径即目标文件相对与某个基准目录的路径。比如: 存在文件: A: /home/yukai/test.txt B: /home/yukai/test1.txt C: /home/yukai/test/test.txt 那么: 若基准目录为A, B的路径表示为”test1.txt” 若基准目录为B, C的路径表示为”test/test.txt” 若基准目录为C, B的路径表示为”../test1.txt” 另外,“./”表示当前目录,“../”表示上级目录 java io定位文件资源那么,我们在java中如何读取某个文件?上代码: 12345678910// 目录结构src | |--test | | | |--PathTest.java |resource | |--data.txt 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657public class PathTest { public static String dataFilePath = "resource/data.txty"; public static String getUserDir() { //打印运行程序的目录,即启动java程序的目录 String userDir = System.getProperty("user.dir"); System.out.println("运行java的工作目录 System.getProperty(\"user.dir\"): " + userDir); return userDir; } public static void testDataPath1() { String msg = ">> Path = System.getProperty(\"user.dir\") + \"resource/data.txt\""; String userDir = getUserDir(); //等同于 "resource/data.txt" String filePath = userDir + System.getProperty("file.separator") + dataFilePath; printFileContent(filePath ,msg); } public static void testDataPath2() { String msg = "Path = \"resource/data.txt\""; String filePath = dataFilePath; printFileContent(filePath, msg); } public static void testDataPath3() { String msg = "Path = \"./resource/data.txt\""; String filePath = "./resource/data.txt"; printFileContent(filePath, msg); } private static void printFileContent(String filePath, String msg) { System.out.println("****************************"); System.out.println(msg); File file = new File(filePath); InputStream stream; try { stream = new FileInputStream(file); BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); String line = null; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("****************************"); } } public static void main(String[] args) { testDataPath1(); testDataPath2(); testDataPath3(); }} 其中testDataPath2、testDataPath1、testDataPath3这三个方法的效果是一样的。 System.getProperty("user.dir")这个方法获取了当前的工作目录,也就是启动jvm的目录。例如: ps: 更多关于System.getProperty()的介绍:System Properties 你的java程序位于:/home/yukai/code/test.jar 若你在目录/home/yukai下使用命令 java -cp code/test.jar test.PathTest 启动java程序时,目录/home/yukai就是你的工作目录。 在testDataPath2当中,直接使用”resource/data.txty”来作为文件读取路径时,java会默认为基准目录为当前的工作目录,”resource/data.txty”此时就是相对于这个基准目录的路径。也就是说,java会把工作目录与”resource/data.txty”拼接起来,作为一个绝对路径去读取文件,也就是testDataPath1中的例子。 有Eclipse中启动你的java工程的目录就是工程根目录,工程根目录也就是工作目录,如果你读取文件的方式类似于testDataPath2的话,他会在工程根目录下找你的文件。有时候我们会遇到,在Eclipse中读取文件没有问题,但是打成jar包之后运行(像上面运行jar包的例子)就会报找不到文件的错误。那么此时就要检查你的当前工作目录下(/home/yukai)是否有这些文件的存在了。 File.getPath & File.getAbsolutePath & File.getCanonicalPath12345678910public static void testGetPath() { File file = new File("./resource/data.txt"); System.out.println("File.getPath(): " + file.getPath()); System.out.println("File.getAbsolutePath(): "+ file.getAbsolutePath()); try { System.out.println("File.getCanonicalPath(): " + file.getCanonicalPath()); } catch (Exception e) { e.printStackTrace(); }} 上述代码运行结果: 123File.getPath(): ./resource/data.txtFile.getAbsolutePath(): /home/yukai/workspace/test/./resource/data.txtFile.getCanonicalPath(): /home/yukai/workspace/test/resource/data.txt 摘抄一段stackoverflow上的答案: 12345getPath() gets the path string that the File object was constructed with, and it may be relative current directory.getAbsolutePath() gets the path string after resolving it against the current directory if it's relative, resulting in a fully qualified path.getCanonicalPath() gets the path string after resolving any relative path against current directory, and removes any relative pathing (. and ..), and any file system links to return a path which the file system considers the canonical means to reference the file system object to which it points. 读取classpath下的文件1234567public static void getClassPath() { System.out.println("PathTest.class.getResource(\"/\"): " + PathTest.class.getResource("/")); System.out.println("PathTest.class.getResource(\"\"): " + PathTest.class.getResource("")); System.out.println("PathTest.class.getClassLoader().getResource(\"/\"): " + PathTest.class.getClassLoader().getResource("/")); System.out.println("PathTest.class.getClassLoader().getResource(\"\"): " + PathTest.class.getClassLoader().getResource(""));} 运行结果(Eclipse): 12345678//以“/”开头,表示相对于package根目录PathTest.class.getResource("/"): file:/home/yukai/workspace/test/bin///不以“/”开头,表示相对于class文件所在目录PathTest.class.getResource(""): file:/home/yukai/workspace/test/bin/test///PathTest.class.getClassLoader().getResource("/"): null//getClassLoader().getResource("")不以“/”开头,相对于package根目录PathTest.class.getClassLoader().getResource(""): file:/home/yukai/workspace/test/bin/ 获取jar包路径java开发中常常需要取得程序生成的jar包所在的路径,比如生成一些log文件的时候,需要把该程序生成的log放到jar包的的同级目录下。此时,我们需要知道jar包所在的位置,注意,不是启动jvm实例的位置。 12345678910111213141516URL url = Config.class.getProtectionDomain().getCodeSource().getLocation();String path = new URI(url.getProtocol(), url.getHost(), url.getPath(), url.getQuery(), null).getPath(); if (path != null) { if (path.endsWith("/") || path.endsWith("\\")) { path = path.substring(0, path.length() - 1); } if (path.endsWith(".jar")) { int index = path.lastIndexOf("/"); if (index != -1) { path = path.substring(0, index); } defultLogPath = path + File.separator + "log"; } else { defultLogPath = path + File.separator + ".." + File.separator + "log" ; }} Path.javajdk1.7之后,新增了一个类File,位于java.nio.file下,该类提供了一些路径的操作。 具体的使用不一一介绍,用到时可查找oracle的官方手册,已经写的很详细了:Path Operations]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
</entry>
<entry>
<title><![CDATA[java中的int-byte转换]]></title>
<url>%2F2016%2F11%2F24%2Fjava%E4%B8%AD%E7%9A%84int-byte%E8%BD%AC%E6%8D%A2%2F</url>
<content type="text"><![CDATA[前言好久没有动博客了,看了下,将近两个月了吧。最近的两个月里面变懒了,没有了记录的动力。。。最近好像也没啥收获,惭愧。之前电脑中病毒之后(cryptolcker),还好没有神马重要的东西,索性把硬盘重新格式化一遍,重做了系统。保留一套windows(实在受不了win10的自动更新了…),又装了一套opensuse的系统。希望可以坚持学点linux。 这段时间没有怎么看书,说一下读书情况。《重构》这本还是没有看完,读了大半。《深入理解jvm虚拟机》这本算是大致上浏览了一遍,发现确实是一本挺好的java书籍,日后还需多加翻阅理解。《大话设计模式》也是浏览了一遍,其中比较常见的模式有时候也可以用到,需要的时候再看吧。双十一买了一本《java并发编程实战》,看了大半,似懂非懂,但也是有收获的。不得不吐槽一下该书的翻译,实在是不敢恭维,但是想想自己的英语水平,目前还是忍了。。 必须要振作起精神来了,总觉得自己每天无所事事呢。接下来的计划…..还是把并发编程这块看完吧,虽然没有什么实战的机会,总比不看强。要多写总结了,不然看了跟不看差不多。异常、反射、动态代理、注解等等这些也要学习然后做一个记录。网络、io、并发这三块,并发还在看,也要做好记录,然后网络这部分把大学课本重读一遍,至于io不知道有什么好的书籍,放到最后规划学习把。 嗯,又想起来最近还写了一个自动打卡的程序,增加了短信通知的功能。总觉得有些不诚信的感觉呢,但是没有迟到早退过呀,好玩罢了。读了webmagic的core部分的源码,感觉理解起来轻松许多。记得大学那会看这个的时候还是一头雾水,结果现在两个小时就把代码搞清楚了(主要是代码量也不大拉)。嗯,说明自己还是在进步的麻,哈哈哈。下载了junit的源码都没有看过,有空研究把。 终于要进入正题了。这次的记录很短很简单,是关于java中int和byte之间的转换的。虽然简单,但是以前就是拿来即用,没有搞清楚其中的原理,在byte这块就很纠结(只能说基础比较差),今天来学习一下。 正文java中: byte: 1字节 int: 4字节 12int i = -123byte x = (byte)i; //-123 ps:([]代表几进制,()代表前面的内容重复几次) ps: -123[2] = 11111111(3)10000101 int 强转为 byte,直接截取低8位即10000101 java把byte当做有符号处理,故此时x=-123 1int j = (int)x; //-123 ps: byte强转为int,高24位补1(自动扩展) 此时,j[2]=11111111(3)11101010 即,j=-123 负数似乎没有问题… 12int i = 234;byte x = (byte)i; //-22 ps: 234[2] = 00000000(3)11101010 int 强转为 byte,直接截取低8位即11101010 java把byte当做有符号处理,故此时x=-22 1int j = (int)x; //-22 ps: byte强转为int,高24位补1(自动扩展) 此时,j[2]=11111111(3)11101010 即,j=-22 可见,byte单纯的强转为int是行不通的。(注意,示例中int强转为一个byte,如果int值超过255,发生数据丢失,正确的int转为4字节byte方法见下文) 123int i = 234;byte x = (byte)i; //-22int j = x & 0xFF; //j=234 ps: x & 0xFF = 00000000(3)11101010 即,j=234 可见0xFF的作用… http://yukai.space/2016/04/28/java%E4%B8%AD%E7%9A%84%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2/ byte short char 他们三者在计算时,首先会转换为int类型,故下面几个表达式进行|运算时,是int类型之间的|运算,存在自动扩展(移位操作也会出现这种情况,即int转byte的时候),不进行& 0xFF的运算,则有可能造成第一种情况的出现。 java字节序 java为大端字节序(bigendian):高字节数据存放在低地址处,低字节数据存放在高地址处。 0x01020304 故b[0]=0x01,b[1]=0x02,b[2]=0x03,b[3]=0x04 12345678910111213141516//byte 数组与 int 的相互转换 public static int byteArrayToInt(byte[] b) { return b[3] & 0xFF | (b[2] & 0xFF) << 8 | (b[1] & 0xFF) << 16 | (b[0] & 0xFF) << 24; } public static byte[] intToByteArray(int a) { return new byte[] { (byte) ((a >> 24) & 0xFF), (byte) ((a >> 16) & 0xFF), (byte) ((a >> 8) & 0xFF), (byte) (a & 0xFF) }; }]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java泛型学习]]></title>
<url>%2F2016%2F10%2F10%2Fjava%E6%B3%9B%E5%9E%8B%E5%AD%A6%E4%B9%A0%2F</url>
<content type="text"><![CDATA[有一段时间没有更新博客了,过了国庆之后,好像变得更懒了~~前两天开发了一个用于记账的公众号,功能很简单,就是普通的增删查改…由于是个人开发者,无法进行公众号的认证,所以没什么高级的接口权限,只能搞的简陋一点了…下一步计划再丰富丰富,目前想到了这几个功能:配合有道或者金山词霸的Api进行中英互译(API怎么用,可不可以用目前还不清楚),配合微博的Api接口整点微博热门什么的,配合豆瓣的Api接口做一个电影或者书籍查询的功能等等….公众号的服务器代码目前还部署在自己的电脑上,用nat123做了一下公网ip的映射,接下来考虑是不是要换成云服务器更为妥当…. 公众号在这里: 目前,公众号已经开源:WechatSubscriptionNumber 好了,言归正传。今天学习了java的泛型知识,不总结一下老是觉得跟没学一样… 泛型的好处使用泛型的好处我觉得有两点:1:类型安全 2:减少类型强转 下面通过一个例子说明: 假设有一个Test类,通用的实现是: 123456789101112131415class Test { private Object o; public Test(Object o) { this.o = o; } public Object getObject() { return o; } public void setObject(Object o) { this.o = o; }} 我们可以这样使用它: 123456public static void main(String[] args) { Test test = new Test(new Integer(1)); //编译时不报错 //运行时报 java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String String o = (String) test.getObject();} 看一个使用泛型的例子: 123456789101112131415161718192021222324class Test<T> { private T o; public Test(T o) { this.o = o; } public T getObject() { return o; } public void setObject(T o) { this.o = o; }}public static void main(String[] args) { Test1<Integer> test = new Test1<Integer>(new Integer(1)); //编译时报错,无法通过编译 //String o = test.getObject(); //正常运行 Integer o = test.getObject();} 从上面的对比中能够看出两点: 1.使用泛型之后在编译时报错而非运行时,减少了出错的几率; 2.使用泛型之后编译器不再要求强转 定义泛型泛型的机制能够在定义类、接口、方法时把类型参数化,也就是类似于方法的形参一样,把类型当做参数使用。 泛型参数部分使用<>包裹起来,比如<T>,T声明了一种类型,习惯上,这个类型参数使用单个大写字母来表示,指示所定义的参数类型。有如下惯例: E:表示元素 T:表示类型 K:表示键 V:表示值 N:表示数字 定义泛型类上面的例子就是一个很好的演示 123456789101112131415class Test<T> { private T o; public Test(T o) { this.o = o; } public T getObject() { return o; } public void setObject(T o) { this.o = o; }} 使用泛型定义类之后,泛型参数 T 可以运用到该类中:可以声明成员变量类型、可以声明成员函数返回值类型、可以声明成员函数参数类型。 要注意:泛型参数T不能用于声明静态变量,同时也不能用于new一个对象比如:T o = new T(); 下面的类型擦除会说到原因。 定义泛型接口123interface Test<T> { public T test(T t);} 使用泛型定义接口之后,泛型参数 T 可以运用到该接口中:可以声明接口函数返回值类型、可以声明接口函数参数类型。 定义泛型方法可以单独给方法使用泛型,而不是泛化整个类: 123public static <T> T getT(T t){ return t;} 使用泛型定义方法后,泛型参数 T 可以声明该方法的返回值类型、可以声明该方法的参数类型。 要注意,定义方法所用的泛型参数需要在修饰符之后添加。 定义多个泛型参数以接口为例: 123456789interface Test<T, S> { public T testT(T t); public S testS(S s);}public static void main(String[] args) { //编译时报错 //getT("s");} 多个泛型参数在尖括号中使用逗号隔开。类的泛化与方法的泛化类似。 泛型参数的界限定义泛型参数界限有这样两种意义: 1.有时候我们希望限定这个泛型参数的类型为某个类的子类或者超类; 2.上面的例子中可以看到,我们定义了泛型参数,向方法中传入某种类型,这种类型是未知的,因此我们无法使用这种类型定义的变量,不能够调用它的方法。 123public static <T extends Number> Integer getT(T t) { return new Integer(t.intValue());} 上面例子中,<T extends A>表示T是A或A的子类,他限定了传入泛型方法参数的类型必须为A或A的子类,同时,在方法体中我们也可以使用t这个实参就像使用A的实例一样,调用Number具有的public方法。 除了<T extends A>限定T是A或A的子类外,还可以使用<T super A>这种方式来限定T是A或A的超类。 A可以是某个类或者接口。 除此以外,还可以为泛型参数限定多个限制范围,如<T extends A & B & C>,限定范围中最多只能有一个类(某个类只能有一个父类~~),并且他必须是限定列表中的第一个。 12345678Class A { // }interface B { // }interface C { // }//正确class D <T extends A & B & C> { // }//编译时报错class D <T extends A & B & C> { // } 泛型的继承看一下jdk中List的泛型继承例子: 1public interface List<E> extends Collection<E>{//...} List<String> 就是 Collection<String> 的子类。 假如定义自己的接口: 1234interface MyList<E,P> extends List<E> { void setPay(E e,P p); //...} MyList<String,String>、MyList<String,Integer>、MyList<String,Exception>都是List<String>的子类。 使用泛型上面的例子中已经列举了一些使用泛化类或者泛化函数的例子,但是还有一些问题需要指出: 1.泛型参数只接受引用类型,不适用于基本类型 比如: 123456class Test<T> {}public static void main(String[] args) { //无法通过编译,不接受int类型的泛化参数 //Test<int> test = new Test();} 而我们使用泛化函数时: 12345public static void main(String[] args) { getT(1);}public static <T> void getT(T t) {} 是没有问题的,通过查看生成的字节码,发现getT(1)这个方法的字节码中1被自动装箱为Integer类型。 2.通配符的使用 考虑下面的情况: 123456789class Test<T> {}public static void getT(Test<Number> t) {}public static void main(String[] args) { Test<Double> test = new Test(); //编译时报错 //getT(test);} 报错的原因很好理解,虽然Double是Number的子类,但Test并不是Test的子类,故类型检查无法通过。这一点一定要明白。 那么如果我们确实想要传入一个Test类型的形参呢?可以使用通配符: 12345678class Test<T> {}public static void main(String[] args) { Test<Double> test = new Test(); //正常运行 getT(test);}public static void getT(Test<? extends Number> t) {} Test<? extends Number>扩展了形参的类型,可以是Test<Double>、Test<Integer>等,尖括号中的类型必须是Number或继承于Number。同样的,通配符也适用于super,如Test<? super A>。 如果类型参数中既没有extends 关键字,也没有super关键字,只有一个?,代表无限定通配符。 Test<?>与Test<Object>并不相同,无论T是什么类型,Test<T> 是 Test<?>的子类,但是,Test<T> 不是 Test<Object> 的子类,想想上面的例子。 通常在两种情况下会使用无限定通配符: 如果正在编写一个方法,可以使用Object类中提供的功能来实现 代码实现的功能与类型参数无关,比如List.clear()与List.size()方法,还有经常使用的Class<?>方法,其实现的功能都与类型参数无关。 一般情况下,通配符<? extends Number>只是出现在使用泛型的时候,而不是定义泛型的时候,就像上面的例子那样。而<T extends Number>这种形式出现在定义泛型的时候,而不是使用泛型的时候,不要搞混了。 结合泛型的继承和通配符的使用,理解一下泛型的类型系统,也就是泛型类的继承关系: 以下内容来自:Java深度历险(五)——Java泛型 引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List<String>和List<Object>这样的情况,类型参数String是继承自Object的。而第二种指的是 List接口继承自Collection接口。对于这个类型系统,有如下的一些规则: 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即List<String>是Collection<String> 的子类型,List<String>可以替换Collection<String>。这种情况也适用于带有上下界的类型声明。当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对Collection<? extends Number>来说,其子类型可以在Collection这个维度上展开,即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开,即Collection<Double>和 Collection<Integer>等。如此循环下去,ArrayList<Long>和 HashSet<Double>等也都算是Collection<? extends Number>的子类型。如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。 关于通配符的理解可以参考Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同? 类型擦除类型擦除发生在编译阶段,对于运行期的JVM来说,List<int>与List<String>就是同一个类,因为在编译结束之后,生成的字节码文件中,他们都是List类型。 1.java编译器会在编译前进行类型检查 java编译器承担了所有泛型的类型安全检查工作。 2.类型擦除后保留的原始类型 原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除(crased),并使用其限定类型(无限定的变量用Object)替换。 3.自动类型转换 因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢? 比如: 1234567public class Test { public static void main(String[] args) { ArrayList<Date> list=new ArrayList<Date>(); list.add(new Date()); Date myDate=list.get(0); }} Date myDate=list.get(0);这里我们并没有对其返回值进行强转就可以直接获取Date类型的返回值。原因在于在字节码当中,有checkcast这么一个操作帮助我们进行了强转,这是java自动进行的。 更多的关于类型擦除的知识,参考 java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题 实践最近在重构公众号服务器的过程中,用到了泛型编程的知识。 12345public interface BaseServiceContext<T extends ReqBaseMessage, R> { public void selectService(T reqMeg); public R executeRequest();} 上面是一个选择service的上下文接口,接收到用户请求后通过这个接口选择对应的service并且执行service。这个接口相当于一个工厂和策略模式的结合体。下面是这个接口的一种实现: 12345678910111213//请求为文本类型,返回string类型的处理结果public class TextServiceContext implements BaseServiceContext<ReqTextMessage,String> { @Override public void selectService(ReqTextMessage reqMeg) { //..... } @Override public String executeRequest() { //..... }} 可以看到,BaseServiceContext限定了selectService方法的参数类型和executeRequest方法的返回值类型,使其能够灵活的支持各种类型的参数和返回值。 看一下在没有学习泛型之前,这个接口是怎么实现的: 1234567891011121314151617181920public interface BaseServiceContext { public void selectService(ReqBaseMessage reqMeg); public Object executeRequest();}public class TextServiceContext implements BaseServiceContext { @Override public void selectService(ReqBaseMessage reqMeg) { //根据业务逻辑对reqMeg进行强转,需要程序员自己判断 //很有可能强转失败 } @Override public Object executeRequest() { //返回类型为object,在调用方法的外部强转为需要的类型 //很有可能强转失败 }} 可以看到没有使用泛型接口的情况下,类型不安全且增大了强转失败的风险。同时也需要程序员根据业务逻辑去判断该强转成什么类型。使用泛型接口之后就没有了这些问题,只需要在使用接口时声明好他的泛型参数就o了。 上面只是我在开发过程中体会到泛型的一个好处,类似的例子还有很多。 注意事项参考java 泛型编程(一) 泛型:工作原理及其重要性 Java深度历险(五)——Java泛型 java泛型详解 Java 泛型 <? super T> 中 super 怎么 理解?与 extends 有何不同?]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ssl使用总结]]></title>
<url>%2F2016%2F09%2F25%2Fssl%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[这次维护的web服务器要求使用Https双向认证,了解了一下如何在客户端和服务器之间进行ssl的配置,在此记录。另外,这篇日志主要记录如何使用,并不深入底层原理。 简单认识ssl和证书在理解这部分内容之前,建议先看看前面对的一篇总结:关于加密的一点总结 SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS与SSL在传输层对网络连接进行加密。 SSL协议提供的服务主要有: 1、认证用户和服务器,确保数据发送到正确的客户机和服务器; 2、加密数据以防止数据中途被窃取; 3、维护数据的完整性,确保数据在传输过程中不被改变。 其实,ssl就是一种协议。我们知道Http协议是明文传输的,安全性不高。Https就是在Http基础之上加一层ssl协议,来达到加密通信的目的。具体的ssl握手过程,可以参考百度百科 附上一张图方便理解: 图片来自网络 可以看到ssl握手过程中服务器向客户端发送了自己的证书。这个证书是加密过程的重要内容。在前面的关于加密的一点总结这篇博客中,在关于数字证书的一节,我们知道了数字证书可以证明服务器的身份,服务器证书中包含了服务器的公钥,用于之后的通信。 那么,简单了解下证书的工作原理。 首先看一下证书中有哪些内容: 上面是我从谷chorme浏览器中截取的支付宝所使用证书的信息截图。 可以看到,里面有颁发者和使用者。颁发者就是颁发此证书的CA(证书颁发机构),使用者就是与我们通信的服务器,也就是该证书的持有者。证书中还包含了我们上面提到的公钥。 我们知道,证书能够证明服务器的身份,那么他是如何证明的呢?我们以浏览器为例: 首先,浏览器(客户端)接收到服务器发来的证书A之后,会去验证这个证书A是否被篡改或者是伪造的。浏览器首先会去操作系统中内置根证书库中搜索A的颁发者的证书。关于这一点要解释一下,数字证书的颁发机构也有自己的证书,这个证书就是根证书,这个根证书在我的系统刚安装好时,就被微软等公司默认安装在操作系统当中了。 如果在操作系统中找到了服务器证书中的颁发者的证书Root,那么继续下一步,否则,就知道这个服务器证书的颁发者是个不受信任的CA发布的,此时,浏览器会给出警告。若我们选择相信这个CA并继续,则继续下一步。 接下来,浏览器从Root中得到Root的公钥,然后用该公钥对A中的指纹算法和指纹进行解密。注意,为了保证安全,在证书的发布机构发布证书时,证书的指纹和指纹算法,都会加密后再和证书放到一起发布,以防有人修改指纹后伪造相应的数字证书。 那么什么是指纹算法和指纹呢?这个是用来保证证书的完整性的,也就是说确保证书没有被修改过。 其原理就是在发布证书时,发布者根据指纹算法(一个hash算法)计算整个证书的hash值(指纹)并和证书放在一起,使用者在打开证书时,自己也根据指纹算法计算一下证书的hash值(指纹),如果和刚开始的值对得上,就说明证书没有被修改过,因为证书的内容被修改后,根据证书的内容计算的出的hash值(指纹)是会变化的。 此时就可以判断该证书是否是仿冒或者经过伪造篡改的。如没有,则证明这个服务器是可信任的。接下来就可以使用该服务器提供的公钥来进行通信了。 通过上面的分析我们知道了,ssl协议中,首先通过证书来证明服务器的身份,然后取出证书中的公钥,接下来就可以按照关于加密的一点总结这篇博客中说到的通信方式通信了。这大概上就是ssl的原理,当然实际中ssl的细节还是要复杂很多。 了解了ssl的基本原理,对我们实现服务器的https通信有很大的帮助。 Https 单向认证单向认证就是服务器必须向客户端发送自己的证书来证明自己的身份,然后进行加密通信。现在服务器的基础框架是springboot,演示如何配置服务器和客户端: 服务器: 打开springboot的配置文件application.properties: server.ssl.key-store=server.keystore server.ssl.key-store-password=123456 server.ssl.keyStoreType=JKS server.ssl.keyAlias:server 先解释一下上面几个配置: .keystore: keystore文件中存储了密钥和证书。密钥包括私钥和公钥,而证书中只包含公钥。这个证书就是我们上面所说的证书。注意:密钥和证书是一一对应的。 password: 生成keystore文件时所填的密码。 keyStoreType: keystore类型。 keyAlias: 生成密钥的别名。 keysotre文件是怎么生成的呢?使用jdk自带的工具:keytool。生成keystore文件的命令如下: keytool -genkey -alias server -keyalg RSA -validity 365 -keystore server.keystore 其中:-alias 指定生成密钥的别名,-keystore 指定生成的keystore文件的位置和名称。其他的参数的含义可以通过keytool -help来查找。 一个keystore文件中可以存储多个证书和证书对应的密钥,这些证书和其对应的密钥通过唯一的别名alias来指定,也就是说,通过alias可以导出证书。但是要注意,无法通过keytool导出私钥。 看一下如何从keystore文件中导出证书: keytool -export -alias server -keystore server.keystore -file serverCA.crt -alias指定了要导出的证书文件,-file 指定了要导出的证书文件的位置和名称。 了解了上面的几个配置,再结合之前对ssl原理的总结,不难知道: 我们指定的别名(server.ssl.keyAlias:server)之后,服务器通过该别名从keystore文件中获得对应的证书,并将其发送给客户端,同时使用keystore中alias对应的私钥(keytool虽然导不出私钥,但可以通过代码等方式获得)可以对与客户端通信的内容进行加密和解密。这便是keystore文件的作用:存储证书和私钥。 服务器端的配置完成了。现在有一个问题,虽然服务器可以发送证书到客户端了,但是客户端并不会信任我们这个证书。如果这个时候通过浏览器以https的方式访问服务器,浏览器会提醒你,此连接不受信任。那是因为虽然服务器将证书发送到了浏览器(客户端),但是浏览器并不认为这个证书能够证明服务器的身份。那为什么https访问支付宝等网站没有报这个警告呢?想想上面的内容,浏览器或操作系统内置了颁发给支付宝证书的机构的根证书,通过这个根证书的公钥对支付宝发来的证书进行解密可以证明这个证书确实是由支付宝发过来的,从而证明了服务器的身份。 虽然浏览器并不能验证我们的证书,我们可以手动的把证书添加到浏览器的信任列表中。这个证书就是我们上面通过keytool导出的证书,将这个证书手动添加到浏览器信任列表里面,再次访问服务器就不会有警告啦。(具体的添加方式不再赘述)(从这点上也可以看出自签名证书的不安全性,有可能被假冒和伪造) so,通过上面的方式我们已经将ssl单向验证配置好了。那么,如果客户端是自己用java写的呢?下面举一个例子,使用HttpsURLConnection实现: 首先生成客户端的信任证书库,用来存放客户端所信任的证书: keytool -keystore truststore.jks -alias client -import -trustcacerts -file serverCA.crt -file 指定要信任的证书,此例中应该是我们上面导出的证书serverCA.crt -keystore 指定要生成的信任库路径和名称。 然后生成我们自己的信任证书管理器: 参考了网上的代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465public class MyX509TrustManager implements X509TrustManager { /* * The default X509TrustManager returned by SunX509. We'll delegate * decisions to it, and fall back to the logic in this class if the default * X509TrustManager doesn't trust it. */ X509TrustManager sunJSSEX509TrustManager; public MyX509TrustManager() throws Exception { // create a "default" JSSE X509TrustManager. KeyStore ks = KeyStore.getInstance("JKS"); //注意 src/clientTrustCA.jks就是上面生成的信任证书库 ks.load(new FileInputStream("src/clientTrustCA.jks"), "123456".toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509", "SunJSSE"); tmf.init(ks); TrustManager tms[] = tmf.getTrustManagers(); /* * Iterate over the returned trustmanagers, look for an instance of * X509TrustManager. If found, use that as our "default" trust manager. */ for (int i = 0; i < tms.length; i++) { if (tms[i] instanceof X509TrustManager) { sunJSSEX509TrustManager = (X509TrustManager) tms[i]; return; } } /* * Find some other way to initialize, or else we have to fail the * constructor. */ throw new Exception("Couldn't initialize"); } /* * Delegate to the default trust manager. */ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { try { sunJSSEX509TrustManager.checkClientTrusted(chain, authType); } catch (CertificateException excep) { // do any special handling here, or rethrow exception. } } /* * Delegate to the default trust manager. */ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { try { sunJSSEX509TrustManager.checkServerTrusted(chain, authType); } catch (CertificateException excep) { /* * Possibly pop up a dialog box asking whether to trust the cert * chain. */ } } /* * Merely pass this through. */ public X509Certificate[] getAcceptedIssuers() { return sunJSSEX509TrustManager.getAcceptedIssuers(); }} 接着使用这个管理器: 1234567891011TrustManager[] tm = { new MyX509TrustManager() };SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");sslContext.init(null, tm, new java.security.SecureRandom());// 从上述SSLContext对象中得到SSLSocketFactory对象SSLSocketFactory ssf = sslContext.getSocketFactory();// 创建URL对象URL myURL = new URL(url);// 创建HttpsURLConnection对象,并设置其SSLSocketFactory对象HttpsURLConnection httpsConn = (HttpsURLConnection) myURL.openConnection();httpsConn.setSSLSocketFactory(ssf); 接下来就可以使用这个HttpsURLConnection来访问服务器啦。 Https双向认证所谓的Https双向认证就是不仅仅客户端要验证浏览器的身份,浏览器也要向服务器证明自己的身份,也就是第一幅图片中的5。 理解了单向认证,双向认证实现起来也就不难了。 首先像我们之前一样使用keytool生成keystore文件client.keystore,这个文件是为客户端使用准备的。 然后通过client.keystore导出证书client.crt。 接着生成信任库文件serverTrust.jks,将client.crt导入其中。 在单向认证中我们给出了客户端单向认证时的java代码,用来下面给出双向认证的代码: 生成读取keystore的类: 123456789101112131415161718192021public class MyKeyManager{ // 相关的 jks 文件及其密码定义 private final static String CERT_STORE="src/client.keystore"; private final static String CERT_STORE_PASSWORD="123456"; public static KeyManager[] getKeyManagers() throws Exception{ // 载入 jks 文件 FileInputStream f_certStore=new FileInputStream(CERT_STORE); KeyStore ks=KeyStore.getInstance("jks"); ks.load(f_certStore, CERT_STORE_PASSWORD.toCharArray()); f_certStore.close(); // 创建并初始化证书库工厂 String alg=KeyManagerFactory.getDefaultAlgorithm(); KeyManagerFactory kmFact=KeyManagerFactory.getInstance(alg); kmFact.init(ks, CERT_STORE_PASSWORD.toCharArray()); KeyManager[] kms=kmFact.getKeyManagers(); return kms; }} 像上面单向认证中那样使用它: 123456789101112TrustManager[] tm = { new MyX509TrustManager() };KeyManager[] km = MyKeyManager.getKeyManagers();SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");sslContext.init(km, tm, new java.security.SecureRandom());// 从上述SSLContext对象中得到SSLSocketFactory对象SSLSocketFactory ssf = sslContext.getSocketFactory();// 创建URL对象URL myURL = new URL(url);// 创建HttpsURLConnection对象,并设置其SSLSocketFactory对象HttpsURLConnection httpsConn = (HttpsURLConnection) myURL.openConnection();httpsConn.setSSLSocketFactory(ssf); 然后使用HttpsURLConnection就可以使用了。但是此时服务器端还需要配置,因为客户端仅仅是将自己的证书发过去了,服务器应该如何信任它呢? 打开springboot的配置文件application.properties添加以下配置: #Whether client authentication is wanted (“want”) or needed (“need”). Requires a trust store. server.ssl.client-auth=need server.ssl.trust-store=serverTrust.jks server.ssl.trust-store-password=123456 其中,serverTrust.jks就是刚刚生成的服务器端信任库。 以上步骤全部做完,服务器和客户端就可以愉快的使用Https进行双向认证通信了。 关于证书的一些格式在使用keytool和openssl的时候,会发现有好多种格式让人头晕眼花。不妨参考这篇文章:那些证书相关的玩意儿(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等) 我现在也不是理解的十分清楚,故不再赘述。 总结感觉这篇博客没有把自己想要总结的内容全部记录下来,但是现在也不想再写了。服务器实际测试的时候是使用curl这样一个命令行浏览器进行的,使用curl作为客户端进行双向认证的时候也遇到了不少问题,总结了一份配置的readme,放在github上面,以作记录。readme中的配置方法可能还有一些瑕疵,有些格式转换可能会有冗余,但是整个配置是没有问题的,经过了自己的验证。证书这一块内容感觉还不是理解的特别透彻,尤其是ssl协议的交互和证书格式的一些问题,留作以后学习吧!真的不想看了… 参考数字证书原理:这篇一定要看,总结的很好! 浏览器和SSL证书通讯过程 Java安全通信:HTTPS与SSL Java 安全套接字编程以及 keytool 使用最佳实践 openssl生成SSL证书的流程 如何添加自签名SSL证书 自签名SSL证书存风险 keystore提取私钥和证书(重要×××)]]></content>
<categories>
<category>技术</category>
<category>加密</category>
</categories>
<tags>
<tag>java</tag>
<tag>加密</tag>
</tags>
</entry>
<entry>
<title><![CDATA[记录工作遇到的坑]]></title>
<url>%2F2016%2F09%2F24%2F%E8%AE%B0%E5%BD%95%E8%BF%99%E5%91%A8%E5%B7%A5%E4%BD%9C%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91%2F</url>
<content type="text"><![CDATA[昨天晚上12.30才进的家门,回来之后还远程合了个代码打了个包,发布restAPI新版本之用。这一周的主要工作就是在做一个web服务器,是一个集群的中间件。因为着急发布新版本,所以昨晚加班到12点多… 主要实现了几个接口,实现了ssl双向验证功能。其实ssl双向验证的功能主要是了解ssl原理之后如何配置的问题。期间还学习了session和cookie的原理。ssl与cookie、session单开一篇进行总结。这篇日志记录一下这周工作中遇到的一些坑,不一定是技术上的,但是遇到这些小问题还是耽误了一些时间。这篇日志也会持续更新,把以后遇到的坑都总结到这里来。 希望以后遇到类似的情况能够迅速找到原因将其解决,同时也希望能帮助到可能会看到这篇文章的人。 Spring项目通过export方法打包为jar的问题在前面的日志中已经有专门的一篇来总结这个问题:当通过export方式打包Spring项目时,一定要记得勾选Add Directory Entries这个选项,否则spring无法正确扫描到controller,发生404 使用@PathVariable注解时抛出java.lang.IllegalArgumentException异常假如你的项目中有一个类似下面的controller来接收一个请求: 12345678@GET@RequestMapping("web/{groupName}")@Consumes(MediaType.APPLICATION_JSON)@Produces(MediaType.APPLICATION_JSON)public void getWorkerAddress(HttpServletRequest request, HttpServletResponse response, @PathVariable String groupName) { doSomething...} 当你的项目运行时你发起一个http://ip:port/web/groupName1这样的请求,那么很有可能服务器会报这样一个异常:java.lang.IllegalArgumentException: Name for argument type [java.lang.String] not available, and parameter name information not found in class file either. 大概的意思就是参数名称信息没有在类文件中找到。若是浏览器为客户端,浏览器的页面是这样的: 解决这个异常的方法有两种: 1.在@PathVariable注解后面加上参数名,比如:@PathVariable(“groupName”) 2.编译时将debug信息勾选进去: 编译时把上图绿色框中的单选框勾选,就不会报异常。原因在于Local variable属性建立了方法的栈帧中局部变量部分内容与源代码中局部变量名称和描述符之间的映射关系。 顺便了解一下上面四个选项的作用: 图片截取自:Eclipse用户手册 其实就是java工程编译时的一些选项。当我们在debug时遇到问题,比如断点打不上,source not found等等或许修改这些选项可以得到解决。 参考一篇文章:java编译时生成调试信息选项详解(javac -g) Windows环境下使用Curl,要使用双引号包裹内容web服务器是通过curl来测试的。之前自己也没用过这个所谓的命令行浏览器工具。在机器上装好curl之后,把测试那边提供的脚本拿过来,由于脚本是linux环境下的,做了一点修改之后(其实就是直接换成.bat啦)就直接跑了。脚本内容类似这样: 1curl -X POST -H 'content-type: application/json' -d @del.txt https://localhost:8899/web/rest/data/load/topictarget -k -b cookie.txt>delresult.txt 但是实际测试的过程中,发现收到的post的数据为null,确定了不是服务器的问题之后,感觉应该是curl使用的问题。查了一些资料,是说post的数据要使用双引号包裹,而不是单引号。由于是从del.txt中提取post的数据,所以不存在这个问题。后来将content-type: application/json 的单引号换成双引号之后,问题解决。 所以:Windows环境下使用Curl,要使用双引号包裹需要加引号的内容,而不是单引号 Spring项目运行过程中,@Autowired注解的变量为null的问题2016/09/09 更新 这两天开发了一个公众账号来做一些简单的记账工作。后台服务器是用springboot搭建的,测试过程中遇到了这么个情况: 在运行时服务器报了空指针NullPointerException异常,debug看了一下,发现是一个使用了@Autowired注解的变量没有被初始化。但是按理来说使用了@Autowired注解某个变量后spring会自动为我们实例化这个变量的啊!谷歌了一下,找到了一个类似的情况:Why is my Spring @Autowired field null?. 下面模拟一下问题出现的场景(代码来自上面的链接): 12345678910111213141516171819202122232425262728293031@Controllerpublic class MileageFeeController { @RequestMapping("/mileage/{miles}") @ResponseBody public float mileageFee(@PathVariable int miles) { MileageFeeCalculator calc = new MileageFeeCalculator(); return calc.mileageCharge(miles); }}@Servicepublic class MileageFeeCalculator { @Autowired private MileageRateService rateService; // <--- should be autowired, is null public float mileageCharge(final int miles) { return (miles * rateService.ratePerMile()); // <--- throws NPE }}@Servicepublic class MileageFeeCalculator { @Autowired private MileageRateService rateService; // <--- should be autowired, is null public float mileageCharge(final int miles) { return (miles * rateService.ratePerMile()); // <--- throws NPE }} 访问MileageFeeController时报错: 123java.lang.NullPointerException: null at com.chrylis.example.spring_autowired_npe.MileageFeeCalculator.mileageCharge(MileageFeeCalculator.java:13) at com.chrylis.example.spring_autowired_npe.MileageFeeController.mileageFee(MileageFeeController.java:14) 可以看到,变量rateService是个空值。导致空指针异常… 原因就在于MileageFeeController.mileageFee中使用了new这种方式实例化了MileageFeeCalculator对象,该对象中的rateService为空,我们使用new这种方式实例化对象时,无法通过spring的@Autowired注解自动实例化对象中的属性。解决的方法就是实例化对象时应该通知spring,使其能够自动配置。 我采用了上述链接中的第三种方法,解决了问题: 1234567891011121314151617181920212223@Componentpublic class ApplicationContextHolder implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { context = applicationContext; } public static ApplicationContext getContext() { return context; }}@Controllerpublic class MileageFeeController { @RequestMapping("/mileage/{miles}") @ResponseBody public float mileageFee(@PathVariable int miles) { MileageFeeCalculator calc = ApplicationContextHolder.getContext().getBean(MileageFeeCalculator.class); return calc.mileageCharge(miles); }} 发生这些问题的原因还是在于对框架的不熟悉,内部原理不甚明了。有机会真得研究研究spring的源码~~ @RestController与@Controller2016/10/24 更新 Springboot 使用 Thymeleaf 模板 打算给公众号加一点地图功能,按照这个教程在springboot项目中配置Thymeleaf,但是每次都没有正确返回html页面,反而是返回了这个html的名称。试了几次才发现自己用错了@RestController这个注解…Controller代码如下: 12345678910111213@RestController@RequestMapping("/wechat")public class MapController { @RequestMapping("/map/showRestaurant") public String showRestaurant(@RequestParam(value = "location_x", required = true) String location_x, @RequestParam(value = "location_y", required = true) String location_y, @RequestParam(value = "label", required = true) String label, Model model) { model.addAttribute("locationx", location_x); model.addAttribute("locationy", location_y); model.addAttribute("label",label); return "hello"; }} 问题原因就在于上面的@RestController这个注解,将其替换为@Controller便解决了。 根本原因在于@RestController与@Controller这两个注解之间的区别 Linux下可靠的启动webservice2016/12/14 更新 今天线上webservice发现了一个问题,技服把出现的问题描述和日志发给了我,需要解决一下。最终确定不是代码的问题,而是linux下启动程序的方式引发的一个错误。虽然简单,却很容易被疏忽导致问题的发生,值得记录下来。情况是这样的,每次springboot程序会不定时自动停止,观察log发现,每次挂掉之前都会打印一行log,大致意思是,代码中调用的系统程序tail退出了。查看代码时发现确实在tail程序退出时会打印这么一行日志,但却是正常表现,程序运行在一个死循环当中,随后就会重启tail这个程序。循环当中也做了相应的异常捕获,按道理来说webservice不应该挂掉。初步怀疑多次启动tail导致内存泄漏以至于程序退出。由于代码中没有对内存泄漏这种error类型的异常捕获和记录到日志,从日志中无法判断是否发生了内存泄漏。代码中实现了Throwable级别异常的捕获,并记录异常信息到日志,再次抛出该异常。打包,发送给技服到测试机上去跑。一会之后,又挂了。观察新的Log,竟然与之前的没什么区别,并没有什么新的异常信息输出。也就是说,压根没有异常从代码中抛出。很有可能是程序被认为关闭或被系统干掉了。但是之前询问技服,对方否定了这种情况。这就怪了。继续观察日志,此时,发现这么一行:Unregistering JMX-exposed beans on shutdown.确实是springboot退出时打印的信息。google之,大部分的答案是没有添加依赖spring-boot-starter-web所导致springboot无法启动。与现在的情况不符,当前情况是启动正常,运行时突然挂掉。再次询问技服是否kill掉了运行webservice的进程。技服否认,说该程序时运行在后台的,继续询问如何运行在后台,回答是: java -jar MyApplication.jar &果然,使用这种方式运行在后台的程序,当ssh断掉或关闭bash窗口后,程序依然会被系统干掉,父进程都被干掉了,子进程也一并干掉了…正确的启动方式应为:nohup java -jar MyApplication.jar & 1234567891011nohup命令:如果你正在运行一个进程,而且你觉得在退出帐户时该进程还不会结束,那么可以使用nohup命令。该命令可以在你退出帐户/关闭终端之后继续运行相应的进程。nohup就是不挂起的意思( n ohang up)。该命令的一般形式为:nohup command &使用nohup命令提交作业如果使用nohup命令提交作业,那么在缺省情况下该作业的所有输出都被重定向到一个名为nohup.out的文件中,除非另外指定了输出文件:nohup command > myout.file 2>&1 &在上面的例子中,输出被重定向到myout.file文件中。 该问题虽然不是很难解决,但是很容易被忽略。故在此记录。 持续更新中….]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>spring</tag>
<tag>curl</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Eclipse乱码问题终极解决方案]]></title>
<url>%2F2016%2F09%2F20%2FEclipse%E7%BC%96%E7%A0%81%E9%97%AE%E9%A2%98%E7%BB%88%E6%9E%81%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88%2F</url>
<content type="text"><![CDATA[今天在Eclipse中导入一个新项目的时候,出现了乱码问题。研究一番… 显示乱码的根本原因在于:文件本身的编码和编辑器打开和编辑文件使用的编码不一致。当编辑器以不同于文件的编码去解码该文件的时候,就会导致读出来乱码。大部分情况下英文显示正常是因为大部分编码都兼容ASCII,中文则会显示乱码。 编辑器显示乱码问题在Eclipse中,设置编辑器显示和编辑文件所采用编码的方法有四种: 1.Windows->Preference->General->Workspace :修改整个工作空间 2.右键Project->Properties->Resource :修改某个工程 3.右键文件->Properties :修改某个文件 4.Windows->Preference->General->Content types->Default encoding :全局修改某种类型的文件 其中,3的优先级最高。也就是说某个文件被编辑器以哪种编码读取最终是由3中的Text file encoding决定的。一般情况下,如果我们不在3中显示指定编码(选择Other那一项),Eclipse都会默认使用Default那一项的编码。那么,这个Default的编码又是如何决定的呢? 注意上图中圈起来的部分:这部分表示了Default采用哪种编码是如何决定的。 可能会出现三种情况: 1.determined from content: xxx 2.determined from content type: xxx 3.inherit from container 其中: 1表示文件内容本身指定了编码,比如下图。 Encoding Bit Order Mark,简写为BOM,表明文件本身指定了采用哪种编码方式。编辑器会优先采用这种编码。 2表示采用了针对该文件类型的全局修改,也就是上面提到的第4种设置方式。 3表示继承了上一级指定的编码,这个继承关系就是我们在上面提到的:修改某个文件->修改文件所属的工程->修改整个工作空间。->表示继承于。 上面这3种情况的优先级:1>2>3 。也就是说如果文件本身表明了编码方式,Eclipse优先采用这种编码解码文件。若没有指定,Eclipse会查看该文件类型是否在上面的第4种方式设置中指定了编码,如有则采用该编码。如没有指定,则从下往上搜索继承关系链中指定的编码。(继承关系链:文件->工程->工作空间) 通过上面的分析明白了Eclipse 编辑器选择显示文件所采用编码的原理,相信再次遇到编辑器中的乱码问题都可以找到合适的解决方案。上面的分析不仅仅适用于java文件,同样适用于xml等其他类型文件的乱码问题。 另外一定要注意:如果通过上面4中方式的某一种修改影响了某个文件的编码,则当你再次编辑完保存该文件时,则采用此时该文件在Eclipse中指定的编码方式保存到本地。 以上的对乱码问题的分析都是基于通过修改Eclipse编辑器显示文件内容采用的编码,使其与文件本身的编码一致的方式来解决的。还有另一种思路是反过来,修改文件编码与Eclipse显示文件内容所采用的编码一致来解决乱码问题。可以使用文件编码批量转换工具进行批量转换,比如UltraCodingSwitch等。 Console显示乱码问题右键要运行的类->Run As->Run Configuratios 可修改上图中Other中的编码来指定显示打印内容的编码。 总结通过以上分析知道了如何在Eclipse中解决乱码的问题。其实文章一开始就说了,显示乱码的根本原因在于:文件本身的编码和编辑器打开文件使用的编码不一致。明白了这一点,就算在其他编辑器或编译器中或者任何能够显示文本内容的遇到乱码的情况都能够找到问题的根源,从而解决乱码问题。 至于文件本身的编码,可以通过一定的手段去改变。如通过记事本等工具修改编码后另存为等方式。 Java中输出一段内容到文件可以通过指定流的编码来指定输出字符的编码。 以上]]></content>
<categories>
<category>技术</category>
<category>工具</category>
</categories>
<tags>
<tag>Eclipse</tag>
</tags>
</entry>
<entry>
<title><![CDATA[记一次苦逼经历--关于spring项目打包为jar运行]]></title>
<url>%2F2016%2F09%2F19%2FSpring%E9%A1%B9%E7%9B%AE%E6%89%93%E5%8C%85%E4%B8%BAjar%2F</url>
<content type="text"><![CDATA[上周二刚从外包那里接手了一个的项目,主要是实现了一个web服务器,客户端可以以http请求的方式通过该服务器获取一些数据库的信息,技术上主要是使用了SpringBoot和REST API。周三就来新的需求了,实现一个接口,大概看了下代码,感觉soeasy(虽然之前从没有接触过springboot和spring,说来惭愧。不过这也说明了使用框架的好处,可以快速上手)。 中秋假期结束之后把这个接口弄完了。Eclipse里面跑起来完全没有问题。自信心爆棚。该服务器要求以jar包的方式运行,那就打个包吧。结果苦逼的经历开始了。 由于项目没有使用任何工具构建,没有maven,也没有Gradle。想着就凑合着用Eclipse里面自带的export导出jar包的功能吧。于是:右键工程,选择export。然后java->JAR file jar包打好了。把依赖什么的都放到jar所在的同一个目录,紧接着使用 java -cp interfaceService.jar;lib/* com.XXX.Application 执行。 服务器跑起来了。nice 浏览器输入URL,回车,出现了这样的画面: what? 404?赶紧看看服务器打印的日志,发现dispatcherServlet初始化完之后就没有其他动作了,也没有报错信息。跟在eclipse中正常执行的情况对比了一下,eclipse在打印完这几句Log之后就开始进入controller方法了。 初步怀疑是找不到对应的controller。 输入关键字:springboot jar 和404页面的提示,谷歌之。 找出来一堆答案,基本都是说没有正确添加注解那一类的意思。回头看看代码,该加注解的地方都加了。而且eclipse里跑着没问题啊,说明不是注解的问题。 感觉应该是打包的问题。把没用的.classpath等文件都排除,重新打包。再次执行,结果一样。 把lib和properties文件都打到包里,执行,还是没用。 然后,上图这几个勾选框吸引了我的目光,查了一下这几个框勾选之后的作用,感觉跟这个关系不大,但是也都选上试了一下,失败。 接着又以各种姿势谷歌了一遍,毫无结果。此刻心情比较郁闷,上网刷会微博吧,有点晕… 接下来重新振作精神,再打一次包。 抱着随便试试的心情,把下面的的框也选上了。 结果,这次竟然成功了!服务器返回了正确的结果。激动…原来就因为Add Directory Entries这个单选框没勾选就浪费了我两个多小时! 然后各种姿势测了一下,没问题了。 Add Directory Entries谷歌之。神了,出现了很多与我这个情况一样的描述和解决办法。看来还是有人掉过坑啊! 浏览了一下大家的分析,结果就是:当使用export方式打jar包来运行spring项目时,一定要记得把Add Directory Entries这个单选框勾选上!否则会扫描不到指定的controller 原因也很简单:当勾选Add Directory Entries这个选项时,生成的jar会添加文件夹信息。spring就可以扫描到相关的包信息。具体的解释看这篇文章:spring 扫描包不起作用 其实这次遇到的问题也不算是技术上的问题,但前前后后也花了将近三个小时才解决。有三点认识: 1.搜索技巧很重要,如果早一点能用spring、扫描包等关键字去搜索的话,问题早已经解决了。 2.打包尽量用构建工具,避免重复劳动还不易出错。Gradle使用学习中… 3.应该从问题的根源上想才会得到解决方法。比如,如果知道了spring获取controller的实现方式,或许能更快解决这个问题。(spring真的是一点都不懂,需要学习) 以上]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>服务器</tag>
<tag>spring</tag>
</tags>
</entry>
<entry>
<title><![CDATA[理解java内存模型]]></title>
<url>%2F2016%2F09%2F10%2F%E7%90%86%E8%A7%A3java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%2F</url>
<content type="text"><![CDATA[最近一直在看周志明所著的《深入理解Java虚拟机》,看到java内存模型这一章。自己从网上也查了一些资料,算是对java内存模型有了一个大概的认识,对理解和编写java并发有很大的帮助。有一段时间没再写博客了,正好利用周末的时间把自己学到的java内存模型的知识总结一下。Have a nice day~ 并发为啥会出现问题PS:2016/9/13更新:今天在地铁上看到这篇文章:Java 并发原理无废话指南,感觉跟我这一小节要说明的问题比较相似,参考一下。 原子性其实去了解java内存模型主要是为java并发打下基础。我刚学编程接触多线程的时候,关于多线程并发为什么会有并发问题有过一些思考,老师或者网上的例子都会给出一个类似这样的例子: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263public class BankAccount { private static int accountBalance = 10000; static class Save implements Runnable { private int money; public Save(int money) { this.money = money; } @Override public void run() { //存钱 int tempAccount = accountBalance + money; //设置余额 accountBalance = tempAccount; System.out.println("此次存入:" + money); System.out.println("当前余额:" + accountBalance); } } static class Obtain implements Runnable { private int money; public Obtain(int money) { this.money = money; } @Override public void run() { if (money > accountBalance) { System.out.println("余额不足"); return; } //取钱 int tempAccount = accountBalance - money; try { Thread.sleep(1000);//艾玛,卡了一秒 } catch (InterruptedException e) { e.printStackTrace(); } //设置余额 accountBalance = tempAccount; System.out.println("此次取出:" + money); System.out.println("当前余额:" + accountBalance); } } public static void main(String args[]) { int saveMoney = 10000; int obtainMoney = 10000; //自己取钱 Thread obtain = new Thread(new Obtain(obtainMoney)); //老婆存钱 Thread save = new Thread(new Save(saveMoney)); obtain.start(); save.start(); }} 输出结果: 1234此次存入:10000当前余额:20000此次取出:10000当前余额:0 可以看到,老婆往账户上存钱,自己从账户上取钱,两个线程同时发生,(为了保证获得演示效果,我们让取钱过程卡了一秒),结果苦逼了,账户上余额为零了,跟老婆解释不清了。 分析一下出现问题的原因: 出现这个问题的原因就在于两个人操作同一个账户,在一个人修改账户余额的时候另一个人也在修改账户余额,造成结果混乱。账户余额就是共享变量,操作账户的人就是并发线程,我们把这两个线程叫做自己线程和老婆线程。 这就涉及到线程并发的第一个问题:原子性。 我们可以看出来,出现上面的问题的主要原因其实有两部分:取钱和设置余额。取钱和设置余额这两个动作并不是原子操作,他们是分开执行的。如果在取完钱之后自己线程被挂起(这个挂起跟线程调度有关,我们在程序中模拟了这个挂起操作),老婆线程开始存钱。老婆线程存完钱后,自己线程又把刚刚的tempAccount设回余额,使旧的tempAccount覆盖了新的accountBalance,造成结果错误。 12345//取钱int tempAccount = accountBalance - money;//设置余额accountBalance = tempAccount; 为了实现这种错误的效果,我故意把 1accountBalance -= accountBalance; 拆成了上面的两行。其实accountBalance -= accountBalance;本身就不是一个原子操作,拆成两行是为了放大这种效果。 通过上面的分析,我们得出,某些读写共享变量的操作如果不是原子操作,多线程并发的情况下会出现并发问题。如何判断是否需要进行原子操作,跟业务逻辑有关,需要我们自己去判断。注意,常见的x=y,x++等都不是原子操作。 原子性是出现并发问题的重要因素,大多数情况下多线程并发出现问题都跟没有实现原子操作有关。原子性实现了多个线程并发访问某段代码的时候,使这些线程能够有序访问。因为实现原子操作代码的一旦被执行,就不能被打断,其他线程想要访问的时候,只能阻塞等待。 java中实现原子性使用了synchronized关键字,在synchronized块之间的代码具备原子性。把上面代码中的两个run方法声明为synchronized的,这样的话,这段代码中涉及到的对共享变量的操作就不会随意被打断,要么存完钱再去取,要么取完钱再去存,不会有上述代码提到的问题。 那么,该段代码出现并发问题仅仅是因为没有对共享变量实现原子操作吗?下面看内存可见性。 可见性组成原理中学过,为了更充分的利用CPU的性能,往往要在内存与处理器之间加一层:Cache(缓存),来作为内存与处理器之间的缓冲:将处理器需要的数据复制到缓存当中,当运算结束后再从缓存同步回内存当中。因为缓存的速度远远快于内存,这样处理器无需等待缓慢的内存读写,解决了处理器与内存的速度矛盾。 Java虚拟机也有类似的机制,每个线程有其自己的工作内存(类似前面的Cache),线程对变量的读写必须在工作内存中进行,而不能直接读写主存中的变量。(这里的变量指被各个线程共享的变量,比如堆中的对象和方法区中的变量。) 画个图: 这样的机制会带来另一个问题:缓存一致性。多个线程共同处理同一个变量时,各自的缓存中的数据并不一致,同步回主内存的数据以谁的缓存数据为准呢?这就带来了并发问题。 我们回到上述的例子: 上面例子中的代码出现并发问题仅仅是因为没有对共享变量实现原子操作吗?现在我们知道自己线程和老婆线程有各自的工作内存,他们各自对accountBalance 的读写都是基于工作内存的。然后在恰当的时机同步回主内存。现在我们假设类似accountBalance -= accountBalance;这样的操作是原子性操作,设想以下的场景: 1.老婆线程向账户中存10000,此时操作老婆线程工作内存中的accountBalance~(我们使用~来表明这个变量是工作内存当中的),此时accountBalance~ = 20000;accountBalance = 10000; 2.自己线程现在向账户中取10000,此时操作自己线程工作内存中的accountBalance~(注意此accountBalance~跟老婆线程中的accountBalance~不是同一个),此时accountBalance~ = 0;accountBalance = 10000; 3.现在老婆线程把自己的accountBalance~刷回主内存,此时accountBalance = 20000; 4.现在自己线程把自己的accountBalance~刷回主内存,此时accountBalance = 0; 通过以上的分析,看到了即使我们使对共享变量的写操作实现了原子性,但由于内存可见性的问题,依然存在并发问题。这就是造成多线程并发的第二个原因:内存可见性。 我们在原子性分析最后还说了,通过使用synchronized关键字可以保证不存在并发问题,是因为synchronized不仅实现了代码原子性操作,还保证了内存可见性。每次执行加锁和释放锁的同时,都会把线程的工作内存和主内存进行同步。一方面,它使自己线程和老婆线程只能串行操作账户余额,另一方面,他保证了当老婆线程存完钱之后会把自己工作内存中的accountBalance~刷回主内存。设想synchronized没有实现内存可见性的话,上面的问题依旧存在,注意这和互斥没有什么关系,此时两个线程依旧是串行访问。解释这么啰嗦主要是让大家明白原子操作和内存可见是造成并发问题的两个不同因素,但是通过锁可以同时解决这两个因素带来的问题。 有序性Cpu在执行指令的时候,为了优化提高Cpu运行程序的速度,会将多条指令不按程序规定的顺序分发给各个不同的电路单元处理,叫做指令重排序。注意乱序执行的指令之间没有数据依赖关系,因为乱序执行的结果必须保证结果的正确性。理解起来比较麻烦,通过一个例子来看一下。 以下例子来自《深入理解Java虚拟机》: 12345678910111213141516171819Map configOptions;char[] configText;boolean initialized = false;//假设以下代码在线程A中执行//模拟读取配置信息,当读取完成后将initialized设置为true通知其他线程配置可用configOptions = new HashMap();configText = readConfigFile(flieName);processConfigOptions(configText,configOptions);initialized = true;//假设以下代码在线程B中执行//等待initialized为true,代表线程A已经把配置信息初始化完成while(!initialized){ sleep();}//使用线程A中初始化好的配置信息doSomethingWithConfig(); 在上面的例子中,由于指令重排序的优化,导致线程A中最后一句代码initialized=true被提前执行,这样线程B中使用配置信息的代码就可能出现错误。 所以,指令重排序也是造成并发问题的一个因素。在java中,synchronized关键字也可以解决指令重排序带来的并发问题,他可以保证线程之间操作的有序性。如果使用synchronized关键字将上面例子中访问initialized的相关代码包裹起来,就保证了这种多线程之间操作的有序性。因为使用synchronized关键字后,持有同一个锁的两个同步块只能串行的进入,比如: 1234567891011121314151617181920212223Map configOptions;char[] configText;boolean initialized = false;//假设以下代码在线程A中执行//模拟读取配置信息,当读取完成后将initialized设置为true通知其他线程配置可用public synchronized void init(){ configOptions = new HashMap(); configText = readConfigFile(flieName); processConfigOptions(configText,configOptions); initialized = true;}//假设以下代码在线程B中执行//等待initialized为true,代表线程A已经把配置信息初始化完成public synchronized void doSomething(){ while(!initialized){ sleep(); } //使用线程A中初始化好的配置信息 doSomethingWithConfig();} 注意上面两个方法在同一个类中实现。 至此,我们分析出了造成多线程并发问题的三个原因:原子性、可见性、原子性。并且知道了通过synchronized可以解决这三个因素带来的并发问题。java中大部分的并发控制都能通过synchronized来实现。再结合之前写的一篇synchronized的用法,对synchronized的使用更加得心应手啦! 先行发生原则没有理解先行发生原则之前,看到网上很多博客提到这个,感觉很高深有木有~~~,理解了他之后,发现其实也挺简单。理解先行发生原则有助于我们判断线程是否安全,并发环境下两个操作之间是否存在数据冲突的问题。通过阅读《深入理解java虚拟机》和参阅网上的一些博客,我认为通过先行发生原则可以使我们知道自己写的多线程程序是否会因为可见性、原子性两个因素导致并发问题产生。至于原子性带来的问题,应该是程序员自己去分析具体的业务逻辑场景,并不能通过套用先行发生原则来判断自己的程序是否有并发问题。 比如我想到了之前宇哥跟我提到的一个bug: 在JDBC中获取日期之后通过一个静态的SimpleDateFormat对象把日期类型转换为字符串返回给用户。高并发情况下出现了这样一个问题:返回的日期是错误的,跟用户期待的日期不一致。 后来通过反复排查,最后发现是这个静态SimpleDateFormat对象造成的并发问题,他内部有一个Calendar对象,每次执行format方法的时候会调用calendar.setTime(date);,很明显当某个线程中在日期转换过程中被挂起的时候,恰好另一个线程也在执行转换日期的代码,他们调用同一个SimpleDateFormat对象中的同一个calendar.setTime(date);,结果肯定就变得混乱了。 上面的问题就是静态SimpleDateFormat对象被共享带来的结果,实际上也是原子性的问题,跟有序性和可见性并没有太大的关系。这就是所谓的业务逻辑相关,需要我们自己去分析。 解释了半天先行发生原则的作用和使用条件,下面该说说先行发生原则本身。 先行发生原则是指:如果说操作A先行发生于操作B,也就是发生在操作B之前,操作A产生的影响能被操作B观察到。 还是用《深入理解java虚拟机》中的例子来解释(真的是一本好书啊,一定要多看几遍): 12345678//以下操作在线程A中执行i = 1;//以下操作在线程B中执行j = i;//以下操作在线程C中执行i = 2; 假设线程A中的操作i=1先行发生于线程B的操作j=i,那么可以确定在线程B的操作执行之后,j一定等于1。因为:根据先行发生原则,i=1的结果可以被B观察到。 现在保持A先行发生于B,线程C出现在A与B之间,但是线程C与B没有先行发生关系。那么j会等于多少呢?答案至不确定。因为线程C对变量i的影响可能会被B观察到,也可能不会。因为两者之间没有先行发生关系。 其实说白了,先行发生原则就是操作A在时间上或者逻辑上比B先发生,那么B一定能看到A操作带来的影响(修改了共享变量的值等等),那么此时A就是先行发生于B。 你可能会说难道B还有可能不会看到A带来的影响吗?A操作先执行的呀!想一想我们上面提到的内存可见性和有序性… 如果还没有理解所谓的先行发生原则的话,可以看一下这篇文章。 下面介绍几个java内存模型中存在的先行发生关系: 程序次序规则:一个线程内,按照程序代码的顺序,书写在前面的操作先行发生于(逻辑上)书写在后面的操作。 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。后面指时间上的先后顺序。 volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。这里的后面指时间上的先后顺序。 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么,操作A也就先行发生于操作C。 以上只是一部分先行发生关系,其他的不再一一介绍。 那么,先行发生原则如何使用呢?我们看一个例子: 123456789private int value = 0;public void setValue(int value){ this.value = value;}public int getValue(){ return value;} 假设存在线程A和B,A先调用了setValue(1),然后线程B调用了同一个对象的getValue(),那么线程B收到的返回值是多少? 套用上面存在的先行发生关系,我们发现,虽然线程A在操作时间上先行发生于线程B,但是无法确定B中的getValue()方法的返回结果。也就是说,这里的操作是不安全的。此时我们可以通过synchronized关键字来解决。可以看看这个 通过上面的分析,我们了解了先行发生原则的作用:判断内存可见性与重排序是否造成并发问题。 Volatile关键字通过上面的学习,再来看volatile就变得简单很多了。之前我对这个关键字也是看的云里雾里,现在仍然有个小疑问,后面会提到。 volatile关键字想必都不陌生,有时候在同步中会看到他。那么volatile究竟有什么作用呢?其实他实现了两个功能:保证内存可见性和禁止重排序。基于上面的内容应该对这个关键字心里有了个大概。那么,如何使用它呢?看一个例子就会用了。同样来自《深入理解java虚拟机》: 1234567891011121314151617181920212223242526272829public class VolatileTest { public static volatile int race = 0; public static void increase(){ race++; } private static int THREAD_COUNT = 20; public static void main(String args[]) { Thread[] threads = new Thread[THREAD_COUNT]; for (int i = 0; i < THREAD_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for(int i = 0; i< 10000; i++) { increase(); } } }); threads[i].start(); } while (Thread.activeCount()>1) { Thread.yield(); } System.out.println(race); }} 1运行结果:130310 //每次运行结果并不相同 可以看到,虽然使用了volatile关键字,但是并没有达到我们预期的效果:race=200000。Execute me?你特么在逗我?原因就在于race++,我们上面也提到过,这种自增运算并不是原子性的,恰好,volatile也没有保证原子性。所以出现了不理想的结果。 这个时候应该会有人说:volatile不是实现了内存可见性吗?自增运算虽然不是原子性的,但20个线程在访问race的时候不应该看到的是最新的值嘛?赋值的时候不是对主内存中的race操作吗?跟原子性有毛关系?以前的我就是这么想的。 现在我们来了解一下volatile的内存可见性是怎么实现的。前面也说了:每个线程有其自己的工作内存,线程对变量的读写必须在工作内存中进行,而不能直接读写主存中的变量。 当遇到读volatile变量的时候,会立即把主存中的变量值同步到工作内存当中。 当遇到写volatile变量的时候,会立即把工作内存中的变量值同步到主存中。 关于volatile怎么实现内存可见性,是通过一个叫内存屏障的东西来实现的。具体的可以看下这个 所以说:所谓的实现内存可见性并不是直接操作主内存,还是通过工作内存来实现的。当某线程把race的值取到操作栈顶的时候,volatile关键字保证了race值在此时是正确的,与主内存同步的。但是在执行race++的后续指令的时候(race++不是原子性操作,通过多个指令完成),其他线程可能已经更新了race的值了,操作栈顶的race值变成了过期的数据,race++执行完毕后可能把较小的race值同步回主内存。 关于volatile关键字的禁止重排序,具体的可以看下这个 我们从先行发生原则的角度看一下volatile的禁止重排序: 其中:i是普通变量,x是被volatile修饰的变量。A、B操作在一个线程当中,C、D操作在另一个线程当中。B先于C执行。 根据前面的volatile先行发生关系,我们可以得出,B先行发生于C,又因为A先行发生于B(程序次序规则),所以A先行发生于C。那么A产生的影响一定会被C观察到,当B被执行的时候,会将当前工作内存中的变量都刷回到主内存当中,并通知其他线程同步主内存到自己的工作内存。这样便保证了A产生的影响一定会被C观察到。同时,A不能被重排序到B之后,因为这样的话,A产生是影响便不能被C观察到了,违背了先行发生原则。 即:普通读写不能与其后的所有写volatile变量重排序。同理,普通读写不能与之前的所有读volatile变量重排序。 下面看看volatile的使用场景: 1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。 2.变量不需要与其他状态的变量共同参与不变约束。 第一点就很好理解了,volatile不保证原子性嘛~~ 第二点我也有点疑惑,待研究… 关于volatile的使用场景,可以看看这篇文章:Java 理论与实践: 正确使用 Volatile 变量 最后终于把自己想要总结的写完了~~断断续续写了四个小时…好累… 加油!]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[理解notify notifyall sleep]]></title>
<url>%2F2016%2F08%2F31%2F%E7%90%86%E8%A7%A3notify%20notifyall%20sleep%2F</url>
<content type="text"><![CDATA[今天下班时和同事偶尔谈起了锁的一些问题,发现自己对这些基本一无所知。只是会使用synchronized关键字来进行简单的同步。之前也想把java并发包的源码看一下,但是最近一直没有时间,阅读源码计划也搁置了一段时间了,等重构完JDBC就先把java IO部分理一下吧。晚上回来翻了一下操作系统的书,总结一下java中基础的notify、notifyall、sleep。网上关于这三个关键字的文章有很多,理解一下,记下来。 从线程状态说起一般的操作系统中,线程的状态大概有这么几个: 执行:线程获得cpu,程序正在执行 就绪:线程已经准备好运行,只要获得cpu,便立即执行 阻塞:线程等待某些资源,暂时无法执行。这时该线程放弃cpu,引起线程调度 挂起:由于某种原因,线程被挂起,线程处于静止状态。若此时线程正在执行,挂起后暂停执行,让出cpu;若原本处于就绪状态,则暂不接受调度。 激活:把一个挂起的线程激活 着重理解一下几个关键状态的转换: 活动就绪–>静止就绪:当线程处于未被挂起的就绪状态时,称为活动就绪,此时线程接收调度。当该线程被挂起后,转变为静止就绪,暂不接受调度。 活动阻塞–>静止阻塞:当线程处于未被挂起的阻塞状态时,称为活动阻塞。当该线程被挂起后,转变为静止阻塞。当线程等待的事件出现后,从静止阻塞转变为静止就绪。 注意:挂起和阻塞均会使线程让出cpu,不同就在于阻塞一旦获得等待事件,就转变为就绪状态,接受调度;挂起需要激活操作后,转变为就绪状态,接收调度。 画个图理解一下: 线程创建和终止状态不再讨论。 waitwait、notify、notifyAll三个方法都是Object的方法。每个对象都拥有这三个方法。 当在线程中调用某个对象A的wait方法时,释放该对象的锁,同时让出cpu的使用权。前提是需拥有该对象A的锁,故wait方法需要在synchronized(A)的作用域中使用。 调用wait方法,相当于上图中执行->活动阻塞->静止阻塞的过程。同时,wait方法释放了对象的锁,然后阻塞,挂起,等待被唤醒。 notify当在线程Y中调用某个对象A的notify方法时,系统从众多等待被唤醒(挂起)的线程(调用了A的wait方法的线程)中选出一个线程X,将其唤醒(激活),这个过程相当于上图中静止阻塞->活动阻塞的过程。此时该线程X会重新开始对A对象锁的请求。跟wait一样,notify也需要在synchronized(A)的作用域中调用,当Y线程运行出synchronized(A)的作用域后,释放A对象的锁。 此时,线程X会得到A对象的锁(只唤醒了他一个嘛),这个过程相当于上图活动阻塞->活动就绪的过程。此时线程等待调度执行。 需要注意的是,notify不恰当使用很有可能造成死锁问题。 notifyAll当在线程Y中调用某个对象A的notifyAll方法时,系统将所有等待被唤醒(挂起)的线程(调用了A的wait方法的线程)唤醒(激活)。这些线程会开始竞争A对象的锁。(注意notify与notifyAll的区别就在于唤醒一个等待线程还是所有的等待线程。线程被唤醒后将参与对锁的竞争,未被唤醒的线程不参与锁的竞争。)之后就跟notify一样了,线程Y运行出synchronized(A)的作用域,释放A对象的锁。 sleep与以上三个关键字不同的是,sleep方法是Thread中的方法。 当调用某个线程Y的sleep(1000)方法时,线程Y挂起,放弃cpu的使用权。与wait不同的是,他不会释放对象的锁。故sleep方法可以在非synchronized作用域使用。调用sleep的目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会; 调用线程Y的sleep(1000)sleep方法,相当于上图中执行->静止就绪状态。当过1000毫秒之后,线程Y被激活,从静止就绪转为活动就绪状态。此时线程Y等待调度执行。 synchronized当程序执行遇到synchronized关键字的时候,若此时锁被占用,则线程被阻塞。相当于上图中执行->活动阻塞过程。 PS:上述的几个状态跟java中描述的Thread.state并不尽相同。而且底层真正的运行情况(OS的线程状态)是否是跟以上描述相同也有待考究。但是我理解的这几个关键字对外的表现应该与上述描述是一样的。 参考知乎上的一个提问:java sleep和wait的区别的疑惑? 使用场景Object.wait()方法需要在synchronized的作用域中使用。某线程中调用对象A的wait方法会释放该对象A的锁,线程让出cpu。什么时候会用到这个方法?获得对象的锁却又主动释放它? 在一些生产者消费者的问题中可能会用到wait和notify方法。比如使用一个buffer,当消费者获得buffer的锁,检测到buffer为空时使用wait方法,释放锁,等待生产者填满buffer后调用buffer的notify方法通知消费者去使用buffer。 例子自己写了一个,感觉挺low,网上很多优秀的例子,代码就不贴了。]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>
<tags>
<tag>java</tag>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[关于加密的一点总结]]></title>
<url>%2F2016%2F08%2F22%2F%E5%85%B3%E4%BA%8E%E5%8A%A0%E5%AF%86%E7%9A%84%E4%B8%80%E7%82%B9%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[前两天了解了一下数据库JDBC创建Connection的过程,其中验证用户密码的过程使用了加密算法,研究之。 信息摘要算法简介验证用户名密码的整个过程主要使用了MD5算法。严格来说,MD5不是一种加密算法,而是一种信息摘要算法。 所谓的信息摘要算法,其实就是一种特殊的散列算法。关于散列,之前在这篇总结中提到过。简单来说,就是把给出的”信息”经过哈希之后得到的值,就是这些内容的”摘要”。 特点信息摘要算法有这样几个特点: 1.生成固定长度的摘要。以MD5为例,无论信息有多大,经过MD5哈希之后,得到的是一个128bit的值。 2.算法不可逆。也就是说,不能通过摘要得到信息。可以参考一下知乎上的这个提问. 3.一般来说,信息不同,摘要也不同。虽然说冲突是一定存在的(无限多可能的信息映射到2的128次方这么大的集合当中),但是好的摘要算法,要求无法找到两条消息,使他们的摘要相同。 满足了以上几点的散列算法,就是一种消息摘要算法。那么消息摘要又有什么作用呢? 1.一致性检验。 我们在一些网站上下载软件的时候,这些网站会给出软件的MD5校验值。你下载完软件,把整个软件进行MD5散列,得到的值与其给出的校验值一致,则证明这个软件是正版的,未经别人修改的原装软件。这其实很好理解,理解了上面信息摘要算法的特点,就可以知道这种一致性检验的原理。MD5也可以用来检验压缩包的完整性等等。 下面是jdk-7u79对应的MD5校验值,从官方网站截图。 2.数字签名 其实数字签名就是加密后的摘要,保证了信息的完整性。后面会说到。 3.登陆验证 模拟一次数据库用户名密码验证过程(只是模拟,真实的验证过程稍复杂)。 1.首先客户端向数据库后台发起创建connection的请求,将要连接数据库名DB和用户名User发送到后台。 2.数据库后台查到确实存在数据库DB与用户User,返回success 3.客户端将密码经过MD5计算后的散列值发给数据库后台 4.数据库后台取出之前收到的User对应的密码(假设密码是明文),使用MD5进行散列 5.将4中得到的散列值与3中的散列值比较,一致则返回连接创建成功,否则创建连接失败 从上面的过程中可以看出,我们并没有在网络中传输密码的明文,而是经过散列之后的散列值。即使黑客截获了这个密码的散列值,他也无法逆向推算出我们的密码。 那么,设想一种情况,黑客截获了密码的散列值,是不是他可以向后台发送这个散列值来进行数据库的连接呢?好像是存在这种风险,所以,我们就需要一种 加盐 的手段来保证我们密码的安全。 所谓加盐,就是在密码的任意固定位置插入一段随机字符串,让散列后的结果和使用原始密码的散列结果不相符。 使用加盐手段后,上面的验证过程改为如下: 1.首先客户端向数据库后台发起创建connection的请求,将要连接数据库名DB和用户名User发送到后台。 2.数据库后台查到确实存在数据库DB与用户User,返回盐Salt 3.客户端将密码与2中得到的Salt拼接,将得到新的字符串经过MD5计算后的散列值发给数据库后台 4.数据库后台取出之前收到的User对应的密码(假设密码是明文),与2中发给客户端的Salt拼接得到新的字符串,使用MD5进行散列 5.将4中得到的散列值与3中的散列值比较,一致则返回连接创建成功,否则创建连接失败 注意,每次后台向客户端返回的Salt都是一个随机值,所以即使黑客截获了我们经过散列后的值,也是无法用在下一次登陆的。 java中的MD5123456789101112131415161718192021222324252627282930313233343536373839404142434445public static String stringMD5(String input) { try { // 拿到一个MD5转换器(如果想要SHA1参数换成”SHA1”) MessageDigest messageDigest =MessageDigest.getInstance("MD5"); // 输入的字符串转换成字节数组 byte[] inputByteArray = input.getBytes(); // inputByteArray是输入字符串转换得到的字节数组 messageDigest.update(inputByteArray); // 转换并返回结果,也是字节数组,包含16个元素 byte[] resultByteArray = messageDigest.digest(); // 字符数组转换成字符串返回 return byteArrayToHex(resultByteArray); } catch (NoSuchAlgorithmException e) { return null; } } public static String byteArrayToHex(byte[] byteArray) { // 首先初始化一个字符数组,用来存放每个16进制字符 char[] hexDigits = {'0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F' }; // new一个字符数组,这个就是用来组成结果字符串的(解释一下:一个byte是八位二进制,也就是2位十六进制字符(2的8次方等于16的2次方)) char[] resultCharArray =new char[byteArray.length * 2]; // 遍历字节数组,通过位运算(位运算效率高),转换成字符放到字符数组中去 int index = 0; int pos,tmp; for (byte b : byteArray) { //当时对这段代码不是很理解,使用计算器二进制、十进制、十六进制倒腾一下就好啦! //为便于理解,此处对原文进行修改 int tmp = b & 0xFF; pos = tmp >> 4;//得到低四位 resultCharArray[index++] = hexDigits[pos]; pos = tmp & 0xF;//得到高四位 resultCharArray[index++] = hexDigits[pos]; } // 字符数组组合成字符串返回 return new String(resultCharArray); } 代码参考自叉叉哥的BLOG 数字签名前面也说了,数字签名其实就是加密后的摘要,也用到了信息摘要算法。 首先了解一下两个概念:对称加密和非对称加密。 对称加密,就是信息加密解密使用相同的密钥。 非对称加密,有两种密钥,公钥和私钥。 公钥私钥满足以下几个特点: 1.公钥与私钥一一对应,一把公钥只对应一把私钥,反过来也成立。 2.顾名思义,公钥是可以公开的,而私钥是不公开的。 3.公钥与私钥,知道其中一个,并不能计算出另外一个。即公开的公钥不能威胁到私钥的秘密性质。 4.公钥可以解密私钥加密的内容,私钥也可以解密公钥加密的内容 5.公钥加密的内容,通过公钥无法解密,只能由私钥解密。私钥同理。 与对称密钥加密相比,公钥加密无需共享的通用密钥,解密的私钥不发往任何用户。即使公钥在网上被截获,如果没有与其匹配的私钥,也无法解密,所截获的公钥是没有任何用处的。 下面看一下数字签名使用过程: 1.数字签名是将摘要信息用发送者A的私钥加密,与原始信息一起传送给接收者B。 2.B使用A的公钥对A私钥加密后的摘要信息进行解密,如果解密成功,则说明接收到的内容确实由A发出。这就完成了发送者A的身份认证。 3.B对收到的原始信息使用信息摘要算法得到其哈希值,与2中解密的内容进行比较,如果一致,证明原始信息未经篡改。保证了信息完整性(一致性检验)。 上面的过程能够得出数字签名的作用:身份认证与完整性检验 使用数字签名可以用来声明版权,检查盗版等等。Android中也有数字签名的影子。 数字证书数字证书是用来解决公钥从哪里来的问题。 可能有以下两种方法: a)把公钥放到互联网的某个地方的一个下载地址,事先给“客户”去下载。 b)每次和“客户”开始通信时,“服务器”把公钥发给“客户”。 但是这个两个方法都有一定的问题, 对于a)方法,“客户”无法确定这个下载地址是不是“服务器”发布的,你凭什么就相信这个地址下载的东西就是“服务器”发布的而不是别人伪造的呢,万一下载到一个假的怎么办?另外要所有的“客户”都在通信前事先去下载公钥也很不现实。 对于b)方法,也有问题,因为任何人都可以自己生成一对公钥和私钥,他只要向“客户”发送他自己的私钥就可以冒充“服务器”了 所以,数字证书就出现了。数字证书可以保证数字证书里的公钥确实是这个证书的所有者(Subject)的,或者证书可以用来确认对方的身份。 关于数字证书,网上也有很多解释。由于我自己也没有十分了解数字证书,故不再赘述。 参考网上看到一篇文章,对数字签名和数字证书进行了十分生动的介绍。 以下内容来自无恙-数字证书原理 http://www.cnblogs.com/JeffreySun/archive/2010/06/24/1627247.html &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& 2、一个加密通信过程的演化 我们来看一个例子,现在假设“服务器”和“客户”要在网络上通信,并且他们打算使用RSA(参看前面的RSA简介)来对通信进行加密以保证谈话内容的安全。由于是使用RSA这种公钥密码体制,“服务器”需要对外发布公钥(算法不需要公布,RSA的算法大家都知道),自己留着私钥。“客户”通过某些途径拿到了“服务器”发布的公钥,客户并不知道私钥。“客户”具体是通过什么途径获取公钥的,我们后面再来说明,下面看一下双方如何进行保密的通信: 2.1 第一回合: “客户”->“服务器”:你好 “服务器”->“客户”:你好,我是服务器 “客户”->“服务器”:???? 因为消息是在网络上传输的,有人可以冒充自己是“服务器”来向客户发送信息。例如上面的消息可以被黑客截获如下: “客户”->“服务器”:你好 “服务器”->“客户”:你好,我是服务器 “客户”->“黑客”:你好 // 黑客在“客户”和“服务器”之间的某个路由器上截获“客户”发给服务器的信息,然后自己冒充“服务器” “黑客”->“客户”:你好,我是服务器 因此“客户”在接到消息后,并不能肯定这个消息就是由“服务器”发出的,某些“黑客”也可以冒充“服务器”发出这个消息。如何确定信息是由“服务器”发过来的呢?有一个解决方法,因为只有服务器有私钥,所以如果只要能够确认对方有私钥,那么对方就是“服务器”。因此通信过程可以改进为如下: 2.2 第二回合: “客户”->“服务器”:你好 “服务器”->“客户”:你好,我是服务器 “客户”->“服务器”:向我证明你就是服务器 “服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA] // 注意这里约定一下,{} 表示RSA加密后的内容,[ | ]表示用什么密钥和算法进行加密,后面的示例中都用这种表示方式,例如上面的 {你好,我是服务器}[私钥|RSA] 就表示用私钥对“你好,我是服务器”进行加密后的结果。 为了向“客户”证明自己是“服务器”, “服务器”把一个字符串用自己的私钥加密,把明文和加密后的密文一起发给“客户”。对于这里的例子来说,就是把字符串 “你好,我是服务器”和这个字符串用私钥加密后的内容 {你好,我是服务器}[私钥|RSA] 发给客户。 “客户”收到信息后,她用自己持有的公钥解密密文,和明文进行对比,如果一致,说明信息的确是由服务器发过来的。也就是说“客户”把 {你好,我是服务器}[私钥|RSA] 这个内容用公钥进行解密,然后和“你好,我是服务器”对比。因为由“服务器”用私钥加密后的内容,由并且只能由公钥进行解密,私钥只有“服务器”持有,所以如果解密出来的内容是能够对得上的,那说明信息一定是从“服务器”发过来的。 假设“黑客”想冒充“服务器”: “黑客”->“客户”:你好,我是服务器 “客户”->“黑客”:向我证明你就是服务器 “黑客”->“客户”:你好,我是服务器 {你好,我是服务器}[???|RSA] //这里黑客无法冒充,因为他不知道私钥,无法用私钥加密某个字符串后发送给客户去验证。 “客户”->“黑客”:???? 由于“黑客”没有“服务器”的私钥,因此它发送过去的内容,“客户”是无法通过服务器的公钥解密的,因此可以认定对方是个冒牌货! 到这里为止,“客户”就可以确认“服务器”的身份了,可以放心和“服务器”进行通信,但是这里有一个问题,通信的内容在网络上还是无法保密。为什么无法保密呢?通信过程不是可以用公钥、私钥加密吗?其实用RSA的私钥和公钥是不行的,我们来具体分析下过程,看下面的演示: 2.3 第三回合: “客户”->“服务器”:你好 “服务器”->“客户”:你好,我是服务器 “客户”->“服务器”:向我证明你就是服务器 “服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA] “客户”->“服务器”:{我的帐号是aaa,密码是123,把我的余额的信息发给我看看}[公钥|RSA] “服务器”->“客户”:{你的余额是100元}[私钥|RSA] 注意上面的的信息 {你的余额是100元}[私钥],这个是“服务器”用私钥加密后的内容,但是我们之前说了,公钥是发布出去的,因此所有的人都知道公钥,所以除了“客户”,其它的人也可以用公钥对{你的余额是100元}[私钥]进行解密。所以如果“服务器”用私钥加密发给“客户”,这个信息是无法保密的,因为只要有公钥就可以解密这内容。然而“服务器”也不能用公钥对发送的内容进行加密,因为“客户”没有私钥,发送个“客户”也解密不了。 这样问题就又来了,那又如何解决呢?在实际的应用过程,一般是通过引入对称加密来解决这个问题,看下面的演示: 2.4 第四回合: “客户”->“服务器”:你好 “服务器”->“客户”:你好,我是服务器 “客户”->“服务器”:向我证明你就是服务器 “服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA] “客户”->“服务器”:{我们后面的通信过程,用对称加密来进行,这里是对称加密算法和密钥}[公钥|RSA] //蓝色字体的部分是对称加密的算法和密钥的具体内容,客户把它们发送给服务器。 “服务器”->“客户”:{OK,收到!}[密钥|对称加密算法] “客户”->“服务器”:{我的帐号是aaa,密码是123,把我的余额的信息发给我看看}[密钥|对称加密算法] “服务器”->“客户”:{你的余额是100元}[密钥|对称加密算法] 在上面的通信过程中,“客户”在确认了“服务器”的身份后,“客户”自己选择一个对称加密算法和一个密钥,把这个对称加密算法和密钥一起用公钥加密后发送给“服务器”。注意,由于对称加密算法和密钥是用公钥加密的,就算这个加密后的内容被“黑客”截获了,由于没有私钥,“黑客”也无从知道对称加密算法和密钥的内容。 由于是用公钥加密的,只有私钥能够解密,这样就可以保证只有服务器可以知道对称加密算法和密钥,而其它人不可能知道(这个对称加密算法和密钥是“客户”自己选择的,所以“客户”自己当然知道如何解密加密)。这样“服务器”和“客户”就可以用对称加密算法和密钥来加密通信的内容了。 总结一下,RSA加密算法在这个通信过程中所起到的作用主要有两个: 因为私钥只有“服务器”拥有,因此“客户”可以通过判断对方是否有私钥来判断对方是否是“服务器”。客户端通过RSA的掩护,安全的和服务器商量好一个对称加密算法和密钥来保证后面通信过程内容的安全。如果这里您理解了为什么不用RSA去加密通信过程,而是要再确定一个对称加密算法来保证通信过程的安全,那么就说明前面的内容您已经理解了。(如果不清楚,再看下2.3和2.4,如果还是不清楚,那应该是我们说清楚,您可以留言提问。) 到这里,“客户”就可以确认“服务器”的身份,并且双方的通信内容可以进行加密,其他人就算截获了通信内容,也无法解密。的确,好像通信的过程是比较安全了。 但是这里还留有一个问题,在最开始我们就说过,“服务器”要对外发布公钥,那“服务器”如何把公钥发送给“客户”呢?我们第一反应可能会想到以下的两个方法: a)把公钥放到互联网的某个地方的一个下载地址,事先给“客户”去下载。 b)每次和“客户”开始通信时,“服务器”把公钥发给“客户”。 但是这个两个方法都有一定的问题, 对于a)方法,“客户”无法确定这个下载地址是不是“服务器”发布的,你凭什么就相信这个地址下载的东西就是“服务器”发布的而不是别人伪造的呢,万一下载到一个假的怎么办?另外要所有的“客户”都在通信前事先去下载公钥也很不现实。 对于b)方法,也有问题,因为任何人都可以自己生成一对公钥和私钥,他只要向“客户”发送他自己的私钥就可以冒充“服务器”了。示意如下: “客户”->“黑客”:你好 //黑客截获“客户”发给“服务器”的消息 “黑客”->“客户”:你好,我是服务器,这个是我的公钥 //黑客自己生成一对公钥和私钥,把公钥发给“客户”,自己保留私钥 “客户”->“黑客”:向我证明你就是服务器 “黑客”->“客户”:你好,我是服务器 {你好,我是服务器}[黑客自己的私钥|RSA] //客户收到“黑客”用私钥加密的信息后,是可以用“黑客”发给自己的公钥解密的,从而会误认为“黑客”是“服务器” 因此“黑客”只需要自己生成一对公钥和私钥,然后把公钥发送给“客户”,自己保留私钥,这样由于“客户”可以用黑客的公钥解密黑客的私钥加密的内容,“客户”就会相信“黑客”是“服务器”,从而导致了安全问题。这里问题的根源就在于,大家都可以生成公钥、私钥对,无法确认公钥对到底是谁的。 如果能够确定公钥到底是谁的,就不会有这个问题了。例如,如果收到“黑客”冒充“服务器”发过来的公钥,经过某种检查,如果能够发现这个公钥不是“服务器”的就好了。 为了解决这个问题,数字证书出现了,它可以解决我们上面的问题。先大概看下什么是数字证书,一个证书包含下面的具体内容: 证书的发布机构证书的有效期公钥证书所有者(Subject)签名所使用的算法指纹以及指纹算法证书的内容的详细解释会在后面详细解释,这里先只需要搞清楚一点,数字证书可以保证数字证书里的公钥确实是这个证书的所有者(Subject)的,或者证书可以用来确认对方的身份。也就是说,我们拿到一个数字证书,我们可以判断出这个数字证书到底是谁的。至于是如何判断的,后面会在详细讨论数字证书时详细解释。现在把前面的通信过程使用数字证书修改为如下: 2.5 第五回合: “客户”->“服务器”:你好 “服务器”->“客户”:你好,我是服务器,这里是我的数字证书 //这里用证书代替了公钥 “客户”->“服务器”:向我证明你就是服务器 “服务器”->“客户”:你好,我是服务器 {你好,我是服务器}[私钥|RSA] 注意,上面第二次通信,“服务器”把自己的证书发给了“客户”,而不是发送公钥。“客户”可以根据证书校验这个证书到底是不是“服务器”的,也就是能校验这个证书的所有者是不是“服务器”,从而确认这个证书中的公钥的确是“服务器”的。后面的过程和以前是一样,“客户”让“服务器”证明自己的身份,“服务器”用私钥加密一段内容连同明文一起发给“客户”,“客户”把加密内容用数字证书中的公钥解密后和明文对比,如果一致,那么对方就确实是“服务器”,然后双方协商一个对称加密来保证通信过程的安全。到这里,整个过程就完整了,我们回顾一下: 2.6 完整过程: step1: “客户”向服务端发送一个通信请求 “客户”->“服务器”:你好 step2: “服务器”向客户发送自己的数字证书。证书中有一个公钥用来加密信息,私钥由“服务器”持有 “服务器”->“客户”:你好,我是服务器,这里是我的数字证书 step3: “客户”收到“服务器”的证书后,它会去验证这个数字证书到底是不是“服务器”的,数字证书有没有什么问题,数字证书如果检查没有问题,就说明数字证书中的公钥确实是“服务器”的。检查数字证书后,“客户”会发送一个随机的字符串给“服务器”用私钥去加密,服务器把加密的结果返回给“客户”,“客户”用公钥解密这个返回结果,如果解密结果与之前生成的随机字符串一致,那说明对方确实是私钥的持有者,或者说对方确实是“服务器”。 “客户”->“服务器”:向我证明你就是服务器,这是一个随机字符串 //前面的例子中为了方便解释,用的是“你好”等内容,实际情况下一般是随机生成的一个字符串。 “服务器”->“客户”:{一个随机字符串}[私钥|RSA] step4: 验证“服务器”的身份后,“客户”生成一个对称加密算法和密钥,用于后面的通信的加密和解密。这个对称加密算法和密钥,“客户”会用公钥加密后发送给“服务器”,别人截获了也没用,因为只有“服务器”手中有可以解密的私钥。这样,后面“服务器”和“客户”就都可以用对称加密算法来加密和解密通信内容了。 “服务器”->“客户”:{OK,已经收到你发来的对称加密算法和密钥!有什么可以帮到你的?}[密钥|对称加密算法] “客户”->“服务器”:{我的帐号是aaa,密码是123,把我的余额的信息发给我看看}[密钥|对称加密算法] “服务器”->“客户”:{你好,你的余额是100元}[密钥|对称加密算法] …… //继续其它的通信 &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&& 另外,也可以看看这篇博客:阮一峰-数字签名是什么?]]></content>
<categories>
<category>技术</category>
<category>加密</category>
</categories>
<tags>
<tag>加密</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java集合框架学习总结]]></title>
<url>%2F2016%2F08%2F16%2FJava%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[看Jdk的源码有大概一个月时间了,中间零零散散算是把Java的集合看了个大概。源码还有很多地方不甚明白,但对java集合框架总体上有了个认识。总结一下,以后有时间再把源码理一遍。下一步的Jdk源码阅读计划是:Java IO 框架 上图并没有把所有的接口和类都列出来,只是把我认为最常用和最核心的几个类和接口的继承关系表示出来。 通过上图可以看出,java集合框架主要分为两棵树,一棵继承自Collection,一棵继承自Map。接下来分四个部分总结一下。 ListIterator在总结List之前,先看一下Iterable这个接口,它只包含一个方法: 1Iterator<T> iterator(); 这个方法返回一个Iterator,也就是一个迭代器,通过这个迭代器,我们可以在不了解集合内部实现的情况下遍历他,这也是设计模式中很重要的一个模式:迭代器模式(关于设计模式,待学习透彻后,会再写一篇博客总结).关于迭代器模式的好处就不再多说,顺便提一下,我们经常用到的foreach循环,内部也是通过迭代器实现的。 CollectionCollection 提供了集合的一些基本操作,Collection 接口提供的主要方法: 1234567891011boolean add(Object o) 添加对象到集合;boolean remove(Object o) 删除指定的对象;int size() 返回当前集合中元素的数量;boolean contains(Object o) 查找集合中是否有指定的对象;boolean isEmpty() 判断集合是否为空;Iterator iterator() 返回一个迭代器;boolean containsAll(Collection c) 查找集合中是否有集合 C 中的元素;boolean addAll(Collection c) 将集合 C 中所有的元素添加给该集合;void clear() 删除集合中所有元素;void removeAll(Collection c) 从集合中删除 C 集合中也有的元素;void retainAll(Collection c) 从集合中删除集合 C 中不包含的元素。 没发现有什么好说的。 ListList]]></content>
<categories>
<category>技术</category>
<category>编程</category>
</categories>