下载文件

直接请求文件接口文件打开将会失败,例如xlsx打开将会无法解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
reportApi.getIncome().then(res => {
const fileManagerObj = uni.getFileSystemManager() // 获取全局的文件管理器
console.log(fileManagerObj);
// 文件存储到本地的路径
const filePath = `${wx.env.USER_DATA_PATH}/${new Date().getTime()}.xlsx`
fileManagerObj.writeFile({
data: res, // 拿到的arraybuffer数据
filePath: filePath,
encoding: 'binary',
success: (res) => {
console.log(res) // 成功了的话这里会打印 writeFile:ok
viewDoc(filePath)
}
})
})

使用uniapp提供的downloadFile函数

1
2
3
4
5
6
7
8
9
10
11
12
uni.downloadFile({
url: pjConfig.baseUrl + "/rhhs/reports",
header: {
Authorization: 'Bearer ' + getToken()
},
success: res => {
if (res.statusCode == 200) {
console.log(res)
viewDoc(res.tempFilePath)
}
}
})

打开文件

1
2
3
4
5
6
7
8
9
10
11
function viewDoc(filePath) {
uni.openDocument({
// 直接打开
filePath: filePath, // 这里填上面写入本地的文件路径
fileType: 'xlsx',
showMenu: true, // 右上角是否有可以转发分享的功能,配不配随意
success: (res) => {
console.log('打开文档成功')
}
})
}

概述

本文档分析 JVM Metaspace 区域的 OutOfMemoryError 问题,包括处理方法、JVM 内存区域分类以及 OOM 发生原因。

什么是 Metaspace

Metaspace(元数据空间)是 Java 8 引入的概念,替代了永久代(PermGen)。它存储类的元数据信息,如:

  • 类定义信息
  • 方法信息
  • 常量池
  • 注解信息

Metaspace OOM 处理方法

1. 调整 Metaspace 大小

1
2
3
4
5
6
7
8
# 设置初始大小
-XX:MetaspaceSize=256m

# 设置最大大小
-XX:MaxMetaspaceSize=512m

# 禁用类卸载(谨慎使用)
-XX:-UseCompressedClassPointers

2. 详细排查步骤

第一步:实时监控 Metaspace 状态

1
2
3
4
5
6
7
8
# 查看当前 Metaspace 使用情况
jstat -gc <pid> 1s 10

# 查看详细的垃圾回收信息
jstat -gcutil <pid> 1s 10

# 查看类加载统计
jstat -class <pid> 1s 10

结果分析:

  • MC:Metaspace 容量
  • MU:Metaspace 使用量
  • M:使用率,如果持续 > 90% 就有问题
  • CCS:压缩类空间容量
  • CCSU:压缩类空间使用量

第二步:分析类加载情况

1
2
3
4
5
6
7
8
# 获取详细类加载信息
jcmd <pid> VM.classloader_stats

# 查看所有加载的类
jcmd <pid> VM.class_hierarchy

# 导出类直方图
jcmd <pid> GC.class_histogram > class_histogram.txt

重点关注:

  • 类数量异常增长的包
  • 重复加载的类
  • 匿名类($$Lambda$$ 或 $$Generated$$)

第三步:检查类加载器泄漏

1
2
3
4
5
# 生成堆转储文件
jcmd <pid> GC.dump_heap heap.hprof

# 或使用 jmap
jmap -dump:live,format=b,file=heap.hprof <pid>

使用 MAT(Memory Analyzer Tool)分析:

  1. 打开 heap.hprof 文件
  2. 查找 ClassLoader 实例
  3. 分析 “Duplicate Classes” 报告
  4. 检查 “Class Loader Explorer”

关键指标:

  • ClassLoader 实例数量是否异常
  • 是否存在大量未回收的 ClassLoader
  • 每个 ClassLoader 加载的类数量

第四步:动态生成类排查

1
2
3
4
5
6
7
# 启动时添加JVM参数来跟踪类加载
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
-verbose:class

# 或运行时启用
jcmd <pid> VM.classloader_stats

查找动态生成类:

1
2
3
4
5
# 在类加载日志中查找动态生成的类
grep -E '(Proxy|CGLIB|$$Lambda|$$Generated)' gc.log

# 分析应用日志中的反射使用
grep -E '(getDeclaredMethod|newProxyInstance|Enhancer)' app.log

第五步:第三方库分析

1
2
3
4
5
# 查看所有jar包中的类数量
find /path/to/libs -name "*.jar" -exec jar tf {} \; | wc -l

# 分析Spring等框架的代理类
jcmd <pid> VM.class_hierarchy | grep -E '(Proxy|CGLIB|Spring)'

重点检查的库:

  • Spring(AOP代理)
  • Hibernate(实体代理)
  • Jackson(序列化)
  • ASM/ByteBuddy(字节码生成)

第六步:内存泄漏具体排查

1
2
3
4
5
6
7
// 添加JVM参数监控类卸载
-XX:+TraceClassUnloading
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps

// 分析类是否能正常卸载
jcmd <pid> GC.class_stats

排查脚本示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
PID=$1
echo "=== Metaspace 监控 ==="
jstat -gc $PID

echo "=== 类加载统计 ==="
jstat -class $PID

echo "=== Top 20 占用内存的类 ==="
jcmd $PID GC.class_histogram | head -20

echo "=== 类加载器统计 ==="
jcmd $PID VM.classloader_stats

echo "=== 查找可疑的动态生成类 ==="
jcmd $PID VM.class_hierarchy | grep -E "(Proxy|CGLIB|\$\$Lambda|\$\$Generated)" | head -10

第七步:应用代码排查

检查代码中的问题模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 检查是否有类似代码(容易导致类泄漏)
URLClassLoader loader = new URLClassLoader(urls);
Class<?> clazz = loader.loadClass("SomeClass");
// 注意:如果不关闭 loader,会导致类加载器泄漏

// 2. 检查大量使用反射的地方
Method[] methods = clazz.getDeclaredMethods(); // 每次调用都可能产生新类

// 3. 检查动态代理使用
Proxy.newProxyInstance(classLoader, interfaces, handler); // 会生成代理类

// 4. 检查序列化框架配置
ObjectMapper mapper = new ObjectMapper(); // Jackson 可能生成大量类

日志排查命令:

1
2
3
4
5
6
7
8
# 在应用日志中查找类加载相关异常
grep -i "classloader\|metaspace\|outofmemory" app.log

# 查找频繁的反射调用
grep -i "reflection\|getDeclaredMethod\|newInstance" app.log

# 分析GC日志中的Metaspace回收情况
grep -i "metaspace" gc.log

3. 排查结果判断标准

正常情况:

  • Metaspace 使用率 < 80%
  • 类加载和卸载基本平衡
  • ClassLoader 数量稳定

异常情况:

  • Metaspace 使用率持续 > 90%
  • 类数量持续增长不回收
  • ClassLoader 实例数量异常增长
  • 频繁 Full GC 但 Metaspace 使用率不降

紧急情况处理:

1
2
3
4
5
6
7
8
# 临时增加 Metaspace 大小
jcmd <pid> VM.set_flag MaxMetaspaceSize 1024m

# 强制触发类卸载
jcmd <pid> GC.class_unloading

# 如果问题严重,准备重启
jcmd <pid> Thread.print > thread_dump.txt # 保留现场信息

JVM 内存区域分类

堆内内存(On-Heap Memory)

堆内内存是受 JVM 堆空间管理的内存区域。

特点:

  • 受 GC 管理和回收
  • -Xmx 参数限制
  • 对象分配在堆中
  • 内存访问相对较慢(需要 GC 开销)

包含区域:

  • 新生代(Young Generation)
    • Eden 区
    • Survivor0 区(S0)
    • Survivor1 区(S1)
  • 老年代(Old Generation/Tenured)

堆外内存(Off-Heap Memory)

堆外内存是不受 JVM 堆空间管理的内存区域。

特点:

  • 不受 GC 管理
  • 不受 -Xmx 参数限制
  • 需要手动释放
  • 内存访问速度快(无 GC 停顿)
  • 受操作系统物理内存限制

包含区域:

  • Metaspace(元数据空间)
  • Code Cache(代码缓存)
  • Compressed Class Space(压缩类空间)
  • JVM Stack(虚拟机栈)
  • Native Method Stack(本地方法栈)
  • Program Counter Register(程序计数器)
  • Direct Memory(直接内存)

常见使用场景:

1
2
3
4
5
// Direct ByteBuffer 使用堆外内存
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

// NIO 文件映射
FileChannel.map(MapMode.READ_WRITE, 0, fileSize);

堆内 vs 堆外内存对比:

特性 堆内内存 堆外内存
GC 管理
大小限制 -Xmx 物理内存
访问速度 较慢 较快
内存泄漏风险
典型用途 对象存储 缓存、IO缓冲区

OOM 按区域分类

1. Heap Space OOM

1
java.lang.OutOfMemoryError: Java heap space

发生原因:

  • 对象创建过多
  • 内存泄漏
  • 堆空间配置过小

2. Metaspace OOM

1
java.lang.OutOfMemoryError: Metaspace

发生原因:

  • 类加载过多
  • 类加载器泄漏
  • 动态代理类过多
  • 反射使用不当

3. Code Cache OOM

1
java.lang.OutOfMemoryError: Code cache

发生原因:

  • JIT 编译的代码过多
  • Code Cache 空间不足

4. Direct Memory OOM

1
java.lang.OutOfMemoryError: Direct buffer memory

发生原因:

  • NIO 直接内存使用过多
  • 堆外内存泄漏

5. Stack Overflow

1
java.lang.StackOverflowError

发生原因:

  • 递归调用过深
  • 方法调用栈过深

各区域 OOM 发生场景

区域 OOM 场景 常见原因
Heap 对象分配失败 内存泄漏、大对象、配置不当
Metaspace 类元数据存储失败 类加载器泄漏、动态类生成
Code Cache JIT 编译空间不足 热点代码过多、缓存配置小
Direct Memory 堆外内存不足 NIO 使用不当、直接内存泄漏
Stack 栈空间耗尽 递归过深、线程栈配置小

为什么服务卡住而不是挂掉

1. JVM 的容错机制

  • JVM 在遇到 Metaspace OOM 时,会尝试进行类卸载和垃圾回收
  • 如果回收失败,JVM 会进入”自救模式”

2. 线程阻塞

  • 类加载线程可能被阻塞在获取 Metaspace 内存的操作上
  • 其他线程等待类加载完成,形成连锁反应

3. GC 频繁执行

  • JVM 会频繁执行 Full GC 尝试释放 Metaspace
  • 这会导致应用响应缓慢,表现为”卡死”

4. 类加载锁竞争

  • 多个线程同时进行类加载时,会竞争类加载锁
  • Metaspace 不足时,这种竞争会加剧

5. 应用级别的保护

1
2
3
4
5
6
7
// 应用可能有异常处理机制
try {
// 类加载或反射操作
} catch (OutOfMemoryError e) {
// 记录日志但不终止进程
logger.error("Metaspace OOM occurred", e);
}

预防和监控建议

1. 合理配置参数

1
2
3
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
-XX:+UseCompressedClassPointers

2. 监控指标

  • Metaspace 使用率
  • 类加载数量
  • 类卸载频率
  • GC 频率和耗时

3. 代码最佳实践

  • 避免动态生成过多类
  • 正确关闭类加载器
  • 合理使用反射和动态代理
  • 定期检查第三方库的类加载行为

总结

Metaspace OOM 是 JVM 内存管理中的常见问题,理解其发生机制和处理方法对于 Java 应用的稳定运行至关重要。通过合理的配置、监控和代码实践,可以有效预防和解决此类问题。

概述

本文档整理了从Java 8到Java 21中Web系统开发必须掌握的关键特性,按照学习优先级进行分类。

🔥 立即学习(核心必备)

1. Java 8 - Stream API & Lambda表达式

为什么重要: 流式编程是现代Java开发的标准方式,极大提升代码简洁性和可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 传统写法
List<User> activeUsers = new ArrayList<>();
for (User user : users) {
if (user.isActive() && user.getRole().equals("USER")) {
activeUsers.add(user);
}
}

// Stream API写法
List<User> activeUsers = users.stream()
.filter(User::isActive)
.filter(user -> user.getRole().equals("USER"))
.collect(Collectors.toList());

// 数据转换
List<UserDto> userDtos = users.stream()
.map(user -> new UserDto(user.getId(), user.getUsername(), user.getEmail()))
.collect(Collectors.toList());

// 聚合操作
Map<String, Long> userCountByRole = users.stream()
.collect(Collectors.groupingBy(User::getRole, Collectors.counting()));

应用场景:

  • Controller层数据转换
  • 业务逻辑处理
  • 数据统计和分析
  • 复杂查询结果处理

2. Java 8 - Optional类

为什么重要: 避免NPE(空指针异常),让代码更安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Service层安全处理
@Service
public class UserService {
public UserDto getUserById(Long id) {
return userRepository.findById(id)
.map(this::convertToDto)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
}

public void sendWelcomeEmailIfActive(Long userId) {
userRepository.findById(userId)
.filter(User::isActive)
.ifPresent(this::sendWelcomeEmail);
}
}

3. Java 14 - Records (数据载体)

为什么重要: 大幅简化DTO/VO类的编写,减少80%样板代码。

Records vs 传统类对比

传统DTO类写法:

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
// 传统的DTO类 - 需要大量样板代码
public class UserDto {
private Long id;
private String username;
private String email;

// 构造函数
public UserDto(Long id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}

// Getter方法
public Long getId() { return id; }
public String getUsername() { return username; }
public String getEmail() { return email; }

// Setter方法
public void setId(Long id) { this.id = id; }
public void setUsername(String username) { this.username = username; }
public void setEmail(String email) { this.email = email; }

// equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserDto userDto = (UserDto) o;
return Objects.equals(id, userDto.id) &&
Objects.equals(username, userDto.username) &&
Objects.equals(email, userDto.email);
}

// hashCode方法
@Override
public int hashCode() {
return Objects.hash(id, username, email);
}

// toString方法
@Override
public String toString() {
return "UserDto{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
'}';
}
}

Records写法 - 一行搞定:

1
2
3
4
5
6
7
8
// Record类 - 自动生成所有样板代码
public record UserDto(Long id, String username, String email) {}

// 相当于自动生成:
// - 构造函数:public UserDto(Long id, String username, String email)
// - 访问器方法:id(), username(), email()
// - equals(), hashCode(), toString()
// - 所有字段都是 private final

Records特点和限制

特点:

  • 不可变: 所有字段都是final,创建后无法修改
  • 简洁: 一行代码替代几十行样板代码
  • 类型安全: 编译时检查,避免运行时错误
  • 自动生成: 构造函数、getter、equals、hashCode、toString

限制:

  • 只能用于数据载体: 不能包含复杂业务逻辑
  • 不可继承: 不能被继承,也不能继承其他类
  • 不可变性: 创建后不能修改字段值

实际使用示例

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
// 1. 简单DTO
public record UserDto(Long id, String username, String email) {}

// 2. 嵌套Records
public record UserProfileDto(
Long id,
String username,
String email,
AddressDto address // 嵌套另一个Record
) {}

public record AddressDto(String street, String city, String zipCode) {}

// 3. 带验证的Records
public record CreateUserRequest(
String username,
String email,
String password
) {
// 可以添加验证逻辑
public CreateUserRequest {
if (username == null || username.isBlank()) {
throw new IllegalArgumentException("Username cannot be blank");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email format");
}
}
}

// 4. 带静态方法的Records
public record UserDto(Long id, String username, String email) {
// 静态工厂方法
public static UserDto from(User user) {
return new UserDto(user.getId(), user.getUsername(), user.getEmail());
}

// 实例方法
public boolean isValidEmail() {
return email != null && email.contains("@");
}
}

// 5. 使用示例
@RestController
public class UserController {

@PostMapping("/users")
public ApiResponse<UserDto> createUser(@RequestBody CreateUserRequest request) {
// 创建Record实例
var user = userService.createUser(request.username(), request.email(), request.password());

// 使用静态工厂方法
var userDto = UserDto.from(user);

return new ApiResponse<>(200, "success", userDto);
}

@GetMapping("/users/{id}")
public ApiResponse<UserDto> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(user -> {
// 直接创建Record
var dto = new UserDto(user.getId(), user.getUsername(), user.getEmail());
return new ApiResponse<>(200, "success", dto);
})
.orElse(new ApiResponse<>(404, "User not found", null));
}
}

何时使用Records

适合使用Records的场景:

  • DTO(数据传输对象)
  • VO(值对象)
  • API请求/响应对象
  • 配置类
  • 数据库查询结果映射

不适合使用Records的场景:

  • 需要继承的类
  • 需要可变性的数据
  • 包含复杂业务逻辑的类
  • 需要延迟初始化的类

📚 重点掌握(显著提升开发效率)

4. Java 11 - HTTP Client API

为什么重要: 原生HTTP客户端,无需依赖第三方库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 微服务间通信
@Service
public class ExternalApiService {
private final HttpClient client = HttpClient.newHttpClient();

public CompletableFuture<UserProfile> getUserProfile(String userId) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users/" + userId))
.header("Authorization", "Bearer " + getToken())
.GET()
.build();

return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenApply(this::parseUserProfile);
}
}

5. Java 10 - var关键字

为什么重要: 提高代码可读性,减少冗余类型声明。

1
2
3
4
5
6
7
8
9
10
// 简化变量声明
var userList = userRepository.findAll();
var filteredUsers = userList.stream()
.filter(user -> user.isActive())
.collect(Collectors.toList());

// 复杂泛型简化
var userMap = users.stream()
.collect(Collectors.groupingBy(User::getRole,
Collectors.mapping(User::getUsername, Collectors.toList())));

6. Java 15 - Text Blocks

为什么重要: 优雅处理多行字符串,特别是JSON和SQL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 复杂SQL查询
@Repository
public class UserRepository {
private static final String COMPLEX_QUERY = """
SELECT u.id, u.username, u.email, p.name as profile_name
FROM users u
LEFT JOIN profiles p ON u.id = p.user_id
WHERE u.active = true
AND u.created_date > ?
ORDER BY u.created_date DESC
""";

// JSON模板
private static final String USER_JSON_TEMPLATE = """
{
"id": %d,
"username": "%s",
"email": "%s",
"profile": {
"name": "%s"
}
}
""";
}

🚀 未来趋势(高级特性)

7. Java 21 - Virtual Threads (虚拟线程)

为什么重要: 处理高并发请求,特别适合I/O密集型Web应用。

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
// 异步Controller
@RestController
public class UserController {
@GetMapping("/users/{id}")
public CompletableFuture<UserDto> getUser(@PathVariable Long id) {
return CompletableFuture.supplyAsync(() -> {
// 可能的阻塞操作:数据库查询、外部API调用
return userService.findById(id);
}, virtualThreadExecutor);
}
}

// 配置虚拟线程
@Configuration
public class AsyncConfig {
@Bean
public Executor virtualThreadExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
}

// 批量处理
@Service
public class BatchUserService {
public List<UserDto> processUsers(List<Long> userIds) {
return userIds.parallelStream()
.map(id -> CompletableFuture.supplyAsync(() ->
userService.findById(id), virtualThreadExecutor))
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
}

8. Java 17 - Sealed Classes

为什么重要: 更好的API设计,限制继承层次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 支付方式建模
public sealed interface PaymentMethod
permits CreditCard, PayPal, BankTransfer {
}

public record CreditCard(String number, String cvv) implements PaymentMethod {}
public record PayPal(String email) implements PaymentMethod {}
public record BankTransfer(String account, String routingNumber) implements PaymentMethod {}

// 订单状态
public sealed interface OrderStatus
permits Pending, Processing, Shipped, Delivered, Cancelled {
}

public record Pending() implements OrderStatus {}
public record Processing(String operatorId) implements OrderStatus {}
public record Shipped(String trackingNumber) implements OrderStatus {}
public record Delivered(LocalDateTime deliveredAt) implements OrderStatus {}
public record Cancelled(String reason) implements OrderStatus {}

9. Java 21 - Pattern Matching for Switch

为什么重要: 更优雅的条件处理,减少if-else链。

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
// 处理不同支付方式
@Service
public class PaymentService {
public PaymentResult processPayment(PaymentMethod method, BigDecimal amount) {
return switch (method) {
case CreditCard(var number, var cvv) ->
processCreditCard(number, cvv, amount);
case PayPal(var email) ->
processPayPal(email, amount);
case BankTransfer(var account, var routing) ->
processBankTransfer(account, routing, amount);
};
}
}

// 订单状态处理
public String getOrderStatusMessage(OrderStatus status) {
return switch (status) {
case Pending() -> "订单等待处理";
case Processing(var operatorId) -> "订单处理中,操作员:" + operatorId;
case Shipped(var trackingNumber) -> "订单已发货,追踪号:" + trackingNumber;
case Delivered(var deliveredAt) -> "订单已送达,时间:" + deliveredAt;
case Cancelled(var reason) -> "订单已取消,原因:" + reason;
};
}

实际项目应用示例

完整的用户管理API

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
// 用户DTO
public record UserDto(Long id, String username, String email, String role) {}

// 分页查询结果
public record PageResult<T>(List<T> content, int page, int size, long total) {}

// API响应
public record ApiResponse<T>(int status, String message, T data) {}

// Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;

@GetMapping
public ApiResponse<PageResult<UserDto>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String role) {

var pageResult = userService.findUsers(page, size, role);
return new ApiResponse<>(200, "success", pageResult);
}

@GetMapping("/{id}")
public ApiResponse<UserDto> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(user -> new ApiResponse<>(200, "success", user))
.orElse(new ApiResponse<>(404, "User not found", null));
}
}

// Service
@Service
public class UserService {
private final UserRepository userRepository;

public PageResult<UserDto> findUsers(int page, int size, String role) {
var users = userRepository.findAll();

var filteredUsers = users.stream()
.filter(user -> role == null || user.getRole().equals(role))
.skip((long) page * size)
.limit(size)
.map(this::convertToDto)
.collect(Collectors.toList());

return new PageResult<>(filteredUsers, page, size, users.size());
}

public Optional<UserDto> findById(Long id) {
return userRepository.findById(id)
.map(this::convertToDto);
}

private UserDto convertToDto(User user) {
return new UserDto(user.getId(), user.getUsername(),
user.getEmail(), user.getRole());
}
}

学习路径建议

第一阶段:基础掌握 (1-2周)

  1. Stream API - 每天练习不同的流操作
  2. Optional - 在所有可能为null的地方使用
  3. Records - 替换所有DTO类

第二阶段:进阶应用 (2-3周)

  1. HTTP Client - 实现微服务通信
  2. var关键字 - 简化变量声明
  3. Text Blocks - 处理复杂字符串

第三阶段:高级特性 (1-2个月)

  1. Virtual Threads - 高并发场景应用
  2. Pattern Matching - 复杂条件处理
  3. Sealed Classes - 领域建模

性能收益

  • 开发效率: Records减少80%样板代码
  • 代码质量: Optional减少NPE风险
  • 性能提升: Virtual Threads提高并发处理能力
  • 维护性: Pattern Matching提高代码可读性

注意事项

  1. 渐进式引入: 不要一次性改变所有代码
  2. 团队统一: 确保团队成员都理解新特性
  3. 测试覆盖: 使用新特性时要有充分的测试
  4. 版本兼容: 确认项目Java版本支持相应特性

最后更新时间:2025-07-18
适用版本:Java 8 - Java 21

java如何创建异步任务

1
2
3
4
5
6
7
8
9
10
11
12
public class TestThread {

public static void main(String[] args) {
Runnable task = () -> {
System.out.println("异步执行");
};
new Thread(task).start();

System.out.println("同步执行");
}

}

在java中执行异步任务需要新建一个Thread线程,然后通过Runnable接口实现一个任务,最后将任务通过参数的方式传递给Thread类。

获取异步任务的的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestThread {

public static void main(String[] args) {
// 结果集合
List<String> pdfList = new LinkedList<>();

// 异步任务
Runnable task = () -> {
String pdf = downloadPdf();
// 将结果放入list
pdfList.add(pdf);
};
new Thread(task).start();

// 输出结果集合的大小
System.out.println(pdfList.size());
}

private static String downloadPdf() {
return "pdf";
}

}

上面这种方式获取异步执行结果是有问题的,输出集合的大小可能为0,这是因为在输出结果的时候异步任务可能还没开始执行。

2.Future解决了什么问题

为了能够在获取到异步任务执行的结果后再继续之后的代码,我们可以使用CountDownLatch或者FutureTask,因为本篇内容是讲Future,所以仅说明Future的使用方法。

为了解决Runnable不能适配没有返回值的问题,jdk使用适配器模式为我们实现了FutureTask类,FutureTask实现了Runnable接口和Future接口

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
public class TestThread {

public static void main(String[] args) throws ExecutionException, InterruptedException {
// 结果集合
List<String> pdfList = new LinkedList<>();

// 异步任务
FutureTask<String> task = new FutureTask(() -> {
String pdf = downloadPdf();
// 返回结果
return pdf;
});
new Thread(task).start();

// 等待异步任务执行完成后,添加到集合
pdfList.add(task.get());

// 输出集合大小
System.out.println(pdfList.size());
}

private static String downloadPdf() {
return "pdf";
}

}

通过调用FutureTask的get()方法,当前线程会阻塞等待异步线程执行完成并将结果返回。

3.CompetableFuture解决了什么问题

了解了FutureTask之后,我们知道FutureTask.get()方法会阻塞当前线程,等待异步执行结果返回。我们来看下面这个例子。

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
public class TestThread {

public static void main(String[] args) throws ExecutionException, InterruptedException {
// 结果集合
List<String> pdfList = new LinkedList<>();

// 异步任务1
FutureTask<String> task1 = new FutureTask(() -> {
String pdf = downloadPdf();
// 返回结果
return pdf;
});
new Thread(task1).start();

// 异步任务2
FutureTask<String> task2 = new FutureTask(() -> {
String pdf = downloadPdf();
// 返回结果
return pdf;
});
new Thread(task2).start();

// 异步任务执行完成后,添加到集合
String pdfFont1 = getFontFromPdf(task1.get());
String pdfFont2 = getFontFromPdf(task2.get());

pdfList.add(pdfFont1);
pdfList.add(pdfFont2);

// 输出集合大小
System.out.println(pdfList.size());
}

private static String getFontFromPdf(String pdf) {
return "pdf-font";
}

private static String downloadPdf() {
return "pdf";
}

}

String pdfFont1 = getFontFromPdf(task1.get());在执行该代码的时候会等待task1执行完成在继续后面的代码,那么如果task2先执行完成的话就要干等着。

这时候我们可以将getFontFromPdf方法也放到task中如下,这样就能避免这个问题。

1
2
3
4
5
6
// 异步任务
FutureTask<String> task1 = new FutureTask(() -> {
String pdf = downloadPdf();
// 返回结果
return getFontFromPdf(pdf);
});

另外,我们还能通过CompetableFuture来解决这个问题,代码如下

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
public class TestThread {

public static void main(String[] args) throws ExecutionException, InterruptedException {
// 结果集合
List<String> pdfList = new LinkedList<>();

// 异步任务1
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
String pdf = downloadPdf();
// 返回结果
return pdf;
}).thenApplyAsync(pdf -> getFontFromPdf(pdf));

// 异步任务2
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
String pdf = downloadPdf();
// 返回结果
return pdf;
}).thenApplyAsync(pdf -> getFontFromPdf(pdf));

pdfList.add(task1.get());
pdfList.add(task2.get());

// 输出集合大小
System.out.println(pdfList.size());
}

private static String getFontFromPdf(String pdf) {
return "pdf-font";
}

private static String downloadPdf() {
return "pdf";
}

}

CompletableFuture通过thenApplyAsync方法追加了对方法结果的二次处理,CompletableFuture是jdk8的新特性,设计更符合lamda的流编程。

​ 将linux的cas项目部署到windows上,访问https://cas.example.com:8443显示如下页面。

image-20230927171634190

误认为和项目配置有关系

​ cas-template有个配置文件在/etc/cas/config/cas.properties,配置信息如下。

1
2
3
4
5
6
cas.server.name: https://cas.example.org:8443
cas.server.prefix: https://cas.example.org:8443/cas

cas.adminPagesSecurity.ip=127\.0\.0\.1

logging.config: file:/etc/cas/config/log4j2.xml

​ 这里配置的域名是在集群的时候才有用的,单机并没有意义,而我误以为不能访问是因为这个配置没有生效。

确认https是正常的

​ 因为我的启动方式是./gradlew run通过内置的tomcat启动,所以https证书是通过springboot进行管理的,application.yml的配置没有问题。

1
2
3
4
5
server:
ssl:
key-store: file:/etc/cas/thekeystore
key-store-password: changeit
key-password: changeit

代理可能影响

​ 关掉clash代理,果然可以了。可以在Clash For Windows中Settings > System Proxy > Bypass Domain中添加不走代理的列表。

有些Docker镜像的生成有时候分为多个阶段,如:

  • 构建阶段:maven将java源代码转为jar
  • 运行阶段:tomcat运行jar

可以看到构建阶段需要依赖于maven,运行阶段需要依赖于tomcat。为了减小镜像大小,理想的镜像是仅包含运行环境的tomcat而不包含编译打包环境maven的镜像。

这时候有两种方式可以实现该目的。

方式一:使用gitlab runner的Docker excutor构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
stages:
- build
- deploy

build:
stage: build
image: maven:3.6.3-jdk-8
script:
# 生成 JAR 文件
- mvn clean package
# 使用 Dockerfile 构建 JAR 镜像
- docker build -t myapp-jar:latest .

deploy:
stage: deploy
image: docker:latest
services:
- docker:dind
script:
# 将 JAR 文件复制到 Tomcat 镜像中
- docker run --rm -v $CI_PROJECT_DIR:/app myapp-jar:latest cp /app/target/myapp.jar /path/to/tomcat/webapps/
# 最终构建包含 JAR 的 Tomcat 镜像
- docker build -t my-tomcat-image:latest .

通过runner启动一个maven容器,在maven容器中将java源代码转为jar,然后使用dind技术将jar放入tomcat镜像,这时候就得到tomcat + jar的镜像。

方式二:使用docker的多阶段构建

1
2
3
4
5
6
7
8
9
10
# 构建阶段
FROM maven:3.6.3-jdk-8 AS build
WORKDIR /app
COPY . .
RUN mvn clean package # 生成 jar

# 运行阶段
FROM tomcat:9.0
# 将上一个镜像生成的jar复制到当前镜像,构建包含 JAR 的 Tomcat 镜像
COPY --from=build /app/target/myapp.jar /usr/local/tomcat/webapps/myapp.jar

将生成jar逻辑放在Dockerfile中,先用maven镜像生成jar,得到一个maven + jar的镜像,然后使用tomcat镜像将刚刚生成的镜像中的jar复制过来,得到一个tomcat + jar的镜像。

二者的区别就是生成jar的逻辑是写在gitlab的.gitlab-ci.yml中还是写在Dockerfile中。

我个人倾向写在Dockerfile中,主要有一下几点考量。

  • gitlab runner专注cicd如git push触发钩子、ssh、测试、发布,运行脚本等宏观的事情,细节交给其他工具来做,如Dockerfile负责构建镜像、docker-compose负责挂载和网络。
  • 减少对gitlab runner的依赖,即使没有gitlab runner也能通过Docker build命令构建镜像
  • 可利用Docker layer的缓存,加快镜像构建速度。

背景

本文记录了将Hexo博客从传统Docker部署方案迁移至CNB(Cloud Native Build)自动部署平台的完整过程,包括遇到的问题和解决方案。

原先部署方案

部署架构

原先使用GitLab CI/CD + Docker的自动化部署方式:

  1. 构建环境:基于GitLab CI/CD的自动化构建

  2. 构建流程

    • 当Dockerfile变更时触发构建阶段
    • 使用Docker构建包含Node.js和Hexo的镜像
    • 推送镜像到私有镜像仓库
    • 当相关文件变更时触发部署阶段
    • 通过SSH连接到服务器执行git pull
    • 使用docker-compose拉取新镜像并重新创建容器
  3. 部署方式:通过GitLab CI/CD自动部署到服务器Docker环境

原有Dockerfile结构

1
2
3
4
5
6
7
8
9
10
11
12
FROM node
RUN npm config set registry https://registry.npmmirror.com
RUN npm i hexo-cli -g
RUN hexo init /app/hexo
WORKDIR /app/hexo
RUN git clone [主题仓库] themes/next
RUN npm install hexo-generator-searchdb
RUN npm install hexo-asset-img --save
# 初始化必要页面
RUN hexo new page categories && \
sed -i "s/title: categories/title: 分类/" source/categories/index.md && \
sed -i "/^---$/i type: categories" source/categories/index.md

原有GitLab CI/CD配置

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
# .gitlab-ci.yml
stages:
- build
- deploy

build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build --no-cache -t [私有镜像仓库]/hexo:latest .
- docker push [私有镜像仓库]/hexo:latest
rules:
- changes:
- Dockerfile

deploy:
stage: deploy
image: docker-executor:latest
script:
- ssh root@[服务器地址] "cd /app/hexo && git pull"
- docker-compose --context production pull
- docker-compose --context production up -d --force-recreate
rules:
- changes:
- Dockerfile
- docker-compose.yml
- config/*
- blog/**/*

存在的问题

  1. 基础设施依赖:需要维护GitLab Runner和私有镜像仓库
  2. 构建资源消耗:每次构建都需要完整的Docker镜像构建过程
  3. 部署复杂性:需要配置SSH密钥、Docker context等多个组件
  4. 维护成本高:需要管理GitLab CI/CD环境和相关基础设施

现在的CNB部署方案

CNB平台优势

CNB(Cloud Native Build)是腾讯云提供的云原生构建平台,具有以下优势:

  • 全托管的CI/CD环境
  • 支持多种构建环境和插件
  • 与Git仓库深度集成
  • 支持自动触发和部署

新的部署架构

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
# .cnb.yml配置文件
master:
push:
- docker:
image: node:18-alpine
volumes:
- /root/.npm:cow
- node_modules:cow
stages:
- name: install dependencies
script:
- npm config set registry https://registry.npmmirror.com
- npm install -g hexo-cli
- hexo init hexo-build
- cd hexo-build
- npm install hexo-generator-searchdb hexo-asset-img
- name: init pages
script:
- cd hexo-build
- cp -r ../next ./themes/
- cp -r ../blog/* ./source/_posts/
- cp ../config/_config.yml ./
- cp ../config/_config.next.yml ./
- hexo new page categories
- hexo new page tags
- hexo new page about
- sed -i "s/title: categories/title: 分类/" source/categories/index.md
- sed -i "/^---$/i type: categories" source/categories/index.md
- sed -i "s/title: tags/title: 标签/" source/tags/index.md
- sed -i "/^---$/i type: tags" source/tags/index.md
- name: build hexo
script:
- cd hexo-build
- hexo clean
- hexo generate
- name: rsync 上传到服务器
image: tencentcom/rsync
settings:
user: root
key: ${SSH_PRIVATE_KEY}
port: 22
hosts:
- [服务器地址]
source: ./hexo-build/public/
target: /app/nginx/html/blog/
script:
- echo "博客部署完成\!"

迁移过程中的主要改变

1. 构建环境变化

  • 原先:GitLab CI/CD + Docker镜像构建
  • 现在:CNB云端直接构建,无需Docker镜像

2. 部署流程优化

  • 原先:Docker镜像推送→服务器拉取→容器重建
  • 现在:静态文件生成→rsync直接同步到nginx目录

3. 配置管理

  • 原先:配置文件分散在不同位置
  • 现在:集中在config目录,便于管理

3. 基础设施简化

  • 原先:需要维护GitLab Runner、私有镜像仓库、Docker环境
  • 现在:只需CNB平台,无额外基础设施

4. 主题管理

  • 原先:通过git clone获取主题
  • 现在:将主题文件直接包含在项目中

5. 部署目标变化

  • 原先:部署到Docker容器,通过端口4000提供服务
  • 现在:直接部署静态文件到nginx目录,性能更优

遇到的问题及解决方案

1. YAML语法错误

问题描述

1
YAMLException: bad indentation of a mapping entry

原因:sed命令中包含特殊字符和中文,YAML解析器无法正确处理

解决方案

1
2
3
4
5
# 错误写法
- sed -i "s/title: categories/title: 分类/" source/categories/index.md

# 正确写法
- "sed -i \"s/title: categories/title: 分类/\" source/categories/index.md"

使用双引号包裹整个命令,内部使用转义字符处理特殊字符。

2. Nginx 404错误和重定向循环

问题描述

1
rewrite or internal redirection cycle while internally redirecting to "/index.html"

原因:Hexo站点缺少必要的页面(categories、tags、about),导致Next主题出现内部重定向循环

解决方案
在构建过程中添加页面初始化步骤:

1
2
3
4
5
6
hexo new page categories
hexo new page tags
hexo new page about
# 配置页面类型
sed -i "/^---$/i type: categories" source/categories/index.md
sed -i "/^---$/i type: tags" source/tags/index.md

3. 构建顺序问题

问题描述:构建时找不到配置文件或主题文件

解决方案
调整构建步骤顺序,确保在初始化页面前先复制所有必要文件:

  1. 复制主题文件到themes目录
  2. 复制博客文章到source/_posts
  3. 复制配置文件
  4. 初始化页面
  5. 构建生成静态文件

4. 权限和密钥管理

问题描述:rsync部署时需要SSH私钥认证

解决方案

  • 使用CNB的密钥仓库功能存储敏感信息
  • 通过环境变量${SSH_PRIVATE_KEY}引用私钥
  • 配置imports字段导入环境变量

5. Nginx配置问题

5.1 Docker挂载路径不一致

问题描述

1
"/app/chlm-nginx/html/blog/index.html" is not found (2: No such file or directory)

原因:nginx配置中使用的路径与docker-compose.yml中的挂载路径不一致

1
2
3
# docker-compose.yml
volumes:
- ./html:/home/chlm/html # 实际挂载路径

但nginx配置中使用了错误路径:

1
root /app/chlm-nginx/html/blog;  # 错误路径

解决方案
修正nginx配置中的路径:

1
2
3
4
location / {
root /home/chlm/html/blog; # 正确路径
index index.html index.htm;
}

5.2 目录访问403错误

问题描述

1
directory index of "/home/chlm/html/blog/categories/" is forbidden

原因:访问目录时nginx尝试列出目录内容而不是查找index.html

解决方案
使用正确的try_files配置:

1
2
3
4
5
location / {
root /home/chlm/html/blog;
index index.html index.htm;
try_files $uri $uri/ $uri/index.html =404;
}

5.3 try_files配置导致的重定向循环

问题描述

1
rewrite or internal redirection cycle while internally redirecting to "/index.html"

原因:不当的try_files配置导致无限重定向

常见错误配置

1
2
# 错误:会导致循环重定向
try_files $uri $uri/ /index.html;

正确配置

1
2
# 正确:避免循环重定向
try_files $uri $uri/index.html =404;

5.4 Hexo博客的完整nginx配置

最终优化配置

1
2
3
4
5
location / {
root /home/chlm/html/blog;
index index.html;
try_files $uri $uri/ $uri/index.html =404;
}

注意:不要添加静态资源过滤规则,会与其他服务冲突:

1
2
3
4
5
6
# ❌ 错误:不要添加这样的配置
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ {
root /home/chlm/html/blog;
expires 1y;
add_header Cache-Control "public, immutable";
}

原因

  • 图片等静态资源可能由其他服务处理(如MinIO对象存储)
  • 过度的location匹配会导致路径冲突
  • 应该让专门的服务处理对应的资源类型

5.5 Nginx核心指令详解

root vs alias 的区别

root指令

  • 定义文档根目录,会将location路径附加到root路径后面
  • 适用于大多数静态文件服务场景
1
2
3
4
# 访问 /blog/index.html 会查找 /var/www/blog/index.html
location /blog/ {
root /var/www; # 实际路径: /var/www + /blog/
}

alias指令

  • 定义路径替换,直接将location路径替换为alias路径
  • 适用于需要路径映射的场景
1
2
3
4
# 访问 /blog/index.html 会查找 /var/www/hexo/index.html
location /blog/ {
alias /var/www/hexo/; # 直接替换为 /var/www/hexo/
}

选择原则

  • 当URL路径与文件系统路径结构一致时使用 root
  • 当需要将URL路径映射到不同的文件系统路径时使用 alias

try_files指令详解

作用:按顺序尝试多个文件/URI,直到找到存在的文件或执行fallback

语法try_files file1 file2 ... uri;

常见用法场景

  1. 静态网站路径重写(如Hexo博客):

    1
    2
    3
    4
    5
    6
    try_files $uri $uri/ $uri/index.html =404;
    # 访问 /categories/ 时:
    # 1. 先找 /categories/ 文件(通常不存在)
    # 2. 再找 /categories/ 目录(存在但是目录)
    # 3. 再找 /categories/index.html(Hexo生成的页面)
    # 4. 都找不到返回404
  2. SPA应用路由支持

    1
    2
    try_files $uri $uri/ /index.html;
    # 对于Vue/React等SPA应用,所有路由都回退到index.html
  3. 多语言站点回退

    1
    2
    try_files $uri $uri/ /zh/index.html /en/index.html =404;
    # 按语言优先级依次尝试
  4. 缓存穿透处理

    1
    2
    try_files $uri @backend;
    # 静态文件不存在时转发到后端服务

什么情况需要try_files

必须使用try_files的场景

  1. 静态网站生成器(Hexo/Hugo/Jekyll等):

    • URL结构:/categories/
    • 文件结构:/categories/index.html
    • 需要try_files处理目录到文件的映射
  2. 单页应用(SPA)

    • 前端路由:/user/profile
    • 实际文件:只有 /index.html
    • 需要所有路由都回退到index.html
  3. 伪静态URL

    • 用户友好URL:/article/123
    • 实际处理:/article.php?id=123

不需要try_files的场景

  1. 纯静态文件服务:文件路径与URL完全对应
  2. 纯代理服务:所有请求都转发到后端
  3. API服务:每个endpoint都有明确的处理逻辑

Hexo博客为什么需要try_files

1
try_files $uri $uri/ $uri/index.html =404;

原因分析

  1. URL美化:Hexo生成美化的URL结构

    • 访问:https://blog.com/categories/
    • 文件:/categories/index.html
  2. 目录结构映射

    • //index.html
    • /categories//categories/index.html
    • /archives/2025//archives/2025/index.html
  3. 兼容性处理:同时支持带trailing slash和不带的URL

5.6 调试技巧

查看目录结构

1
2
3
4
5
6
7
8
9
# 安装tree工具
yum install -y tree

# 查看blog目录结构
tree /app/chlm-nginx/html/blog

# 检查关键文件是否存在
ls -la /home/chlm/html/blog/index.html
ls -la /home/chlm/html/blog/categories/index.html

nginx日志分析

1
2
3
4
5
6
7
8
# 实时查看错误日志
tail -f /var/log/nginx/error.log

# 检查配置语法
nginx -t

# 重载配置
nginx -s reload

try_files调试

1
2
3
4
5
6
# 临时添加调试日志
location / {
root /home/chlm/html/blog;
try_files $uri $uri/ $uri/index.html =404;
# 在error.log中会显示try_files的尝试过程
}

最佳实践建议

1. 配置文件管理

  • 将所有配置文件集中存放在config目录
  • 使用版本控制跟踪配置变更
  • 敏感信息使用环境变量管理

2. 构建优化

  • 使用volumes缓存npm包和node_modules
  • 选择合适的基础镜像(如node:18-alpine)
  • 合理安排构建步骤顺序

3. 部署策略

  • 使用rsync进行增量同步
  • 配置部署前后的检查脚本
  • 保留部署日志便于问题排查

4. 问题排查

  • 关注CNB构建日志
  • 检查YAML语法格式
  • 验证文件路径和权限
  • 测试nginx配置

总结

通过将Hexo博客迁移至CNB平台,实现了:

  • 自动化部署:Git推送即可触发构建和部署
  • 环境一致性:云端标准化构建环境
  • 操作简化:减少了手动操作步骤
  • 可维护性:配置文件化,便于版本管理

迁移过程中主要的挑战在于YAML配置语法和Hexo页面初始化,通过合适的转义和构建步骤安排成功解决。CNB平台为静态博客提供了一个高效、稳定的自动化部署解决方案。

在gitlab runner写自动发布脚本的时候,有如下这段代码,git clone一直显示没有权限,然而使用相同密钥的宿主机是没有问题的,只有而runner容器可以ssh却不能clone。

1
2
3
4
5
6
7
8
9
10
build:
stage: build
image:
name: docker-executor:latest
pull_policy: if-not-present
before_script:
- source /app/setup.sh
script:
- ssh root@10.0.24.9 "cd /app/hexo && git pull"
- git clone ssh://git@10.0.24.9:7003/root/chlm-common.git

经过一番折腾,检查了下宿主机know_hosts文件,发现了端倪,git连接的地址加上了端口,所以know_host需要添加一个带端口的记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
build:
stage: build
image:
name: docker-executor:latest
pull_policy: if-not-present
before_script:
- source /app/setup.sh
script:
- ssh root@10.0.24.9 "cd /app/hexo && git pull"
# 添加端口的know_hosts
- ssh-keyscan -p 7003 10.0.24.9 >> ~/.ssh/known_hosts
- git clone ssh://git@10.0.24.9:7003/root/chlm-common.git
- git clone ssh://git@10.0.24.9:7003/root/chlm-redis.git

linux后台运行命令的两种方式

Bilibili 视频

command &

适合场景:命令还未启动

1
./test &

Ctrl + z

适合场景: 命令已运行

1
2
3
4
5
6
7
8
# 运行命令
./test
# 挂起命令到后台,并暂停运行
Ctrl + z
# 查看后台运行的任务
jobs
# 将暂停的命令重新运行
bg %1

这两种方式存在的问题是会话关闭后可能会导致命令停止。

linux关闭会话保持命令运行

关闭会话,发生了什么

步骤1:系统发送Sighup信号给控制进程(通常为shell);

步骤2:控制进程停止,发送Sighup到前台进程和后台进程;

步骤3:子进程收到信号;

步骤4:子进程关闭当前进程;

测试方法

编写一个实时输出的脚本

1
vim test
1
2
3
4
5
6
7
8
9
#!/bin/bash
while true
do
# 输出当前时间到log文件
time=$(date "+%Y-%m-%d %H:%M:%S")
echo $time >> log
# 休息一秒
sleep 1s
done
1
chmod +x test

另外启动一个控制台实时查看log文件是否有更新

1
tail -f log

干涉步骤1,不发送Sighup信号给shell

从根源解决问题,不关闭终端,而是把当前shell隐藏起来。

终端复用软件

screen和tmux是一个终端复用软件,通过该软件能够使终端隐藏起来而不是关闭。

以tmux为例

1
2
3
4
5
6
7
8
# 进入tmux
tmux
# 执行命令
./test &
# 分离当前session,此时这个session就会进入到后台运行
tmux detach
# 重新进入会话
tmux detach -t 0

干涉步骤2,不发送信号给当前shell的子进程

不同的shell有不同的实现。

bash: huponexit off

这个是shell的配置,是否在终端退出时发出挂起信号?on为是,off为否

1
2
3
4
5
6
# 查看huponexit命令的状态
shopt | grep huponexit
# 开启终端退出挂起
shopt -s huponexit
# 关闭终端退出挂起
shopt -u huponexit

zsh: nohup

1
2
3
4
# 关闭终端退出挂起
setopt NOhup
# 开启终端退出挂起
setopt hup

干涉步骤3,子进程不接收父进程发送过来的信号

setsid

使用该命令运行的命令会运行在init进程中,这就使得命令的运行与当前的shell没有关系了

1
setsid ./test
1
2
# 使用pstree查看,发现进程在init进程上
pstree

image-20230306231841204

nohup

忽略Sighup信号

1
nohup ./test.sh

disown

该命令需要set选项monitor处于开启状态时才能执行

1
2
# 查看monitor是否开启
set -o

将任务从当前jobs列表中移除并忽略Sighup信号,但是进程还属于当前shell

1
2
3
4
5
6
# 运行任务
./test &
# 查看任务
jobs
# 从当前shell移除作业
disown %1

干涉步骤4,接受父进程发送过来的Sighup信号但是不执行停止的操作

trap

trap 命令用于指定当前进程在接受到信号后将要采取的行动

1
2
3
4
5
6
# 设置接受到Sighup执行echo hello
trap "echo hello" HUP
# 查看当前shell pid
ps
# 发送Sighup信号给自己
kill -1 7830

image-20230306233343787

因为trap的作用域是进程,所以编写一个脚本,使trap和要运行的脚本运行在同一个进程中。

1
2
3
4
5
6
7
8
9
#!/bin/bash
# 接受停止信号,“”表示但是不采取任何行动
trap "" HUP
# 执行命令
source ./test
# 或者
. ./test
# 或者
sh test
0%