开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

springboot整合jsp,完成公交车站路线图

发表于 2021-01-03

点赞再看,养成习惯

开发环境:

  1. jdk 8
  2. intellij idea
  3. tomcat 8
  4. mysql 5.7
  5. maven 3.6

所用技术:

  1. springboot
  2. jsp
  3. 数据静态初始化

项目介绍

使用springboot整合jsp,在后端写入公交路线名称和详细站点,前端页面可条件查询具体的内容,如公交路线,公交名称,车俩信息等。

运行效果

前台用户端:

  • 路线选择

  • 路线详情

数据准备:

  • BusData.txt

准备工作:

  1. pom.xml加入jsp模板引擎支持:
1
2
3
4
5
diff复制代码<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
  1. springboot配置jsp
1
2
diff复制代码spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

重要代码:

  1. bus数据初始化
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
diff复制代码@PostConstruct
private void initBusData(){
try{
File file = new File(BusMap.getClass().getResource("/").getPath());
FileReader fileReader = new FileReader(file.getPath()+"/static/BusData.txt","GBK"); //初始化BusData.txt 数据
List<String> readLines = fileReader.readLines();
for(String str:readLines){
if(!"".equals(str)){
String[] data=str.split("#");
String way=data[0]; //几路线
String location=data[1];/ /地名
String[] locations=location.split(",");
List<Bus> list=new ArrayList<>();
for(int i=0;i<locations.length;i++){
int busnum=0;
if(i%4==0){ //随机busnum
busnum=1;
}if(i%5==0){
busnum=2;
}
Bus bus=new Bus(locations[i],busnum);
list.add(bus);
}
WayList.add(way); //添加路线
BusMap.put(way,list); //添加车站
}
}
}catch (Exception e){
e.printStackTrace();
}
}
  1. 路线查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
diff复制代码@RequestMapping("/way")
public String search(HttpServletRequest request,String way) {
try {
if(null==way||"".equalsIgnoreCase(way)){
request.setAttribute("list", BusMap.WayList); //没有搜索默认显示所有路线
return "way";
}else{
List<String> wayList=new ArrayList<>();
//模糊查询路线
for(String str:BusMap.WayList){
if(str.indexOf(way)>-1){
wayList.add(str);
}
}
if(wayList.size()>0){
request.setAttribute("list", wayList); //模糊搜索出来的路线列表
return "way";
}else{
return "noView"; //没有所选路线
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "way";
}
  1. 公交车路线站展示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
diff复制代码@RequestMapping("/view")
public String view(HttpServletRequest request,String way) {
try {
List<Bus> list= BusMap.getBusMap(way);
if(list.size()>0){
request.setAttribute("list",list ); //获取总路线
request.setAttribute("firstBus", list.get(0).getLocation()); //第一站
request.setAttribute("lastBus", list.get(list.size()-1).getLocation()); //最后一站
int size = list.size();
size =(size-1)*99;
request.setAttribute("size",size);
return "view";
}
} catch (Exception e) {
e.printStackTrace();
}
return "noView";//没有对应公交车站
}
//前端页面数据渲染
<div class="pageContent" style="background: #eeeeee;">
<div class="pageFormContent" layoutH="55">
<div class="timeText">${firstBus}<----->${lastBus}
<span>( 首/末班车时间:<span style="color: red">6:00 / 23:00</span>)</span>
</div>
<div class="timezone" style="margin-top: 20px">
<c:forEach var="list" items="${list}" varStatus="s">
<div class="time" <c:if test="${s.index!=0}"> style="top: ${s.index*100+25}px;" a="1" </c:if> ><a onclick="javascript:alert(1);">${s.index+1}</a>
<h2>${list.location}</h2>
<c:if test="${list.busNum>0}">
<span class="timezone3"></span>
<div>
<p><span style="padding-left: 30px;">${list.busNum}辆公交</span></p>
</div>
</c:if>
</div>
</c:forEach>
</div>
</div>
<div class="formBar"></div>
</div>

项目总结

  1. 项目存放路径最好不要带中文路径,否则可能存在静态busData资源初始化失败
  2. 页面时间车站路线所采用时间轴方式展示,长度动态计算,部分浏览器显示可能有点错位
  3. 其他后续迭代功能后续开发,敬请关注

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

每日一面 - java中,描述一下什么情况下,对象会从年轻代

发表于 2021-01-03

本问题参考自: www.zhihu.com/question/43… 解答为个人原创

Key Takeaway

  • Java 默认启用了分代 GC
  • 启用分代 GC 的,在发生 Young GC,更准确地说是在 Survivor 区复制的时候,存活的对象的分代年龄会加1。
  • 当分代年龄 = -XX:MaxTenuringThreshold 指定的大小时,对象进入老年代
  • 还有动态晋升到老年代的机制,首先根据 -XX:TargetSurvivorRatio (默认 50,也就是 50%) 指定的比例,乘以 survivor 一个区的大小,得出目标晋升空间大小。然后将分代对象大小,按照分代年龄从小到大相加,直到大于目标晋升空间大小。之后,将得出的这个分代年龄以上的对象全部晋升。
  • 对于一些的 GC 算法,还可能直接在老年代上面分配,例如 G1 GC 中的 humongous allocations(大对象分配),就是对象在超过 Region 一半大小的时候,直接在老年代的连续空间分配。

对象分配

我们一般认为 Java 中 new 的对象都是在堆上分配,这个说法不够准确,应该是大部分对象在堆上的 TLAB分配,还有一部分在 栈上分配 或者是 堆上直接分配,可能 Eden 区也可能年老代。同时,对于一些的 GC 算法,还可能直接在老年代上面分配,例如 G1 GC 中的 humongous allocations(大对象分配),就是对象在超过 Region 一半大小的时候,直接在老年代的连续空间分配。

这里,我们先只关心 TLAB 分配。 对于单线程应用,每次分配内存,会记录上次分配对象内存地址末尾的指针,之后分配对象会从这个指针开始检索分配。这个机制叫做 bump-the-pointer (撞针)。 对于多线程应用来说,内存分配需要考虑线程安全。最直接的想法就是通过全局锁,但是这个性能会很差。为了优化这个性能,我们考虑可以每个线程分配一个线程本地私有的内存池,然后采用 bump-the-pointer 机制进行内存分配。这个线程本地私有的内存池,就是 TLAB。只有 TLAB 满了,再去申请内存的时候,需要扩充 TLAB 或者使用新的 TLAB,这时候才需要锁。这样大大减少了锁使用。

image

更详细的 TLAB 理解,请参考: 通过 JFR 与日志深入探索 JVM - TLAB 原理详解

分代年龄

分代年龄位于对象头中,用于分代 GC.记录分代年龄一共 4 bit,所以最大为 2^4 - 1 = 15。所以配置最大分代年龄-XX:MaxTenuringThreshold=n这个n不能大于16,当然也不能小于 0.等于 0 的话,就直接入老年代。等于 16 的话(但是不能设置为 16 哟),就是从不进入老年代。默认是 15。

在发生 Young GC,更准确地说是在 Survivor 区复制的时候,存活的对象的分代年龄会加1。我们编写程序测试下,由于 编译器会优化代码,同时调用System.gc()并不是立刻触发 GC,并且是 Full GC,可能会使对象直接进入老年代,分代年龄不再增长,所以我们可以使用 volatile 属性辅助我们真正创建对象,避免编译器优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini复制代码static volatile Object consumer;
public static void main(String[] args) throws Exception {
//这是我们要观察的对象
Object instance = new Object();
long lastAddr = VM.current().addressOf(instance);
for (int i = 0; i < 10000; i++) {
//查看地址是否发生了变化,代表是否发生了 Survivor 复制,或者是移动到老年代
long currentAddr = VM.current().addressOf(instance);
if (currentAddr != lastAddr) {
//地址发生变化的时候,打印对象结构
ClassLayout layout = ClassLayout.parseInstance(instance);
System.out.println(layout.toPrintable());
lastAddr = currentAddr;
}
for (int j = 0; j < 10000; j++) {
//一直创建新对象
//因为是volatile的属性更新,不会被编译器优化
consumer = new Object();
}
}
}

可以配合 GC 日志一起观察,关于 JVM 日志配置可以参考这篇文章:OpenJDK 11 JVM日志相关参数解析与使用

首先我们用这个参数运行程序-Xmx128m -Xlog:gc=info,输出:

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
ini复制代码[0.016s][info][gc] Using G1
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
[2.540s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 24M->1M(128M) 2.600ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0d 00 00 00 (00001101 00000000 00000000 00000000) (13)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[2.627s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.273ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 15 00 00 00 (00010101 00000000 00000000 00000000) (21)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[2.675s][info][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.063ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 1d 00 00 00 (00011101 00000000 00000000 00000000) (29)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[2.724s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.068ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 25 00 00 00 (00100101 00000000 00000000 00000000) (37)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[2.772s][info][gc] GC(4) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.212ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 2d 00 00 00 (00101101 00000000 00000000 00000000) (45)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[2.821s][info][gc] GC(5) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.202ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 35 00 00 00 (00110101 00000000 00000000 00000000) (53)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[2.869s][info][gc] GC(6) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.143ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 3d 00 00 00 (00111101 00000000 00000000 00000000) (61)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[2.917s][info][gc] GC(7) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.313ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 45 00 00 00 (01000101 00000000 00000000 00000000) (69)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[2.969s][info][gc] GC(8) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.473ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 4d 00 00 00 (01001101 00000000 00000000 00000000) (77)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[3.021s][info][gc] GC(9) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.283ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 55 00 00 00 (01010101 00000000 00000000 00000000) (85)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[3.072s][info][gc] GC(10) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.648ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 5d 00 00 00 (01011101 00000000 00000000 00000000) (93)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[3.122s][info][gc] GC(11) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.585ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 65 00 00 00 (01100101 00000000 00000000 00000000) (101)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[3.173s][info][gc] GC(12) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.130ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 6d 00 00 00 (01101101 00000000 00000000 00000000) (109)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[3.224s][info][gc] GC(13) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.078ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 75 00 00 00 (01110101 00000000 00000000 00000000) (117)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[3.273s][info][gc] GC(14) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.135ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 7d 00 00 00 (01111101 00000000 00000000 00000000) (125)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[3.322s][info][gc] GC(15) Pause Young (Normal) (G1 Evacuation Pause) 75M->1M(128M) 2.467ms
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 7d 00 00 00 (01111101 00000000 00000000 00000000) (125)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 02 00 20 (00000000 00000010 00000000 00100000) (536871424)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

[3.404s][info][gc] GC(16) Pause Young (Normal) (G1 Evacuation Pause) 76M->1M(128M) 0.556ms
[3.485s][info][gc] GC(17) Pause Young (Normal) (G1 Evacuation Pause) 76M->1M(128M) 0.303ms
[3.566s][info][gc] GC(18) Pause Young (Normal) (G1 Evacuation Pause) 76M->1M(128M) 0.288ms
[3.647s][info][gc] GC(19) Pause Young (Normal) (G1 Evacuation Pause) 76M->1M(128M) 0.317ms
[3.727s][info][gc] GC(20) Pause Young (Normal) (G1 Evacuation Pause) 76M->1M(128M) 0.286ms

可以看到,在第 15 次 GC 的时候,对象进入了老年代,内存地址不再随着 Young GC 的进行而变化。 更对对象头的详细信息,请参考:Java GC详解 - 1. 最全面的理解Java对象结构 - 对象指针 OOPs

动态晋升

动态晋升首先根据 TargetSurvivorRatio 指定的比例,乘以 survivor 一个区的大小,得出目标晋升空间大小。然后将分代对象大小,按照分代年龄从小到大相加,直到大于目标晋升空间大小。之后,将得出的这个分代年龄以上的对象全部晋升。

动态修改 Tenuring Threshold,也就是晋升的分代年龄,源代码对应:src/hotspot/share/gc/serial/defNewGeneration.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码void DefNewGeneration::adjust_desired_tenuring_threshold() {
// 获取 survivor 区大小,有两个 survivor 区,获取当前拷贝目标的那一个的大小
size_t const survivor_capacity = to()->capacity() / HeapWordSize;
// 计算 desired_survivor_size,通过 TargetSurvivorRatio
size_t const desired_survivor_size = (size_t)((((double)survivor_capacity) * TargetSurvivorRatio) / 100);

// 计算目标 Tenuring Threshold
_tenuring_threshold = age_table()->compute_tenuring_threshold(desired_survivor_size);

//后续都是数据采集统计以及日志,不用看
if (UsePerfData) {
GCPolicyCounters* gc_counters = GenCollectedHeap::heap()->counters();
gc_counters->tenuring_threshold()->set_value(_tenuring_threshold);
gc_counters->desired_survivor_size()->set_value(desired_survivor_size * oopSize);
}

age_table()->print_age_table(_tenuring_threshold);
}

计算目标 Tenuring Threshold 对应源码:src/hotspot/share/gc/shared/ageTable.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码uint AgeTable::compute_tenuring_threshold(size_t desired_survivor_size) {
uint result;
//如果是永远直接晋升或者从不晋升,则直接返回结果,不计算
if (AlwaysTenure || NeverTenure) {
assert(MaxTenuringThreshold == 0 || MaxTenuringThreshold == markWord::max_age + 1,
"MaxTenuringThreshold should be 0 or markWord::max_age + 1, but is " UINTX_FORMAT, MaxTenuringThreshold);
result = MaxTenuringThreshold;
} else {
size_t total = 0;
uint age = 1;
assert(sizes[0] == 0, "no objects with age zero should be recorded");
//每个分代对象从分代年龄小加到大,直到大于 desired_survivor_size
while (age < table_size) {
total += sizes[age];
// check if including objects of age 'age' made us pass the desired
// size, if so 'age' is the new threshold
if (total > desired_survivor_size) break;
age++;
}
// 获取当前大于 desired_survivor_size 的分代年龄,将这个分代年龄以上的对象全部晋升到老年代
result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
}

每日一刷,轻松提升技术,斩获各种offer:

image

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

程序员必备基础:如何安全传输存储用户密码?

发表于 2021-01-03

前言

我们开发网站或者APP的时候,首先要解决的问题,就是「如何安全传输和存储用户的密码」。一些大公司的用户数据库泄露事件也时有发生,带来非常大的负面影响。因此,如何安全传输存储用户密码,是每位程序员必备的基础。本文将跟大家一起学习,如何安全传输存储用户的密码。


公众号:「捡田螺的小男孩」(一起讨论密码传输存储问题)

1. 如何安全地传输用户的密码

要拒绝用户密码在网络上裸奔,我们很容易就想到使用https协议,那先来回顾下https相关知识吧~

1.1 https 协议

  • 「http的三大风险」

为什么要使用https协议呢?「http它不香」吗? 因为http是明文信息传输的。如果在茫茫的网络海洋,使用http协议,有以下三大风险:

❝

  • 窃听/嗅探风险:第三方可以截获通信数据。
  • 数据篡改风险:第三方获取到通信数据后,会进行恶意修改。
  • 身份伪造风险:第三方可以冒充他人身份参与通信。

❞

如果传输不重要的信息还好,但是传输用户密码这些敏感信息,那可不得了。所以一般都要使用「https协议」传输用户密码信息。

  • 「https 原理」

https原理是什么呢?为什么它能解决http的三大风险呢?

❝
https = http + SSL/TLS, SSL/TLS 是传输层加密协议,它提供内容加密、身份认证、数据完整性校验,以解决数据传输的安全性问题。

❞

为了加深https原理的理解,我们一起复习一下「一次完整https的请求流程」吧~

❝

    1. 客户端发起https请求
    1. 服务器必须要有一套数字证书,可以自己制作,也可以向权威机构申请。这套证书其实就是一对公私钥。
    1. 服务器将自己的数字证书(含有公钥、证书的颁发机构等)发送给客户端。
    1. 客户端收到服务器端的数字证书之后,会对其进行验证,主要验证公钥是否有效,比如颁发机构,过期时间等等。如果不通过,则弹出警告框。如果证书没问题,则生成一个密钥(对称加密算法的密钥,其实是一个随机值),并且用证书的公钥对这个随机值加密。
    1. 客户端会发起https中的第二个请求,将加密之后的客户端密钥(随机值)发送给服务器。
    1. 服务器接收到客户端发来的密钥之后,会用自己的私钥对其进行非对称解密,解密之后得到客户端密钥,然后用客户端密钥对返回数据进行对称加密,这样数据就变成了密文。
    1. 服务器将加密后的密文返回给客户端。
    1. 客户端收到服务器发返回的密文,用自己的密钥(客户端密钥)对其进行对称解密,得到服务器返回的数据。

❞

  • 「https一定安全吗?」

https的数据传输过程,数据都是密文的,那么,使用了https协议传输密码信息,一定是安全的吗?其实「不然」~

❝

  • 比如,https 完全就是建立在证书可信的基础上的呢。但是如果遇到中间人伪造证书,一旦客户端通过验证,安全性顿时就没了哦!平时各种钓鱼不可描述的网站,很可能就是黑客在诱导用户安装它们的伪造证书!
  • 通过伪造证书,https也是可能被抓包的哦。

❞

1.2 对称加密算法

既然使用了https协议传输用户密码,还是「不一定安全」,那么,我们就给用户密码「加密再传输」呗~

加密算法有「对称加密」和「非对称加密」两大类。用哪种类型的加密算法「靠谱」呢?

❝
对称加密:加密和解密使用「相同密钥」的加密算法。

❞

常用的对称加密算法主要有以下几种哈:

如果使用对称加密算法,需要考虑「密钥如何给到对方」,如果密钥还是网络传输给对方,传输过程,被中间人拿到的话,也是有风险的哦。

1.3 非对称加密算法

再考虑一下非对称加密算法呢?

❝
「非对称加密:」 非对称加密算法需要两个密钥(公开密钥和私有密钥)。公钥与私钥是成对存在的,如果用公钥对数据进行加密,只有对应的私钥才能解密。

❞


常用的非对称加密算法主要有以下几种哈:

❝
如果使用非对称加密算法,也需要考虑「密钥公钥如何给到对方」,如果公钥还是网络传输给对方,传输过程,被中间人拿到的话,会有什么问题呢?「他们是不是可以伪造公钥,把伪造的公钥给客户端,然后,用自己的私钥等公钥加密的数据过来?」 大家可以思考下这个问题哈~

❞

我们直接「登录一下百度」,抓下接口请求,验证一发大厂是怎么加密的。可以发现有获取公钥接口,如下:


再看下登录接口,发现就是RSA算法,RSA就是「非对称加密算法」。其实百度前端是用了JavaScript库「jsencrypt」,在github的star还挺多的。


因此,我们可以用「https + 非对称加密算法(如RSA)」 传输用户密码~

2. 如何安全地存储你的密码?

假设密码已经安全到达服务端啦,那么,如何存储用户的密码呢?一定不能明文存储密码到数据库哦!可以用「哈希摘要算法加密密码」,再保存到数据库。

❝
哈希摘要算法: 只能从明文生成一个对应的哈希值,不能反过来根据哈希值得到对应的明文。

❞

2.1 MD5摘要算法保护你的密码

MD5 是一种非常经典的哈希摘要算法,被广泛应用于数据完整性校验、数据(消息)摘要、数据加密等。但是仅仅使用 MD5 对密码进行摘要,并不安全。我们看个例子,如下:

1
2
3
4
5
6
复制代码public class MD5Test {
    public static void main(String[] args) {
        String password = "abc123456";
        System.out.println(DigestUtils.md5Hex(password));
    }
}

运行结果:

1
复制代码0659c7992e268962384eb17fafe88364

在MD5免费破解网站一输入,马上就可以看到原密码了。。。


试想一下,如果黑客构建一个超大的数据库,把所有20位数字以内的数字和字母组合的密码全部计算MD5哈希值出来,并且把密码和它们对应的哈希值存到里面去(这就是「彩虹表」)。在破解密码的时候,只需要查一下这个彩虹表就完事了。所以「单单MD5对密码取哈希值存储」,已经不安全啦~

2.2 MD5+盐摘要算法保护用户的密码

那么,为什么不试一下MD5+盐呢?什么是「加盐」?

❝
在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。

❞

用户密码+盐之后,进行哈希散列,再保存到数据库。这样可以有效应对彩虹表破解法。但是呢,使用加盐,需要注意一下几点:

❝

  • 不能在代码中写死盐,且盐需要有一定的长度(盐写死太简单的话,黑客可能注册几个账号反推出来)
  • 每一个密码都有独立的盐,并且盐要长一点,比如超过 20 位。(盐太短,加上原始密码太短,容易破解)
  • 最好是随机的值,并且是全球唯一的,意味着全球不可能有现成的彩虹表给你用。

❞

2.3 提升密码存储安全的利器登场,Bcrypt

即使是加了盐,密码仍有可能被暴力破解。因此,我们可以采取更「慢一点」的算法,让黑客破解密码付出更大的代价,甚至迫使他们放弃。提升密码存储安全的利器~Bcrypt,可以闪亮登场啦。

❝
实际上,Spring Security 已经废弃了 MessageDigestPasswordEncoder,推荐使用BCryptPasswordEncoder,也就是BCrypt来进行密码哈希。BCrypt 生而为保存密码设计的算法,相比 MD5 要慢很多。

❞

看个例子对比一下吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public class BCryptTest {

    public static void main(String[] args) {
        String password = "123456";
        long md5Begin = System.currentTimeMillis();
        DigestUtils.md5Hex(password);
        long md5End = System.currentTimeMillis();
        System.out.println("md5 time:"+(md5End - md5Begin));
        long bcrytBegin = System.currentTimeMillis();
        BCrypt.hashpw(password, BCrypt.gensalt(10));
        long bcrytEnd = System.currentTimeMillis();
        System.out.println("bcrypt Time:" + (bcrytEnd- bcrytBegin));
    }
}

运行结果:

1
2
复制代码md5 time:47
bcrypt Time:1597

粗略对比发现,BCrypt比MD5慢几十倍,黑客想暴力破解的话,就需要花费几十倍的代价。因此一般情况,建议使用Bcrypt来存储用户的密码

3. 总结

  • 因此,一般使用https 协议 + 非对称加密算法(如RSA)来传输用户密码,为了更加安全,可以在前端构造一下随机因子哦。
  • 使用BCrypt + 盐存储用户密码。
  • 在感知到暴力破解危害的时候,「开启短信验证、图形验证码、账号暂时锁定」等防御机制来抵御暴力破解。

参考与感谢

  • 如何正确保存和传输敏感数据? https://time.geekbang.org/column/article/239150[1]
  • 如何加密传输和存储用户密码 https://juejin.cn/post/6844903604944371726#heading-8[2]

公众号

  • 公众号:「捡田螺的小男孩」
  • github地址:https://github.com/whx123/JavaHome

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用JPA自动建表时优雅的添加注释

发表于 2021-01-02

在使用JPA自动建表时,如果需要添加注释,一般需要这样使用Column注解的columnDefinition属性。

1
2
java复制代码@Column(columnDefinition="INT COMMENT '...'")
private int foo;

这种方式可以添加注释,但是需要把SQL片段写入注解中。不仅编写麻烦,而且当更换数据库时,因为不同的SQL方言,很有可能遇到不兼容的问题。本文介绍一种通过Hibernate的Integrator实现,且不需要修改columnDefinition的方法。

首先定义Comment注解:

1
2
3
4
5
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface Comment {
String value() default "";
}

将CommentIntegrator.java添加到项目中,并添加Hibernate配置:

1
2
3
4
5
6
7
8
9
java复制代码@Component
public class HibernateConfig implements HibernatePropertiesCustomizer {
@Override
public void customize(Map<String, Object> hibernateProperties) {
hibernateProperties.put("hibernate.use_sql_comments", true);
hibernateProperties.put("hibernate.integrator_provider",
(IntegratorProvider) () -> Collections.singletonList(CommentIntegrator.INSTANCE));
}
}

完成以上步骤之后,就可以直接使用Column注解了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Table
@Entity
@Comment("Table comment")
public class DemoTable {
@Id
@GeneratedValue
@Column
@Comment("Identifier comment")
private Integer id;

@Column
@Comment("Field comment")
private String field;

getter setter ...
}

完整的代码:

github.com/elyar-adil/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Python 虚拟环境 看这一篇就够了

发表于 2021-01-02
文:太阳雪
来源:Python技术 [公众号ID:pythonall]

Python 之所以强大,除了语言本身的特性外,更重要的是拥有无所不及的第三方库。强大的软件库,让开发者将精力集中在业务上,而避免重复造轮子的浪费。但众多的软件库,形成了复杂的依赖关系,加上 Python2 和 Python3 旷日持久之争,对采用 Python 开发的项目造成了不少困扰,所以 Python 建议,通过虚拟环境工具为项目创建纯净的依赖环境,今天我们就来了解下 Python 虚拟环境

一些概念

Python 虚拟环境,涉及到很多概念和工具,会对使用造成孔娆和障碍,所以我们先了解一些概念和与之相关的工具

python 版本

Python 版本指得是 Python 解析器本身的版本。由于 Python3 不能与 Python2 兼容,而且两大正营之争持续了很长时间,导致一些软件库需要设配两种版本的 Python,同时开发者可能需要在一个环境中,部署不同版本的 Python,对开发和维护造成了麻烦。因此出现了版本管理器 Pyenv,类似于 nodejs 的 nvm,可以创建出相互隔离的 Python 环境,并且可以方便的切换环境中的 Python 版本,但和 Python 虚拟环境关系不大

python 包库

包库或者叫软件源是 Python 第三方软件的库的集合,或者市场,可以发布、下载和管理软件包,其中 pypi (Python Package Index) pypi.org/ 是官方指定的软件包库,基于其上的 pip 工具就是从这里查找、下载安装软件包的。为了提高下载速度,世界上有很多 Pypi 的镜像服务器,在国内也有多个软件源,例如阿里的软件源是:mirrors.aliyun.com/pypi/simple…。
除此之外,还有其他软件源,如正对科学计算的 anaconda 的软件源 repo.anaconda.com/

python 包管理器

软件包源中的软件包数量巨大,版本多样,所以需要借助于软件源管理工具,例如 pip、conda、Pipenv、Poetry 等

  • pip 是最常用的包管理工具,通过 pip install <packagename> 命令格式来安装软件包,使用的是 pypi 软件包源
  • conda 多用作科学计算领域的包管理工具,功能丰富且强大,使用的软件包源是 Anaconda repository 和 Anaconda Cloud,conda 不仅支持 Python 软件包,还可以安装 C、C++ 、R 以及其他语言的二定制软件包。除了软件包管理外,还能提供相互隔离的软件环境。
  • Pipenv 是 Kenneth Reitz 在2017年1月发布的Python依赖管理工具,现在由PyPA维护。Pipenv 会自动帮你管理虚拟环境和依赖文件,并且提供了一系列命令和选项来帮助你实现各种依赖和环境管理相关的操作
  • Poetry 和 Pipenv 类似,是一个 Python 虚拟环境和依赖管理工具,另外它还提供了包管理功能,比如打包和发布。你可以把它看做是 Pipenv 和 Flit 这些工具的超集。它可以让你用 Poetry 来同时管理 Python 库和 Python 程序

很多包管理工具不仅提供了基本的包管理功能,还提供了虚拟环境构建,程序管理的等功能

Python 虚拟环境

Python 应用经常需要使用一些包第三方包或者模块,有时需要依赖特定的包或者库的版本,所以不能有一个能适应所有 Python 应用的软件环境,很多时候不同的 Python 应用所依赖的版本是冲突的,满足了其中一个,另一个则无法运行,解决这一问题的方法是 虚拟环境。
虚拟环境是一个包含了特定 Python 解析器以及一些软件包的自包含目录,不同的应用程序可以使用不同的虚拟环境,从而解决了依赖冲突问题,而且虚拟环境中只需要安装应用相关的包或者模块,可以给部署提供便利

构建虚拟环境

原理

虚拟环境并不是什么新技术,主要是利用了操作系统中环境变量以及进程间环境隔离的特性

操作系统的环境变量可以为程序提供信息和做信息交换介质,进程可以共享操作系统中的环境变量,也可以为进程指定环境变量,其中 PATH 是很重要的环境变量,用于为操作系统和程序提供可执行文件的访问路径,例如写一个程序 a.exe,存放在 D:\MyProgram 中,在命令行中执行 a.exe ,会得到提示“ 无法找到程序 a.exe”,为了让系统找到,可以将 D:\MyProgram 路径加入到 PATH 环境变量中,当输入 a.exe 时,操作系统就会从 PATH 所提供的路径中逐个查找,这时就可以找到了。Linux 和 MacOS 具有相似的特性,甚至比 Windows 的功能更丰富。

Python 虚拟环境就是利用这个特性构建的,在激活虚拟环境之时,激活脚本会将当前命令行程序的 PATH 修改为虚拟环境的,这样执行命令就会在被修改的 PATH 中查找,从而避免了原本 PATH 可以找到的命令,从而实现了 Python 环境的隔离。

为了让开发这容易区分当前环境是否虚拟环境以及是那个虚拟环境,命令提示符前会加上特殊标记,例如:

Python 虚拟环境

创建

virtualenv 工具

在 python3.3 之前,只能通过 virtualenv 创建虚拟环境,首先需要安装 virtualenv

1
bash复制代码pip install virtualenv

安装完后,在当前目录下创建一个名为 myvenv 的虚拟环境:

1
bash复制代码virtualenv --no-site-packages myvenv

参数 --no-site-packages 的意思是创建虚拟环境时,不复制主环境中安装的第三方包,也就是创建一个 “干净的” 虚拟环境

virtualenv 还有很多参数,用于不同的使用场景,例如:

  • -p: 用于指定 Python 解析器,就是安装好的 Python 应用程序,默认为当前环境中的 Python
  • –no-pip:不需要安装 pip,默认为安装
  • –clear:如果创建虚拟环境的目录已经有了其他虚拟环境,清楚重建

venv 模块

Python3.3 之后,可以用模块 venv 代替 virtualenv 工具,好处时不用单独安装,3.3 及之后的版本,都可以通过安装好的 Python 来创建虚拟环境:

1
bash复制代码python -m venv myvenv

可以在当前目录创建一个名为 myvenv 的虚拟环境

venv 有些才参数,不过相比 virtualenv 少了些,这里简单介绍几个:

  • –without-pip: 不需要安装 pip,默认为安装
  • –clear:如果创建虚拟环境的目录已经有了其他虚拟环境,清楚重建

因为 venv 是依附于一个 Python 解析器创建的,所以不需要指定 Python 解释器版本

激活

虚拟环境创建好后,需要激活才能在当前命令行中使用,可以理解成将当前命令行环境中 PATH 变量的值替换掉

通过 virtualenv 和 模块 venv 创建的虚拟环境,激活方式是一样的,即运行激活脚本

  • Windows 系统中,激活脚本路径是 <myvenv>\Scripts\activate.bat,如果是 powershell 命令行,脚本换成 Activate.ps1 , 注意将 <myvenv> 换成你自己的虚拟环境目录
  • Linux 系统中,激活脚本路径是 <myvenv>/bin/activate,默认脚本没有执行权限,要么设置脚本为可执行,要么用 source 命令执行,例如
1
bash复制代码$ source myvenv/bin/activate

激活后,可以在命令行中看到虚拟环境标记,如上图

打印 PATH,命令如下:

Linux 下:

1
bash复制代码echo $PATH

Windows 下

1
bash复制代码echo %PATH%

可以看到创建的虚拟环境脚本目录被加载了最前面

退出

退出虚拟环境很简单,只需要执行 deactivate 命令就行,这个命令也在虚拟环境的脚本目录下,因为激活时,将脚本目录设置到 PATH 中了,所以可以直接使用

退出虚拟环境相当于将 PATH 恢复成原来的

与开发工具配合

虽然通过激活脚本,很容易切换到虚拟环境,但是在实际开发中,还是不够方便,而且现在很多开发工具,特别是提供 Python 解析环境的开发工具,都可以和虚拟环境配合,在开发过程中几乎无感,对开发工作是很大的帮助

Visual Studio Code

VS Code 是个后起之秀,功能强大且具有丰富的插件资源,无疑是这两年发展最快的综合开发工具。现在的版本配置 Python 虚拟环境很简单,只需要选择一个 Python 解释器就好了

同时按下 Ctrl+Shift+P, 在弹出的命令窗口中输入 “解析器”,然后在下拉列表中选择 “Python:选择解析器”,这里会缓存一些已经创建好的解析器,如果没有想要的,可以选择 “Enter interpreter path” 来选择解析器路径,即已经创建好的虚拟环境脚本文件夹中的 Python 程序,就可以创建一个新的解析器

选择 Python 解析器

如果编辑的是 Python 代码文件,在状态栏中也可以选择和切换解释器,更为方便

选择 Python 解析器

Pycharm

Pycharm 应该是功能最好的 Python 开发工具,转为 Python 开发而生,除了基本的开发功能外,还提供项目创建、打包、测试等丰富功能,有很大的市场占有率

创建项目时,在项目创建对话框中,可以创建或者选择已经已有的解析器

选择 Python 解析器

选择创建新的解析器时,需要选择创建虚拟环境的工具,如 virtualenv;指定虚拟环境的目录;选择 Python 基础解析器,同 virtualenv 工具的 -p 参数的效果;以及是否要继承基础解析器的第三方库 和 是否将这个虚拟环境作为默认环境,即创建其他项目时默认选择

如果选择已存在的解析器,和 VS Code 差不多,可以选择已经缓存的或者指定解析器的路径

部署虚拟环境

之所以在开发时选择虚拟环境,除了避免库之间的冲突,还有重要的原因时方便部署,因为虚拟环境时独立的,仅包含了项目相关的依赖库,所以部署的效率更高,风险更小

一般部署流程是:

  1. 开发完成后,使用 pip freeze > requirements.txt 命令将项目的库依赖导出,作为代码的一部分
  2. 将代码上传到服务器
  3. 在服务器上创建一个虚拟环境
  4. 激活虚拟环境,执行 pip install -r requirements.txt,安装项目依赖

怎么运行项目,需要看项目的具体情况

  • Web 项目
    Web 项目一般使用 Django、Flask 的 Web 开发的提供 Web 服务的项目,部署时需要一个 Web 容器,作为程序的运行环境,容器的配置中都有一个虚拟环境的设置,其实是指定 Python 解析器的路径,将其设置为虚拟环境的目录或者 Python 解析器就可以了,启动时就用使用虚拟环境,并与其他环境隔离。例如 uWSGI 配置文件中 home 参数是用来指定解析器的。
  • 服务类项目
    服务类项目就是需要以服务的形式长时间运行的,例如之前介绍的 公交闹钟,或者一些定时爬虫之类的,对于 Linux 而言,当前主流的服务方式是 Systemd,是一种比 init 更先进的服务管理工具,在服务脚本中,
    设置 ExecStart 执行命令为全路径的虚拟环境的 Python 解析器,服务启动时,使用独立的虚拟环境了。
    Windows 服务,对于达成 EXE 包的,不需要配置虚拟环境,因为打包时已经考虑了环境问题了,如果时脚本运行的话,需要指定全路径的 Python 解析器
  • 单次运行项目
    对于一些测试或者实验性质的项目,大多数情况下手动执行,只要在激活的虚拟环境下,或者用特定的 Python 解析器运行就好了,和在开发过程中运行区别不大

其他虚拟环境管理工具

  • virtualenvwrapper: 是对 virtualenv 的一个封装,还有针对 vim 用户和 emacs 用户的 扩展,能支持 bash/ksh/zsh
  • virtualenvwrapper-win: 针对 Windows batch shell 的 virtualenvwrapper
  • pyenv: 用来解决这类问题。它可以安装、卸载、编译、管理多个 python 版本,并随时将其中一个设置为工作环境
  • pyenv-win: 针对 Windows 的 pyenv

有兴趣的话可以试用一下

总结

今天主要了解了 Python 虚拟环境的相关概念和工具,并简单描述了实际工作中的一些使用方式,以便能在开发过程中使用。限于篇幅,没法就更多的内容详细展开,需要在实践中多试多用,Just DO It!

参考

  • docs.python.org/3/tutorial/…
  • greyli.com/back-to-vir…
  • www.jianshu.com/p/dcb281ee5…
  • blog.csdn.net/SpuerCheng/…
  • www.cnblogs.com/qinhan/p/92…

欢迎关注微信公众号:Python技术,在这里我们有亲自编写的100天入门实战教程,有各种有趣的编程实践,有各种学习资料,还有一大群可爱的小伙伴互相探讨。

image

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

接口测试用例设计 主题列表:juejin, github,

发表于 2021-01-02

接口测试 [腾讯 TMQ] 接口测试用例设计
testerhome.com/topics/1167…
转载


主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green

贡献主题:github.com/xitu/juejin…

theme: juejin
highlight:


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

每日一面 - java中,MinorGC、MajorGC、F

发表于 2021-01-02

问题引用自: www.zhihu.com/question/43…

MinorGC 一般指清理 Young space (Eden and Survivor spaces) 的 GC。例如 G1GC 还有 ShenandoahGC 中的 YoungGC. 触发一般是:

  • Allocation Failure: 分配对象失败,空间不足. 内存分配流程,涉及到了 bump-the-pointer, TLAB,Allocation Prematch 这些机制, 请参考
  • Survivor 区满了,需要拷贝

不同的 GC 还会有自己个性化的触发机制,例如 G1GC 还有Shenandoah GC 的 TLAB 分配失败剩余空间大于最大浪费空间直接在Eden分配也失败,ZGC 的预热触发等等。

MajorGC 一般指清理 Tenured space 的 GC。例如 G1GC 还有 ShenandoahGC 中的 OldGC. 一般由 MinorGC 触发,并且回收的空间依然不足,则可能触发 MajorGC。还有一些特殊的机制,例如 G1GC 的Homongous Allocation(大对象分配),在分配超过 RegionSize 一半大小的对象时,会触发 OldGC。 FullGC 一般指清理 所有 space 的 GC。触发时机一般是:

  • System.gc()被调用并且没有指定关闭显示GC,就是没有指定-XX:+DisableExplicitGC这个JVM flag
  • 老年代也满了
  • 堆外内存满了(JVM内存结构请参考:谁能给我详细讲解一下JVM的详细内存?),例如metaspace,代码即时编译缓存,直接内存,mmap内存
  • gc 担保失败,请参考:-XX:-HandlePromotionFailure

一般的,我们现在不会去太关心到底是哪种 GC,而是主要关心哪些 GC STW **的时间长,导致所有线程停止工作的时间长,关于为何会 STW 以及所有出发 STW 的 JVM机制以及如何优化**,请参考我的另一篇文章: JVM相关 - SafePoint 与 Stop The World 全解

想模拟 GC 的各种情况,可以通过 WhiteBox API,参考:JVM 相关 - 深入 JVM 的钥匙 WhiteBox API

关于如何通过日志查看 GC 详情,请参考:OpenJDK 11 JVM日志相关参数解析与使用

关于如何通过 JFR 快速可视化定位 GC 问题,请参考: JFR全解

通过JFR与日志深入探索JVM

每日一刷,轻松提升技术,斩获各种offer:

image

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring+Netty+Vue 网页版聊天应用,仿微信网页

发表于 2021-01-01

1:前言

最近在学习网络知识,对于java开发来说,Netty是一个非常重要的框架,无论是为了面试还是日常工作中,如RPC框架Dubbo底层其实是用了Netty, 又或者我们的聊天通信功能,都可能有Netty的身影,作为java开发者 我们应该掌握它。此项目是一个前后端分离的、以Netty为核心,以Websocket为通信协议的聊天系统,整体功能仿照电脑版微信, 但是电脑版微信功能非常多,因此只实现部分功能,如:用户登录(oauth2认证)、查看我的好友列表、单聊、创建群聊、群消息发送、删除聊天、置顶聊天、表情功能发送、文件发送、心跳和空闲检测。
git项目地址:github.com/holiday-jq/…

2:技术路线

  • 前端: Webpack + vue + less + element ui
  • 后端:springboot框架 + Netty网络编程框架 + spring-security-oauth2 + jdbcTemplate
  • 数据库: mysql
  • 协议: websocket

什么是Spring Boot?

Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。用我的话来理解,就是 Spring Boot 其实不是什么新的框架,它默认配置了很多框架的使用方式,就像 Maven 整合了所有的 Jar 包,Spring Boot 整合了所有的框架。

什么是Netty?

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke’s Choice Award,见www.java.net/dukeschoice… Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。

关于WebSocket通信协议

简单说一下WebSocket通信协议,WebSocket是为了解决HTTP协议中通信只能由客户端发起这个弊端而出现的,WebSocket基于HTTP5协议,借用HTTP进行握手、升级,能够做到轻量的、高效的、双向的在客户端和服务端之间传输文本数据。

  • WebSocket 是什么原理?为什么可以实现持久连接?

3:环境搭建

前端: 运行环境搭建:首先要安装Node.js 安装链接nodejs.org/zh-cn/
然后运行cmd进入控制台 验证是否安装成功: node -v 和 npm -v 如果能显示版本号出来说明Node.js安装成功 用VsCode(或看个人喜欢用什么编辑器),到项目根目录,首先执行npm install安装依赖,然后npm run serve启动一个node服务就可以访问前端资源

后端: 安装配置好maven,并在开发编辑器(如eclipse、intellij idea)中配置好maven,然后启动springboot工程

4:具体功能展示

  • 用户登录:

    这里采用oauth2的授权模式是password模式,用户验证成功后返回token,然后如果访问接口需要带token访问,因为设置了对接口资源进行保护,不然提示401错误
  • 查看我的好友列表:
  • 单聊:


  • 创建群聊和群消息发送:
  • 此处做了判断,如果选择人数大于1就创建的是群聊,如果选择人数是1的话,那就是创建单聊。*


  • 备注:这里的群聊图片不知道怎么实现将群成员的头像组合一张图片作为群聊的头像,就像微信创建群聊那样,所以随便找了张图片作为群聊的头像,所以无论创建多少个群,群聊头像都是一样的。*
  • 右键鼠标弹出删除聊天和置顶聊天:

  • 备注: 此处置顶和取消置顶交互效果写得不好,如果再新增聊天,原来置顶的那条聊天就不能一直在最前面,待优化。!!*
  • 表情功能发送:

  • 文件发送和下载:

  • 心跳和空闲检测:

    Netty的IdleStateHandler心跳机制主要是用来检测远端是否存活,如果不存活或活跃则对空闲Socket连接进行处理避免资源的浪费;前端会有一个定时器,每隔30秒就会发一个心跳包到Netty,如果触发了检测机制,就会执行channelIdle方法,你可以在这里关闭channel连接。
  • 如果没有空闲检测的话,可能会出现连接假死的情况,会带来以下问题:
    • 1:对于服务端来说,因为每条连接都会耗费cpu和内存资源,(当服务端accpt成功会返回一个文件描述符,linux一切皆文件),大量假死的连接会逐渐耗光服务器的资源,最终导致性能逐渐下降。
    • 2:对于客户端来说:连接假死会造成发送数据超时,影响用户体验。

5:总结与扩展

因为聊天软件的功能非常多,所以不可能全部功能都实现(太难了- _ -),其实还有很多优化和没实现的功能,例如群聊那里,可以增加退群功能,和拉人进群功能,在服务端无非是对ChannelGroup进行添加和删除,前端交互效果稍微繁琐点,还有聊天文字输入框可以用富文本编辑器实现,此项目是用了textarea标签(新浪博客网页版聊天也是用的textarea哈哈),然后聊天记录和列表怎么做持久化,滚动加载以前的聊天记录,此项目没做(能力有限- _ -),所以刷新界面后就看不到之前的聊天记录。

6:巨人的肩膀

  • Netty 入门与实战:仿写微信 IM 即时通讯系统
  • Netty 核心原理剖析与 RPC 实践

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

手把手教你做一款音乐播放器(csharp的winform)

发表于 2021-01-01

前言:项目是c#的winform 写的,使用的播放器是基于AxWindowsMediaPlayer。

AxWindowsMediaPlayer的方法

file

1.1 首先新建一个页面 如图所示: 图片左侧是列表 使用listview 右侧是背景图片。图片框框的地方是后面可以实现的,+和-按钮分别代表添加文件和删除文件 还有就是控制播放的顺序。下面的分别是修改歌词的字体 和展示/隐藏

file

1.2 新建一个透明的歌词页面[窗体]

file

1.3 新建一个半透明的页面[窗体]

file

1.4 业务代码

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
ini复制代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using WMPLib;
using System.Media;
using System.IO;
using System.Text.RegularExpressions;
using AxWMPLib;
using System.Drawing.Drawing2D;
using CCWin;


namespace KenMusicPlayer
{
public partial class MusicPlayer : Skin_DevExpress
{


public int index = 1;
public int listIndex;
private bool first_in = true; //是否第一次进入歌词区域
private bool showLrc = true;//默认显示歌词
private int imageInd = 0;//播放的图片下标
private List<string> imageList;//播放的图片
private Point closePoint;//关闭按钮的位置
private Size dfSize;//最初的位置


//声音
SoundPlayer player = new SoundPlayer();
Dictionary<string, string> dic = new Dictionary<string, string>();

//播放列表
Dictionary<string, IWMPMedia> playListDict = new Dictionary<string, IWMPMedia>();

List<string> al = new List<string>(); //当前歌词时间表

IWMPMedia media;

/*
*下面这一大段API调用,主要是用来设置歌词窗口的滚动条的
*但其实后面,我并没有怎么用到,只是在将滚动条滚动到底部时,用了一下
*/
private const int WM_VSCROLL = 0x115;
private const int SB_HORZ = 0;
private const int SB_VERT = 1;
private const int SB_CTL = 2;
private const int SB_BOTH = 3;
private const int SB_LINEUP = 0;
private const int SB_LINELEFT = 0;
private const int SB_LINEDOWN = 1;
private const int SB_LINERIGHT = 1;
private const int SB_PAGEUP = 2;
private const int SB_PAGELEFT = 2;
private const int SB_PAGEDOWN = 3;
private const int SB_PAGERIGHT = 3;
private const int SB_THUMBPOSITION = 4;
private const int SB_THUMBTRACK = 5;
private const int SB_TOP = 6;
private const int SB_LEFT = 6;
private const int SB_BOTTOM = 7;
private const int SB_RIGHT = 7;
private const int SB_ENDSCROLL = 8;
private const int WM_PAINT = 0x000F;


[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern bool ScrollWindow(IntPtr hWnd, int XAmount, int YAmount, ref Rectangle lpRect, ref Rectangle lpClipRect);

[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern int SetScrollPos(IntPtr hwnd, int nBar, int nPos, bool bRedraw);

[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern int SetScrollPos(int nBar, int nPos, bool bRedraw);

[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern int GetScrollPos(IntPtr hwnd, int nBar);


[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern bool UpdateWindow(IntPtr hWnd);

[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hwnd, int wMsg, int wParam, int lParam);

public void setWord()
{

}


public MusicPlayer()
{
this.StartPosition = FormStartPosition.CenterScreen;//窗口居中显示
InitializeComponent();
}


private void MusicPlayer_Load(object sender, EventArgs e)
{
InitLoad();
}


/// <summary>
/// 初始化 加载播放列表 如歌词 背景图 定时器等等
/// </summary>
private void InitLoad()
{
try
{
bool flag = false;
string folder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bgImages");
DirectoryInfo root = new DirectoryInfo(folder);
FileInfo[] files = root.GetFiles();
string fileName;
for (int i = 0; i < files.Length; i++)
{
fileName = files[i].Name.ToLower();
if (fileName.EndsWith(".png") || fileName.EndsWith(".jpeg") || fileName.EndsWith(".jpg"))
{
if (!flag)
{
imageList = new List<string>();
this.pictureBox1.Image = Image.FromFile(files[i].FullName);
}
imageList.Add(files[i].FullName);
flag = true;
}
}

playerType.Text = playerType.Items[0].ToString();//默认第一个
closePoint = this.skinButtonClose.Location;
dfSize = this.Size;
richTextBox1.BackColor = this.TransparencyKey;
skinComboBoxFontName.Text = skinComboBoxFontName.Items[0].ToString();//默认第一个
skinComboBoxFontSize.Text = skinComboBoxFontSize.Items[0].ToString();//默认第一个
ComboBoxSkinSelect.Text = ComboBoxSkinSelect.Items[0].ToString();//默认第一个
//this.BackPalace = Image.FromFile(ComboBoxSkinSelect.Items[0].ToString());//默认第一个

lvDetail.AllowDrop = true;
lvDetail.View = View.Details;
lvDetail.DragEnter += Files_DragEnter;//对象拖拽事件
lvDetail.DragDrop += Files_DragDrop;//拖拽操作完成事件
//wmp.OpenStateChange += WMP_OpenStateChange;
wmp.PlayStateChange += WMP_PlayStateChange;
timerImgs.Start();
}
catch (Exception ex)
{
Console.WriteLine("错误:" + ex.Message);
}
}

/// <summary>
/// 提供给透明歌词窗口的定时器调用的
/// </summary>
/// <param name="s"></param>
public void showTmform(bool s)
{
if (s)
{
this.btmForm.Show();
if (this.first_in)
{
this.lrcForm.TopMost = true;
Point point = this.Location;
point.Y = point.Y + this.Height;
this.lrcForm.Location = point;
this.btmForm.Location = point;
this.lrcForm.Width = this.Width;
this.btmForm.Width = this.Width;
this.first_in = false;
}
}
else
{
this.first_in = true;
this.btmForm.Hide();
}
}

/// <summary>
/// 播放时会进入这个事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void WMP_PlayStateChange(object sender, _WMPOCXEvents_PlayStateChangeEvent e)
{
loadLrc();
}


/// <summary>
/// 拖拽操作完成事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Files_DragDrop(object sender, DragEventArgs e)
{
try
{
string fileName, fileExtension, fileSize, temp;
FileInfo fi = null;
ListViewItem lvi = null;
Array array = (Array)e.Data.GetData(DataFormats.FileDrop);
Regex regex = new Regex("(\\.mp3|\\.wav|\\.wma)");
string filePath;
for (int i = 0; i < array.Length; i++)
{
filePath = array.GetValue(i).ToString();
//属于音乐文件 且列表中不存在
if (regex.IsMatch(filePath) &&
!dic.ContainsKey(filePath))
{
wmp.Ctlcontrols.stop();
InsertPlayList(out fileName, out fileExtension, out fileSize, out temp, out fi, out lvi, filePath);
}
}

}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}


/// <summary>
/// 插入播放列表 和字典集
/// </summary>
/// <param name="fileName"></param>
/// <param name="fileExtension"></param>
/// <param name="fileSize"></param>
/// <param name="temp"></param>
/// <param name="fi"></param>
/// <param name="lvi"></param>
/// <param name="filePath"></param>
private void InsertPlayList(out string fileName, out string fileExtension, out string fileSize, out string temp, out FileInfo fileInfo, out ListViewItem listViewItem, string filePath)
{
fileInfo = new FileInfo(filePath);
temp = filePath.Remove(filePath.LastIndexOf('.'));
fileName = Path.GetFileNameWithoutExtension(filePath);
fileExtension = Path.GetExtension(filePath);
fileSize = (fileInfo.Length / 1024).ToString() + "KB";

listViewItem = new ListViewItem();
listViewItem.Text = index++.ToString();
listViewItem.SubItems.AddRange(new string[] { fileName, fileExtension, fileSize, filePath });

lvDetail.Items.Add(listViewItem);
//添加到播放列表
media = wmp.newMedia(filePath);
//listIndex++,
//wmp.currentPlaylist.insertItem(media);
wmp.currentPlaylist.appendItem(media);
playListDict.Add(filePath, media);
//杜绝重复项
dic.Add(filePath, fileName);
}

/// <summary>
/// 文件拖拽进入
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Files_DragEnter(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
e.Effect = DragDropEffects.Link;
}
else
{
e.Effect = DragDropEffects.None;
}
}

/// <summary>
/// 导入文件
/// </summary>
private void tsmiLoading_Click(object sender, EventArgs e)
{
string fileName, fileExtension, fileSize, temp;
FileInfo fi = null;
ListViewItem lvi = null;
openFileDialog1.Multiselect = true;
openFileDialog1.Filter = "mp3文件(*.mp3)|*.mp3|wav文件(*.wav)|*.wav|wma文件(*.wma)|*.wma";
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
//顺序播放
wmp.settings.setMode("shuffle", false);
foreach (string filePath in openFileDialog1.FileNames)
{
if (!dic.ContainsKey(filePath))
{
InsertPlayList(out fileName, out fileExtension, out fileSize, out temp, out fi, out lvi, filePath);
}
}
}
}

/// <summary>
/// 清除列表
/// </summary>
private void ListViewClear_Click(object sender, EventArgs e)
{
lvDetail.Items.Clear();
}

/// <summary>
/// 播放
/// </summary>
private void tsmiPlayer_Click(object sender, EventArgs e)
{
wmp.Ctlcontrols.play();
}

/// <summary>
/// 暂停
/// </summary>
private void tsmiWaiting_Click(object sender, EventArgs e)
{
wmp.Ctlcontrols.pause();
}

/// <summary>
/// 停止
/// </summary>
private void tsmiStop_Click(object sender, EventArgs e)
{
wmp.Ctlcontrols.stop();
}

/// <summary>
/// 退出
/// </summary>
private void tsmiExit_Click(object sender, EventArgs e)
{
Application.Exit();
}

/// <summary>
/// 双击某项播放
/// </summary>
private void lvDetail_DoubleClick(object sender, EventArgs e)
{
if (lvDetail.SelectedItems.Count > 0)
{
wmp.currentMedia = wmp.currentPlaylist.Item[int.Parse(lvDetail.SelectedItems[0].Text) - 1];
}
}

/// <summary>
/// 循环播放
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void tsmiXunHuan_Click(object sender, EventArgs e)
{
//点击项
ToolStripMenuItem currentItem = (ToolStripMenuItem)sender;

ToolStripItemCollection items = this.tsmiSet.DropDownItems;

ToolStripMenuItem item;
for (int i = 0; i < items.Count; i++)
{

item = (ToolStripMenuItem)items[i];
item.Checked = false;
if (item.Name == currentItem.Name)
{
item.Checked = true;
}
}

wmp.settings.setMode("loop", true);
}

//顺序
private void tsmiDanQu_Click(object sender, EventArgs e)
{
IWMPMedia currentMedia = wmp.currentMedia;
//点击项
ToolStripMenuItem currentItem = (ToolStripMenuItem)sender;

ToolStripItemCollection items = this.tsmiSet.DropDownItems;

ToolStripMenuItem item;
for (int i = 0; i < items.Count; i++)
{

item = (ToolStripMenuItem)items[i];
item.Checked = false;
if (item.Name == currentItem.Name)
{
item.Checked = true;
}
}
//顺序播放
wmp.settings.setMode("shuffle", false);
}


/// <summary>
/// 图片宽高设置
/// </summary>
/// <param name="imgToResize"></param>
/// <param name="size"></param>
/// <returns></returns>
public static Image ResizeImage(Image imgToResize, Size size)
{
//获取图片宽度
int sourceWidth = imgToResize.Width;
//获取图片高度
int sourceHeight = imgToResize.Height;

float nPercent = 0;
float nPercentW = 0;
float nPercentH = 0;
//计算宽度的缩放比例
nPercentW = ((float)size.Width / (float)sourceWidth);
//计算高度的缩放比例
nPercentH = ((float)size.Height / (float)sourceHeight);

if (nPercentH < nPercentW)
nPercent = nPercentH;
else
nPercent = nPercentW;
//期望的宽度
int destWidth = (int)(sourceWidth * nPercent);
//期望的高度
int destHeight = (int)(sourceHeight * nPercent);

Bitmap b = new Bitmap(destWidth, destHeight);
Graphics g = Graphics.FromImage((System.Drawing.Image)b);
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
//绘制图像
g.DrawImage(imgToResize, 0, 0, destWidth, destHeight);
g.Dispose();
return (System.Drawing.Image)b;
}


private void playerType_SelectedIndexChanged(object sender, EventArgs e)
{
if (playerType.Text == "顺序播放")
{
//顺序播放
wmp.settings.setMode("shuffle", false);
}
else if (playerType.Text == "循环播放")
{
wmp.settings.setMode("loop", true);
}
else if (playerType.Text == "随机播放")
{
//顺序播放
wmp.settings.setMode("shuffle", true);
}

}


/// <summary>
/// 定时器执行的方法,每隔1秒执行一次 歌词逐行显示
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ShowLineLrc(object sender, EventArgs e)
{
this.label1.Text = this.wmp.Ctlcontrols.currentPositionString;

if (this.lrcForm == null)
{
this.lrcForm = new TMForm();

Point pos = this.Location;
pos.Y = lrcForm.Location.Y + lrcForm.Height;
this.Location = pos;

this.btmForm = new BTMForm();
MusicPlayer mainForm = (MusicPlayer)this.Owner;
btmForm.Location = pos;

this.lrcForm.Owner = this;
this.btmForm.Owner = this;
this.btmForm.Hide();
this.lrcForm.Show();
}



if (this.wmp.currentMedia == null)
{
this.richTextBox1.Text = "";
return;
}


string durationString = this.wmp.currentMedia.durationString;
int trackBarValue = Convert.ToInt32(this.wmp.Ctlcontrols.currentPosition);
//this.trackBar1.Maximum = Convert.ToInt32(this.wmp.currentMedia.duration);
//this.trackBar1.Value = Convert.ToInt32(this.wmp.Ctlcontrols.currentPosition);

if (this.richTextBox1.Text != "歌词文件不存在" && this.richTextBox1.Text != "歌词文件内容为空")
{
int pos = al.IndexOf(trackBarValue.ToString());
bool isAr = this.richTextBox1.Text.Contains("歌手:");
bool isTi = this.richTextBox1.Text.Contains("歌名:");


if (pos >= 0)
{
int n = isAr ? 1 : 0;
int m = isTi ? 1 : 0;

int height = 28 * (this.al.Count + m + n);
int max = height - this.richTextBox1.Height;


this.richTextBox1.SelectAll();
this.richTextBox1.SelectionColor = Color.Black;
this.richTextBox1.SelectionLength = 0;/**/

int l = this.richTextBox1.Lines[pos + m + n].Length;
this.richTextBox1.Select(this.richTextBox1.GetFirstCharIndexFromLine(pos + m + n), l);
this.richTextBox1.SelectionColor = Color.OrangeRed;
this.richTextBox1.SelectionLength = 0;
//this.Text = GetScrollPos(this.richTextBox1.Handle, SB_VERT).ToString() + "-" + al.Count + "-" + this.richTextBox1.Height;

if ((pos + m + n) * 28 <= max)
{
int start = this.richTextBox1.GetFirstCharIndexFromLine(pos + m + n);
this.richTextBox1.SelectionStart = start;
this.richTextBox1.ScrollToCaret();

}
else
{
//this.richTextBox1.Focus();
SendMessage(this.richTextBox1.Handle, WM_VSCROLL, SB_BOTTOM, 0);
UpdateWindow(this.richTextBox1.Handle);
//this.richTextBox1.SelectionStart = this.richTextBox1.Text.Length;
//this.richTextBox1.ScrollToCaret();
}

if (this.lrcForm != null)
{
string l1 = this.richTextBox1.Lines[pos + m + n];
string l2;
if ((pos + m + n) < this.richTextBox1.Lines.Length - 1)
{
l2 = this.richTextBox1.Lines[pos + m + n + 1];
}
else
{
l2 = "。。。。。";
}

this.lrcForm.setLrc(l1, l2, pos);
//this.lrcForm.setLrc(ArrayList al,);

}

}
}
//this.Text = this.trackBar1.Value.ToString();
/*if (trackBarValue >= (this.trackBar1.Maximum - 2))
{
this.PlayModeChange();
}*/
}

/// <summary>
/// 载入歌词
/// </summary>
/// <param name="lrc_filename">歌词路径名</param>
public void loadLrc()
{

if (this.wmp.playState == WMPPlayState.wmppsPlaying)
{

IWMPMedia currentMedia = wmp.currentMedia;
string fullPath = currentMedia.sourceURL;
string geciPath = Path.Combine(Path.GetDirectoryName(fullPath), Path.GetFileNameWithoutExtension(fullPath) + ".lrc");

//播放哪个资源 列表需选中
ListView.ListViewItemCollection listViewItems = lvDetail.Items;
lvDetail.FullRowSelect = true;
for (int i = 0; i < listViewItems.Count; i++)
{
listViewItems[i].Checked = false;
listViewItems[i].Selected = false;
listViewItems[i].BackColor = Color.White;
if (listViewItems[i].SubItems[4].Text == fullPath)
{
listViewItems[i].Checked = true;
listViewItems[i].Selected = true;
listViewItems[i].BackColor = Color.LightBlue;
}
}

if (!File.Exists(geciPath))
{
this.richTextBox1.Text = "歌词文件内容为空";
return;
}


using (StreamReader sr = new StreamReader(new FileStream(geciPath, FileMode.Open), Encoding.Default))
{
string tempLrc = "";
while (!sr.EndOfStream)
{
tempLrc = sr.ReadToEnd();
}

if (tempLrc.Trim() == "")
{
this.richTextBox1.Text = "歌词文件内容为空";
return;
}

tempLrc = tempLrc.Trim();
Regex rg = new Regex("\r*\n*\\[ver:(.*)\\]\r*\n*");
tempLrc = rg.Replace(tempLrc, "");
rg = new Regex("\r*\n*\\[al:(.*)\\]\r*\n*");
tempLrc = rg.Replace(tempLrc, "");
rg = new Regex("\r*\n*\\[by:(.*)\\]\r*\n*");
tempLrc = rg.Replace(tempLrc, "");
rg = new Regex("\r*\n*\\[offset:(.*)\\]\r*\n*");
tempLrc = rg.Replace(tempLrc, "");
rg = new Regex("\r*\n*\\[ar:(.*)\\]\r*\n*");
Match mtch;
mtch = rg.Match(tempLrc);
tempLrc = rg.Replace(tempLrc, "\n歌手:" + mtch.Groups[1].Value + "\n");
rg = new Regex("\r*\n*\\[ti:(.+?)\\]\r*\n*"); //这里注意贪婪匹配问题.+?
mtch = rg.Match(tempLrc);
tempLrc = rg.Replace(tempLrc, "\n歌名:" + mtch.Groups[1].Value + "\n");
rg = new Regex("\r*\n*\\[[0-9][0-9]:[0-9][0-9].[0-9][0-9]\\]");
MatchCollection mc = rg.Matches(tempLrc);
al.Clear();

foreach (Match m in mc)
{
string temp = m.Groups[0].Value;
//this.Text += temp + "+";
string mi = temp.Substring(temp.IndexOf('[') + 1, 2);
string se = temp.Substring(temp.IndexOf(':') + 1, 2);
string ms = temp.Substring(temp.IndexOf('.') + 1, 2); //这是毫秒,其实我只精确到秒,毫秒后面并没有用
//this.Text += mi + ":" + se + "+";
string time = Convert.ToInt32(mi) * 60 + Convert.ToInt32(se) + ""; //这里并没有添加毫秒
al.Add(time);
}

tempLrc = rg.Replace(tempLrc, "\n");
char[] remove = { '\r', '\n', ' ' };
this.richTextBox1.Text = tempLrc.TrimStart(remove);
this.timer1.Interval = 1000;
this.timer1.Tick += ShowLineLrc;
this.timer1.Start();
}
}

}

private void wmp_Enter(object sender, EventArgs e)
{
loadLrc();//点击播放
}



/// <summary>
/// 删除选中的文件 并停止播放
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void skinButton2_Click(object sender, EventArgs e)
{
try
{
ListView.SelectedIndexCollection indexes = this.lvDetail.SelectedIndices;
if (indexes.Count > 0)
{
int index = indexes[0];
string path = this.lvDetail.Items[index].SubItems[4].Text;
IWMPPlaylist iWMPPlaylist = wmp.currentPlaylist;
//先移除播放列表 再移除listview列表
wmp.currentPlaylist.removeItem(playListDict[path]);
playListDict.Remove(path);
this.lvDetail.Items[index].Remove();
dic.Remove(path);
wmp.Ctlcontrols.stop();
}
}
catch (Exception ex)
{
MessageBox.Show("操作失败!\n" + ex.Message, "提示", MessageBoxButtons.OK,
MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1);

}
}

/// <summary>
/// 列表鼠标双击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lvDetail_MouseDoubleClick(object sender, MouseEventArgs e)
{
try
{
ListView.SelectedIndexCollection indexes = this.lvDetail.SelectedIndices;
if (indexes.Count > 0)
{
int index = indexes[0];
string path = this.lvDetail.Items[index].SubItems[4].Text;
wmp.Ctlcontrols.playItem(playListDict[path]);
//wmp.URL = path;//这种方式会失去播放列表【不是肉眼可见的listview列表】
wmp.Ctlcontrols.stop();
wmp.Ctlcontrols.play();
}
}
catch (Exception ex)
{
MessageBox.Show("操作失败!\n" + ex.Message, "提示", MessageBoxButtons.OK,
MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1);

}
}

/// <summary>
/// 字体改变
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void skinComboBoxFontName_SelectedIndexChanged(object sender, EventArgs e)
{
if (this.lrcForm != null)
{
this.lrcForm.ChangeLabelFont(this.skinComboBoxFontName.SelectedItem.ToString(), this.skinComboBoxFontSize.SelectedItem.ToString());

}
}

/// <summary>
/// 字体大小改变
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void skinComboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
if (this.lrcForm != null)
{
this.lrcForm.ChangeLabelFont(this.skinComboBoxFontName.SelectedItem.ToString(), this.skinComboBoxFontSize.SelectedItem.ToString());

}
}

/// <summary>
/// 歌词是否显示
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void skinButton5_Click(object sender, EventArgs e)
{
if (this.lrcForm == null)
{
return;
}
this.showLrc = !this.showLrc;
this.skinButton5.Text = this.showLrc ? "关闭" : "显示";
if (this.showLrc)
{
this.btmForm.Show();
this.lrcForm.Show();
}
else
{
this.btmForm.Hide();
this.lrcForm.Hide();
}

}

/// <summary>
/// 背景图片切换
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void timerImgs_Tick(object sender, EventArgs e)
{
if (imageInd <= imageList.Count - 2)
{
imageInd++;
this.pictureBox1.Image.Dispose();
this.pictureBox1.Image = Image.FromFile(imageList[imageInd]);
}
else
{
imageInd=0;
}
}


/// <summary>
/// 粘贴音乐到播放列表
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void lvDetail_KeyDown(object sender, KeyEventArgs e)
{
System.Collections.Specialized.StringCollection stringCollection = Clipboard.GetFileDropList();
if (stringCollection != null)
{
string fileName, fileExtension, fileSize, temp;
FileInfo fi = null;
ListViewItem lvi = null;
foreach (var item in stringCollection)
{
if ((item.ToLower().EndsWith(".mp3") ||
item.ToLower().EndsWith(".wav") ||
item.ToLower().EndsWith(".wma")) && !dic.ContainsKey(item) )
{
InsertPlayList(out fileName, out fileExtension, out fileSize, out temp, out fi, out lvi, item);
}
}
}
}


//选择皮肤
private void ComboBoxSkinSelect_SelectedIndexChanged(object sender, EventArgs e)
{
string bgColor = ComboBoxSkinSelect.SelectedItem.ToString();
if (!string.IsNullOrEmpty(ComboBoxSkinSelect.SelectedItem.ToString()))
{
switch (bgColor)
{
case "渐变黑":
this.BorderPalace = kenMusicPlayer.Properties.Resources.bg_black;
break;
case "天蓝色":
this.BorderPalace = kenMusicPlayer.Properties.Resources.bg_light_blue;
break;
case "墨绿色":
this.BorderPalace = kenMusicPlayer.Properties.Resources.bg_green;
break;
case "蓝色":
this.BorderPalace = kenMusicPlayer.Properties.Resources.bg_s_blue;
break;
case "浅灰色":
this.BorderPalace = kenMusicPlayer.Properties.Resources.bg_grey;
break;
case "亮色":
this.BorderPalace = kenMusicPlayer.Properties.Resources.bg_light;
break;
default:
this.BorderPalace = kenMusicPlayer.Properties.Resources.bg_black;
break;
}
/*this.UpdateStyles();
this.Update();*/
//this.BackPalace = Image.FromFile(ComboBoxSkinSelect.SelectedItem.ToString());
}
}

/// <summary>
/// 退出
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void skinButtonClose_Click(object sender, EventArgs e)
{
//this.Close();
Application.Exit();
}

private void MusicPlayer_Resize(object sender, EventArgs e)
{
try
{
Size size = this.Size;
int x = (size.Width - dfSize.Width) + closePoint.X;
int y = closePoint.Y;//(size.Height - dfSize.Height) + closePoint.Y;
Point point = new Point(x, y);
this.skinButtonClose.Location = point;
}
catch (Exception ex)
{
Console.WriteLine("异常:" + ex.Message);
}

}


/// <summary>
/// 改方法也是提供给透明歌词窗口用的,用来修改半透明窗体的位置,是透明歌词窗口和半透明窗体始终重合
/// </summary>
/// <param name="pos"></param>
public void moveTmform(Point pos)
{
this.btmForm.Location = pos;
}
}
}

源码地址:gitee.com/ten-ken/mus…

本文来源于:程序员ken,专属平台有csdn、思否(SegmentFault)、 简书、 开源中国(oschina)、掘金,转载请注明出处。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Dubbo源码分析(2)—— 服务是如何暴露的

发表于 2021-01-01

前言

上一篇文章解释了dubbo-spi的实现原理,这篇文章介绍如何暴露一个接口服务

正文

刚开始使用dubbo的使用,使用了XML配置去配置服务:

表示了DemoService这个接口,依赖了domoService这个bean,表示我们使用DemoService这个接口去请求dubbo服务器,会调用DemoServiceImpl这个实现类去处理。

dubbo:service是dubbo自定义的xml方式,bean标签则是属于spring,而spring也提供了自定义xml解析方式,,检查一下dubbo-config-spring这个包。

可以看到dubbo.xsd文件,里面规定了dubbo-xml所有标签以及参数:

看spring.handlers内部填写的解析类

使用了DubboBeanDefinitionParser,继承spring的BeanDefinitionParser,用于xml的解析

这个类里面内容写得很混乱,把所有的配置项全融合在一起写,大体思路就是解析xml,根据传入的class类型作出不同的解析,然后将类实例化并且给set或者is开头的方法注入值;根据上一张图片可以看出,service配置是由ServiceBean这个类来解析的。

ServiceBean这个类的继承和实现接口关系如下:

继承了spring的一些接口都与bean相关,而真正有用的是ApplicationListener,这个接口是一个Listener接口,ContextRefreshedEvent表明了是在这个类初始化完毕时执行该方法

查看该方法:

在ServiceBean初始化完毕后,检查状态,然后开始暴露服务

export方法交给了它的父类去执行,ServiceBean只是作为一个bean存在,真正干活的是ServiceConfig类

可以看到这就是暴露服务的核心方法,相比于xml配置方法,还有一种使用API配置方法,看一下xml和api配置的对比:

1
2
3
4
java复制代码  ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"META-INF/spring/dubbo-demo-provider.xml"});
context.start();
ServiceConfig<DemoService> serviceServiceConfig = new ServiceConfig<DemoService>();
System.in.read(); // press any key to exit

这是xml配置方法的启动类

1
2
3
4
5
6
7
java复制代码ServiceConfig<DemoService> serviceConfig = new ServiceConfig<DemoService>();
serviceConfig.setApplication(new ApplicationConfig("app"));
serviceConfig.setRegistry(new RegistryConfig("zookpeer://127.0.0.1:2181"));
serviceConfig.setInterface(DemoService.class);
serviceConfig.setRef(new DemoServiceImpl());
serviceConfig.export();
System.in.read();

只需要执行export方法,就能将dubbo服务启动,说明ServiceConfig不仅做了服务暴露的事情,还把做了注册中心给一并添加了,先看export内部

image.png

同步方法,判断一些null值并获取,对延迟属性的判断,主要方法进入doExport

image.png

这一长段方法,主要是对监控、协议、注册中心、接口、实现类等的一些判断,然后进入 doExportUrls方法。

image.png

image.png

这里的protocols和registryURLs对应了xml上面的这两个部分,把注册中心配置转变成URL格式,其格式为:

image.png

主要是:ip-路径(类名)-param,param除了xml配置的一些属性外,还有dubbo版本,时间戳等。

在生成了注册中心URL和协议后,交给doExportUrlsFor1Protocol处理,
这个方法很长,前面很大一部分都是在构造URL,最后构造的URL如下所示:

image.png

根据不同协议和配置的参数,构造出这样一个URL,包含的信息全部在URL中。

最关键是这一段代码

image.png

image.png

proxyFactory是一个spi方法,默认使用JavassistProxyFactory,这里将上面的url的param和registryURL拼接在一起

image.png

这里将实现类用wrapper包装了一下,然后返回了一个AbstractProxyInvoker,内部调用了wrapper的invokeMethod方法,这个方法和反射类似

进入Wrapper内部

image.png

注意这个makeWrapper

使用Javassist,使用builder构造字节码,反编译出来是这样:

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
java复制代码package org.apache.dubbo.common.bytecode;

import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import org.apache.dubbo.common.bytecode.ClassGenerator.DC;
import org.apache.dubbo.demo.DemoService;

public class Wrapper0 extends Wrapper implements DC {
public static String[] pns;
public static Map pts;
public static String[] mns;
public static String[] dmns;
public static Class[] mts0;

public String[] getPropertyNames() {
return pns;
}

public boolean hasProperty(String var1) {
return pts.containsKey(var1);
}

public Class getPropertyType(String var1) {
return (Class)pts.get(var1);
}

public String[] getMethodNames() {
return mns;
}

public String[] getDeclaredMethodNames() {
return dmns;
}

public void setPropertyValue(Object var1, String var2, Object var3) {
try {
DemoService var4 = (DemoService)var1;
} catch (Throwable var6) {
throw new IllegalArgumentException(var6);
}

throw new NoSuchPropertyException("Not found property \"" + var2 + "\" filed or setter method in class org.apache.dubbo.demo.DemoService.");
}

public Object getPropertyValue(Object var1, String var2) {
try {
DemoService var3 = (DemoService)var1;
} catch (Throwable var5) {
throw new IllegalArgumentException(var5);
}

throw new NoSuchPropertyException("Not found property \"" + var2 + "\" filed or setter method in class org.apache.dubbo.demo.DemoService.");
}

public Object invokeMethod(Object var1, String var2, Class[] var3, Object[] var4) throws InvocationTargetException {
DemoService var5;
try {
var5 = (DemoService)var1;
} catch (Throwable var8) {
throw new IllegalArgumentException(var8);
}

try {
if ("sayHello".equals(var2) && var3.length == 1) {
return var5.sayHello((String)var4[0]);
}
} catch (Throwable var9) {
throw new InvocationTargetException(var9);
}

throw new NoSuchMethodException("Not found method \"" + var2 + "\" in class org.apache.dubbo.demo.DemoService.");
}

public Wrapper0() {
}
}

继承了Wrapper,实现了invokeMethod方法,根据方法的名称来反射调用方法,如果是多个方法那就是多个if操作而已

到目前为止,已经将需要暴露的服务实例包装起来了,下面就是将这个服务器暴露出去了。

image.png

所支持的协议如包名所见,默认为dubbo

image.png

调用Protocol的export方法暴露服务,默认使用dubbo

看DubboProtocol

image.png

主要是这个openServer方法

image.png

进入createServer

image.png

这里绑定了一个服务器和用于处理请求的hander

image.png

使用了tranposrt,属于dubbo-remoting层

image.png

tranposrt也是一个spi,默认使用netty,也可以跟换为其他比如http,根据xml配置构造成URL,选择不同的服务主要是在URL的param参数里面体现

image.png

打开一个nettyServer的方法,和经典netty方法差不多,主要做了一些封装。

到此为止,已经开启了一个netty服务器,绑定了指定端口号,并且将配置的service的实现类包装起来,通过netty服务器的请求,填入指定接口名称、方法名称以及参数,就可以获取invoker然后调用invokeMethod方法调用实现类的方法,实现了一次RPC

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…744745746…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%