Java 创建线程

1
2
3
4
5
6
public class thread {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("thread start"));
thread.start();
}
}

Java 同步工具

synchronized

Lock + Condition

Sem

创建不同类型的线程池: Executors 提供了多种工厂方法来创建 ExecutorService 实现,每种线程池适用于特定场景:

  • newFixedThreadPool(int nThreads):
    • 创建一个固定大小的线程池,维护指定数量的线程,适合需要限制线程数的任务。
    • 作用:确保线程资源可控,适用于长期运行的任务或负载均衡的场景。
  • newCachedThreadPool():
    • 创建一个可根据需要动态创建和回收线程的线程池,适合执行大量短期任务。
    • 作用:提高短任务的响应速度,但在高负载下可能创建过多线程。
  • newSingleThreadExecutor():
    • 创建一个单线程的线程池,按顺序执行任务,适合需要严格顺序执行的场景。
    • 作用:保证任务按提交顺序执行,类似单线程模型但支持异步提交。
  • newScheduledThreadPool(int corePoolSize):
    • 创建一个支持定时和周期性任务的线程池,适合定时任务或延迟执行。
    • 作用:用于调度定期任务,如定时刷新缓存或心跳检测。
  • newVirtualThreadPerTaskExecutor()(JDK 21 及以上):
    • 创建一个为每个任务分配一个虚拟线程的线程池,适合高并发的 I/O 密集型任务。
    • 作用:利用虚拟线程的轻量级特性,支持大规模并发,简化同步编程模型。
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
public class VirtualThreadExample {
public static void main(String[] args) throws InterruptedException {
// 方式 1: 使用 Thread.ofVirtual()
Thread.ofVirtual().start(() -> {
System.out.println("Virtual thread 1: " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});

// 方式 2: 使用 ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
System.out.println("Virtual thread 2: " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

// 方式 3: 使用 Thread.startVirtualThread()
Thread.startVirtualThread(() -> {
System.out.println("Virtual thread 3: " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});

// 等待任务完成
Thread.sleep(2000);
}
}

练习题

按序打印

信号量

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
class Foo {

private final Semaphore semaphore1 = new Semaphore(0);
private final Semaphore semaphore2 = new Semaphore(0);

public Foo() {

}

public void first(Runnable printFirst) throws InterruptedException {
// printFirst.run() outputs "first". Do not change or remove this line.
printFirst.run();
semaphore1.release(); // 释放第一个信号量,允许第二个线程执行
}

public void second(Runnable printSecond) throws InterruptedException {
semaphore1.acquire(); // 等待第一个线程完成
// printSecond.run() outputs "second". Do not change or remove this line.
printSecond.run();
semaphore2.release(); // 释放第二个信号量,允许第三个线程执行
}

public void third(Runnable printThird) throws InterruptedException {
semaphore2.acquire(); // 等待第二个线程完成
// printThird.run() outputs "third". Do not change or remove this line.
printThird.run();
semaphore2.release(); // 释放第三个信号量,允许其他线程执行
}
}

CountDownLatch

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
class Foo {

private final CountDownLatch firstLatch = new CountDownLatch(1);
private final CountDownLatch secondLatch = new CountDownLatch(1);

public Foo() {

}

public void first(Runnable printFirst) throws InterruptedException {

// printFirst.run() outputs "first". Do not change or remove this line.
try {
printFirst.run();
} finally {
firstLatch.countDown();
}
}

public void second(Runnable printSecond) throws InterruptedException {
firstLatch.await();
try {
// printSecond.run() outputs "second". Do not change or remove this line.
printSecond.run();
} finally {
secondLatch.countDown();
}
}

public void third(Runnable printThird) throws InterruptedException {
secondLatch.await();
// printThird.run() outputs "third". Do not change or remove this line.
printThird.run();

}
}

安装

在nextjs框架中,通过npm下载video.js依赖包

1
2
npm install video.js
npm install @types/video.js

使用

将下面两个文件写入项目后,可以直接导入VideoPlayer来使用自定义的video.js

注:视频大小为16:9的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// page.tsx
import VideoPlayer from "./components/VideoPlayer";

export default function Home() {
const videoJsOptions = {
autoplay: true,
controls: true,
sources: [
{
src: "http://192.168.101.67:9000/%E8%AF%9B%E4%BB%994K/52.mp4",
type: "video/mp4",
},
],
};
return (
<div className="w-[800px] aspect-video">
<VideoPlayer options={videoJsOptions} />
</div>
);
}

TSX 文件

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
"use client";
import { useEffect, useRef } from "react";
// 引入 video.js 及样式
import videojs from "video.js";
import "video.js/dist/video-js.css";
import "@videojs/themes/dist/forest/index.css";
import "./custom-videojs.css";

export interface VideoJSProps {
options: {
autoplay?: boolean;
controls?: boolean;
responsive?: boolean;
sources: { src: string; type: string }[];
};
onReady?: (player: any) => void;
}

export const VideoPlayer: React.FC<VideoJSProps> = ({ options, onReady }) => {
const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<any>(null);

useEffect(() => {
if (!playerRef.current) {
// 创建 video 元素并插入到容器中
const videoElement = document.createElement("video-js");
videoElement.classList.add(
"vjs-fluid",
"vjs-responsive",
"vjs-theme-forest",
"vjs-16-9"
);
videoRef.current?.appendChild(videoElement);

// 注册回退 10 秒按钮
class Rewind10Button extends videojs.getComponent("Button") {
buildCSSClass() {
return "vjs-control vjs-button vjs-rewind-10-button";
}

handleClick() {
const player = this.player_;
if (player && !player.isDisposed()) {
player.currentTime(
Math.max(player.currentTime() - 10, 0)
);
}
}
}
// 注册快进 10 秒按钮
class Forward10Button extends videojs.getComponent("Button") {
buildCSSClass() {
return "vjs-control vjs-button vjs-forward-10-button"; // 自定义类名
}

handleClick() {
const player = this.player_;
if (player && !player.isDisposed()) {
const duration = player.duration();
player.currentTime(
Math.min(player.currentTime() + 10, duration)
);
}
}
}
// 注册 play 按钮
class PlayButton extends videojs.getComponent("Button") {
constructor(player: any, options: any) {
super(player, options);
// 初始化按钮状态
this.updateClasses();
// 监听播放器事件以更新按钮状态
this.player_.on("play", () => this.updateClasses());
this.player_.on("pause", () => this.updateClasses());
}

buildCSSClass() {
// 确保按钮包含默认的播放控制类和自定义类
return "vjs-control vjs-button vjs-play-button";
}

handleClick() {
const player = this.player_;
if (player && !player.isDisposed()) {
if (player.paused()) {
player.play();
} else {
player.pause();
}
}
}

updateClasses() {
// 直接使用 Video.js 默认的类切换逻辑
if (this.player_ && !this.player_.isDisposed()) {
this.removeClass("vjs-playing");
this.removeClass("vjs-paused");
if (this.player_.paused()) {
this.addClass("vjs-paused");
} else {
this.addClass("vjs-playing");
}
}
}
}
// 注册音量按钮
class VolumeButton extends videojs.getComponent("Button") {
// 声明类属性
private sliderContainer: HTMLElement | null = null;
private volumeSlider: HTMLElement | null = null;
private volumeBar: HTMLElement | null = null;

private defaultVolume = 0.3;

constructor(player: any, options: any) {
super(player, options);
this.initializePlayer();
// 初始化按钮状态
this.updateClasses();
// 监听音量变化和静音状态变化
this.player_.on("volumechange", () => this.updateClasses());
// 创建音量滑块
this.createVolumeSlider();
// 监听鼠标悬停事件
this.on("mouseenter", () => this.showVolumeSlider());
this.on("mouseleave", () => this.hideVolumeSlider());
}

buildCSSClass() {
return "vjs-control vjs-button vjs-volume-button";
}

handleClick() {
const player = this.player_;
if (player && !player.isDisposed()) {
// 切换静音状态
if (player.muted()) {
player.muted(false);
const lastVolume = player.lastVolume_() || 0.3;
player.volume(lastVolume);
} else {
console.log("click muted handle");
player.muted(true);
}
}
}

// 初始化播放器状态
initializePlayer() {
if (this.player_ && !this.player_.isDisposed()) {
// 设置初始音量为30%
this.player_.volume(this.defaultVolume);
// 设置为静音状态
this.player_.muted(true);
// 保存默认音量到 lastVolume
this.player_.lastVolume_(this.defaultVolume);
}
}

updateClasses() {
if (this.player_ && !this.player_.isDisposed()) {
this.removeClass("vjs-vol-0");
this.removeClass("vjs-vol-1");
this.removeClass("vjs-vol-2");
this.removeClass("vjs-vol-3");
const vol = this.player_.volume();
const muted = this.player_.muted();
let level = 3;
if (muted || vol === 0) {
level = 0;
} else if (vol < 0.33) {
level = 1;
} else if (vol < 0.67) {
level = 2;
}
this.addClass(`vjs-vol-${level}`);
}
}

createVolumeSlider() {
// 创建音量滑块容器
this.sliderContainer = document.createElement("div");
this.sliderContainer.className =
"vjs-volume-slider-container vjs-hidden";
this.el().appendChild(this.sliderContainer);

// 创建音量滑块
this.volumeSlider = videojs.dom.createEl("div", {
className: "vjs-volume-slider-down",
}) as HTMLElement;

// 创建滑块条
this.volumeBar = videojs.dom.createEl("div", {
className: "vjs-volume-bar-down",
}) as HTMLElement;

this.volumeSlider.appendChild(this.volumeBar);
this.sliderContainer.appendChild(this.volumeSlider);

// 初始化滑块
this.updateSlider();

// 绑定滑块事件
this.volumeSlider.addEventListener("mousedown", (e) =>
this.handleSliderInteraction(e)
);
this.volumeSlider.addEventListener("click", (e) =>
e.stopPropagation()
);
this.player_.on("volumechange", () => this.updateSlider());
}

updateSlider() {
if (
this.player_ &&
!this.player_.isDisposed() &&
this.volumeBar
) {
const volume = this.player_.muted()
? 0
: this.player_.volume();
// 更新滑块条的高度(垂直滑块)
this.volumeBar.style.height = `${volume * 100}%`;
}
}

handleSliderInteraction(e: MouseEvent) {
e.stopPropagation();

if (
this.volumeSlider &&
this.player_ &&
!this.player_.isDisposed()
) {
const rect = this.volumeSlider.getBoundingClientRect();
const updateVolume = (event: MouseEvent) => {
const y = event.clientY - rect.top;
const height = rect.height;
let newVolume = Math.max(
0,
Math.min(1, 1 - y / height)
);
this.player_.muted(false);
this.player_.volume(newVolume);
};
updateVolume(e);
const onMouseMove = (event: MouseEvent) =>
updateVolume(event);
const onMouseUp = () => {
document.removeEventListener(
"mousemove",
onMouseMove
);
document.removeEventListener("mouseup", onMouseUp);
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
}
}

showVolumeSlider() {
if (this.sliderContainer) {
this.sliderContainer.classList.remove("vjs-hidden");
}
}

hideVolumeSlider() {
if (this.sliderContainer) {
this.sliderContainer.classList.add("vjs-hidden");
}
}

dispose() {
if (this.volumeSlider) {
this.volumeSlider.removeEventListener(
"mousedown",
this.handleSliderInteraction
);
this.volumeSlider.removeEventListener("click", (e) =>
e.stopPropagation()
);
}
super.dispose();
}
}
// 注册设置按钮
class settingButton extends videojs.getComponent("Button") {
private selectContainer: HTMLElement | null = null;
private mainContainer: HTMLElement | null = null;
private speedTitleElement: HTMLElement | null = null;
private qualityTitleElement: HTMLElement | null = null;
private speedContainer: HTMLElement | null = null;
private speedElement: HTMLElement | null = null;
private qualityContainer: HTMLElement | null = null;
private qualityElement: HTMLElement | null = null;

constructor(player: any, options: any) {
super(player, options);
this.createSelectContainer(); // 在构造函数中调用
}

buildCSSClass() {
return "vjs-control vjs-button vjs-setting-button";
}

handleClick() {
const player = this.player_;
if (player && !player.isDisposed()) {
if (this.selectContainer) {
this.selectContainer.classList.toggle("vjs-hidden");
this.initSettingView();
}
}
}

// 设置点击外部关闭的处理函数
documentClickHandler = (event: Event) => {
if (
this.selectContainer &&
!this.selectContainer.contains(event.target as Node) &&
!this.el().contains(event.target as Node)
) {
this.selectContainer.classList.add("vjs-hidden");
}
};

createSelectContainer() {
this.selectContainer = document.createElement("div");
this.selectContainer.className =
"vjs-setting-container vjs-hidden";
this.el().appendChild(this.selectContainer);

this.createMainContainer();
this.createSpeedContainer();
this.createQualityContainer();

this.selectContainer.appendChild(this.mainContainer!);
this.selectContainer.appendChild(this.speedContainer!);
this.selectContainer.appendChild(this.qualityContainer!);

this.initSettingView();

// 点击外部关闭选择容器
document.addEventListener(
"click",
this.documentClickHandler
);
}

createMainContainer() {
this.mainContainer = videojs.dom.createEl("div", {
className: "vjs-setting-main-container",
}) as HTMLElement;

// setting 按钮的点击事列表
// Quality
this.qualityTitleElement = videojs.dom.createEl("div", {
className: "vjs-setting-title",
innerHTML: `清晰度 1080P`,
}) as HTMLElement;
this.qualityTitleElement.addEventListener(
"click",
(e: Event) => {
e.stopPropagation();
if (this.mainContainer && this.qualityContainer) {
this.mainContainer.style.display = "none";
this.qualityContainer.style.display = "block";
}
}
);
this.mainContainer.appendChild(this.qualityTitleElement);

// speed
this.speedTitleElement = videojs.dom.createEl("div", {
className: "vjs-setting-title",
innerHTML: `倍速 ${this.player_.playbackRate()}x`,
}) as HTMLElement;
this.speedTitleElement.addEventListener(
"click",
(e: Event) => {
e.stopPropagation();
if (this.mainContainer && this.speedContainer) {
this.mainContainer.style.display = "none";
this.speedContainer.style.display = "block";
}
}
);
this.mainContainer.appendChild(this.speedTitleElement);
}

createSpeedContainer() {
// 倍速设置
this.speedContainer = videojs.dom.createEl("div", {
className: "vjs-setting-speed-container",
}) as HTMLElement;
this.speedElement = videojs.dom.createEl("div", {
className: "vjs-setting-select-element",
}) as HTMLElement;

const rates = [0.5, 1, 1.5, 2, 3];
const currentRate = this.player_.playbackRate();
rates.forEach((rate) => {
const option = videojs.dom.createEl("div", {
className: "vjs-rate-option",
innerHTML: `${rate}x`,
}) as HTMLElement;

option.addEventListener("click", (e: Event) => {
e.stopPropagation();
this.setPlaybackRate(rate);
});
this.speedElement?.appendChild(option);
});
this.setPlaybackRate(currentRate);

this.speedContainer.appendChild(this.speedElement);

this.bindPlayerEvents(); // 绑定播放器事件
}

createQualityContainer() {
// 清晰度设置
this.qualityContainer = videojs.dom.createEl("div", {
className: "vjs-setting-quality-container",
}) as HTMLElement;
this.qualityElement = videojs.dom.createEl("div", {
className: "vjs-setting-select-element",
}) as HTMLElement;
const qualities = [
"4k",
"2K",
"1080P",
"720P",
"480P",
"360P",
];
// const currentQuality = this.player_.qualityLevel();
qualities.forEach((quality) => {
const option = videojs.dom.createEl("div", {
className: "vjs-rate-option",
innerHTML: quality,
}) as HTMLElement;
this.qualityElement?.appendChild(option);
});
this.qualityContainer.appendChild(this.qualityElement);
}

initSettingView() {
if (this.mainContainer)
this.mainContainer.style.display = "block";
if (this.speedContainer)
this.speedContainer.style.display = "none";
if (this.qualityContainer)
this.qualityContainer.style.display = "none";
}

setPlaybackRate(rate: number) {
// 更新播放器倍速
this.player_.playbackRate(rate);
// 更新选中状态
this.updateSelectedOption(rate);
// 更新主容器中的倍速显示文本
this.updateSpeedTitle(rate);
this.initSettingView();
}

// 新增方法:更新主容器中的倍速显示文本
updateSpeedTitle(rate: number) {
if (this.speedTitleElement) {
this.speedTitleElement.innerHTML = `倍速 ${rate}x`;
}
}

updateSelectedOption(selectedRate: number) {
if (this.speedElement) {
const options =
this.speedElement.querySelectorAll(
".vjs-rate-option"
);
options.forEach((option) => {
const rate = parseFloat(
(option as HTMLElement).innerText.replace(
"x",
""
)
);
if (rate === selectedRate) {
option.classList.add("vjs-rate-selected");
} else {
option.classList.remove("vjs-rate-selected");
}
});
}
}

bindPlayerEvents() {
// 监听播放器倍速变化事件(可能由其他方式改变)
this.player_.on("ratechange", () => {
const currentRate = this.player_.playbackRate();
this.updateSelectedOption(currentRate);
});
}

dispose() {
// 清理事件监听器
if (this.documentClickHandler) {
document.removeEventListener(
"click",
this.documentClickHandler
);
}

// 调用父类的 dispose 方法
super.dispose();
}
}

videojs.registerComponent("Rewind10Button", Rewind10Button);
videojs.registerComponent("Forward10Button", Forward10Button);
videojs.registerComponent("PlayButton", PlayButton);
videojs.registerComponent("VolumeButton", VolumeButton);
videojs.registerComponent("SettingButton", settingButton);

const player = videojs(
videoElement,
{
...options,
controlBar: {
children: [
"rewind10Button",
"playButton",
"forward10Button",
"progressControl",
"currentTimeDisplay",
"volumeButton",
"settingButton",
"pictureInPictureToggle",
"fullscreenToggle",
],
},
},
() => {
console.log("player is ready");
playerRef.current = player;
onReady && onReady(player);
}
);

playerRef.current = player;
} else {
const player = playerRef.current;
player.autoplay(options.autoplay || false);
player.src(options.sources);
}
}, [options, videoRef, onReady]);

// 组件卸载时销毁播放器
useEffect(() => {
return () => {
if (playerRef.current && !playerRef.current.isDisposed()) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, []);

return (
<div data-vjs-player className="rounded-2xl overflow-hidden">
<div ref={videoRef} />
</div>
);
};

export default VideoPlayer;

CSS 文件

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
/* custom-videojs.css */

.vjs-theme-forest {
--custom-theme-color: #34495e;
}

/* 确保 Video.js 字体加载 */
@font-face {
font-family: "VideoJS";
src: url("https://vjs.zencdn.net/font/VideoJS.woff") format("woff");
font-weight: normal;
font-style: normal;
}

/* ********************************************************** 按钮图标 ********************************************************** */

/* 后退 10 秒按钮图标 */
.vjs-theme-forest .vjs-rewind-10-button .vjs-icon-placeholder::before {
content: "\f11d"; /* Video.js 图标字体中的后退图标 */
font-family: "VideoJS";
font-size: 1.5em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
}

/* 快进 10 秒按钮图标 */
.vjs-theme-forest .vjs-forward-10-button .vjs-icon-placeholder::before {
content: "\f120"; /* Video.js 图标字体中的快进图标 */
font-family: "VideoJS";
font-size: 1.5em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
}

/* 播放按钮 默认样式(暂停状态显示播放图标) */
.vjs-theme-forest .vjs-play-button.vjs-paused .vjs-icon-placeholder::before {
content: "\f101"; /* Video.js 播放图标 */
font-family: "VideoJS";
font-size: 2em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
}

/* 播放按钮 暂停图标 */
.vjs-theme-forest .vjs-play-button.vjs-playing .vjs-icon-placeholder::before {
content: "\f103"; /* Video.js 暂停图标 */
font-family: "VideoJS";
font-size: 2em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
}

/* 音量按钮 - 3级音量图标 */
.vjs-theme-forest .vjs-volume-button.vjs-vol-3 .vjs-icon-placeholder::before {
content: "\f107"; /* Video.js 音量图标 */
font-family: "VideoJS";
font-size: 1.7em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 音量按钮 - 2级音量图标 */
.vjs-theme-forest .vjs-volume-button.vjs-vol-2 .vjs-icon-placeholder::before {
content: "\f106"; /* Video.js 音量图标 */
font-family: "VideoJS";
font-size: 1.7em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 音量按钮 - 1级音量图标 */
.vjs-theme-forest .vjs-volume-button.vjs-vol-1 .vjs-icon-placeholder::before {
content: "\f105"; /* Video.js 音量图标 */
font-family: "VideoJS";
font-size: 1.7em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 音量按钮 - 静音图标 */
.vjs-theme-forest .vjs-volume-button.vjs-vol-0 .vjs-icon-placeholder::before {
content: "\f104"; /* Video.js 静音图标 */
font-family: "VideoJS";
font-size: 1.7em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
}

/* 音量按钮 - 3级音量图标 */
.vjs-theme-forest
.vjs-picture-in-picture-control
.vjs-icon-placeholder::before {
content: "\f127"; /* Video.js 图标字体中的画中画图标 */
font-family: "VideoJS";
font-size: 1.5em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
}

/* 全屏按钮 */
.vjs-theme-forest .vjs-fullscreen-control .vjs-icon-placeholder::before {
content: "\f108"; /* Video.js 全屏图标 */
font-family: "VideoJS";
font-size: 1.5em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
}

/* 设置按钮 */
.vjs-theme-forest .vjs-setting-button:before {
content: "\f114";
font-family: "VideoJS";
font-size: 1.5em;
line-height: inherit;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
}

/* ********************************************************** 设置样式 ********************************************************** */

.vjs-theme-forest .vjs-setting-container {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
width: 60px;
background: transparent; /* 白色半透明背景,透明度 0.2 */
/* 应用磨砂效果 */
backdrop-filter: blur(10px); /* 模糊半径为 10px */
border-radius: 4px;
padding: 0;
z-index: 100;
}
.vjs-theme-forest .vjs-setting-main-container {
position: relative;
bottom: 5px;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.2); /* 白色半透明背景,透明度 0.2 */
/* 应用磨砂效果 */
backdrop-filter: blur(10px); /* 模糊半径为 10px */
border-radius: 4px;
}
.vjs-theme-forest .vjs-setting-title {
color: #ecf0f1; /* 浅色标题 */
font-size: 1em;
text-align: center;
padding: 7px 0;
width: 100%;
}
.vjs-theme-forest .vjs-setting-title:hover {
background: var(--custom-theme-color);
border-radius: 4px;
}

.vjs-theme-forest .vjs-setting-speed-container {
position: relative;
left: 20%;
bottom: 5px;
width: 60%;
height: 100%;
background: rgba(255, 255, 255, 0.2); /* 白色半透明背景,透明度 0.2 */
/* 应用磨砂效果 */
backdrop-filter: blur(10px); /* 模糊半径为 10px */
border-radius: 1px;
cursor: pointer;
border-radius: 4px;
}

.vjs-theme-forest .vjs-setting-container:not(.vjs-hidden) {
display: block;
}
.vjs-theme-forest .vjs-rate-option {
padding: 5px 0;
cursor: pointer;
text-align: center;
}
.vjs-theme-forest .vjs-rate-option:hover {
background: var(--custom-theme-color);
border-radius: 4px;
}

.vjs-theme-forest .vjs-rate-selected {
background: var(--custom-theme-color);
border-radius: 4px;
}

/* ********************************************************** 音量容器 ********************************************************** */

/* 音量滑块容器 */
.vjs-theme-forest .vjs-volume-slider-container {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 100px;
background: transparent;
border-radius: 4px;
padding: 0;
display: none;
z-index: 100;
}

/* 显示滑块 */
.vjs-theme-forest .vjs-volume-slider-container:not(.vjs-hidden) {
display: block;
}

/* 音量滑块 */
.vjs-theme-forest .vjs-volume-slider-down {
position: relative;
bottom: 5px;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.2); /* 白色半透明背景,透明度 0.2 */
/* 应用磨砂效果 */
backdrop-filter: blur(10px); /* 模糊半径为 10px */
border-radius: 1px;
cursor: pointer;
border-radius: 4px;
}

/* 音量滑块条 */
.vjs-theme-forest .vjs-volume-bar-down {
position: absolute;
bottom: 0;
width: 100%;
background: var(--custom-theme-color);
border-radius: 1px;
transition: height 0.1s;
border-radius: 4px;
}

/* ********************************************************** 通用设置 ********************************************************** */

/* 按钮通用样式,适用于所有控制栏按钮 */
.vjs-theme-forest .vjs-rewind-10-button,
.vjs-theme-forest .vjs-forward-10-button,
.vjs-theme-forest .vjs-play-button,
.vjs-theme-forest .vjs-volume-button,
.vjs-theme-forest .vjs-setting-button,
.vjs-theme-forest .vjs-picture-in-picture-control,
.vjs-theme-forest .vjs-fullscreen-control {
background-color: transparent !important;
color: #ecf0f1 !important; /* 浅色图标 */
width: 30px;
height: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
}

/* 按钮悬停时的样式 */
.vjs-theme-forest .vjs-play-button:hover,
.vjs-theme-forest .vjs-rewind-10-button:hover,
.vjs-theme-forest .vjs-forward-10-button:hover,
.vjs-theme-forest .vjs-volume-button:hover,
.vjs-theme-forest .vjs-setting-button:hover,
.vjs-theme-forest .vjs-picture-in-picture-control:hover,
.vjs-theme-forest .vjs-fullscreen-control:hover {
background-color: var(--custom-theme-color) !important; /* 悬停时的颜色 */
border-radius: 4px; /* 圆角 */
}

/* 防止点击按钮时出现光标 */
.vjs-theme-forest .vjs-play-button:focus,
.vjs-theme-forest .vjs-play-button:focus-visible,
.vjs-theme-forest .vjs-rewind-10-button:focus,
.vjs-theme-forest .vjs-rewind-10-button:focus-visible,
.vjs-theme-forest .vjs-forward-10-button:focus,
.vjs-theme-forest .vjs-forward-10-button:focus-visible,
.vjs-theme-forest .vjs-volume-button:focus,
.vjs-theme-forest .vjs-volume-button:focus-visible,
.vjs-theme-forest .vjs-picture-in-picture-control:focus,
.vjs-theme-forest .vjs-picture-in-picture-control:focus-visible,
.vjs-theme-forest .vjs-fullscreen-control:focus,
.vjs-theme-forest .vjs-fullscreen-control:focus-visible,
.vjs-theme-forest.vjs-fullscreen .vjs-play-button:focus,
.vjs-theme-forest.vjs-fullscreen .vjs-play-button:focus-visible,
.vjs-theme-forest.vjs-fullscreen .vjs-rewind-10-button:focus,
.vjs-theme-forest.vjs-fullscreen .vjs-rewind-10-button:focus-visible,
.vjs-theme-forest.vjs-fullscreen .vjs-forward-10-button:focus,
.vjs-theme-forest.vjs-fullscreen .vjs-forward-10-button:focus-visible,
.vjs-theme-forest.vjs-fullscreen .vjs-volume-button:focus,
.vjs-theme-forest.vjs-fullscreen .vjs-volume-button:focus-visible,
.vjs-theme-forest.vjs-fullscreen .vjs-picture-in-picture-control:focus,
.vjs-theme-forest.vjs-fullscreen .vjs-picture-in-picture-control:focus-visible,
.vjs-theme-forest.vjs-fullscreen .vjs-fullscreen-control:focus,
.vjs-theme-forest.vjs-fullscreen .vjs-fullscreen-control:focus-visible {
outline: none;
border: none; /* 防止可能的边框闪烁 */
box-shadow: none; /* 防止可能的阴影效果 */
user-select: none;
cursor: default;
}

/* 时间标签 样式 */
.vjs-theme-forest .vjs-current-time,
.vjs-theme-forest .vjs-time-divider,
.vjs-theme-forest .vjs-duration {
display: inline-block;
padding: 0;
}

/* 确保 vjs-icon-placeholder 填充整个按钮并居中 */
.vjs-theme-forest .vjs-rewind-10-button .vjs-icon-placeholder,
.vjs-theme-forest .vjs-forward-10-button .vjs-icon-placeholder,
.vjs-theme-forest .vjs-play-button .vjs-icon-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
position: relative;
top: 0;
left: 0;
margin: 0;
padding: 0;
}

/* 控件顺序 */
.vjs-theme-forest .vjs-rewind-10-button {
order: 0;
}
.vjs-theme-forest .vjs-play-button {
order: 1;
}
.vjs-theme-forest .vjs-forward-10-button {
order: 2;
}
.vjs-theme-forest .vjs-progress-control {
order: 3;
}
.vjs-theme-forest .vjs-current-time {
order: 4;
}
.vjs-theme-forest .vjs-time-divider {
order: 5;
}
.vjs-theme-forest .vjs-duration {
order: 6;
}
.vjs-theme-forest .vjs-volume-button {
order: 7;
}
.vjs-theme-forest .vjs-setting-button {
order: 8;
}
.vjs-theme-forest .vjs-picture-in-picture-control {
width: 30px;
order: 9;
}
.vjs-theme-forest .vjs-fullscreen-control {
width: 30px;
order: 10;
}

某些国家对网络进行了封锁,需要通过代理来绕过封锁,某些软件可以直接使用操作系统的代理,只需要设置操作系统的代理即可,但某些应用,尤其是命令行工具需要单独设置代理。

代理的设置取决于应用层的协议,比如HTTPS代理,SSH代理等。本地代理软件如v2ray等一般提供HTTP,HTTPS,SOCKS代理协议。下图是ubuntu24的系统代理设置(其中本地代理端口为127.0.0.1:7897)。

image-20250512163458271

设置命令行代理

1
sudo vim ~/.bashrc

Add proxy configuration in ~/.bashrc:

1
2
export HTTP_PROXY="<http://127.0.0.1:7897>"
export HTTPS_PROXY="<http://127.0.0.1:7897>"

GIT 设置代理

在使用GIT时,会用到HTTPS或者SSH协议来下载上传文件,在GIT中两种协议需要分别设置:

全局代理HTTPS:

  • 使用http代理:git config --global http.proxy http://127.0.0.1:58591

  • 使用socks5代理:git config --global http.proxy socks5://127.0.0.1:51837

只对Github代理HTTPS:git config --global http.https://github.com.proxy socks5://127.0.0.1:51837

全局代理SSH:

1
vim ~/.ssh/config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ProxyCommand nc -v -x 127.0.0.1:7897 %h %p

Host github.com
User git
Port 443
Hostname ssh.github.com
IdentityFile "/home/amber/.ssh/id_rsa"
TCPKeepAlive yes

Host ssh.github.com
User git
Port 443
Hostname ssh.github.com
IdentityFile "/home/amber/.ssh/id_rsa"
TCPKeepAlive yes

其中ProxyCommand nc -v -x 127.0.0.1:7897 %h %p中的地址为本地代理地址,IdentityFile "/home/amber/.ssh/id_rsa"时SSH密钥地址

Set npm proxy

Prerequisite: Set a proxy such as clash at localhost or remote server.

1
2
3
4
5
6
npm config set https-proxy <http://id:pass@proxy.example.com>:port
npm config set proxy <http://id:pass@proxy.example.com>:port

# for example
npm config set https-proxy <http://127.0.0.1:7897>
npm config set proxy <http://127.0.0.1:7897>

Set Electron proxy for download

1
sudo vim ~/.bashrc

Add GLOBAL_AGENT_HTTPS_PROXY to ~/.bashrc

1
2
3
# for electron
export ELECTRON_GET_USE_PROXY='true'
export GLOBAL_AGENT_HTTPS_PROXY="<http://127.0.0.1:7897>"

SpringMVC 是基于 Servlet 为核心,核心组件为DispatcherServlet。其基于Tomcat或Jetty或Undertow或WebSphere。

模型(Model):模型表示应用程序的数据、业务逻辑和规则。它是系统的核心,负责管理数据、状态以及与数据库或外部服务的交互。

视图(View):视图负责向用户呈现数据,是用户与应用程序交互的界面。

控制器(Controller):控制器是模型和视图之间的中介,负责处理用户输入并协调模型与视图的交互。

工作原理:

  1. 用户通过视图(如点击按钮、提交表单)与应用程序交互。
  2. 控制器接收用户输入,解析请求,并调用相应的模型进行数据处理。
  3. 模型执行必要的业务逻辑,更新数据状态,并通知视图需要更新。
  4. 视图根据模型的最新数据重新渲染,呈现给用户。
  5. 循环重复此过程,保持用户界面的动态更新。

SpringBoot 加载 DispatcherServlet

若按照XML的形式配置文件,需要在Tomcat中的web.xml中添加DispatcherServlet

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<!-- 配置 DispatcherServlet -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 指定 Spring MVC 配置文件路径(可选) -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-mvc-config.xml</param-value>
</init-param>
<!-- 启动时加载 Servlet -->
<load-on-startup>1</load-on-startup>
</servlet>

<!-- 映射 DispatcherServlet 处理的 URL 模式 -->
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

</web-app>

此XML配置下,所有的请求都会被传递给DispatcherServlet处理。默认 DispatcherServlet 会加载 WEB-INF/[DispatcherServlet的Servlet名字]-servlet.xml 配 置 文 件 。 本 示 例 为 /WEB-INF/spring-mvc-config.xml 其中会注册一些bean类。

若按照Java注解配置,SpringBoot会根据自动配置将DispatcherServlet传递给内置Tomcat

在springboot启动过程中,会处理自动配置注解@EnableAutoConfiguration,其中通过@Import(AutoConfigurationImportSelector.class)注解执行AutoConfigurationImportSelector.classselectImports()方法。在此方法中会将路径中所有的org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中的配置类读入。其中与MVC的有关的主要类是org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfigurationorg.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfigurationorg.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration

其中

  • WebMvcAutoConfiguration中存储Spring Web MVC的一些默认配置;

  • DispatcherServletAutoConfiguration中注册两个bean类DispatcherServletDispatcherServletRegistrationBean 后者中会注入前者,并在Servlet容器(如Tomcat)启动时会将前者的Sevlet类自动加载。即在内置Servlet容器时会自动执行DispatcherServletRegistrationBean的父类方法ServletContextInitializeronStartup()方法,将DispatcherServlet注册到Servlet容器中;

    SpringBoot启动过程中,因为是 web servlet 所以执行 ServletWebServerApplicationContext.onRefresh().createWebServer() 其中会调用TomcatServletWebServerFactory.getWebServer() 其中会创建 Tomcat类并在prepareContext(tomcat.getHost(), initializers);中将SpringBoot中的ServletContextInitializer注册进Tomcat中。

  • ServletWebServerFactoryAutoConfiguration会将SpringBoot支持的Servlet容器根据条件自动导入,包括EmbeddedTomcat EmbeddedJetty EmbeddedUndertow 然后注册两个bean类ServletWebServerFactoryCustomizerServletWebServerFactoryCustomizer

DispatcherServlet 处理请求

DispatcherServlet主要组成:

  • HandlerMapping:映射请求
  • HandlerAdapter:执行控制器
  • ViewResolver:用于解析视图(如Thymeleaf 或 JSP)。
  • HttpMessageConverter:支持JSON/XML的序列化和反序列化

当DispatcherServlet注册到Tomcat中后,每当请求进入后会自动执行DispatcherServlet的Service()方法,在其父类FrameworkServlet

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

if (HTTP_SERVLET_METHODS.contains(request.getMethod())) {
super.service(request, response);
}
else {
processRequest(request, response);
}
}

最终都会执行processRequest(request, response)方法,其中会执行doService(request, response)方法,DispatcherServlet.doService()会执行DispatcherServlet.doDispatch(request, response)

主要步骤有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
try {
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
applyDefaultViewName(processedRequest, mv);
}
catch (Exception ex) {
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
}
}

  • mappedHandler = getHandler(processedRequest); 通过handlerMappings寻找符合条件的handler(即Controller)
  • getHandlerAdapter(mappedHandler.getHandler()); 找到适配handler的适配器HandlerAdapter
  • ha.handle(processedRequest, response, mappedHandler.getHandler());handler 传入适配的HandlerAdapter并执行其hanlder()方法,适配器调用处理器方法(如 @RequestMapping 方法),生成 ModelAndView
  • processDispatchResult():通过 ViewResolver 解析视图 和 统一处理异常,生成错误响应。

整体处理流程:

  1. Servlet 容器(Tomcat)的请求转发:
  • Tomcat 接收请求:客户端请求首先由 Tomcat 的 Connector 接收,解析 HTTP 协议后生成 HttpServletRequestHttpServletResponse 对象。

  • Servlet 匹配:Tomcat 根据 URL 映射规则,将请求转发到对应的 Servlet(此处为 DispatcherServlet)。

  1. DispatcherServlet 的初始化流程:
  • service() 方法:Tomcat 调用 DispatcherServletservice() 方法(继承自 HttpServlet)。

  • doService() 方法DispatcherServlet 重写了 doService(),在其中初始化一些上下文(如 LocaleContextRequestAttributes),然后调用 doDispatch()

  1. doDispatch() 的核心逻辑:

    doDispatch() 负责以下关键步骤:

    1. 处理 Multipart 请求(如文件上传)。
    2. 获取处理器链HandlerExecutionChain),包含目标处理器和拦截器。
    3. **调用拦截器的 preHandle()**。
    4. 执行处理器方法(如 @Controller 中的方法),生成 ModelAndView
    5. **调用拦截器的 postHandle()**。
    6. 处理结果(渲染视图或处理异常)。
  2. 视图渲染与响应写入

  • **processDispatchResult()**:在 doDispatch() 的末尾,调用此方法处理视图渲染或异常。
    • 视图渲染:通过 ViewResolver 解析视图,调用 View.render() 将模型数据写入响应。
    • 异常处理:通过 HandlerExceptionResolver 生成错误响应。

请求类型

GET

1
@RequestMapping(value="/create", method = RequestMethod.GET)
1
@GetMapping("/create")

POST

1
@RequestMapping(value="/create", method = RequestMethod.POST)
1
@PostMapping("/create")

其他请求类型限制:

1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/user", params = "id") // 必须有id参数
@RequestMapping(value = "/user", params = "!id") // 不能有id参数
@RequestMapping(value = "/user", params = "id=123") // id参数必须为123

@RequestMapping(value = "/user", headers = "content-type=text/*") // 匹配特定Content-Type
@RequestMapping(value = "/user", headers = "!X-Custom-Header") // 不能包含特定头

@RequestMapping(value = "/user", consumes = "application/json") // 只处理Content-Type为JSON的请求
@RequestMapping(value = "/user", produces = "application/json") // 只产生JSON响应

请求数据格式

Form格式

前端代码

1
2
3
4
5
6
7
<form action="/login" method="post">
<label for="username">账号:</label>
<input type="text" id="username" name="username" required>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
<button type="submit">登录</button>
</form>

传输数据

1
2
3
4
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=user123&password=pass123

后端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/login_form")
public ResponseEntity<String> loginForm(@ModelAttribute LoginRequest loginRequest) {
String username = loginRequest.getUsername();
String password = loginRequest.getPassword();

// 模拟验证逻辑(实际中应调用服务层验证)
if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) {
// 假设验证通过
return ResponseEntity.success("登录成功,用户: " + username);
} else {
return ResponseEntity.badRequest("账号或密码无效");
}
}

JSON格式

JavaScript代码

1
2
3
4
5
6
7
8
9
10
11
12
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'user123',
password: 'pass123'
})
})
.then(response => response.text())
.then(data => console.log(data));

传输数据

1
2
3
4
POST /login HTTP/1.1
Content-Type: application/json

{"username":"user123","password":"pass123"}

后端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
String username = loginRequest.getUsername();
String password = loginRequest.getPassword();

// 模拟验证逻辑(实际中应调用服务层验证)
if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) {
// 假设验证通过
return ResponseEntity.success("登录成功,用户: " + username);
} else {
return ResponseEntity.badRequest("账号或密码无效");
}
}

请求路径格式

普通 URL 路径映射

@RequestMapping(value={"/test1", "/user/create"})

URI 模板模式映射

  • @RequestMapping(value="/users/{userId}")
  • @RequestMapping(value="/users/{userId}/create")
  • @RequestMapping(value="/users/{userId}/topics/{topicId}")

需要通过@PathVariable来获取URL中参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/api")
public class ExampleController {

@GetMapping("/users/{id}")
public String getUserById(@PathVariable Long id) {
return "User ID: " + id;
}

@GetMapping("/users/{userId}/orders/{orderId}")
public String getOrderDetails(@PathVariable Long userId, @PathVariable Long orderId) {
return "User: " + userId + ", Order: " + orderId;
}
}

Ant 风格的 URL 路径映射

  • @RequestMapping(value="/users/**"):可以匹配/users/abc/abc,但/users/123将会被【URI模板模式映射 中的/users/{Id}模式优先映射到】
  • @RequestMapping(value="/product?"):可匹配/product1/producta,但不匹配/product/productaa
  • @RequestMapping(value="/product*"):可匹配/productabc/product,但不匹配/productabc/abc
  • @RequestMapping(value="/product/*"):可匹配/product/abc,但不匹配/productabc
  • @RequestMapping(value="/products/**/{productId}"):可匹配/products/abc/abc/123/products/123,也就是Ant风格和URI模板变量风格可混用

正则表达式风格的 URL 路径映射

@RequestMapping(value="/products/{categoryCode:\\d+}-{pageNumber:\\d+}"):可 以 匹 配 /products/123-1,但不能匹配/products/abc-1,这样可以设计更加严格的规则。

@RequestMapping("/{textualPart:[a-z-]+}-{numericPart:[\\d]+}")

SpringMVC注解

控制器相关注解

  • @Controller
    • 标记一个类为 Spring MVC 控制器,负责处理 HTTP 请求。
    • 示例:@Controller public class MyController { … }
  • @RestController
    • 组合注解,等价于 @Controller + @ResponseBody,表示控制器方法返回的对象直接序列化为 JSON 或 XML。
    • 示例:@RestController public class ApiController { … }
  • @Component
    • 通用注解,可用于控制器类,纳入 Spring 容器管理(通常搭配其他注解使用)。
  • @RequestMapping
    • 映射 HTTP 请求到控制器方法或类,支持 GET、POST 等方法。
    • 属性:value(路径)、method(请求方法)、produces(响应类型)、consumes(请求类型)。
    • 示例:@RequestMapping(value = “/home”, method = RequestMethod.GET)
  • @GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping
    • @RequestMapping 的快捷方式,分别对应 HTTP 的 GET、POST、PUT、DELETE 和 PATCH 请求。
    • 示例:@GetMapping(“/users”)

请求参数与路径处理注解

  • @PathVariable
    • 从 URL 路径中提取变量,绑定到方法参数。
    • 示例:@GetMapping(“/user/{id}”) public String getUser(@PathVariable Long id)
  • @RequestParam
    • 从请求参数(查询字符串或表单数据)中提取值,绑定到方法参数。
    • 属性:name(参数名)、required(是否必须)、defaultValue(默认值)。
    • 示例:@RequestParam(value = “name”, defaultValue = “Guest”) String name
  • @RequestBody
    • 将 HTTP 请求的正文(如 JSON 或 XML)绑定到方法参数,通常用于 RESTful API。
    • 示例:@PostMapping(“/user”) public void saveUser(@RequestBody User user)
  • @RequestHeader
    • 从 HTTP 请求头中提取值,绑定到方法参数。
    • 示例:@RequestHeader(“User-Agent”) String userAgent
  • @CookieValue
    • 从请求的 Cookie 中提取值,绑定到方法参数。
    • 示例:@CookieValue(“sessionId”) String sessionId
  • @MatrixVariable
    • 从 URL 路径中的矩阵变量(如 /path;key=value)提取值。
    • 示例:@GetMapping(“/data/{path}”) public String getMatrix(@MatrixVariable String key)

响应处理注解

  • @ResponseBody
    • 表示方法返回值直接作为 HTTP 响应正文,通常序列化为 JSON 或 XML。
    • 示例:@ResponseBody public User getUser()
  • @ResponseStatus
    • 指定控制器方法或异常处理方法的 HTTP 状态码。
    • 示例:@ResponseStatus(HttpStatus.CREATED)
  • @ModelAttribute
    • 将方法返回值或参数绑定到模型对象,供视图使用;也可用于方法级,预填充模型数据。
    • 示例:@ModelAttribute(“user”) public User getUser()

异常处理注解

  • @ExceptionHandler
    • 标记方法用于处理特定异常,限定在控制器内部。
    • 示例:@ExceptionHandler(NullPointerException.class) public ResponseEntity handleException()
  • @ControllerAdvice
    • 定义全局异常处理、模型增强或绑定器,作用于所有控制器。
    • 示例:@ControllerAdvice public class GlobalExceptionHandler { … }

跨域与配置相关注解

  • @CrossOrigin
    • 启用跨域资源共享(CORS),可用于类或方法级别。
    • 属性:origins(允许的域名)、methods(允许的请求方法)。
    • 示例:@CrossOrigin(origins = “http://example.com“)
  • @SessionAttributes
    • 指定模型属性存储在会话(Session)中,作用于类级别。
    • 示例:@SessionAttributes(“user”)
  • @InitBinder
    • 标记方法用于自定义数据绑定或验证逻辑,作用于控制器内部。
    • 示例:@InitBinder public void initBinder(WebDataBinder binder)

其他高级注解

  • @SessionAttribute
    • 从会话中获取属性值,绑定到方法参数。
    • 示例:@SessionAttribute(“user”) User user
  • @RequestAttribute
    • 从请求属性中获取值,绑定到方法参数。
    • 示例:@RequestAttribute(“data”) String data
  • @EnableWebMvc
    • 启用 Spring MVC 配置,通常用于自定义 MVC 配置类。
    • 示例:@EnableWebMvc @Configuration public class WebConfig { … }

建议

对于前后端分离的项目可以选择RESTful风格的API,使用JSON作为传递类型。通过@RequestBody(将HTTP的body使用JSON反序列化为Java对象) 和 @ResponseBody (将返回数据使用JSON序列化并赋值到HTTP的body中)加载请求和响应。

对于文件等二进制传输,使用Multipart/form-data格式传输:

前端上传代码:

1
2
3
4
5
6
7
8
9
10
const formData = new FormData();
formData.append('username', 'john');
formData.append('avatar', fileInput.files[0]); // 文件

// 使用 Fetch API 发送
fetch('/api/upload', {
method: 'POST',
body: formData
// 不需要设置 Content-Type,浏览器会自动设置
});

MySQL

1
docker run --name local-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=yingzheng -d mysql:latest

用户密码登陆

注册

  1. 前端:用户输入用户名和密码,通过 HTTPS 发送到后端。
  2. 后端:验证输入,生成盐值,哈希密码,存储用户名和哈希值到数据库。
  3. 数据库:保存哈希值(和盐值)。
  4. 后端:返回注册成功响应。

登录

  1. 前端:发送用户名和密码(HTTPS)。
  2. 后端:查询数据库,验证密码哈希。
  3. 后端:验证通过后返回令牌,前端保存令牌用于后续请求。

HTTP 数据类型

在HTTP协议中,请求和响应的内容可以根据Content-Type头部进行分类:

1. 表单数据(Form Data)

  • application/x-www-form-urlencoded
    默认的表单提交格式,数据编码为键值对(如 key1=value1&key2=value2)。

    1
    2
    3
    4
    POST /submit HTTP/1.1
    Content-Type: application/x-www-form-urlencoded

    name=John&age=30
  • multipart/form-data
    用于文件上传或表单中包含二进制数据,数据分多部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    POST /upload HTTP/1.1
    Content-Type: multipart/form-data; boundary=----boundary

    ----boundary
    Content-Disposition: form-data; name="file"; filename="example.txt"
    Content-Type: text/plain

    (文件二进制数据)
    ----boundary--

2. JSON(JavaScript Object Notation)

  • application/json
    用于传输结构化JSON数据,常见于REST API。

    1
    2
    3
    4
    POST /api/data HTTP/1.1
    Content-Type: application/json

    {"name": "John", "age": 30}

3. XML(eXtensible Markup Language)

  • application/xmltext/xml
    用于传输XML格式数据。

    1
    2
    3
    4
    POST /api/data HTTP/1.1
    Content-Type: application/xml

    <user><name>John</name><age>30</age></user>

4. 纯文本(Plain Text)

  • text/plain
    普通文本内容,无格式。

    1
    2
    3
    4
    POST /log HTTP/1.1
    Content-Type: text/plain

    This is a log message.

5. 二进制数据(Binary Data)

  • application/octet-stream
    未知或任意二进制数据(如文件下载)。

    1
    2
    3
    4
    5
    HTTP/1.1 200 OK
    Content-Type: application/octet-stream
    Content-Disposition: attachment; filename="example.zip"

    (文件二进制数据)

6. 其他常见类型

  • application/javascript
    JavaScript代码。
  • text/html
    HTML网页内容。
  • image/png, image/jpeg
    图片资源。
  • application/pdf
    PDF文档。

注:关键头部字段:

  • **Content-Type**:定义请求/响应的主体类型(如 Content-Type: application/json)。
  • **Accept**:客户端声明期望的响应类型(如 Accept: application/json)。

前端处理用户信息

  • 密码输入:
    • 用户在前端(如浏览器、移动端)通过表单输入密码。
    • 密码字段使用 <input type="password">,避免明文显示。
  • 不存储明文:
    • 前端不保存明文密码,仅在用户提交时处理。
  • 加密传输:
    • 使用 HTTPS(TLS/SSL)确保密码在网络传输中加密,防止中间人攻击。
    • 前端将密码作为请求体的一部分(如 JSON)发送到后端。
  • 不预处理密码:
    • 前端不应对密码进行哈希或加密,直接发送明文密码(通过 HTTPS),由后端负责安全处理。哈希在前端可能导致盐值暴露或被拦截。
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
<!DOCTYPE html>
<html lang="en">
<head>
<title>Login Page</title>
<style>
html {
font-family: Arial, sans-serif;
height: 100vh;
width: 100vw;
color: white;
background-color: #333333;
}
body {
margin: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.login_container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2); /* 半透明背景 */
backdrop-filter: blur(10px); /* 模糊强度 */
border: 2px solid rgba(255, 255, 255, 0.3); /* 轻边框增强效果 */
width: 60%;
border-radius: 10px;
}
form {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 20px;
}
.input_group {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
gap: 10px;
}
</style>
</head>
<body>
<div class="login_container">
<h1>Login</h1>
<form action="https://localhost:8083/login" method="post">
<div class="input_group">
<label for="email">Email:</label>
<input
type="email"
id="email"
name="email"
pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
title="输入正确邮箱地址"
required
/>
</div>
<div class="input_group">
<label for="password">Password:</label>
<input
type="password"
id="password"
name="password"
pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{8,}"
title="密码需至少8位,包含大小写字母和数字"
required
/>
</div>
<input type="submit" value="Login" />
</form>
</div>

<script></script>
</body>
</html>

第三方登陆

在一个 Spring Boot 应用中同时实现 OAuth2 Client(用于第三方登录,如 Google) 和 OAuth2 Authorization Server(用于生成自定义 JWT 和 Refresh Token)。

  1. OAuth2 Client:处理第三方登录,获取用户 Access Token 和用户信息。
  2. OAuth2 Authorization Server:接收 OAuth2 Client 传递的信息,生成 JWT 和 Refresh Token,并支持 Refresh Token 刷新流程。
  3. 整合:在一个 Spring Boot 应用中运行这两部分,确保它们协同工作。

SpringBoot支持多种日志框架,包括Logback、Log4j2和Java Util Logging(JUL)。默认情况下,如果你使用SpringBoot的starters启动器,它将使用Logback作为日志框架。

  • Logback:Logback是SpringBoot默认的日志框架,它是Log4j的继任者,提供了更好的性能和可靠性。可以通过在资源目录下创建一个logback-spring.xml文件来配置Logback。
  • Log4j2:Log4j2是Log4j的升级版,它在性能和功能上都有所提升,支持异步日志和插件机制。如果想在SpringBoot中使用Log4j2,需要添加相应的依赖并在配置文件中指定Log4j2作为日志框架。
  • Java Util Logging(JUL):JUL是Java SE的默认日志框架,SpringBoot可以配置使用JUL作为日志框架,但一般不推荐使用,因为它的性能和灵活性相对较差。

除了上述日志框架外,SpringBoot还支持SLF4J和Commons Logging这两个日志门面。这些门面可以与多种日志实现进行集成,使得你可以在不改变代码的情况下更换日志框架。

无论使用哪种日志框架,SpringBoot都支持配置将日志输出到控制台或者文件中。可以在application.properties或application.yml配置文件中设置日志级别、输出格式等参数。

日志门面

在早期使用日志框架时,应用程序通常需要直接与具体的日志框架进行耦合,这就导致了以下几个问题:

  • 代码依赖性

    应用程序需要直接引用具体的日志框架,从而导致代码与日志框架强耦合,难以满足应用程序对日志框架的灵活配置。

  • 日志框架不统一

    在使用不同的日志框架时,应用程序需要根据具体的日志框架来编写代码,这不仅会增加开发难度,而且在多种日志框架中切换时需要进行大量的代码改动。

为了解决这些问题,SLF4J提供了一套通用的日志门面接口,让应用程序可以通过这些接口来记录日志信息,而不需要直接引用具体的日志框架。这样,应用程序就可以在不同的日志框架之间进行灵活配置和切换,同时还可以获得更好的性能表现。

依赖:

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyClass {
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
//...
public static void main(String[] args) {
log.info("This is an info message.");
}
}

如果引入Lombok,可以使用lombok的注解@Slf4j 代替上面log的创建。

1
2
3
4
5
6
7
8
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyClass {
public static void main(String[] args) {
log.info("This is an info message.");
}
}

在SpringBoot中,spring-boot-starter中包含spring-boot-starter-logging,其中会包含slf4j-api依赖。

日志实现

Logback 和 Log4j2是目前常用的两个日志实现框架。

Logback

Logback 的核心模块为 logback-classic,它提供了一个 SLF4J 的实现,兼容 Log4j API,可以无缝地替换 Log4j。它自身已经包含了 logback-core 模块,而 logback-core,顾名思义就是 logback 的核心功能,包括日志记录器 Appender Layout 等。其他 logback 模块都依赖于该模块。

1
2
3
4
5
6
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.18</version>
<scope>compile</scope>
</dependency>

logback 可以通过 XML 或者 Groovy 配置。以 XML 配置为例,logback 的 XML 配置文件名称通常为 logback.xml 或者 logback-spring.xml。在 Spring Boot 中,需要放置在 classpath 的根目录下。

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
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">

<!-- 定义通用变量 -->
<property name="LOG_PATH" value="logs"/>
<property name="LOG_FILE_NAME" value="application"/>
<property name="PATTERN" value="%d{yyyy-MM-dd HH:mm:ss} %-5level [%thread] %logger{36} - %msg%n"/>

<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 文件输出,按日期滚动 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天滚动,历史日志文件名带日期 -->
<fileNamePattern>${LOG_PATH}/${LOG_FILE_NAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>30</maxHistory> <!-- 保存 30 天 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize> <!-- 单文件最大 100MB -->
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>${PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 开发环境配置 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>

<!-- 生产环境配置 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>

<!-- 默认配置(未指定 profile 时使用) -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>

</configuration>

也可以在application.yml或者application.properties中简单配置日志:

1
2
3
4
5
6
7
8
9
10
11
12
logging:
level:
root: INFO
org.springframework.web: DEBUG
com.yourpackage: DEBUG
file:
name: myapp.log
path: /var/log
logback:
rollingpolicy:
max-file-size: 10MB
max-history: 30

注:如果application.ymllogback-spring.xml同时存在,logback-spring.xml会覆盖application.yml

log4j 2

The Apache Log4j SLF4J 2.0 API binding to Log4j 2 Core

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.24.3</version>
<scope>compile</scope>
</dependency>
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
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO" monitorInterval="30">
<Properties>
<Property name="logPath">logs</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{ISO8601} [%t] %-5level %logger{36} - %msg%n" />
</Console>
<RollingFile name="File" fileName="${logPath}/example.log"
filePattern="${logPath}/example-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="%d{ISO8601} [%t] %-5level %logger{36} - %msg%n"/>
<Policies>
<SizeBasedTriggeringPolicy size="10 MB"/>
</Policies>
<DefaultRolloverStrategy max="4"/>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="com.zhanfu.child" level="DEBUG">
<AppenderRef ref="File"/>
<AppenderRef ref="Console"/>
</Logger>
<Logger name="com.zhanfu" level="INFO">
<AppenderRef ref="File"/>
<AppenderRef ref="Console"/>
</Logger>
<Root level="WARN">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>

日志级别

  • TRACE:是最低级别的日志记录,用于输出最详细的调试信息,通常用于开发调试目的。在生产环境中,应该关闭 TRACE 级别的日志记录,以避免输出过多无用信息。
  • DEBUG:是用于输出程序中的一些调试信息,通常用于开发过程中。像 TRACE 一样,在生产环境中应该关闭 DEBUG 级别的日志记录。
  • INFO:用于输出程序正常运行时的一些关键信息,比如程序的启动、运行日志等。通常在生产环境中开启 INFO 级别的日志记录。
  • WARN:是用于输出一些警告信息,提示程序可能会出现一些异常或者错误。在应用程序中,WARN 级别的日志记录通常用于记录一些非致命性异常信息,以便能够及时发现并处理这些问题。
  • ERROR:是用于输出程序运行时的一些错误信息,通常表示程序出现了一些不可预料的错误。在应用程序中,ERROR 级别的日志记录通常用于记录一些致命性的异常信息,以便能够及时发现并处理这些问题。
1
2
3
4
5
6
7
8
9
10
11
public class Main {
private static final Logger log = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
log.trace("This is a Main trace message.");
log.debug("This is a Main debug message.");
log.info("This is a Main info message.");
log.warn("This is a Main warn message.");
log.error("This is a Main error message.");
Slave.main(args);
}
}

SpringBoot不同开发环境

  • application-dev.properties(开发环境)
  • application-test.properties(测试环境)
  • application-prod.properties(生产环境)

然后在的application.propertiesapplication.yml中指定激活的配置文件:

1
2
# application.properties
spring.profiles.active=dev

或者在启动命令行中

1
java -jar yourapp.jar --spring.profiles.active=dev

设定好开发环境后,logback-spring.xml就可以根据不同的开发环境选择日志配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 开发环境配置 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>

<!-- 生产环境配置 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>

<!-- 默认配置(未指定 profile 时使用) -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>

注:只有logback-spring.xml支持<springProfile>等命令,logback.xml不支持。

消息推送

通过一定的技术标准或协议,在互联网上通过定期传送用户需要的信息来减少信息过载。

消息推送的实现方式:

  • AJAX长短轮循:客户端不断发起请求,交换数据,客户端主动。
  • 基于HTTP协议的SSE(Server-Sent Events)技术:实现服务器向客户端实时推送数据的Web技术,服务端主动。SSE基于HTTP协议,允许服务器将数据以事件流(Event Stream)的形式发送给客户端。客户端通过建立持久的HTTP连接,并监听事件流,可以实时接收服务器推送的数据。前端JS通过EventSource结合数据。
  • WebSocket技术:WebSocket 是基于 TCP 的一种新的应用层网络协议。它提供了一个全双工的通道,允许服务器和客户端之间实时双向通信。
  • MQTT通讯协议

WebSocket

websocket是一个新的协议,是直接建立在TCP之上的,不基于HTTP,但握手过程中会使用HTTP。

特点:

  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

应用场景:

  • 即时聊天通信
  • 多玩家游戏
  • 在线协同编辑/编辑
  • 实时数据流的拉取与推送
  • 体育/游戏实况
  • 实时地图位置

原理图:

image-20250411204253429

通过wireshark分析websocket建立过程:

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
// ---------------------------------------- 连接 -------------------------------------------
// TCP握手
127.0.0.1 127.0.0.1 TCP 74 53522 → 8080 [SYN] Seq=0 Win=65495 Len=0 MSS=65495 SACK_PERM TSval=1655867669 TSecr=0 WS=128
127.0.0.1 127.0.0.1 TCP 74 8080 → 53522 [SYN, ACK] Seq=0 Ack=1 Win=65483 Len=0 MSS=65495 SACK_PERM TSval=1655867669 TSecr=1655867669 WS=128
127.0.0.1 127.0.0.1 TCP 66 53522 → 8080 [ACK] Seq=1 Ack=1 Win=65536 Len=0 TSval=1655867669 TSecr=1655867669
//客户端发送HTTP请求携带
127.0.0.1 127.0.0.1 HTTP 293 GET /echo HTTP/1.1
// ACK确认
127.0.0.1 127.0.0.1 TCP 66 8080 → 53522 [ACK] Seq=1 Ack=228 Win=65280 Len=0 TSval=1655867670 TSecr=1655867670
//服务端返回告诉客户端更换协议为websocket
127.0.0.1 127.0.0.1 HTTP 284 HTTP/1.1 101 Switching Protocols
// ACK确认
127.0.0.1 127.0.0.1 TCP 66 53522 → 8080 [ACK] Seq=228 Ack=219 Win=65408 Len=0 TSval=1655867863 TSecr=1655867863


// ---------------------------------------- 数据传递 -------------------------------------------
// 客户端发送数据
127.0.0.1 127.0.0.1 WebSocket 84 WebSocket Text [FIN] [MASKED]
// ACK确认
127.0.0.1 127.0.0.1 TCP 66 8080 → 53522 [ACK] Seq=219 Ack=246 Win=65536 Len=0 TSval=1656126026 TSecr=1656126026
// 服务端回现数据给客户端
127.0.0.1 127.0.0.1 WebSocket 86 WebSocket Text [FIN]
// ACK确认
127.0.0.1 127.0.0.1 TCP 66 53522 → 8080 [ACK] Seq=246 Ack=239 Win=65536 Len=0 TSval=1656126034 TSecr=1656126034


// ---------------------------------------- 关闭连接 -------------------------------------------
// 客户端告知服务端关闭连接
127.0.0.1 127.0.0.1 WebSocket 74 WebSocket Connection Close [FIN] [MASKED]
// 服务端告知客户端关闭连接
127.0.0.1 127.0.0.1 WebSocket 70 WebSocket Connection Close [FIN]
// 客户端收到服务端断开请求并ACK确认
127.0.0.1 127.0.0.1 TCP 66 53522 → 8080 [ACK] Seq=254 Ack=243 Win=65536 Len=0 TSval=1656183231 TSecr=1656183231
// 服务器发起 TCP 连接关闭,发送 FIN 包
127.0.0.1 127.0.0.1 TCP 66 8080 → 53522 [FIN, ACK] Seq=243 Ack=254 Win=65536 Len=0 TSval=1656183232 TSecr=1656183231
// 客户端响应服务器的 FIN,发送自己的 FIN 包
127.0.0.1 127.0.0.1 TCP 66 53522 → 8080 [FIN, ACK] Seq=254 Ack=244 Win=65536 Len=0 TSval=1656183232 TSecr=1656183232
// 服务器确认客户端的 FIN 包
127.0.0.1 127.0.0.1 TCP 66 8080 → 53522 [ACK] Seq=244 Ack=255 Win=65536 Len=0 TSval=1656183232 TSecr=1656183232

创建过程:

  1. 三次握手建立TCP连接
  2. 客户端发送HTTP请求,请求升级通信协议为websocket
  3. 服务端发送HTTP请求,将协议升级为websocket

通信过程:

  1. 使用websocket协议通信

关闭过程:

  1. 客户端使用websocket协议告知服务端关闭连接
  2. 服务端使用websocket协议告知客户端关闭连接
  3. 完成TCP的四次挥手
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
@Configuration
public class WebSocketConfig {

@Bean
public WebSocketHandler echoWebSocketHandler() {
return new EchoWebSocketHandler();
}

@Bean
public WebSocketHandler chatWebSocketHandler() {
return new ChatWebSocketHandler();
}

@Bean
public SimpleUrlHandlerMapping handlerMapping(WebSocketHandler echoWebSocketHandler, WebSocketHandler chatWebSocketHandler) {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/echo", echoWebSocketHandler);
map.put("/chat", chatWebSocketHandler);
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(map);
mapping.setOrder(-1); // 优先级高于其他路由
mapping.setCorsConfigurations(Map.of("*", new CorsConfiguration().applyPermitDefaultValues()));
return mapping;
}
}
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
@Slf4j
public class ChatWebSocketHandler implements WebSocketHandler {
private final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

@Override
public Mono<Void> handle(WebSocketSession session) {
log.info("New client connected: {}, Total: {}", session.getId(), sessions.size());
sessions.put(session.getId(), session);

Mono<Void> receiveAndBrodcast = session.receive()
.map(WebSocketMessage::getPayloadAsText)
.flatMap(msg -> {
log.info("Received from {}: {}", session.getId(), msg);
return Mono.when(
sessions.entrySet().stream()
.filter(entry -> !entry.getKey().equals(session.getId()))
.map(entry -> {
WebSocketSession targetSession = entry.getValue();
return targetSession.send(
Mono.just(targetSession.textMessage("Broadcast: " + msg))
);
})
.toList()
);
})
.doOnError(err -> log.error("Error in session {}: {}", session.getId(), err.getMessage()))
.then();

Mono<Void> sendWelcome = session.send(
Mono.just(session.textMessage("Welcome to broadcast chat!"))
);

return Mono.when(sendWelcome, receiveAndBrodcast)
.doFinally(signal -> {
sessions.remove(session.getId());
log.info("Client disconnect: {}, Total: {}", session.getId(), sessions.size());
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
public class EchoWebSocketHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {

return session.send(
session.receive()
.map(msg -> {
log.info("Received: {}", msg.getPayloadAsText());
return session.textMessage("Echo: " + msg.getPayloadAsText());
})
.doOnError(e -> System.err.println("Error: " + e.getMessage()))
);
}
}

IO流(BIO)

根据数据传输方式,可以将 IO 类分为:

  • 字节流:字节流读取单个字节,字节流用来处理二进制文件(图片、MP3、视频文件)。

  • 字符流:字符流读取单个字符(一个字符根据编码的不同,对应的字节也不同,如 UTF-8 编码中文汉字是 3 个字节,GBK编码中文汉字是 2 个字节。),字符流用来处理文本文件(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)。

根据数据操作对象,可以将 IO 类分为:

设计模式

装饰者模式(Decorator):把装饰者套在被装饰者之上,从而动态扩展被装饰者的功能,装饰器模式通过组合替代继承来扩展原始类的功能。即可以在不改变原有对象的情况下拓展其功能。

1
2
3
4
5
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
ZipInputStream zis = new ZipInputStream(bis);

BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(fileName));
ZipOutputStream zipOut = new ZipOutputStream(bos);

操作系统IO模型

异步IO

Linux MacOS Window
事件驱动IO select poll epoll kqueue
异步IO POSIX
io_uring (kernel5.1+)
IOCP

事件驱动IO / IO多路复用

IO模型 相对性能 关键思路 操作系统 JAVA支持情况
select 较高 Reactor windows/Linux 支持,Reactor模式(反应器设计模式)。Linux操作系统的 kernels 2.4内核版本之前,默认使用select;而目前windows下对同步IO的支持,都是select模型
poll 较高 Reactor Linux Linux下的JAVA NIO框架,Linux kernels 2.6内核版本之前使用poll进行支持。也是使用的Reactor模式
epoll Reactor/Proactor Linux Linux kernels 2.6内核版本及以后使用epoll进行支持;Linux kernels 2.6内核版本之前使用poll进行支持;另外一定注意,由于Linux下没有Windows下的IOCP技术提供真正的 异步IO 支持,所以Linux下使用epoll模拟异步IO
kqueue Proactor FreeBSD/MacOS 目前JAVA的版本不支持

Java IO模型

BIO (Blocking I/O)

BIO 属于同步阻塞 IO 模型,同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

NIO (Non-blocking/New I/O)

Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , SelectorBuffer 等抽象。它是支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO。

Java 异步 I/O 机制:

  • NIO(java.nio):
    • 引入了非阻塞 I/O 和选择器(Selector),基于操作系统的多路复用机制(如 Linux 的 select poll epoll)。
    • 通过 Selector 和 Channel(如 SocketChannel ServerSocketChannel DatagramChannel),Java 可以实现高效的网络 I/O。
    • 但这是 就绪通知(readiness-based)模型,而不是真正的完成型异步 I/O。
  • NIO.2(Asynchronous I/O,Java 7+):
    • 在 java.nio.channels 包中提供了异步通道类,如 AsynchronousSocketChannel AsynchronousFileChannel
    • 支持通过 Future 或回调(CompletionHandler)处理 I/O 操作的结果。
    • 底层实现依赖操作系统的异步 I/O 接口:
      • 在 Linux 上,AsynchronousFileChannel 使用 POSIX AIO 作为其底层实现机制。AsynchronousSocketChannel 使用 epoll
      • 在 Windows 上,使用 IOCP(I/O Completion Ports)。
      • 在 macOS 上,使用线程池或 kqueue。

Java 原生提供的NIO过于底层,编写复杂度很高,不推荐程序员直接使用,可以通过Netty框架来进行网络编程。

AIO (Asynchronous I/O)

Java

  • 在Window上使用IOCP,是完全异步的模型。

  • 在Linux和BSD中 AsynchronousFileChannel 使用 POSIX AIO 作为其底层实现机制是完全异步的操作,但AsynchronousSocketChannel 使用 epoll 加线程池模拟异步行为。

目前Java还没有支持Linux的 io_uring 完全异步IO模型。项目中直接使用AIO的情景很少,主要还是使用NIO,如Netty。

总结:目前主流的Socket IO基本上使用NIO(事件驱动IO),需要自行编写网络IO可使用Netty,需要使用Spring就使用Spring WebFlux(对Netty封装)

0%