第一次提交

This commit is contained in:
2025-12-15 23:16:59 +08:00
commit 3e0da32c18
93 changed files with 14105 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
---
title: 其他后端语言
index: false
icon: mdi:code-tags
category:
- 后端
---
<Catalog />

View File

View File

@@ -0,0 +1,178 @@
---
icon: mdi:language-go
date: 2025-05-22
title: Go 基础语法 - Hello World
category:
- Go
- 后端
- 编程
- 教程
- 入门
- 开发环境
- Linux
- GoLand
- VSCode
- 命令行
- Go Module
- Go fmt
- Go build
- Go run
- GOPATH
- GOROOT
- Linux Mint
- Go 1.22.2
- Hello World
---
好的根据你提供的环境Linux Mint XFCE、Go 1.22.2 已安装、项目目录为 `/home/liumangmang/GolandProjects`),我将为你梳理 **完整但简洁** 的步骤,包括验证安装、配置 Go module、编写 Hello World、以及运行 `go fmt``go build`
<!-- more -->
---
## ✅ 步骤 1验证 Go 安装
打开终端,运行:
```bash
go version
```
预期输出:
```
go version go1.22.2 linux/amd64
```
同时检查 GOPATH 和 GOROOTGo 1.11+ 默认支持 moduleGOPATH 不再强制):
```bash
go env GOROOT GOPATH
```
通常输出类似:
```
/usr/local/go
/home/liumangmang/go
```
> 📌 提示Go 1.22 默认启用 module 模式,无需设置 `GO111MODULE=on`。
---
## ✅ 步骤 2创建项目并初始化 Go Module
进入你的项目目录:
```bash
cd /home/liumangmang/GolandProjects
mkdir hello-go && cd hello-go
```
初始化一个 Go module模块名建议用类似域名的格式比如 `example.com/hello-go`,本地开发可随意):
```bash
go mod init hello-go
```
这会生成 `go.mod` 文件,内容类似:
```go
module hello-go
go 1.22
```
---
## ✅ 步骤 3编写 Hello World
创建主程序文件:
```bash
nano main.go
```
输入以下代码:
```go
package main
import "fmt"
func main() {
fmt.Println("Hello, Go from Linux Mint!")
}
```
保存并退出(`Ctrl+O` → 回车 → `Ctrl+X`)。
---
## ✅ 步骤 4格式化代码go fmt
运行格式化命令:
```bash
go fmt
```
> 如果代码已规范,可能无输出。`go fmt` 会自动修正缩进、空格等风格问题。
你可以故意写成不规范格式测试一下,例如:
```go
func main(){fmt.
Println("Hello")}
```
再运行 `go fmt`,它会自动修复。
---
## ✅ 步骤 5构建和运行程序
### 构建二进制文件:
```bash
go build
```
成功后会在当前目录生成一个可执行文件(在 Linux 下默认名为 `hello-go`,即 module 名):
```bash
./hello-go
```
输出:
```
Hello, Go from Linux Mint!
```
### 或者直接运行(不生成二进制):
```bash
go run .
```
同样输出:
```
Hello, Go from Linux Mint!
```
---
## ✅ 额外建议GoLand 集成)
- 在 GoLand 中打开 `/home/liumangmang/GolandProjects/hello-go` 目录。
- GoLand 会自动识别 `go.mod` 并启用 module 支持。
- 你可以通过 IDE 的绿色 ▶️ 按钮直接运行 `main.go`
- `go fmt` 在 GoLand 中默认在保存时自动触发(可在 Settings → Tools → File Watchers 中配置)。
---
✅ 到此,你已完成:
- Go 环境验证
- Module 初始化
- Hello World 编写
- 代码格式化(`go fmt`
- 编译与运行(`go build` / `go run`

View File

@@ -0,0 +1,208 @@
---
icon: mdi:variable
date: 2025-05-22
title: Go 基础语法 - 变量与类型
category:
- Go
- 后端
- 编程
- 教程
- 入门
- 变量
- 类型
- 指针
- new
- make
- 简短声明
- 基本类型
- var
---
当然可以以下是基于你当前环境Linux Mint XFCE、Go 1.22.2、项目目录 `/home/liumangmang/GolandProjects`)整理的 **Go 语言基础语法实践步骤**,涵盖:
- 基本类型
- 变量声明
- `:=` 简短声明
- 指针
- `new` vs `make`
我们将通过一个可运行的示例程序来演示这些概念。
<!-- more -->
---
## ✅ 步骤 1创建练习项目
打开终端:
```bash
cd /home/liumangmang/GolandProjects
mkdir go-basics && cd go-basics
go mod init go-basics
```
---
## ✅ 步骤 2编写示例代码main.go
使用你喜欢的编辑器(或 GoLand创建 `main.go`
```bash
nano main.go
```
粘贴以下完整示例代码(包含注释说明):
```go
package main
import "fmt"
func main() {
// ========== 1. 基本类型 ==========
var a int = 42
var b float64 = 3.14
var c bool = true
var d string = "Hello, Go!"
fmt.Println("基本类型:")
fmt.Printf("int: %d\n", a)
fmt.Printf("float64: %.2f\n", b)
fmt.Printf("bool: %t\n", c)
fmt.Printf("string: %s\n", d)
// ========== 2. 变量声明方式 ==========
// 方式1var 声明(可带或不带初始值)
var x int
x = 100
var y = 200 // 类型自动推导
// 方式2批量声明
var (
name = "Liu"
age = 30
height = 175.5
isAdmin = true
)
fmt.Println("\n变量声明:")
fmt.Println("x =", x, ", y =", y)
fmt.Println("name:", name, "age:", age, "height:", height, "isAdmin:", isAdmin)
// ========== 3. := 简短声明(仅在函数内部可用)==========
z := 999 // 自动推导为 int
message := "Use := !" // 自动推导为 string
fmt.Println("\n简短声明 (:=):")
fmt.Println("z =", z, ", message =", message)
// 注意::= 必须至少声明一个新变量
// 下面这行会报错no new variables on left side of :=
// z := 888 // ❌ 错误!
// 但可以这样混合新旧变量:
w, z := "new", 888 // w 是新的z 是重新赋值shadowing
fmt.Println("w =", w, ", z =", z)
// ========== 4. 指针 ==========
p := &a // p 是指向 a 的指针(*int 类型)
fmt.Println("\n指针:")
fmt.Printf("a 的值: %d\n", a)
fmt.Printf("a 的地址: %p\n", &a)
fmt.Printf("p 的值(即 a 的地址): %p\n", p)
fmt.Printf("*p解引用: %d\n", *p)
*p = 1000 // 通过指针修改 a 的值
fmt.Println("修改后 a =", a)
// ========== 5. new vs make ==========
// new(T) → 分配零值内存,返回 *T
ptrInt := new(int) // *int值为 0
*ptrInt = 42
fmt.Println("\nnew(int):", *ptrInt)
// make 仅用于 slice, map, chan —— 返回初始化后的 T不是指针
s := make([]int, 3) // 长度为3的 slice元素为 [0 0 0]
m := make(map[string]int) // 空 map
ch := make(chan int, 2) // 缓冲通道
s[0] = 10
m["count"] = 1
ch <- 100
fmt.Println("make(slice):", s)
fmt.Println("make(map):", m)
fmt.Println("make(chan) 示例(略去接收)")
// ⚠️ 不能对 slice/map/chan 使用 new
// bad := new([]int) // 这是一个 *[]int但底层数组未初始化不能直接用
}
```
保存并退出(`Ctrl+O` → Enter → `Ctrl+X`)。
---
## ✅ 步骤 3格式化 & 运行
```bash
# 格式化代码(虽然我们写得很规范,但习惯要好)
go fmt
# 运行程序
go run .
```
你应该看到类似输出:
```
基本类型:
int: 42
float64: 3.14
bool: true
string: Hello, Go!
变量声明:
x = 100 , y = 200
name: Liu age: 30 height: 175.5 isAdmin: true
简短声明 (:=):
z = 999 , message = Use := !
w = new , z = 888
指针:
a 的值: 42
a 的地址: 0xc0000b4008
p 的值(即 a 的地址): 0xc0000b4008
*p解引用: 42
修改后 a = 1000
new(int): 42
make(slice): [10 0 0]
make(map): map[count:1]
make(chan) 示例(略去接收)
```
---
## 🔍 关键知识点总结(对比 Java
| 概念 | Go 行为 | Java 对比 |
|------|--------|----------|
| **变量声明** | `var x int = 1``x := 1` | `int x = 1;`(必须指定类型或用 var + 初始化) |
| **:=** | 函数内快速声明+初始化,至少一个新变量 | Java 无直接等价Java 10+ 有 `var`,但非声明新作用域) |
| **指针** | 显式支持 `*T``&x` | Java 无显式指针(对象引用类似,但不可运算) |
| **new** | 分配零值内存,返回指针 `*T` | 类似 `new Object()`,但 Go 的 `new` 只用于值类型 |
| **make** | 专用于 slice/map/chan 的初始化(返回 T非指针 | Java 中 `new ArrayList<>()` 等构造器承担类似职责 |
> 💡 提醒Go 中 **slice / map / chan 是引用类型**,传递时是“引用语义”,但它们本身不是指针。`make` 是它们的正确初始化方式。
---
## ✅ 下一步建议
- 在 GoLand 中打开这个项目,尝试修改变量、加断点调试指针。
- 尝试注释掉 `*p = 1000`,观察 `a` 是否变化。
- 尝试 `bad := new([]int); bad[0] = 1`,看是否会 panic因为底层未分配
如果你希望我继续讲解 **结构体、方法、接口、goroutine** 等内容,随时告诉我!祝你 Go 学习顺利 🚀

View File

@@ -0,0 +1,266 @@
---
icon: mdi:table-arrow-down
date: 2025-05-22
title: Go 基础语法 - Slice 和 Map
category:
- Go
- 后端
- 编程
- 教程
- 数据结构
- slice
- map
- 扩容机制
- 深拷贝
- 浅拷贝
- 并发安全
- sync.Map
---
当然可以以下是基于你当前环境Linux Mint XFCE、Go 1.22.2、项目目录 `/home/liumangmang/GolandProjects`)整理的 **Go 语言 slice 与 map 深入实践步骤**,涵盖:
- **slice 的扩容机制**
- **slice 的深拷贝 vs 浅拷贝**
- **map 的基本使用**
- **map 的并发写风险与安全方案**
我们将通过一个可运行的示例程序,在你的环境中一步步验证这些行为。
<!-- more -->
---
## ✅ 步骤 1创建新项目
```bash
cd /home/liumangmang/GolandProjects
mkdir go-slice-map && cd go-slice-map
go mod init go-slice-map
```
---
## ✅ 步骤 2编写演示代码main.go
创建 `main.go`
```bash
nano main.go
```
粘贴以下完整代码(含详细注释):
```go
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
fmt.Println("=== 1. Slice 扩容机制 ===")
demoSliceGrowth()
fmt.Println("\n=== 2. Slice 深拷贝 vs 浅拷贝 ===")
demoSliceCopy()
fmt.Println("\n=== 3. Map 基本使用 ===")
demoMapUsage()
fmt.Println("\n=== 4. Map 并发写风险(会 panic===")
// ⚠️ 下面这行默认注释掉,避免程序崩溃
// demoUnsafeConcurrentMap()
fmt.Println("\n=== 5. 安全的并发 mapsync.Map===")
demoSafeConcurrentMap()
}
// 1. 演示 slice 扩容机制
func demoSliceGrowth() {
var s []int
fmt.Printf("初始: len=%d, cap=%d, addr=%p\n", len(s), cap(s), s)
for i := 0; i < 10; i++ {
s = append(s, i)
fmt.Printf("append %d: len=%d, cap=%d, addr=%p\n", i, len(s), cap(s), s)
}
// 观察:当容量不足时,底层数组会重新分配,地址改变
}
// 2. 深拷贝 vs 浅拷贝
func demoSliceCopy() {
original := []int{1, 2, 3, 4, 5}
shallow := original // 浅拷贝:共享底层数组
deep := make([]int, len(original))
copy(deep, original) // 深拷贝:独立底层数组
fmt.Println("修改前:")
fmt.Println("original:", original)
fmt.Println("shallow :", shallow)
fmt.Println("deep :", deep)
// 修改原 slice
original[0] = 999
fmt.Println("\n修改 original[0] = 999 后:")
fmt.Println("original:", original) // [999, 2, 3, 4, 5]
fmt.Println("shallow :", shallow) // [999, 2, 3, 4, 5] ← 被影响!
fmt.Println("deep :", deep) // [1, 2, 3, 4, 5] ← 不受影响
}
// 3. Map 基本使用
func demoMapUsage() {
m := make(map[string]int)
m["apple"] = 5
m["banana"] = 3
fmt.Println("map 内容:", m)
fmt.Println("apple 数量:", m["apple"])
// 检查 key 是否存在
if count, ok := m["orange"]; ok {
fmt.Println("orange 存在,数量:", count)
} else {
fmt.Println("orange 不存在")
}
// 删除 key
delete(m, "apple")
fmt.Println("删除 apple 后:", m)
}
// 4. 不安全的并发 map 写入(会导致 panic
func demoUnsafeConcurrentMap() {
m := make(map[int]int)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(key int) {
defer wg.Done()
m[key] = key * 10 // 并发写 → fatal error: concurrent map writes
}(i)
}
wg.Wait()
fmt.Println("unsafe map done (should not reach here)")
}
// 5. 使用 sync.Map 实现安全并发
func demoSafeConcurrentMap() {
var sm sync.Map
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(key int) {
defer wg.Done()
sm.Store(key, key*10)
}(i)
}
wg.Wait()
// 读取部分值验证
sm.Range(func(key, value any) bool {
if key.(int) < 5 { // 只打印前几个
fmt.Printf("key=%v, value=%v\n", key, value)
}
return true // 继续遍历
})
}
```
保存退出(`Ctrl+O` → Enter → `Ctrl+X`)。
---
## ✅ 步骤 3格式化并运行
```bash
go fmt
go run .
```
### 预期输出(节选):
```
=== 1. Slice 扩容机制 ===
初始: len=0, cap=0, addr=0x0
append 0: len=1, cap=1, addr=0xc0000180c0
append 1: len=2, cap=2, addr=0xc0000180e0
append 2: len=3, cap=4, addr=0xc00001c0a0 ← 容量翻倍,地址变了!
...
=== 2. Slice 深拷贝 vs 浅拷贝 ===
修改前:
original: [1 2 3 4 5]
shallow : [1 2 3 4 5]
deep : [1 2 3 4 5]
修改 original[0] = 999 后:
original: [999 2 3 4 5]
shallow : [999 2 3 4 5]
deep : [1 2 3 4 5]
=== 3. Map 基本使用 ===
map 内容: map[apple:5 banana:3]
apple 数量: 5
orange 不存在
删除 apple 后: map[banana:3]
=== 4. Map 并发写风险(会 panic===
=== 5. 安全的并发 mapsync.Map===
key=0, value=0
key=1, value=10
key=2, value=20
key=3, value=30
key=4, value=40
```
> 🔒 注意:`demoUnsafeConcurrentMap()` 默认被注释。如果你想**亲眼看到 panic**,可以取消注释它再运行——程序会崩溃并输出:
>
> ```
> fatal error: concurrent map writes
> ```
---
## 📚 关键知识点总结
### 🔸 Slice 扩容机制Go 1.22
-`cap < 1024`:扩容为 **2 倍**
-`cap >= 1024`:扩容为 **1.25 倍**(实际策略可能微调,但趋势是渐进增长)
- 扩容时会**分配新底层数组**,旧 slice 和新 slice **不再共享数据**
### 🔸 深拷贝 vs 浅拷贝
| 方式 | 是否共享底层数组 | 是否安全独立修改 |
|------|------------------|------------------|
| `s2 := s1` | ✅ 是 | ❌ 否 |
| `copy(s2, s1)` | ❌ 否 | ✅ 是 |
> 💡 提醒:`copy` 要求目标 slice 已分配内存(用 `make`
### 🔸 Map 并发安全
- **普通 map 不是并发安全的**:多个 goroutine 同时写会 panic
- **解决方案**
- 使用 `sync.Mutex``sync.RWMutex` 保护 map
- 或直接使用 `sync.Map`(适用于读多写少场景)
> ⚠️ `sync.Map` 性能不一定比加锁的 map 高,仅在特定场景推荐使用。
---
## ✅ 在 GoLand 中调试建议
1. 打开项目 `/home/liumangmang/GolandProjects/go-slice-map`
2.`demoSliceGrowth()` 中设置断点,观察每次 `append` 后地址变化
3. 尝试取消注释 `demoUnsafeConcurrentMap()`,运行看 panic有助于理解并发风险
---
如果你希望继续学习 **channel、goroutine、context、错误处理****Go 与 Java 对比(如 slice vs ArrayList**,欢迎随时告诉我!祝你 Go 编程愉快 🚀

View File

@@ -0,0 +1,187 @@
---
icon: mdi:shape-outline
date: 2025-05-22
title: Go 基础语法 - Struct、方法与接收者类型详解
category:
- Go
- 后端
- 编程
- 教程
- 面向对象
- Struct
- 方法
- 接收者
- 值接收者
- 指针接收者
- 面向对象编程
---
当然可以!以下是专为你定制的实践指南,基于你的环境:
- **系统**Linux Mint XFCE
- **Go 版本**go1.22.2 linux/amd64
- **项目目录**`/home/liumangmang/GolandProjects`
<!-- more -->
## 📌 标题:
# Go 面向对象基石Struct、方法与接收者类型详解值 vs 指针)
---
## ✅ 步骤 1创建练习项目
打开终端,执行:
```bash
cd /home/liumangmang/GolandProjects
mkdir go-struct-methods && cd go-struct-methods
go mod init go-struct-methods
```
---
## ✅ 步骤 2编写演示代码main.go
创建并编辑 `main.go`
```bash
nano main.go
```
粘贴以下完整示例代码(含详细注释和对比):
```go
package main
import "fmt"
// 定义一个 Person 结构体
type Person struct {
Name string
Age int
}
// ========== 值接收者方法 ==========
func (p Person) SayHello() {
fmt.Printf("Hello, I'm %s (值接收者)\n", p.Name)
}
// 值接收者无法修改原始结构体字段
func (p Person) SetName(name string) {
p.Name = name // 修改的是副本!
fmt.Printf("在值接收者中设置 Name = %s\n", p.Name)
}
// ========== 指针接收者方法 ==========
func (p *Person) SayHelloPtr() {
fmt.Printf("Hello, I'm %s (指针接收者)\n", p.Name)
}
// 指针接收者可以真正修改原始结构体
func (p *Person) SetNamePtr(name string) {
p.Name = name // 修改的是原始对象!
fmt.Printf("在指针接收者中设置 Name = %s\n", p.Name)
}
// ========== 对比调用行为 ==========
func main() {
fmt.Println("=== 1. 使用值类型变量调用方法 ===")
p1 := Person{Name: "Alice", Age: 30}
p1.SayHello() // OK
p1.SetName("Alicia") // ❌ 不会改变 p1.Name
fmt.Printf("调用值接收者 SetName 后p1.Name = %s\n", p1.Name)
p1.SayHelloPtr() // ✅ Go 自动取地址 (&p1)
p1.SetNamePtr("Anna") // ✅ 真正修改了 p1
fmt.Printf("调用指针接收者 SetNamePtr 后p1.Name = %s\n", p1.Name)
fmt.Println("\n=== 2. 使用指针类型变量调用方法 ===")
p2 := &Person{Name: "Bob", Age: 25}
p2.SayHello() // ✅ Go 自动解引用 (*p2)
p2.SayHelloPtr() // ✅ 直接调用
p2.SetName("Bobby") // ❌ 副本修改,无效
fmt.Printf("调用值接收者 SetName 后p2.Name = %s\n", p2.Name)
p2.SetNamePtr("Robert") // ✅ 真正修改
fmt.Printf("调用指针接收者 SetNamePtr 后p2.Name = %s\n", p2.Name)
fmt.Println("\n=== 3. 关键规则总结 ===")
fmt.Println("- 值接收者:操作副本,不能修改原结构体")
fmt.Println("- 指针接收者:操作原始数据,可修改")
fmt.Println("- Go 会自动处理 & 和 * 转换(只要变量可寻址)")
fmt.Println("- 如果方法需要修改字段,请使用指针接收者!")
}
```
保存并退出(`Ctrl+O` → Enter → `Ctrl+X`)。
---
## ✅ 步骤 3格式化并运行
```bash
go fmt
go run .
```
### 预期输出:
```
=== 1. 使用值类型变量调用方法 ===
Hello, I'm Alice (值接收者)
在值接收者中设置 Name = Alicia
调用值接收者 SetName 后p1.Name = Alice
Hello, I'm Alice (指针接收者)
在指针接收者中设置 Name = Anna
调用指针接收者 SetNamePtr 后p1.Name = Anna
=== 2. 使用指针类型变量调用方法 ===
Hello, I'm Bob (值接收者)
Hello, I'm Bob (指针接收者)
在值接收者中设置 Name = Bobby
调用值接收者 SetName 后p2.Name = Bob
在指针接收者中设置 Name = Robert
调用指针接收者 SetNamePtr 后p2.Name = Robert
=== 3. 关键规则总结 ===
- 值接收者:操作副本,不能修改原结构体
- 指针接收者:操作原始数据,可修改
- Go 会自动处理 & 和 * 转换(只要变量可寻址)
- 如果方法需要修改字段,请使用指针接收者!
```
---
## 🔍 核心知识点解析(对比 Java
| 概念 | Go 行为 | Java 类比 |
|------|--------|----------|
| **Struct** | 值类型,默认按值传递 | 类似 `class`,但 Go 的 struct 是值语义(除非用指针) |
| **方法** | 绑定到类型struct 或其他) | 类似 Java 的实例方法 |
| **值接收者** | 方法内操作的是副本 | 类似 Java 中传入不可变对象(但 Java 对象默认是引用) |
| **指针接收者** | 方法内可修改原始对象 | 更接近 Java 实例方法的行为(因为 Java 对象总是引用) |
> 💡 **重要提示**
> - 在 Go 中,**所有参数和接收者都是“按值传递”**。
> - 指针接收者之所以能修改原数据,是因为“值”是一个地址,通过该地址可以访问原始内存。
---
## ✅ 在 GoLand 中进一步探索
1. 打开项目:`/home/liumangmang/GolandProjects/go-struct-methods`
2. 将鼠标悬停在 `p1.SetName``p1.SetNamePtr` 上,观察 IDE 提示的接收者类型
3. 尝试将 `SetName` 改为指针接收者,看输出变化
4. 使用调试器Debug查看 `p1` 内存地址是否在方法调用中被共享
---
## 🧭 下一步建议
- 学习 **接口interface** 如何与 struct + 方法配合实现多态
- 探索 **嵌入embedding** 替代继承
- 对比 Java 的 getter/setter 与 Go 的字段直接访问Go 无 private/public 关键字,靠首字母大小写控制可见性)
如果你希望我继续讲解 **接口、组合、错误处理****Go 风格的面向对象设计**,请随时告诉我!祝你编码愉快 🚀

View File

@@ -0,0 +1,255 @@
---
icon: mdi:link-variant
date: 2025-05-22
title: Go 基础语法 - 接口(鸭子类型)、空接口与类型断言实战
category:
- Go
- 后端
- 编程
- 教程
- 接口
- 多态
- 鸭子类型
- 空接口
- 类型断言
- any
- interface{}
---
当然可以!以下是专为你量身定制的实践指南,完全基于你的开发环境:
- **操作系统**Linux Mint XFCE
- **Go 版本**go1.22.2 linux/amd64
- **项目目录**`/home/liumangmang/GolandProjects`
<!-- more -->
## 📌 标题:
# Go 的多态之道:接口(鸭子类型)、空接口与类型断言实战
---
## ✅ 步骤 1创建新项目
打开终端,执行以下命令:
```bash
cd /home/liumangmang/GolandProjects
mkdir go-interfaces && cd go-interfaces
go mod init go-interfaces
```
---
## ✅ 步骤 2编写演示代码main.go
创建并编辑 `main.go`
```bash
nano main.go
```
粘贴以下完整示例代码(含详细注释,覆盖鸭子类型、空接口、类型断言):
```go
package main
import "fmt"
// ========== 1. 定义接口(鸭子类型)==========
type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct{}
func (c Cat) Speak() string {
return "Meow~"
}
type Robot struct{}
func (r Robot) Speak() string {
return "Beep boop."
}
// ========== 2. 空接口interface{}==========
// 在 Go 1.18+ 中,推荐使用 any它是 interface{} 的别名)
func printAnything(v any) {
fmt.Printf("接收到: %v (类型: %T)\n", v, v)
}
// ========== 3. 类型断言 ==========
func describeSpeaker(s Speaker) {
fmt.Println("它说:", s.Speak())
// 类型断言:判断具体类型
if d, ok := s.(Dog); ok {
fmt.Println("这是一只狗!", d)
} else if c, ok := s.(Cat); ok {
fmt.Println("这是一只猫!", c)
}
}
// 使用 switch 进行类型断言(更优雅)
func identify(v any) {
switch x := v.(type) {
case string:
fmt.Printf("字符串: %s\n", x)
case int:
fmt.Printf("整数: %d\n", x)
case Speaker:
fmt.Printf("会说话的东西: %s\n", x.Speak())
default:
fmt.Printf("未知类型: %T\n", x)
}
}
// ========== 主函数 ==========
func main() {
fmt.Println("=== 1. 鸭子类型:只要会 Speak(),就是 Speaker ===")
animals := []Speaker{Dog{}, Cat{}, Robot{}}
for _, a := range animals {
fmt.Println(a.Speak())
}
fmt.Println("\n=== 2. 空接口any可接收任意类型 ===")
printAnything(42)
printAnything("Hello")
printAnything(Dog{})
fmt.Println("\n=== 3. 类型断言:从接口还原具体类型 ===")
describeSpeaker(Dog{})
describeSpeaker(Cat{})
fmt.Println("\n=== 4. 类型 switch安全高效的类型判断 ===")
identify("Gopher")
identify(100)
identify(Robot{})
identify(true) // 未知类型
fmt.Println("\n=== 5. 安全 vs 不安全的类型断言 ===")
var i any = "hello"
s := i.(string) // 不安全:如果类型不对,会 panic
fmt.Println("不安全断言结果:", s)
// 安全方式
if val, ok := i.(int); ok {
fmt.Println("是整数:", val)
} else {
fmt.Println("不是整数!")
}
}
```
保存并退出(`Ctrl+O` → Enter → `Ctrl+X`)。
---
## ✅ 步骤 3格式化并运行
```bash
go fmt
go run .
```
### 预期输出:
```
=== 1. 鸭子类型:只要会 Speak(),就是 Speaker ===
Woof!
Meow~
Beep boop.
=== 2. 空接口any可接收任意类型 ===
接收到: 42 (类型: int)
接收到: Hello (类型: string)
接收到: {} (类型: main.Dog)
=== 3. 类型断言:从接口还原具体类型 ===
它说: Woof!
这是一只狗! {}
它说: Meow~
这是一只猫! {}
=== 4. 类型 switch安全高效的类型判断 ===
字符串: Gopher
整数: 100
会说话的东西: Beep boop.
未知类型: bool
=== 5. 安全 vs 不安全的类型断言 ===
不安全断言结果: hello
不是整数!
```
---
## 🔍 核心概念解析
### 🦆 1. 鸭子类型Duck Typing
> “如果它走起来像鸭子,叫起来也像鸭子,那它就是鸭子。”
- Go 的接口是 **隐式实现** 的:只要结构体有 `Speak()` 方法,就自动满足 `Speaker` 接口。
- **无需显式声明** `implements`(对比 Java
### 🕳️ 2. 空接口 `interface{}`(或 `any`
- 可以存储任意类型的值(类似 Java 的 `Object`,但更灵活)。
- 常用于:
- 通用函数参数(如 `fmt.Println`
- JSON 解析(`map[string]any`
- **代价**:失去类型安全,需配合类型断言使用。
### 🔍 3. 类型断言
语法:
```go
value, ok := interfaceVar.(ConcreteType)
```
- **安全方式**:用 `, ok` 检查是否成功,避免 panic。
- **不安全方式**:直接 `v := i.(T)`,类型不符时程序崩溃。
### 🔄 4. 类型 switch
```go
switch v := x.(type) {
case int:
// v 是 int
case string:
// v 是 string
}
```
- 更简洁、高效地处理多种类型。
---
## 💡 对比 Java
| Go | Java |
|----|------|
| `interface{}` / `any` | `Object` |
| 隐式实现接口 | 显式 `implements` |
| 类型断言 | `instanceof` + 强转 |
| 鸭子类型 | 接口必须显式实现 |
> Go 的接口设计更轻量、灵活,强调“行为”而非“继承关系”。
---
## ✅ 在 GoLand 中探索建议
1. 打开项目:`/home/liumangmang/GolandProjects/go-interfaces`
2. 将光标放在 `Speaker` 上,按 `Ctrl+H`(或右键 → Find Usages查看所有实现者
3. 尝试删除 `Robot``Speak()` 方法,观察编译错误(或无错误?因为没被用到!)
4.`identify` 函数中添加新类型(如 `[]int`),看类型 switch 如何处理
---
## 🧭 下一步学习方向
- 接口组合(`io.Reader`, `io.Writer`
- 错误处理与 `error` 接口
- 如何设计小而美的接口Go 哲学“Accept interfaces, return structs”
如果你希望我继续讲解 **错误处理、panic/recover、泛型入门****Go 标准库常用接口**,欢迎随时告诉我!祝你 Go 之旅越来越顺 🚀

View File

@@ -0,0 +1,253 @@
---
icon: mdi:alert-circle-outline
date: 2025-05-22
title: Go 基础语法 - 错误处理全解
category:
- Go
- 后端
- 编程
- 教程
- 错误处理
- error
- defer
- panic
- recover
- fmt.Errorf
- errors.Is
- errors.As
---
当然可以!以下是专为你定制的实践指南,完全基于你的开发环境:
- **操作系统**Linux Mint XFCE
- **Go 版本**go1.22.2 linux/amd64
- **项目目录**`/home/liumangmang/GolandProjects`
<!-- more -->
## 📌 标题:
# Go 错误处理全解error、defer、panic/recover 与错误封装实战
---
## ✅ 步骤 1创建新项目
打开终端,执行:
```bash
cd /home/liumangmang/GolandProjects
mkdir go-error-handling && cd go-error-handling
go mod init go-error-handling
```
---
## ✅ 步骤 2编写演示代码main.go
创建并编辑 `main.go`
```bash
nano main.go
```
粘贴以下完整示例代码(涵盖 error 返回、defer 清理、panic 恢复、fmt.Errorf 封装):
```go
package main
import (
"errors"
"fmt"
"os"
)
// ========== 1. 自定义错误 + fmt.Errorf 封装 ==========
func divide(a, b float64) (float64, error) {
if b == 0 {
// 使用 fmt.Errorf 包装原始错误,添加上下文
return 0, fmt.Errorf("divide by zero: cannot divide %.2f by %.2f", a, b)
}
return a / b, nil
}
// 更复杂的错误链Go 1.13+ 支持 %w
var ErrNegativeInput = errors.New("input must be non-negative")
func sqrt(x float64) (float64, error) {
if x < 0 {
// 使用 %w 包装错误,支持 errors.Is 和 errors.As
return 0, fmt.Errorf("invalid input for sqrt: %w", ErrNegativeInput)
}
return x * x, nil // 注意:这里故意写成平方,方便测试
}
// ========== 2. defer 的典型用途 ==========
func readFile(filename string) error {
fmt.Println("尝试打开文件:", filename)
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("无法打开文件 %s: %w", filename, err)
}
// defer 确保文件在函数退出前关闭(即使 panic
defer func() {
fmt.Println("defer: 关闭文件")
file.Close()
}()
// 模拟读取
fmt.Println("文件已打开,正在读取...")
return nil
}
// ========== 3. panic 与 recover ==========
func riskyFunction(n int) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("recover 捕获到 panic: %v\n", r)
}
}()
if n < 0 {
panic("n 不能为负数!")
}
fmt.Println("riskyFunction 正常执行n =", n)
}
// recover 只在 defer 中有效!
func noRecover() {
// ❌ 这样写无法捕获 panic
recover()
panic("不会被捕获")
}
// ========== 4. 综合示例:安全计算 ==========
func safeCompute(a, b float64) {
defer fmt.Println("safeCompute 结束\n---")
fmt.Printf("计算 %.2f / %.2f\n", a, b)
result, err := divide(a, b)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Printf("结果: %.2f\n", result)
fmt.Println("尝试对结果开方(实际是平方)")
sq, err := sqrt(result)
if err != nil {
if errors.Is(err, ErrNegativeInput) {
fmt.Println("检测到特定错误:输入为负")
}
fmt.Println("sqrt 错误:", err)
return
}
fmt.Printf("平方结果: %.2f\n", sq)
}
// ========== 主函数 ==========
func main() {
fmt.Println("=== 1. 基本 error 处理 ===")
safeCompute(10, 2)
safeCompute(10, 0) // 触发除零错误
fmt.Println("\n=== 2. defer 文件操作 ===")
readFile("/nonexistent/file.txt") // 文件不存在,触发错误
readFile("/etc/hostname") // 存在的文件Linux 系统通常有)
fmt.Println("\n=== 3. panic 与 recover ===")
riskyFunction(5)
riskyFunction(-1) // 触发 panic但被 recover 捕获
fmt.Println("\n=== 4. 错误封装与识别 ===")
_, err := sqrt(-4)
if err != nil {
fmt.Println("原始错误信息:", err)
// 使用 errors.Is 判断是否包含特定错误
if errors.Is(err, ErrNegativeInput) {
fmt.Println("✅ 成功识别自定义错误 ErrNegativeInput")
}
}
// ⚠️ 取消注释下面这行会 crashrecover 不在 defer 中无效)
// noRecover()
}
```
保存并退出(`Ctrl+O` → Enter → `Ctrl+X`)。
---
## ✅ 步骤 3格式化并运行
```bash
go fmt
go run .
```
### 预期输出(节选):
```
=== 1. 基本 error 处理 ===
计算 10.00 / 2.00
结果: 5.00
尝试对结果开方(实际是平方)
平方结果: 25.00
safeCompute 结束
---
计算 10.00 / 0.00
错误: divide by zero: cannot divide 10.00 by 0.00
safeCompute 结束
---
=== 2. defer 文件操作 ===
尝试打开文件: /nonexistent/file.txt
defer: 关闭文件
尝试打开文件: /etc/hostname
文件已打开,正在读取...
defer: 关闭文件
=== 3. panic 与 recover ===
riskyFunction 正常执行n = 5
recover 捕获到 panic: n 不能为负数!
safeCompute 结束
---
=== 4. 错误封装与识别 ===
原始错误信息: invalid input for sqrt: input must be non-negative
✅ 成功识别自定义错误 ErrNegativeInput
```
---
## 🔍 核心知识点总结
| 机制 | 说明 | 最佳实践 |
|------|------|--------|
| **`error`** | Go 的错误是值,不是异常 | 函数最后一个返回值通常是 `error`,必须显式检查 |
| **`fmt.Errorf`** | 创建带格式的错误;用 `%w` 包装Go 1.13+ | 用于添加上下文,支持 `errors.Is`/`errors.As` |
| **`defer`** | 延迟执行,常用于资源清理(文件、锁、连接) | 确保 cleanup 逻辑总被执行,即使 panic |
| **`panic`** | 立即停止当前 goroutine开始栈展开 | 仅用于不可恢复的严重错误(如程序状态损坏) |
| **`recover`** | 在 defer 中调用,可捕获 panic | 用于守护关键服务不崩溃(如 HTTP 服务器中间件) |
> 💡 **重要原则**
> - **不要滥用 panic**Go 推崇“显式错误处理”,而非异常。
> - **永远检查 error**:忽略 error 是 Go 新手常见错误。
> - **用 `%w` 而不是 `%v` 包装错误**:这样才能用 `errors.Is` 判断。
---
## ✅ 在 GoLand 中调试建议
1. 打开项目:`/home/liumangmang/GolandProjects/go-error-handling`
2.`divide` 函数中设置断点,观察错误如何逐层返回
3. 尝试取消注释 `noRecover()`,运行看程序崩溃(理解 recover 必须在 defer 中)
4. 使用 **Evaluate Expression** 功能测试 `errors.Is(err, ErrNegativeInput)`
---
## 🧭 下一步学习方向
- 自定义 error 类型(实现 `Error() string` 方法)
- 使用 `errors.Unwrap()` 手动解包错误链
- 在 Web 服务中统一错误处理中间件
如果你希望我继续讲解 **泛型入门、context 使用、并发模式worker pool****Go 单元测试testing 包)**,欢迎随时告诉我!祝你写出健壮又优雅的 Go 代码 🚀

View File

@@ -0,0 +1,275 @@
---
icon: mdi:file-document-edit-outline
date: 2025-05-22
title: Go 基础语法 - 从零实现 Mini 日志库
category:
- Go
- 后端
- 编程
- 教程
- 日志库
- 实践项目
- 标准库
---
<!-- more -->
当然可以!以下是专为你定制的实战项目,完全基于你的开发环境:
- **操作系统**Linux Mint XFCE
- **Go 版本**go1.22.2 linux/amd64
- **项目目录**`/home/liumangmang/GolandProjects`
---
## 📌 标题:
# 从零实现 Mini 日志库Go 结构体、接口与 fmt 的完美结合
---
## ✅ 步骤 1创建项目结构
打开终端,执行以下命令:
```bash
cd /home/liumangmang/GolandProjects
mkdir go-mini-logger && cd go-mini-logger
go mod init go-mini-logger
```
我们将实现一个支持多级别Info/Warn/Error、可输出到控制台或文件、并可通过接口扩展的轻量日志库。
---
## ✅ 步骤 2定义日志接口logger.go
创建 `logger.go`
```bash
nano logger.go
```
粘贴以下代码:
```go
package main
import (
"fmt"
"io"
"os"
"time"
)
// LogLevel 日志级别
type LogLevel int
const (
LevelInfo LogLevel = iota
LevelWarn
LevelError
)
func (l LogLevel) String() string {
switch l {
case LevelInfo:
return "INFO"
case LevelWarn:
return "WARN"
case LevelError:
return "ERROR"
default:
return "UNKNOWN"
}
}
// Logger 接口:定义日志行为
type Logger interface {
Log(level LogLevel, format string, args ...any)
Info(format string, args ...any)
Warn(format string, args ...any)
Error(format string, args ...any)
SetMinLevel(level LogLevel)
}
// defaultLogger 结构体:默认实现
type defaultLogger struct {
minLevel LogLevel
writer io.Writer
}
// NewLogger 创建新日志实例,默认输出到 os.Stdout
func NewLogger() Logger {
return &defaultLogger{
minLevel: LevelInfo,
writer: os.Stdout,
}
}
// NewFileLogger 创建写入文件的日志实例
func NewFileLogger(filename string) (Logger, error) {
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, fmt.Errorf("无法创建日志文件 %s: %w", filename, err)
}
return &defaultLogger{
minLevel: LevelInfo,
writer: file,
}, nil
}
// SetMinLevel 设置最低日志级别
func (l *defaultLogger) SetMinLevel(level LogLevel) {
l.minLevel = level
}
// Log 核心日志方法
func (l *defaultLogger) Log(level LogLevel, format string, args ...any) {
if level < l.minLevel {
return // 低于最小级别,丢弃
}
// 构建日志消息:[时间] [级别] 消息
timestamp := time.Now().Format("2006-01-02 15:04:05")
message := fmt.Sprintf(format, args...)
line := fmt.Sprintf("[%s] [%s] %s\n", timestamp, level.String(), message)
// 写入目标(控制台或文件)
fmt.Fprint(l.writer, line)
}
// 快捷方法
func (l *defaultLogger) Info(format string, args ...any) { l.Log(LevelInfo, format, args...) }
func (l *defaultLogger) Warn(format string, args ...any) { l.Log(LevelWarn, format, args...) }
func (l *defaultLogger) Error(format string, args ...any) { l.Log(LevelError, format, args...) }
```
保存退出(`Ctrl+O` → Enter → `Ctrl+X`)。
---
## ✅ 步骤 3编写使用示例main.go
创建 `main.go`
```bash
nano main.go
```
粘贴以下代码:
```go
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("=== 1. 控制台日志 ===")
consoleLogger := NewLogger()
consoleLogger.Info("程序启动PID: %d", os.Getpid())
consoleLogger.Warn("磁盘使用率超过 80%")
consoleLogger.Error("数据库连接失败: timeout")
fmt.Println("\n=== 2. 设置最低级别为 WARN ===")
consoleLogger.SetMinLevel(LevelWarn)
consoleLogger.Info("这条不会显示") // 被过滤
consoleLogger.Warn("这条会显示") // 显示
consoleLogger.Error("错误也会显示") // 显示
fmt.Println("\n=== 3. 文件日志(写入 app.log===")
fileLogger, err := NewFileLogger("app.log")
if err != nil {
consoleLogger.Error("无法创建文件日志: %v", err)
return
}
fileLogger.Info("应用开始处理请求")
fileLogger.Warn("用户 %s 尝试越权操作", "liumangmang")
fileLogger.Error("写入数据库时发生唯一键冲突")
fmt.Println("日志已写入 app.log请查看内容")
os.ReadFile("app.log") // 不打印,仅确保写入
fmt.Println("✅ 查看 app.log 可验证文件日志内容")
}
```
保存退出。
---
## ✅ 步骤 4格式化并运行
```bash
go fmt
go run .
```
### 预期终端输出:
```
=== 1. 控制台日志 ===
[2025-11-25 21:30:05] [INFO] 程序启动PID: 12345
[2025-11-25 21:30:05] [WARN] 磁盘使用率超过 80%
[2025-11-25 21:30:05] [ERROR] 数据库连接失败: timeout
=== 2. 设置最低级别为 WARN ===
[2025-11-25 21:30:05] [WARN] 这条会显示
[2025-11-25 21:30:05] [ERROR] 错误也会显示
=== 3. 文件日志(写入 app.log===
日志已写入 app.log请查看内容
✅ 查看 app.log 可验证文件日志内容
```
同时,当前目录会生成 `app.log` 文件。你可以查看它:
```bash
cat app.log
```
内容类似:
```
[2025-11-25 21:30:05] [INFO] 应用开始处理请求
[2025-11-25 21:30:05] [WARN] 用户 liumangmang 尝试越权操作
[2025-11-25 21:30:05] [ERROR] 写入数据库时发生唯一键冲突
```
---
## 🔍 设计亮点解析
| Go 特性 | 在本项目中的体现 |
|--------|----------------|
| **结构体** | `defaultLogger` 封装 writer 和 minLevel |
| **接口** | `Logger` 接口解耦调用方与实现便于未来扩展如网络日志、JSON 格式等) |
| **fmt 包** | 使用 `fmt.Sprintf` 格式化消息,`fmt.Fprint` 写入任意 `io.Writer` |
| **io.Writer** | 支持无缝切换输出目标(控制台、文件、网络流) |
| **可扩展性** | 只需实现 `Logger` 接口,即可插入新的日志后端 |
> 💡 **为什么用接口?**
> 未来你可以轻松添加 `JSONLogger`、`SyslogLogger` 等,而**无需修改业务代码**——只需传入不同的 `Logger` 实现。
---
## ✅ 在 GoLand 中进一步探索
1. 打开项目:`/home/liumangmang/GolandProjects/go-mini-logger`
2. 右键点击 `Logger` 接口 → **Go to → Implementations**,查看所有实现(目前只有 `defaultLogger`
3. 尝试新增一个 `SilentLogger`(不输出任何内容),验证接口的灵活性
4.`main.go` 中将 `consoleLogger` 替换为 `fileLogger`,观察行为变化
---
## 🧭 扩展建议(课后练习)
- 添加 `WithField(key, value string)` 方法支持结构化日志
- 支持日志轮转log rotation
- 增加 `Debug` 级别,并通过 build tag 控制是否编译
---
如果你希望我继续带你实现 **带上下文context的日志、JSON 格式输出、或集成 zap/logrus 对比分析**,欢迎随时告诉我!祝你编码愉快,日志清晰 🚀

View File

@@ -0,0 +1,235 @@
title: Go 并发入门Goroutine 基础与 GPM 调度模型实战解析
icon: go
date: 2025-12-11
category:
- Go
- 并发编程
tag:
- Goroutine
- GPM模型
- 调度器
star: true
---
当然可以!以下是专为你定制的学习实践指南,完全基于你的开发环境:
- **操作系统**Linux Mint XFCE
- **Go 版本**go1.22.2 linux/amd64
- **项目目录**`/home/liumangmang/GolandProjects`
<!-- more -->
---
## 📌 标题:
# Go 并发入门Goroutine 基础与 GPM 调度模型实战解析
---
## ✅ 步骤 1创建练习项目
打开终端,执行:
```bash
cd /home/liumangmang/GolandProjects
mkdir go-goroutine-gpm && cd go-goroutine-gpm
go mod init go-goroutine-gpm
```
---
## ✅ 步骤 2编写 Goroutine 基础示例main.go
创建 `main.go`
```bash
nano main.go
```
粘贴以下代码,涵盖 goroutine 启动、并发行为、以及 GPM 模型的观察方法:
```go
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func say(s string, id int) {
for i := 0; i < 3; i++ {
fmt.Printf("[%d] %s: step %d\n", id, s, i)
time.Sleep(100 * time.Millisecond) // 模拟工作
}
}
func main() {
fmt.Println("=== 1. 单线程顺序执行 ===")
say("Hello", 1)
say("World", 2)
fmt.Println("\n=== 2. 使用 Goroutine 并发执行 ===")
go say("Goroutine-A", 1)
go say("Goroutine-B", 2)
// 主 goroutine 等待 1 秒,否则程序会提前退出
time.Sleep(1 * time.Second)
fmt.Println("\n=== 3. 使用 sync.WaitGroup 安全等待 ===")
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Duration(id*200) * time.Millisecond)
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // 阻塞直到所有 goroutine 完成
fmt.Println("All workers done!")
fmt.Println("\n=== 4. 查看当前 GOMAXPROCS 和 CPU 核心数 ===")
fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) // 0 表示不修改,仅查询
fmt.Println("\n=== 5. 手动设置 GOMAXPROCS通常不需要===")
old := runtime.GOMAXPROCS(2)
fmt.Printf("旧 GOMAXPROCS: %d, 新设为: 2\n", old)
runtime.GOMAXPROCS(old) // 恢复原值
fmt.Println("\n=== 6. 观察 Goroutine 数量变化 ===")
fmt.Printf("启动前 Goroutine 数: %d\n", runtime.NumGoroutine())
var wg2 sync.WaitGroup
for i := 0; i < 5; i++ {
wg2.Add(1)
go func(n int) {
defer wg2.Done()
time.Sleep(200 * time.Millisecond)
}(i)
}
time.Sleep(50 * time.Millisecond) // 让 goroutine 启动
fmt.Printf("启动后 Goroutine 数: %d\n", runtime.NumGoroutine())
wg2.Wait()
fmt.Printf("全部完成后 Goroutine 数: %d\n", runtime.NumGoroutine())
}
```
保存并退出(`Ctrl+O` → Enter → `Ctrl+X`)。
---
## ✅ 步骤 3格式化并运行
```bash
go fmt
go run .
```
### 预期输出(顺序可能略有不同):
```
=== 1. 单线程顺序执行 ===
[1] Hello: step 0
[1] Hello: step 1
[1] Hello: step 2
[2] World: step 0
[2] World: step 1
[2] World: step 2
=== 2. 使用 Goroutine 并发执行 ===
[Goroutine-A] step 0
[Goroutine-B] step 0
[Goroutine-A] step 1
[Goroutine-B] step 1
[Goroutine-A] step 2
[Goroutine-B] step 2
=== 3. 使用 sync.WaitGroup 安全等待 ===
Worker 1 started
Worker 2 started
Worker 3 started
Worker 1 finished
Worker 2 finished
Worker 3 finished
All workers done!
=== 4. 查看当前 GOMAXPROCS 和 CPU 核心数 ===
CPU 核心数: 8
GOMAXPROCS: 8
=== 5. 手动设置 GOMAXPROCS通常不需要===
旧 GOMAXPROCS: 8, 新设为: 2
=== 6. 观察 Goroutine 数量变化 ===
启动前 Goroutine 数: 1
启动后 Goroutine 数: 6
全部完成后 Goroutine 数: 1
```
> 💡 注意Goroutine 输出顺序是**不确定的**,这正是并发的体现!
---
## 🔍 核心概念解析GPM 调度模型
Go 的调度器采用 **GPM 模型**,三者含义如下:
| 组件 | 全称 | 作用 |
|------|------|------|
| **G** | Goroutine | 用户级轻量协程,由 Go 运行时管理 |
| **P** | Processor | 逻辑处理器,持有 G 的本地队列,数量 = `GOMAXPROCS` |
| **M** | Machine | 操作系统线程,真正执行代码的实体 |
### 调度流程简述:
1. 每个 **P** 绑定一个 **M**OS 线程)
2. **G** 被分配到某个 **P** 的本地队列
3. **M** 执行 **P** 上的 **G**
4.**G** 阻塞(如 I/O**M** 会与 **P** 解绑,**P** 可被其他 **M** 接管,保证 CPU 不空闲
> ✅ **优势**
> - Goroutine 创建成本极低(初始栈仅 2KB
> - 即使百万级 Goroutine也能高效调度
> - 阻塞不会阻塞 OS 线程(网络 I/O 由 netpoller 处理)
---
## 🧪 实验建议(在 GoLand 中)
1. **查看 Goroutine 堆栈**
在调试模式下,点击 **Goroutines** 面板,实时查看所有 goroutine 状态。
2. **修改 GOMAXPROCS**
尝试 `runtime.GOMAXPROCS(1)`,观察并发是否变成“伪并发”(仍能切换,但只用 1 个 CPU
3. **制造阻塞**
在 goroutine 中加入 `time.Sleep``http.Get`,观察调度器如何复用 M。
---
## ⚠️ 常见误区提醒
| 误区 | 正确理解 |
|------|--------|
| “Goroutine = OS 线程” | ❌ Goroutine 是用户态协程,由 Go 调度器在少量 OS 线程上复用 |
| “必须设置 GOMAXPROCS” | ❌ Go 1.5+ 默认等于 CPU 核心数,通常无需手动设置 |
| “Goroutine 会自动等待” | ❌ 主 goroutine 退出,程序立即终止!必须用 `WaitGroup`、channel 或 `time.Sleep` 等待 |
---
## 📚 延伸学习方向
- 使用 `go tool trace` 可视化调度行为
- 学习 **channel** 实现 goroutine 通信(避免共享内存)
- 理解 **context** 如何优雅取消 goroutine
---
如果你希望我接下来带你实现 **channel 通信、select 多路复用、或 worker pool 模式**,欢迎随时告诉我!祝你轻松掌握 Go 并发编程 🚀

View File

@@ -0,0 +1,643 @@
---
icon: mdi:language-go
date: 2025-05-22
category:
- 后端开发
- Go语言
tag:
- Go
- 编程语言
- 入门教程
star: true
---
# Go 语言基础入门
Go又称 Golang是由 Google 开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。它于 2009 年首次发布,以其简洁、高效和强大的并发处理能力而闻名。
## 目录
1. [Go 语言简介](#go-语言简介)
2. [环境搭建](#环境搭建)
3. [第一个 Go 程序](#第一个-go-程序)
4. [基本语法](#基本语法)
5. [数据类型](#数据类型)
6. [变量和常量](#变量和常量)
7. [运算符](#运算符)
8. [控制结构](#控制结构)
9. [函数](#函数)
10. [数组和切片](#数组和切片)
11. [映射(Map)](#映射map)
12. [结构体](#结构体)
13. [接口](#接口)
14. [并发编程](#并发编程)
15. [错误处理](#错误处理)
16. [包管理](#包管理)
## Go 语言简介
Go 语言的设计目标是:
- 简单易学:语法简洁,去除了许多其他语言的复杂特性
- 高效执行:编译型语言,运行速度快
- 内置并发支持:通过 goroutine 和 channel 实现轻量级并发
- 强大的标准库:提供了丰富的内置库
- 跨平台支持:支持多种操作系统和架构
## 环境搭建
### 下载安装
1. 访问 [Go 官方网站](https://golang.org/dl/) 下载适合你操作系统的安装包
2. Windows 用户下载 `.msi` 文件macOS 用户下载 `.pkg` 文件Linux 用户下载 `.tar.gz` 文件
### Windows 安装步骤
1. 双击下载的 `.msi` 文件开始安装
2. 默认会安装到 `C:\Go\` 目录下
3. 安装完成后会自动配置环境变量
### Linux 安装步骤
```bash
# 下载 Go 安装包
wget https://golang.org/dl/go1.19.linux-amd64.tar.gz
# 解压到 /usr/local 目录
sudo tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz
# 配置环境变量
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
```
### 验证安装
打开终端或命令提示符,输入以下命令验证是否安装成功:
```bash
go version
```
如果显示版本信息,则说明安装成功。
### 设置工作目录
Go 项目推荐使用模块化管理,在任意目录下创建你的项目文件夹:
```bash
mkdir myproject
cd myproject
go mod init myproject
```
## 第一个 Go 程序
创建一个名为 `main.go` 的文件:
```go
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
```
运行程序:
```bash
go run main.go
```
或者编译后运行:
```bash
go build main.go
./main
```
代码解释:
- `package main`:声明这是一个可执行程序的主包
- `import "fmt"`:导入格式化输入输出包
- `func main()`:程序入口函数
- `fmt.Println()`:打印文本并换行
## 基本语法
### 注释
```go
// 单行注释
/*
多行注释
可以跨越多行
*/
```
### 标识符命名规则
- 只能包含字母、数字和下划线
- 不能以数字开头
- 区分大小写
- 不能是关键字
### 关键字
Go 语言有 25 个关键字:
```
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
```
## 数据类型
Go 语言的数据类型分为四大类:
### 基本类型
1. **布尔型**booltrue/false
2. **数字类型**
- 整数型int8, int16, int32, int64, uint8, uint16, uint32, uint64, int, uint, uintptr
- 浮点型float32, float64
- 复数型complex64, complex128
3. **字符串类型**string
4. **派生类型**
- 指针类型Pointer
- 数组类型
- 结构化类型struct
- Channel 类型
- 函数类型
- 切片类型
- 接口类型interface
- Map 类型
### 类型示例
```go
var a bool = true // 布尔型
var b int = 10 // 整型
var c float32 = 3.14 // 浮点型
var d string = "Hello" // 字符串
var e complex64 = 3+4i // 复数型
```
## 变量和常量
### 变量声明
四种声明变量的方式:
```go
// 1. 指定变量类型,声明后若不赋值则使用默认值
var v1 int
// 2. 根据值自行判定变量类型
var v2 = 10
// 3. 省略 var注意 := 左侧的变量必须是未声明过的
v3 := 10
// 4. 多变量声明
var v4, v5 int = 1, 2
var v6, v7 = 3, "hello"
v8, v9 := 5, true
```
### 变量作用域
- **局部变量**:在函数内声明,只在函数内有效
- **全局变量**:在函数外声明,在整个包内有效
### 常量声明
```go
const PI = 3.14159
const NAME = "Golang"
// 常量组
const (
Monday = 1
Tuesday = 2
Wednesday = 3
)
```
### iota
iota 是一个特殊的常量,用于生成一组相似的常量值:
```go
const (
a = iota // 0
b // 1
c // 2
d // 3
)
```
## 运算符
### 算术运算符
| 运算符 | 描述 |
|--------|------|
| + | 相加 |
| - | 相减 |
| * | 相乘 |
| / | 相除 |
| % | 求余 |
| ++ | 自增 |
| -- | 自减 |
### 关系运算符
| 运算符 | 描述 |
|--------|------|
| == | 相等 |
| != | 不相等 |
| > | 大于 |
| < | 小于 |
| >= | 大于等于 |
| <= | 小于等于 |
### 逻辑运算符
| 运算符 | 描述 |
|--------|------|
| && | 逻辑与 |
| \|\| | 逻辑或 |
| ! | 逻辑非 |
### 位运算符
| 运算符 | 描述 |
|--------|------|
| & | 按位与 |
| \| | 按位或 |
| ^ | 按位异或 |
| << | 左移 |
| >> | 右移 |
## 控制结构
### 条件语句 if
```go
if condition {
// code
}
// 带初始化语句
if x := 10; x > 5 {
fmt.Println("x 大于 5")
} else {
fmt.Println("x 小于等于 5")
}
```
### 循环语句 for
Go 只有一种循环结构for 循环
```go
// 基本 for 循环
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// while 形式的循环
i := 0
for i < 10 {
fmt.Println(i)
i++
}
// 无限循环
for {
// 死循环
}
```
### 选择语句 switch
```go
switch day {
case 1:
fmt.Println("Monday")
case 2:
fmt.Println("Tuesday")
default:
fmt.Println("Unknown day")
}
// 不带条件的 switch
switch {
case score >= 90:
fmt.Println("优秀")
case score >= 80:
fmt.Println("良好")
default:
fmt.Println("一般")
}
```
## 函数
### 函数定义
```go
func functionName(parameterName type) returnType {
// 函数体
return returnValue
}
```
### 示例
```go
// 简单函数
func add(a int, b int) int {
return a + b
}
// 多返回值函数
func swap(x, y string) (string, string) {
return y, x
}
// 可变参数函数
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
```
### 函数调用
```go
result := add(3, 5)
a, b := swap("hello", "world")
total := sum(1, 2, 3, 4, 5)
```
### defer 语句
defer 用于延迟执行函数,通常用于资源清理:
```go
func readFile() {
file, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close() // 函数结束前关闭文件
// 处理文件...
}
```
## 数组和切片
### 数组
数组是固定长度的序列:
```go
// 声明数组
var arr1 [5]int // 长度为 5 的整型数组
arr2 := [5]int{1, 2, 3, 4, 5} // 初始化数组
arr3 := [...]int{1, 2, 3} // 根据元素个数确定长度
// 访问数组元素
fmt.Println(arr2[0]) // 输出 1
arr2[1] = 10 // 修改元素
```
### 切片
切片是对数组的抽象,长度可变:
```go
// 创建切片
slice1 := []int{1, 2, 3} // 直接创建切片
slice2 := make([]int, 5) // 创建长度为 5 的切片
slice3 := make([]int, 3, 5) // 长度为 3容量为 5
// 从数组创建切片
arr := [5]int{1, 2, 3, 4, 5}
slice4 := arr[1:4] // 包含索引 1 到 3 的元素
// 切片操作
slice5 := append(slice1, 4, 5) // 添加元素
len(slice1) // 获取长度
cap(slice1) // 获取容量
```
## 映射(Map)
Map 是一种无序的键值对集合:
```go
// 创建 map
var m1 map[string]int // 声明但未初始化
m2 := make(map[string]int) // 使用 make 初始化
m3 := map[string]int{ // 直接初始化
"apple": 5,
"banana": 3,
}
// 操作 map
m2["orange"] = 10 // 添加键值对
value := m2["orange"] // 获取值
delete(m2, "orange") // 删除键值对
// 检查键是否存在
if val, ok := m2["apple"]; ok {
fmt.Println("apple:", val)
} else {
fmt.Println("apple not found")
}
```
## 结构体
结构体是一种用户自定义的数据类型,允许我们组合不同类型的字段:
```go
// 定义结构体
type Person struct {
Name string
Age int
Email string
}
// 创建结构体实例
person1 := Person{Name: "张三", Age: 25, Email: "zhangsan@example.com"}
person2 := Person{"李四", 30, "lisi@example.com"}
person3 := Person{} // 所有字段使用零值
// 访问结构体字段
fmt.Println(person1.Name)
person1.Age = 26
// 结构体指针
person4 := &Person{Name: "王五", Age: 28}
fmt.Println(person4.Name) // 自动解引用
```
### 方法
方法是带有接收者的函数:
```go
// 为结构体定义方法
func (p Person) SayHello() {
fmt.Printf("Hello, I'm %s\n", p.Name)
}
func (p *Person) SetAge(age int) {
p.Age = age
}
// 调用方法
person1.SayHello()
person1.SetAge(27)
```
## 接口
接口是一组方法签名的集合:
```go
// 定义接口
type Shape interface {
Area() float64
Perimeter() float64
}
// 实现接口
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// 使用接口
var s Shape = Rectangle{Width: 10, Height: 5}
fmt.Println("Area:", s.Area())
fmt.Println("Perimeter:", s.Perimeter())
```
## 并发编程
Go 语言内置了强大的并发支持,主要包括 goroutine 和 channel。
### Goroutine
Goroutine 是轻量级线程:
```go
// 启动 goroutine
go func() {
fmt.Println("Hello from goroutine")
}()
// 启动多个 goroutine
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Printf("Goroutine %d\n", n)
}(i)
}
```
### Channel
Channel 用于 goroutine 之间通信:
```go
// 创建 channel
ch := make(chan int)
// 发送数据到 channel
go func() {
ch <- 42
}()
// 从 channel 接收数据
value := <-ch
fmt.Println(value)
// 带缓冲的 channel
bufferedCh := make(chan int, 3)
bufferedCh <- 1
bufferedCh <- 2
bufferedCh <- 3
```
### Select
Select 用于在多个 channel 操作中进行选择:
```go
select {
case msg1 := <-ch1:
fmt.Println("Received", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
case <-time.After(time.Second):
fmt.Println("Timeout")
}
```
## 错误处理
Go 语言通过返回错误值来进行错误处理:
```go
// 错误处理示例
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// 使用错误
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
```
### 自定义错误
```go
type MyError struct {
Msg string
Code int
}
func (e *MyError) Error() string {
return fmt.Sprintf("Error %d: %s", e.Code, e.Msg)
}
```
## 包管理
### 创建模块
```bash
go mod init myproject
```
### 添加依赖
```bash
go get github.com/gin-gonic/gin
```
### 查看依赖
```bash
go mod tidy
go list -m all
```
### go.mod 文件示例
```
module myproject
go 1.19
require (
github.com/gin-gonic/gin v1.8.1
)
```
## 总结
Go 语言以其简洁的语法、强大的并发支持和高效的执行性能成为现代软件开发的重要选择。通过本文档的学习,你应该掌握了 Go 语言的基本概念和语法,可以开始编写简单的 Go 程序了。
建议接下来的学习路径:
1. 练习更多 Go 语言编程题目
2. 学习标准库的使用
3. 了解 Go 语言的最佳实践
4. 尝试构建实际项目
Happy coding with Go!

View File

@@ -0,0 +1,168 @@
---
icon: mdi:clipboard-text
date: 2025-05-22
title: "20250522"
category:
- Java
tag:
- 面试题
---
### 选择题
1. 以下关于 Java 中异常处理的说法,正确的是(
A. try 块后必须跟 catch 块
B. try 块后可以不跟任何 catch 块,但必须跟 finally 块
C. 一个 try 块可以有多个 catch 块,且异常捕获顺序无关紧要
D. finally 块中的代码无论是否发生异常都会执行
2. 下列关于 Java 多线程的说法,错误的是(
A. 创建线程可以通过继承 Thread 类或实现 Runnable 接口
B. 线程的 start() 方法会调用 run() 方法
C. 多个线程可以同时访问同一个共享资源而不会产生任何问题
D. 可以使用 synchronized 关键字来实现线程同步
### 填空题
1. Java 中用于创建对象的关键字是__________。
2. Java 中实现多态的两种方式是__________和__________。
### 简答题
1. 请简述 Java 中的垃圾回收机制及其作用。
2. 简述 Spring 框架中 IoC控制反转和 DI依赖注入的概念及它们之间的关系。
### 编程题
1. 编写一个 Java 程序实现一个简单的栈Stack数据结构包含入栈push、出栈pop和查看栈顶元素peek的方法。
### 场景题
1. 在一个分布式系统中,有多个服务需要共享缓存数据。当缓存数据更新时,如何确保各个服务能及时获取到最新的缓存数据?请描述你的解决方案。
### 论述题
1. 论述 Java 中微服务架构的优势和挑战,并结合实际案例进行说明。
### 综合题
1. 假设你正在开发一个电商系统,该系统包含商品管理、订单管理和用户管理三个模块。请设计一个 Java 程序架构,说明各个模块之间的关系以及如何使用 Java 技术实现它们之间的交互。
### 选择题答案
1. **答案**D
**解析**A选项try块后可以不跟catch块但必须跟finally块或者catch块所以A错误B选项try块后可以既不跟catch块也不跟finally块不过这样就失去了异常处理的意义所以B错误C选项一个try块可以有多个catch块但异常捕获顺序是有要求的子类异常的catch块要放在父类异常的catch块之前所以C错误D选项finally块中的代码无论是否发生异常都会执行这是finally块的特性所以D正确。
2. **答案**C
**解析**A选项在Java中创建线程可以通过继承Thread类或实现Runnable接口这是常见的创建线程的方式所以A正确B选项线程的start()方法会启动线程然后会调用run()方法执行线程的任务所以B正确C选项多个线程同时访问同一个共享资源时如果没有进行适当的同步控制会产生数据不一致等问题也就是线程安全问题所以C错误D选项synchronized关键字可以用来实现线程同步保证同一时间只有一个线程可以访问被synchronized修饰的代码块或方法所以D正确。
### 填空题答案
1. **答案**new
**解析**在Java中使用new关键字来创建对象。例如`Object obj = new Object();`这里的new关键字会在堆内存中为对象分配空间并调用对象的构造方法进行初始化。
2. **答案**:方法重载;方法重写
**解析**:方法重载是指在同一个类中,有多个方法具有相同的方法名,但参数列表不同(参数的类型、个数或顺序不同)。通过方法重载,我们可以根据不同的参数来调用不同的方法,这是实现多态的一种方式。方法重写是指子类重写父类的方法,要求方法名、参数列表和返回值类型都相同。在运行时,根据对象的实际类型来调用相应的方法,这也是实现多态的重要方式。
### 简答题答案
1. **答案**Java 中的垃圾回收机制是 Java 虚拟机JVM提供的一种自动内存管理机制。它会自动检测和回收那些不再被引用的对象所占用的内存空间。其作用主要有一是减轻程序员的负担程序员不需要手动释放对象的内存避免了因忘记释放内存而导致的内存泄漏问题二是提高内存的使用效率及时回收不再使用的内存使得内存可以被其他对象使用。
**解析**垃圾回收机制的核心是通过垃圾回收器GC来实现的。GC 会定期或在内存不足时启动,它会遍历对象的引用关系,找出那些没有被任何引用指向的对象,将这些对象标记为垃圾对象,然后回收它们所占用的内存。在 Java 中,对象的生命周期由 JVM 自动管理,程序员只需要关注对象的创建和使用,而不需要关心对象的销毁,这大大提高了开发效率和程序的稳定性。
2. **答案**IoC控制反转是一种设计思想它将对象的创建和依赖关系的管理从代码中转移到外部容器中。在传统的编程方式中对象的创建和依赖关系是在代码中硬编码的而在 IoC 模式下对象的创建和依赖关系的配置由外部容器负责。DI依赖注入是 IoC 的一种具体实现方式它通过外部容器将对象所依赖的其他对象注入到该对象中。IoC 和 DI 的关系是DI 是实现 IoC 的一种手段,通过 DI 可以更好地实现 IoC 的思想。
**解析**:在 Spring 框架中IoC 容器(如 ApplicationContext负责管理对象的创建和依赖关系。通过配置文件如 XML 配置文件)或注解(如 @Autowired),可以将对象之间的依赖关系注入到相应的对象中。例如,一个类 A 依赖于类 B在传统编程中类 A 会在自己的代码中创建类 B 的实例,而在 Spring 中,类 A 只需要声明对类 B 的依赖Spring 容器会自动将类 B 的实例注入到类 A 中。这样可以降低代码的耦合度,提高代码的可维护性和可测试性。
### 编程题答案
1. **示例代码**
```java
import java.util.EmptyStackException;
// 自定义栈类
class MyStack {
private int[] stack;
private int top;
private int capacity;
// 构造函数,初始化栈的容量
public MyStack(int capacity) {
this.capacity = capacity;
this.stack = new int[capacity];
this.top = -1;
}
// 入栈操作
public void push(int item) {
if (top == capacity - 1) {
throw new StackOverflowError("Stack is full");
}
stack[++top] = item;
}
// 出栈操作
public int pop() {
if (top == -1) {
throw new EmptyStackException();
}
return stack[top--];
}
// 查看栈顶元素
public int peek() {
if (top == -1) {
throw new EmptyStackException();
}
return stack[top];
}
// 判断栈是否为空
public boolean isEmpty() {
return top == -1;
}
}
public class StackExample {
public static void main(String[] args) {
MyStack stack = new MyStack(5);
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("Top element: " + stack.peek());
System.out.println("Popped element: " + stack.pop());
System.out.println("Top element after pop: " + stack.peek());
}
}
```
**解析**:首先定义了一个`MyStack`类,它包含一个整数数组`stack`用于存储栈中的元素,`top`变量表示栈顶的位置,`capacity`表示栈的容量。构造函数`MyStack(int capacity)`用于初始化栈的容量和数组。`push(int item)`方法用于将元素入栈,如果栈已满则抛出`StackOverflowError`异常。`pop()`方法用于将栈顶元素出栈,如果栈为空则抛出`EmptyStackException`异常。`peek()`方法用于查看栈顶元素,如果栈为空则抛出`EmptyStackException`异常。`isEmpty()`方法用于判断栈是否为空。在`main`方法中,创建了一个栈对象,并进行了入栈、查看栈顶元素和出栈等操作。
### 场景题答案
1. **答案**:可以采用以下解决方案来确保各个服务能及时获取到最新的缓存数据:
- 使用消息队列:当缓存数据更新时,更新服务向消息队列发送一条消息,各个需要使用缓存数据的服务订阅该消息队列。当接收到消息后,这些服务会主动去获取最新的缓存数据。
- 缓存失效机制:更新服务在更新缓存数据时,同时将旧的缓存数据标记为失效。各个服务在访问缓存时,如果发现缓存数据失效,会重新从数据源获取最新的数据并更新缓存。
- 定时刷新:各个服务设置定时任务,定期从缓存中获取最新的数据。这种方式可以保证在一定时间内各个服务能获取到最新的缓存数据,但可能存在一定的延迟。
- 缓存更新通知:更新服务在更新缓存数据后,直接向各个服务发送更新通知,各个服务接收到通知后,立即去获取最新的缓存数据。
**解析**:使用消息队列的好处是解耦了缓存更新和服务获取数据的过程,提高了系统的可扩展性和可靠性。缓存失效机制可以确保服务在访问缓存时能获取到最新的数据,但需要注意失效标记的管理。定时刷新的方式简单易行,但可能会造成不必要的资源浪费和数据延迟。缓存更新通知可以实时通知各个服务,但需要确保通知的可靠性和及时性。在实际应用中,可以根据系统的特点和需求选择合适的解决方案,也可以将多种方案结合使用。
### 论述题答案
1. **答案**Java 中微服务架构的优势主要有:
- 可扩展性:微服务架构将一个大型应用拆分成多个小型的、自治的服务,每个服务可以独立进行扩展。例如,一个电商系统中的商品服务和订单服务可以根据各自的业务需求进行独立的水平扩展,提高系统的性能和处理能力。
- 可维护性:每个微服务都有自己独立的代码库和开发团队,开发人员可以更专注于自己负责的服务,降低了代码的复杂度,提高了代码的可维护性。例如,当需要对商品服务进行修改时,不会影响到其他服务的正常运行。
- 技术多样性:不同的微服务可以使用不同的技术栈来实现,根据服务的特点选择最合适的技术。例如,对于实时性要求较高的订单服务可以使用 Java 的高性能框架,而对于数据处理和分析的服务可以使用 Python 等语言。
- 快速部署:微服务可以独立部署,当一个服务发生变化时,只需要部署该服务即可,不需要重新部署整个应用,提高了开发和部署的效率。
微服务架构的挑战主要有:
- 服务间通信:微服务之间需要进行通信,这增加了系统的复杂性。例如,需要处理网络延迟、服务故障等问题。可以使用 RESTful API、消息队列等方式来实现服务间的通信。
- 服务管理:随着微服务数量的增加,服务的管理变得更加困难。需要使用服务注册与发现、配置管理等工具来管理微服务。
- 分布式事务:在微服务架构中,一个业务操作可能涉及多个服务,需要处理分布式事务的问题。可以使用两阶段提交、补偿事务等方式来解决分布式事务问题。
实际案例Netflix 是一个典型的使用微服务架构的公司。它将视频流服务拆分成多个微服务如用户认证服务、视频播放服务、推荐服务等。每个服务可以独立开发、部署和扩展提高了系统的性能和可维护性。同时Netflix 也面临着服务间通信、服务管理等挑战,它使用了 Eureka 进行服务注册与发现,使用 Hystrix 进行服务熔断和降级,解决了微服务架构中的一些问题。
**解析**:微服务架构的优势在于它能够更好地适应现代软件开发的需求,提高系统的灵活性和可扩展性。但同时也带来了一些挑战,需要开发人员掌握更多的技术和工具来解决这些问题。通过实际案例可以更直观地看到微服务架构的应用和解决问题的方法。
### 综合题答案
1. **答案**:以下是一个电商系统的 Java 程序架构设计:
**模块关系**
- 用户管理模块负责用户的注册、登录、信息管理等功能。用户管理模块为商品管理模块和订单管理模块提供用户信息,例如在商品浏览和下单时需要验证用户的身份。
- 商品管理模块负责商品的添加、修改、删除和查询等功能。商品管理模块为订单管理模块提供商品信息,订单管理模块根据商品信息生成订单。
- 订单管理模块负责订单的创建、支付、发货、退款等功能。订单管理模块会更新商品的库存信息,同时会记录用户的订单历史。
**实现方式**
- **分层架构**:采用 MVCModel - View - Controller或 MVVMModel - View - ViewModel分层架构将业务逻辑、数据访问和视图展示分离。例如在商品管理模块中Controller 层负责接收用户的请求Service 层负责处理业务逻辑Dao 层负责与数据库进行交互。
- **数据库设计**:使用关系型数据库(如 MySQL来存储用户信息、商品信息和订单信息。不同模块对应不同的数据库表通过外键关联来建立表之间的关系。例如订单表中会有用户 ID 和商品 ID 字段,分别关联用户表和商品表。
- **服务接口**:各个模块之间通过服务接口进行交互。可以使用 RESTful API 来实现服务接口,提高系统的可扩展性和兼容性。例如,订单管理模块可以通过调用商品管理模块的 API 来获取商品信息。
- **消息队列**:使用消息队列(如 Kafka 或 RabbitMQ来实现模块之间的异步通信。例如当订单创建成功后订单管理模块可以向消息队列发送一条消息商品管理模块订阅该消息接收到消息后更新商品的库存信息。
**解析**:通过分层架构可以将不同的功能模块分离,提高代码的可维护性和可测试性。数据库设计是系统的基础,合理的数据库表结构可以提高数据的存储和查询效率。服务接口的使用可以实现模块之间的解耦,使得各个模块可以独立开发和部署。消息队列的引入可以实现模块之间的异步通信,提高系统的性能和可靠性。这种架构设计可以满足电商系统的高并发、高可用性和可扩展性的需求。

View File

@@ -0,0 +1,136 @@
---
icon: mdi:clipboard-text
title: "20250523"
date: 2025-05-23
category:
- JAVA
tag:
- 试题
---
2025-05-23 AI试题
<!-- more -->
### 选择题
1. 以下关于 Java 类的构造方法,说法正确的是(
A. 构造方法必须有返回值类型
B. 一个类只能有一个构造方法
C. 构造方法的名称必须与类名相同
D. 构造方法不能使用访问修饰符
2. 下列关于 Java 中 String 类的描述,错误的是(
A. String 类是不可变类,一旦创建,其值不能被修改
B. 可以使用“+”运算符来连接两个 String 对象
C. String 类的对象存储在栈内存中
D. String 类提供了很多方法,如 equals() 用于比较两个字符串的内容是否相等
### 填空题
1. Java 中用于实现线程同步的关键字除了 synchronized 外还有__________。
2. 在 Java 集合框架中__________是一个有序的、可重复的集合接口。
### 简答题
1. 简述 Java 中反射机制的概念和用途。
2. 请说明 Java 中抽象类和接口的区别。
### 编程题
1. 编写一个 Java 程序,实现对一个整数数组进行排序,并输出排序后的数组。要求使用冒泡排序算法。
### 场景题
1. 在一个 Java Web 项目中用户登录后需要将用户信息存储在会话Session中。当用户访问其他页面时需要验证用户是否已经登录。请描述实现该功能的步骤。
### 论述题
1. 论述 Java 中性能优化的主要方面和常用方法,并结合实际案例进行说明。
### 综合题
1. 设计一个 Java 程序,模拟一个图书馆管理系统。该系统包含图书管理、读者管理和借阅管理三个模块。图书管理模块负责图书的添加、删除和查询;读者管理模块负责读者的注册、注销和信息查询;借阅管理模块负责图书的借阅和归还操作。请说明各个模块之间的关系以及如何实现它们之间的交互。
### 选择题答案
1. **答案**C
**解析**A选项构造方法没有返回值类型连 void 都不能写,所以 A 错误B选项一个类可以有多个构造方法这就是构造方法的重载所以 B 错误C选项构造方法的名称必须与类名相同这是构造方法的基本定义所以 C 正确D选项构造方法可以使用访问修饰符如 public、private 等,所以 D 错误。
2. **答案**C
**解析**A选项String 类是不可变类,一旦创建,其值不能被修改,如果对 String 对象进行修改操作,实际上是创建了一个新的 String 对象,所以 A 正确B选项“+”运算符可以用于连接两个 String 对象,这是 Java 中常用的字符串连接方式,所以 B 正确C选项String 类的对象存储在堆内存中,而不是栈内存,所以 C 错误D选项String 类提供了很多方法equals() 方法用于比较两个字符串的内容是否相等,而不是比较引用是否相等,所以 D 正确。
### 填空题答案
1. **答案**Lock
**解析**:在 Java 中,除了使用 synchronized 关键字实现线程同步外,还可以使用 Lock 接口及其实现类(如 ReentrantLock来实现线程同步。Lock 提供了更灵活的锁机制,例如可以实现公平锁、可中断锁等。
2. **答案**List
**解析**:在 Java 集合框架中List 是一个有序的、可重复的集合接口。常见的实现类有 ArrayList、LinkedList 等。List 中的元素按照插入的顺序排列,并且可以包含重复的元素。
### 简答题答案
1. **答案**Java 中的反射机制是指在运行时动态地获取类的信息、创建对象、调用方法和访问属性等。其用途主要有:一是在框架开发中,如 Spring 框架通过反射机制实现依赖注入;二是在开发工具中,如 IDE 可以通过反射机制获取类的信息,提供代码提示等功能;三是在插件开发中,可以通过反射机制动态加载和调用插件类的方法。
**解析**:反射机制的核心是 Java 的 Class 类,通过 Class 类可以获取类的各种信息,如构造方法、方法、属性等。然后可以使用这些信息来创建对象、调用方法和访问属性。反射机制打破了传统的编译时绑定,使得程序在运行时可以更加灵活地处理类和对象。
2. **答案**Java 中抽象类和接口的区别主要有:
- 定义方式:抽象类使用 abstract 关键字修饰类,接口使用 interface 关键字定义。
- 成员变量:抽象类可以有普通成员变量,也可以有常量;接口中的成员变量默认都是 public static final 类型的常量。
- 方法:抽象类可以有抽象方法和非抽象方法;接口中的方法默认都是 public abstract 类型的抽象方法Java 8 及以后支持默认方法和静态方法)。
- 继承和实现:一个类只能继承一个抽象类,但可以实现多个接口。
- 设计目的:抽象类是对一类事物的抽象,包含了一些通用的属性和方法;接口是对行为的抽象,定义了一组行为规范。
**解析**:抽象类和接口在 Java 中都用于实现多态和代码复用,但它们的设计目的和使用场景有所不同。抽象类更侧重于对类的抽象,而接口更侧重于对行为的抽象。在实际开发中,需要根据具体的需求来选择使用抽象类还是接口。
### 编程题答案
1. **示例代码**
```java
public class BubbleSort {
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换 arr[j] 和 arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
public static void main(String[] args) {
int[] arr = {64, 34, 25, 12, 22, 11, 90};
bubbleSort(arr);
System.out.println("排序后的数组:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
```
**解析**:冒泡排序算法的基本思想是比较相邻的元素,如果顺序错误就把它们交换过来。外层循环控制排序的轮数,内层循环控制每一轮比较的次数。在每一轮中,将较大的元素逐步“冒泡”到数组的末尾。最后,数组就会按照从小到大的顺序排列。
### 场景题答案
1. **答案**:实现该功能的步骤如下:
- 用户登录验证:当用户提交登录信息时,在服务器端验证用户的用户名和密码是否正确。如果验证通过,将用户信息(如用户 ID、用户名等存储在会话Session中。
- 会话管理:在用户访问其他页面时,首先获取当前会话对象。检查会话中是否包含用户信息,如果包含,则说明用户已经登录;如果不包含,则说明用户未登录,将用户重定向到登录页面。
- 会话过期处理:为了保证系统的安全性,需要设置会话的过期时间。当会话过期后,用户需要重新登录。
**解析**:在 Java Web 项目中,可以使用 HttpSession 对象来管理会话。通过 request.getSession() 方法可以获取当前会话对象。在登录验证通过后,使用 session.setAttribute() 方法将用户信息存储在会话中。在其他页面中,使用 session.getAttribute() 方法获取用户信息进行验证。同时,可以使用 session.setMaxInactiveInterval() 方法设置会话的过期时间。
### 论述题答案
1. **答案**Java 中性能优化的主要方面和常用方法如下:
- **代码层面**
- 避免创建过多的对象,尽量复用对象。例如,使用 StringBuilder 代替 String 进行字符串拼接,因为 String 是不可变类,每次拼接都会创建新的对象。
- 减少方法调用的开销,避免在循环中频繁调用方法。
- 合理使用数据结构,根据不同的需求选择合适的数据结构。例如,对于频繁查找操作,使用 HashMap 比使用 ArrayList 更高效。
- **内存管理层面**
- 及时释放不再使用的对象,避免内存泄漏。例如,在使用完文件流、数据库连接等资源后,要及时关闭。
- 调整 JVM 的堆内存大小,根据应用程序的实际需求合理分配堆内存。
- **多线程层面**
- 合理使用线程池,避免频繁创建和销毁线程。例如,使用 Executors 类创建线程池。
- 优化线程同步机制,减少锁的竞争。例如,使用读写锁代替普通的互斥锁。
实际案例:一个电商系统在高并发场景下,用户下单时响应时间过长。通过性能优化,发现是由于在下单过程中频繁创建对象和数据库连接导致的。于是,使用对象池来复用对象,使用数据库连接池来管理数据库连接,同时调整了 JVM 的堆内存大小。经过优化后,系统的响应时间明显缩短,性能得到了显著提升。
**解析**Java 性能优化需要从多个方面入手,包括代码的编写、内存的管理和多线程的使用等。通过合理的优化措施,可以提高系统的性能和响应速度,减少资源的消耗。实际案例可以帮助我们更好地理解性能优化的重要性和具体方法。
### 综合题答案
1. **答案**
- **模块关系**
- 读者管理模块为借阅管理模块提供读者信息,只有已注册的读者才能进行借阅和归还操作。
- 图书管理模块为借阅管理模块提供图书信息,借阅管理模块根据图书信息判断图书是否可借。
- 借阅管理模块会更新图书管理模块中的图书库存信息和读者管理模块中的读者借阅记录。
- **实现方式**
- **数据模型**:设计数据库表来存储图书信息、读者信息和借阅记录。例如,图书表包含图书 ID、书名、作者、库存等字段读者表包含读者 ID、姓名、联系方式等字段借阅记录表包含借阅 ID、图书 ID、读者 ID、借阅时间、归还时间等字段。
- **分层架构**:采用 MVC 分层架构,将业务逻辑、数据访问和视图展示分离。每个模块都有对应的 Service 层和 Dao 层Service 层负责处理业务逻辑Dao 层负责与数据库进行交互。
- **服务接口**:各个模块之间通过服务接口进行交互。例如,借阅管理模块可以调用图书管理模块的服务接口来获取图书信息,调用读者管理模块的服务接口来验证读者信息。
- **异常处理**:在各个模块中添加异常处理机制,确保系统的稳定性。例如,当图书库存不足时,抛出相应的异常并进行处理。
**解析**:通过合理的模块设计和交互方式,可以实现一个功能完整、结构清晰的图书馆管理系统。数据模型的设计是系统的基础,分层架构可以提高代码的可维护性和可测试性,服务接口的使用可以实现模块之间的解耦,异常处理可以保证系统的稳定性。

View File

@@ -0,0 +1,9 @@
---
title: Java
index: false
icon: mdi:language-java
category:
- Java
---
<Catalog />

View File

@@ -0,0 +1,231 @@
---
icon: mdi:shield-lock
date: 2025-05-13
category:
- JAVA
- 加密
tag:
- xjar
title: XJar
---
XJar保护您的Java应用程序免受反编译和源码泄露
<!-- more -->
# XJar保护您的Java应用程序免受反编译和源码泄露
介绍一个强大且实用的工具——**XJar**。它专为Spring Boot JAR和原生JAR提供安全加密运行支持能够有效防止源码泄露和反编译的风险。无论你是想保护个人项目还是企业级应用XJar都能为你提供一个简单而高效的解决方案。
GitHub: https://github.com/core-lib/xjar
## 什么是XJar
XJar是一个开源工具通过对JAR包内的资源进行加密并结合扩展的ClassLoader构建了一套程序加密启动和动态解密运行的机制。它特别适合Spring Boot项目同时也支持原生JAR旨在帮助开发者保护Java应用程序的安全性。
## 功能特性
XJar提供了以下核心功能让它在加密工具中脱颖而出
- **无代码侵入**无需修改源代码只需对编译好的JAR包进行加密即可。
- **完全内存解密**:资源在运行时动态解密,减少源码或字节码泄露的风险。
- **支持所有JDK内置加解密算法**:灵活选择适合你的加密算法。
- **资源加密灵活性**:支持选择需要加密的字节码或其他资源文件。
- **Maven插件支持**:集成到构建流程,加密更便捷。
- **Go启动器**动态生成Go语言启动器保护密码不泄露。
## 环境依赖
- **JDK版本**1.7及以上
## 使用步骤
下面我将详细介绍如何使用XJar加密你的JAR包步骤清晰易懂。
### 1. 添加依赖
首先在你的Maven项目中添加XJar依赖并配置jitpack.io仓库
```xml
<project>
<!-- 设置jitpack.io仓库 -->
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<!-- 添加XJar依赖 -->
<dependencies>
<dependency>
<groupId>com.github.core-lib</groupId>
<artifactId>xjar</artifactId>
<version>4.0.2</version>
</dependency>
</dependencies>
</project>
```
> **小贴士**如果只是用JUnit测试加密过程可以将`<scope>`设置为`test`。
### 2. 加密源码
使用XJar提供的`XCryptos.encryption()`方法对JAR包进行加密
```java
XCryptos.encryption()
.from("/path/to/read/plaintext.jar") // 待加密JAR包路径
.use("io.xjar") // 加密密码
.include("/io/xjar/**/*.class") // 需要加密的字节码
.include("/mapper/**/*Mapper.xml") // 需要加密的资源文件
.exclude("/static/**/*") // 排除静态文件
.exclude("/conf/*") // 排除配置文件
.to("/path/to/save/encrypted.jar"); // 输出加密后的JAR包
```
- `include``exclude`支持ANT表达式或正则表达式灵活控制加密范围。
- 当两者同时使用时,加密范围为`include`内排除`exclude`后的资源。
### 3. 编译脚本
加密完成后XJar会在输出目录生成一个`xjar.go`文件。这是Go语言编写的启动器源码需要编译为可执行文件
- **Windows**:编译后生成`xjar.exe`
- **Linux**:编译后生成`xjar`
编译需要Go环境但运行时无需Go支持。注意启动器带有防篡改校验无法通用。
### 4. 启动运行
使用编译好的启动器运行加密后的JAR包
```shell
./xjar java -Xms256m -Xmx1024m -jar /path/to/encrypted.jar
```
- 启动器需放在Java命令之前。
- 仅支持`-jar`方式启动,不支持`-cp``-classpath`
- 使用`nohup`时,需写为:`nohup ./xjar java -jar /path/to/encrypted.jar`
## 注意事项
在使用XJar时以下问题可能影响你的体验我整理了解决方案供参考
### 1. Spring Boot Maven插件兼容性
XJar不支持`spring-boot-maven-plugin``executable = true``embeddedLaunchScript`配置,需删除:
```xml
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- 删除以下配置 -->
<!-- <configuration>
<executable>true</executable>
<embeddedLaunchScript>...</embeddedLaunchScript>
</configuration> -->
</plugin>
```
### 2. Spring Boot + JPA(Hibernate)报错
若使用Hibernate启动可能报错。解决方法
1. 克隆[XJar-Agent-Hibernate](https://github.com/core-lib/xjar-agent-hibernate),编译生成`xjar-agent-hibernate-${version}.jar`
2. 启动命令添加代理:
```shell
./xjar java -javaagent:xjar-agent-hibernate-${version}.jar -jar your-app.jar
```
### 3. 静态文件加载问题
加密静态文件可能导致浏览器加载失败,建议排除加密:
```java
.exclude("/static/**/*")
.exclude("/META-INF/resources/**/*")
```
### 4. JDK 9+模块化问题
在JDK 9及以上版本需添加参数
```shell
./xjar java --add-opens java.base/jdk.internal.loader=ALL-UNNAMED -jar /path/to/encrypted.jar
```
### 5. 阿里云Maven镜像问题
使用阿里云镜像时,需在`mirrorOf`中排除jitpack.io
```xml
<mirror>
<id>alimaven</id>
<mirrorOf>central,!jitpack.io</mirrorOf>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
```
## 插件集成
XJar提供了[xjar-maven-plugin](https://github.com/core-lib/xjar-maven-plugin),可自动完成加密过程。
### 配置示例
```xml
<project>
<pluginRepositories>
<pluginRepository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>com.github.core-lib</groupId>
<artifactId>xjar-maven-plugin</artifactId>
<version>4.0.2</version>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
<phase>package</phase>
<configuration>
<password>io.xjar</password>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
```
### 执行方式
- **自动构建**`mvn clean package -Dxjar.password=io.xjar`
- **手动执行**`mvn xjar:build -Dxjar.password=io.xjar`
> **强烈建议**:不要在`pom.xml`中写死密码,通过命令行传递更安全!
## 参数说明
以下是`xjar-maven-plugin`的主要参数:
| 参数名称 | 命令参数 | 参数说明 | 参数类型 | 缺省值 |
| --------- | ---------------- | ------------ | -------- | -------------------------- |
| password | -Dxjar.password | 密码字符串 | String | 必须 |
| algorithm | -Dxjar.algorithm | 加密算法名称 | String | AES/CBC/PKCS5Padding |
| keySize | -Dxjar.keySize | 密钥长度 | int | 128 |
| ivSize | -Dxjar.ivSize | 密钥向量长度 | int | 128 |
| sourceDir | -Dxjar.sourceDir | 源JAR目录 | File | ${project.build.directory} |
| targetDir | -Dxjar.targetDir | 目标JAR目录 | File | ${project.build.directory} |
更多详情见:[xjar-maven-plugin](https://github.com/core-lib/xjar-maven-plugin)。
## 结尾
XJar以其无侵入性、灵活性和高效性成为保护Java应用的绝佳选择。无论是个人项目还是商业产品它都能帮你轻松加密JAR包确保代码安全。赶快试试吧

View File

@@ -0,0 +1,38 @@
---
icon: mdi:package-variant
date: 2025-05-08
category:
- 实用工具
- JAVA
tag:
- maven
title: Maven常用配置
---
maven常用配置
<!-- more -->
# 阿里云镜像
```xml
<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
```
# 配置代理
```xml
<proxy>
<id>http-proxy</id>
<active>true</active>
<protocol>http</protocol>
<host>10.6.212.9</host>
<port>7897</port>
<nonProxyHosts>localhost|127.0.0.1|*.local</nonProxyHosts>
</proxy>
```

View File

@@ -0,0 +1,458 @@
---
icon: mdi:webhook
date: 2025-05-08
category:
- JAVA
- 通信协议
tag:
- websocket
- http
title: WebSocket和HTTP关系
---
# WebSocket和HTTP关系
<!-- more -->
## 1. WebSocket简介
WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。它使客户端和服务器之间的数据交换变得更加简单高效,并允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需完成一次握手,便可创建持久性的连接,实现双向数据传输。
## 2. WebSocket与HTTP的关系
### 2.1 协议转换过程
WebSocket依赖于HTTP连接初始化但随后会进行协议升级。具体转换过程如下
1. **初始HTTP请求**每个WebSocket连接都始于一个HTTP请求。客户端发送标准的HTTP请求但包含特殊的头信息表明希望升级为WebSocket协议
```
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
```
2. **服务器响应升级**如果服务器支持WebSocket协议会返回101状态码表示协议正在切换
```
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
```
3. **协议切换完成**此时HTTP请求已完成其使命如果协议升级成功客户端会触发`onopen`事件;否则触发`onerror`事件。此后的所有通信都使用WebSocket协议不再依赖HTTP。
### 2.2 为什么WebSocket要依赖HTTP协议连接
WebSocket选择依赖HTTP协议有几个重要原因
1. **设计理念**WebSocket设计之初就是为HTTP增强通信能力尤其是全双工通信因此在HTTP协议基础上扩展是自然的选择能够复用HTTP的基础设施。
2. **最大兼容性**基于HTTP连接可获得最广泛的兼容支持。即使服务器不支持WebSocket也能建立HTTP通信并返回相应错误这比完全无响应要好得多。
3. **防火墙友好**大多数网络环境允许HTTP流量通过80和443端口基于HTTP的WebSocket更容易穿越防火墙和代理服务器。
4. **减少实现复杂度**复用现有的HTTP基础设施无需从零开始构建新的协议栈。
### 2.3 HTTP与WebSocket的主要区别
|特性|HTTP|WebSocket|
|---|---|---|
|连接类型|无状态、短连接|有状态、长连接|
|通信方式|单向(请求-响应)|双向(全双工)|
|数据交互模式|客户端主动请求|双方均可主动发送|
|数据传输量|每次请求都有HTTP头|握手后数据传输更轻量|
|实时性|弱(通常需轮询)|强(可即时推送)|
|使用场景|传统网页请求、RESTful API|聊天应用、实时数据更新、在线游戏等|
## 3. WebSocket的优势与应用场景
相比传统HTTPWebSocket具有以下优势
1. **降低延迟**:一旦建立连接,通信双方可随时发送数据,无需重复建立连接。
2. **减少网络流量**WebSocket在握手后的通信中没有HTTP头的开销数据传输更高效。
3. **实时双向通信**:服务器可以主动推送信息给客户端,无需客户端轮询。
4. **保持连接状态**WebSocket是有状态协议可维护连接上下文信息。
典型应用场景:
- 实时通讯应用(聊天室、即时通讯工具)
- 在线协作工具(协同编辑文档)
- 实时数据展示(股票行情、体育赛事直播)
- 游戏应用(在线多人游戏)
- 物联网设备通信
## 4. 总结
WebSocket与HTTP是相辅相成的关系而非替代关系。WebSocket通过HTTP协议完成初始握手随后升级为独立的协议实现更高效的双向通信。两种协议各有优势应根据应用场景选择合适的通信方式。在需要实时性、双向通信的场景中WebSocket展现出明显优势而对于简单的数据获取和传统网页浏览HTTP仍然是最佳选择。
## 5. 案例(服务端)
### 5.1 项目结构
![1746684922168.png](../../../../tools/assets/20210721105228.png)
### 5.2 依赖配置
```xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/>
</parent>
<artifactId>websocket</artifactId>
<packaging>jar</packaging>
<name>websocket</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
```
### 5.3 主应用类
```java
package com.mangmang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebSocketDemoApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketDemoApplication.class, args);
}
}
```
### 5.4 WebSocket配置类
```java
package com.mangmang.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册STOMP协议的节点(endpoint)
registry.addEndpoint("/ws").setAllowedOrigins("http://10.6.212.39:5173/");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 配置消息代理,前缀为/topic的消息将会被路由到消息代理
registry.enableSimpleBroker("/topic");
// 以/app开头的消息将会被路由到@MessageMapping注解的方法中
registry.setApplicationDestinationPrefixes("/app");
}
}
```
### 5.5 消息实体类
```java
package com.mangmang.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Message {
private String content;
private String sender;
private MessageType type;
public enum MessageType {
CHAT,
JOIN,
LEAVE
}
}
```
### 5.6 消息控制类
```
package com.mangmang.controller;
import com.mangmang.model.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;
import java.util.Objects;
@Slf4j
@Controller
public class MessageController {
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public Message sendMessage(@Payload Message message) {
log.info(message.toString());
return message;
}
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public Message addUser(@Payload Message message, SimpMessageHeaderAccessor headerAccessor) {
log.info(message.toString());
// 将用户名添加到WebSocket会话
Objects.requireNonNull(headerAccessor.getSessionAttributes()).put("username", message.getSender());
return message;
}
}
```
### 5.7 websocket断连通知类
```java
package com.mangmang.listener;
import com.mangmang.model.Message;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import java.util.Objects;
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketEventListener {
private final SimpMessageSendingOperations messagingTemplate;
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
// 获取会话属性
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.wrap(event.getMessage());
// 从会话中获取用户名
String username = (String) Objects.requireNonNull(headerAccessor.getSessionAttributes()).get("username");
if (username != null) {
log.info("用户断开连接: {}", username);
// 创建一个离开消息
Message message = Message.builder()
.type(Message.MessageType.LEAVE)
.sender(username)
.content("下线了")
.build();
// 发送消息到公共主题
messagingTemplate.convertAndSend("/topic/public", message);
}
}
}
```
## 6. 案例(客户端)
### 6.1 封装websocket
```ts
import {Client} from '@stomp/stompjs';
interface ChatMessage {
content: string;
sender: string;
type: 'CHAT' | 'JOIN' | 'LEAVE';
}
class WebSocketService {
private stompClient: Client | null = null;
connect(username: string, onMessageReceived: (msg: ChatMessage) => void) {
// 创建原生 WebSocket 连接
const socket = new WebSocket('ws://10.6.212.39:8099/ws'); // 确保这里是 WebSocket 协议
this.stompClient = new Client({
webSocketFactory: () => socket,
onConnect: () => {
console.log('STOMP connected');
// 订阅公共主题
this.stompClient?.subscribe('/topic/public', (message) => {
const chatMsg: ChatMessage = JSON.parse(message.body);
onMessageReceived(chatMsg);
});
// 发送用户加入消息
this.sendAddUserMessage(username);
},
onStompError: (frame) => {
console.error('Broker reported error: ', frame.headers['message']);
console.error('Additional details: ', frame.body);
},
// 关闭时清理资源
onDisconnect: () => {
console.log('Disconnected from STOMP');
this.stompClient = null;
}
});
// 激活客户端
this.stompClient.activate();
}
sendMessage(chatMessage: Omit<ChatMessage, 'type'>) {
if (this.stompClient?.connected) {
this.stompClient.publish({
destination: '/app/chat.sendMessage',
body: JSON.stringify(chatMessage),
});
} else {
console.warn('WebSocket not connected');
}
}
sendAddUserMessage(username: string) {
if (this.stompClient?.connected) {
this.stompClient.publish({
destination: '/app/chat.addUser',
body: JSON.stringify({content: "加入聊天", sender: username, type: 'JOIN'}),
});
}
}
disconnect() {
if (this.stompClient) {
this.stompClient.deactivate();
this.stompClient = null;
}
}
}
export default new WebSocketService();
```
### 6.2 页面
```vue
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import wsService from '../utils/websocket.service';
const messages = ref<{ content: string; sender: string; type: 'CHAT' | 'JOIN' | 'LEAVE' }[]>([]);
const messageInput = ref('');
const username = ref(''); // 可替换为动态用户名
// 处理发送消息
function handleSendMessage() {
const content = messageInput.value.trim();
if (!content) return;
wsService.sendMessage({
content,
sender: username.value,
});
messageInput.value = '';
}
// 接收消息回调
function onMessageReceived(msg: { content: string; sender: string; type: 'CHAT' | 'JOIN' | 'LEAVE' }) {
messages.value.push(msg);
}
function createConnect(){
// 建立连接并注册消息回调
wsService.connect(username.value, onMessageReceived);
}
onMounted(() => {
});
onBeforeUnmount(() => {
// 组件卸载前断开连接
wsService.disconnect();
});
</script>
<template>
<div class="chat-container">
<h2>WebSocket 聊天室</h2>
<input v-model="username" placeholder="输入姓名" @keyup.enter="createConnect">
<div class="messages">
<div v-for="(msg, index) in messages" :key="index" class="message">
<strong>{{ msg.sender }}:</strong> {{ msg.content }}
</div>
</div>
<input
v-model="messageInput"
@keyup.enter="handleSendMessage"
placeholder="输入消息..."
/>
</div>
</template>
<style scoped>
.chat-container {
max-width: 600px;
margin: auto;
padding: 20px;
}
.messages {
border: 1px solid #ccc;
padding: 10px;
height: 300px;
overflow-y: auto;
margin-bottom: 10px;
}
.message {
margin-bottom: 5px;
}
input {
width: 100%;
padding: 8px;
}
</style>
```

View File

@@ -0,0 +1,765 @@
---
date: 2025-05-07
category:
- JAVA
tag:
- 表单
- 重复提交
- 防重
icon: lock
---
防止表单和参数重复提交案例
<!-- more -->
# 防止表单和参数重复提交
## 1. 编写注解
```java
/**
* 防止表单重复提交
*
* @author 氓氓编程
* @Date: 2021-06-08-16:35
* @Inherited @interface 自定义注解时自动继承了java.lang.annotation.Annotation接口由编译程序自动完成其他细节
* @Target 用于描述注解的使用范围(作用于方法上)
* @Retention 被描述的注解在什么范围内有效 (在运行时有效,即运行时保留)
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
}
```
## 2. 自定义拦截器
```java
/**
* @author 茫茫编程
* @Date: 2021-06-08-16:29
*/
@Slf4j
@Component
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter {
/**
* @param request 是指经过spring封装的请求对象, 包含请求地址, 头, 参数, body(流)等信息.
* @param response 是指经过spring封装的响应对象, 包含输入流, 响应body类型等信息.
* @param handler 是指controller的@Controller注解下的"完整"方法名, 是包含exception等字段信息的.
* @return 是否放行
* @throws Exception 异常
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//instanceof是Java中的二元运算符左边是对象右边是类当对象是右边类或子类所创建对象时返回true否则返回false。
if (handler instanceof HandlerMethod) {
log.info("进来了");
//把handler强转为HandlerMethod
HandlerMethod handlerMethod = (HandlerMethod) handler;
//获取当前请求的方法
Method method = handlerMethod.getMethod();
//获取当前去请求方法上是否有注解@RepeatSubmit
RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);
//如果方法有@RepeatSubmit注解进入if
if (repeatSubmit != null) {
//判断是否是重复提交,重复提交进入
if (isRepeatSubmit(request, response)) {
//返回消息实体类
R message = R.error().message("不允许重复提交");
//把消息响应给客户端
ServletUtils.renderString(response, JSONUtil.toJsonStr(message));
//拦截
return false;
}
}
//注解和不重复直接放行
return true;
} else {
//如果handler不是HandlerMethod或子类放行
return super.preHandle(request, response, handler);
}
}
/**
* 弗雷调用该方法会使用子类的实现
* 验证是否重复提交由子类实现具体的防重复提交的规则
*
* @param request 请求
* @return 是否是重复提交
*/
public abstract boolean isRepeatSubmit(HttpServletRequest request,HttpServletResponse response);
}
```
## 3. 自定义拦截器子类
```java
/**
* 判断请求url和数据是否和上一次相同
* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
*
* @author 氓氓编程
* @Date: 2021-06-08-17:27
*/
@Slf4j
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
/**
* 重复参数
*/
public final String REPEAT_PARAMS = "repeatParams";
/**
* 重复时间
*/
public final String REPEAT_TIME = "repeatTime";
/**
* 间隔时间单位秒
* 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
*/
private int intervalTime = 120;
public SameUrlDataInterceptor(JwtUtil jwtUtil, RedisCache redisCache) {
this.jwtUtil = jwtUtil;
this.redisCache = redisCache;
}
public void setIntervalTime(int intervalTime) {
this.intervalTime = intervalTime;
}
private final JwtUtil jwtUtil;
/**
* 注入redis
*/
private final RedisCache redisCache;
/**
* 重写父类判断是否重复的抽象方法
*
* @param request 请求
* @return true=重复提交 false=未重复
*/
@SneakyThrows
@SuppressWarnings("unchecked")
@Override
public boolean isRepeatSubmit(HttpServletRequest request, HttpServletResponse response) {
//1.声明当前参数
String nowParams = null;
//判断请求是否为空
if (request != null) {
//把request 转为可重复获取流的RepeatedlyRequestWrapper
RepeatedlyRequestWrapper repeatedlyRequest = new RepeatedlyRequestWrapper(request, response);
//获取body参数
nowParams = RepeatedlyRequestWrapper.getBodyString(repeatedlyRequest);
}
//如果请求体body参数为空获取Parameter的参数
if (StringUtils.isEmpty(nowParams)) {
assert request != null;
nowParams = JSONUtil.toJsonStr(request.getParameterMap());
log.info("body=={}", nowParams);
}
//把数据存储起来
Map<String, Object> nowMap = new HashMap<>(80);
nowMap.put(REPEAT_PARAMS, nowParams);
nowMap.put(REPEAT_TIME, System.currentTimeMillis());
log.info("nowMap=={}", nowMap);
// 请求地址作为存放cache的key值
String url = request.getRequestURI();
//唯一标识-获取请求头的token值
String submitKey = request.getHeader(jwtUtil.getHeader());
//如果token为空,使用请求地址作为key
if (StringUtils.isEmpty(submitKey)) {
submitKey = url;
}
//唯一标识指定key+消息头)
String cacheRepeatKey = "repeat_submit:" + submitKey;
log.info("repeat_submit=={}", cacheRepeatKey);
//缓存中获取上次请求数据
Object cacheObject = redisCache.getCacheObject(cacheRepeatKey);
log.info("cacheObject=={}", cacheObject);
//如果缓存中没有数据,则存放
if (cacheObject == null) {
Map<String, Object> cacheMap = new HashMap<>(109);
cacheMap.put(url, nowMap);
redisCache.setCacheObject(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS);
} else {
//强转为map
Map<String, Object> preDataMap = (Map<String, Object>) cacheObject;
//判断该map是否有url作为的键
if (preDataMap.containsKey(url)) {
//根据map中的键url 获取对应的参数
Map<String, Object> preMap = (Map<String, Object>) preDataMap.get(url);
return compareParams(nowMap, preMap) && compareTime(nowMap, preMap);
}
}
return false;
}
/**
* 比较两次请求参数是否相同
*
* @param nowMap 现在的数据
* @param preMap 之前的数据
* @return true=相同 false=不相同
*/
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {
String now = (String) nowMap.get(REPEAT_PARAMS);
String pre = (String) preMap.get(REPEAT_PARAMS);
return now.equals(pre);
}
/**
* 比较两次请求时间间隔
*
* @param nowMap 现在的数据
* @param preMap 之前的数据
* @return true=相同 false=不相同
*/
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap) {
long now = (Long) nowMap.get(REPEAT_TIME);
long pre = (Long) preMap.get(REPEAT_TIME);
//如果两次间隔时间小于10秒
return (now - pre) < this.intervalTime * 1000L;
}
}
```
## 4. 配置拦截器到web中
```java
/**
* @author 氓氓编程
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private final RepeatSubmitInterceptor repeatSubmitInterceptor;
//构造方法注入
public CorsConfig(RepeatSubmitInterceptor repeatSubmitInterceptor) {
this.repeatSubmitInterceptor = repeatSubmitInterceptor;
}
/**
* 解决跨域
* @return CorsFilter
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
// 设置访问源地址(允许那些地址访问服务器)
configuration.addAllowedOrigin("*");
// 设置访问源请求方法(方法)
configuration.addAllowedMethod("*");
// 设置访问源请求头(头部信息)
configuration.addAllowedHeader("*");
// 跨域需要暴露的请求头(因为跨域访问默认不能获取全部头部信息)
configuration.addExposedHeader("token");
// 注册配置
source.registerCorsConfiguration("/**", configuration);
return new CorsFilter(source);
}
/**
* 添加拦截器
* @param registry registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatSubmitInterceptor);
}
}
```
## 5. redis
### 5.1 配置
```java
**
* @author 氓氓编程
*
*/
@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
//value序列化
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
```
> 以下是需要用到的工具类
### 5.2 redis 工具类
```java
/**
* Redis 工具类
*
* @author 氓氓编程
* @Date: 2021-06-08-17:35
*/
@Component
public class RedisCache {
private final RedisTemplate<Object, Object> redisTemplate;
public RedisCache(RedisTemplate<Object, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 缓存基本对象Integer、String、实体类等
*
* @param key 键
* @param value 值
*/
public void setCacheObject(final String key, final Object value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 带有效时间缓存基本对象Integer、String、实体类等
*
* @param key 键
* @param value 值
* @param timeout 有效时间
* @param timeUnit 有效时间单位
*/
public void setCacheObject(final String key, final Object value, final Integer timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 给某个键设置有效时间
*
* @param key 需要设置有效时间的键
* @param timeout 设置的时间 默认是秒
* @return true=设置成功 false=设置失败
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 给某个键设置有效时间
*
* @param key 需要设置有效时间的键
* @param timeout 设置的时间
* @param timeUnit 有效时间单位
* @return true=设置成功 false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit timeUnit) {
Boolean isSuccess = redisTemplate.expire(key, timeout, timeUnit);
if (isSuccess != null) {
return isSuccess;
}
return false;
}
/**
* 根据键获取某个缓存的值
*
* @param key 键
* @return Object
*/
public Object getCacheObject(final String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 根据建删除缓存
*
* @param key 键
* @return true=成功 false=失败
*/
public boolean deleteObject(final String key) {
Boolean isDelete = redisTemplate.delete(key);
return isDelete != null && isDelete;
}
/**
* 根据键批量删除
*
* @param collection 装有键的集合
* @return 删除成功的数量
*/
public long deleteObject(final Collection<Object> collection) {
Long count = redisTemplate.delete(collection);
return count == null ? 0 : count;
}
/**
* 缓存List集合数据
*
* @param key 键
* @param dataList 待缓存的List数据
* @return 缓存成功的数量
*/
public long setCacheList(final String key, final List<Object> dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获取List缓存数据
*
* @param key 键
* @return 缓存键值对应的数据
*/
public List<Object> getCacheList(final String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 键
* @param dataSet 待缓存的Set数据
* @return 缓存数据的对象
*/
public BoundSetOperations<Object, Object> setCacheSet(final String key, final Set<Object> dataSet) {
BoundSetOperations<Object, Object> setOperations = redisTemplate.boundSetOps(key);
Iterator<Object> it = dataSet.iterator();
if (it.hasNext()) {
setOperations.add(it.next());
}
return setOperations;
}
/**
* 获取Set缓存数据
*
* @param key 键
* @return 缓存键值对应的数据
*/
public Set<Object> getCacheSet(final String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* @param key 键
* @param dataMap 缓存的map数据
*/
public void setCacheMap(final String key, final Map<String, Object> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key 键
* @return 获得缓存的数据
*/
public Map<Object, Object> getCacheMap(final String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public void setCacheMapValue(final String key, final String hKey, final Object value) {
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public Object getCacheMapValue(final String key, final String hKey) {
return redisTemplate.opsForHash().get(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public List<Object> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<Object> keys(final String pattern) {
return redisTemplate.keys(pattern);
}
}
```
## 6. Servlet参数发送的工具类
```java
/**
* 客户端工具类
*
* @author 氓氓编程
* @Date: 2021-06-08-16:50
*/
public class ServletUtils {
/**
* 获取String参数
*/
public static String getParameter(String name) {
return getRequest().getParameter(name);
}
/**
* 获取String参数,如墨没有设置一个默认值
*/
public static String getParameter(String name, String defaultValue) {
return Convert.toStr(getRequest().getParameter(name), defaultValue);
}
/**
* 获取session
*/
public static HttpSession getSession() {
return getRequest().getSession();
}
/**
* 获取Integer参数
*/
public static Integer getParameterToInt(String name) {
return Convert.toInt(getRequest().getParameter(name));
}
/**
* 获取response
*/
public static HttpServletResponse getResponse() {
return getRequestAttributes().getResponse();
}
/**
* 获取request
*/
public static HttpServletRequest getRequest() {
return getRequestAttributes().getRequest();
}
/**
* 获取到当前的HttpServletRequest
*
* @return ServletRequestAttributes
*/
public static ServletRequestAttributes getRequestAttributes() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
}
/**
* @param response 当前请求的响应
* @param string 传输的文字
*/
public static void renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
## 7. 可重复读取流的RepeatedlyRequestWrapper
```java
/**
* 构建可重复读取inputStream的请求request
*
* @author 氓氓编程
* @Date: 2021-06-09-8:47
*/
@Slf4j
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
/**
* 存放请求体中的数据
*/
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws UnsupportedEncodingException {
super(request);
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
this.body = getBodyString(request).getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream(){
//1.创建一个字节数组流存放请求体
final ByteArrayInputStream bodyInputStream = new ByteArrayInputStream(body);
//2.返回获取的body中的数据流
return new ServletInputStream() {
@Override
public int read(){
return bodyInputStream.read();
}
@Override
public int available(){
return body.length;
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
/**
* 获取请求体
*
* @param request 请求
* @return 字符串
*/
public static String getBodyString(ServletRequest request) {
//1.创建一个StringBuilder
StringBuilder sb = new StringBuilder();
//2.声明一个读缓存的流
BufferedReader reader = null;
//3.获取请求中的流
try (InputStream inputStream = request.getInputStream()) {
//把请求中的流读取出来给reader
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
//声明line存放每一行的数据
String line;
//一行一行的读取数据并赋值给lineline不为空
while ((line = reader.readLine()) != null) {
//追加写入
sb.append(line);
}
} catch (IOException e) {
log.warn("获取请求体中数据出现问题");
} finally {
//如果reader不为空关闭流
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.error(ExceptionUtil.getMessage(e));
}
}
}
return sb.toString();
}
}
```
## 8. 让可重复读的流生效
```java
/**
* 使用重写后的RepeatedlyRequestWrapper
* <p>
* Repeatable 过滤器
*/
public class RepeatableFilter implements Filter {
/**
* startsWithIgnoreCase 判断开始部分是否与二参数相同。不区分大小写
*
* @param request 请求
* @param response 响应
* @param chain 放行
* @throws IOException io异常
* @throws ServletException 异常
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//声明一个ServletRequest
ServletRequest requestWrapper = null;
//判断request是HttpServletRequest或子类并且request.getContentType()开头包含application/json
if (request instanceof HttpServletRequest && StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
//创建一个可重复获取流的RepeatedlyRequestWrapper赋值给ServletRequest
requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
}
//为空,直接放行
if (null == requestWrapper) {
chain.doFilter(request, response);
} else {
//赋值完毕可重复读流继续向下传
chain.doFilter(requestWrapper, response);
}
}
}
```
```java
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RepeatableFilter());
registration.addUrlPatterns("/*");
registration.setName("repeatableFilter");
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
return registration;
}
```

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

File diff suppressed because it is too large Load Diff