1、问题描述
服务文件对接功能,选中需要同步的文件后,提示"文件不存在"(文件名称为中文存在该问题,英文名称的文件无此问题)
2、问题根本问题原因
-
核心代码
Path filePath = Paths.get(sourcePath).resolve(fileName).normalize(); File file = filePath.toFile(); // 验证文件存在且是普通文件 if (!file.exists() || !file.isFile()) { return R.error(fileName+"在服务器的"+sourcePath+"位置不存在该文件"); } -
代码实现逻辑 (
java.io.File.exists()的路径转换)- 底层通过 JNI 调用 C 库函数(如
access或stat)。 - 它依赖
sun.jnu.encoding将 Java 的String(UTF-16)转换为字节序列。 - 在 Linux 上,这个字节序列会被直接传递给系统调用。如果
sun.jnu.encoding设置不正确,或与文件系统实际使用的编码不匹配,字节序列就会错误,导致access找不到文件。
- 底层通过 JNI 调用 C 库函数(如
-
问题说明
-
查看docker容器内的jvm编码
bash-5.1# java -XshowSettings:properties -version 2>&1 | grep jnu.encoding sun.jnu.encoding = ANSI_X3.4-1968 -
问题描述
代码中使用的文件名称是utf-8编码,而jvm使用的是ansi编码,导致传递到操作系统层面后识别为"???"文件找不到
-
3.问题处理
3.0 模拟验证代码
import java.nio.file.*;
import java.util.Arrays;
import java.nio.charset.StandardCharsets;
public class TestPath {
public static void main(String[] args) {
String sourcePath = "/home/app/csvfile";
String fileName = args.length > 0 ? args[0] : "标签labels.csv";
System.out.println("fileName: " + fileName);
System.out.println("fileName bytes (UTF-8): " + Arrays.toString(fileName.getBytes(StandardCharsets.UTF_8)));
// 跟应用代码一模一样
Path filePath = Paths.get(sourcePath).resolve(fileName).normalize();
System.out.println("resolved path: " + filePath);
System.out.println("resolved bytes: " + Arrays.toString(filePath.toString().getBytes(StandardCharsets.UTF_8)));
java.io.File file = filePath.toFile();
System.out.println("file.exists(): " + file.exists());
System.out.println("file.isFile(): " + file.isFile());
System.out.println("Files.exists(path): " + Files.exists(filePath));
}
}
3.1 环境层面
既然默认的sun.jnu.encoding的为设置为utf-8,那需要将其设置为utf-8,而其于系统环境变量(如 LANG、LC_ALL)。如果容器的 LANG 未设置为 UTF-8,C 库可能使用默认的 C locale,从而将路径视为 ASCII,非 ASCII 字符会被视为非法。
-
验证方法 :在容器内执行
locale命令,查看LANG和LC_CTYPE的值。apk add --no-cache musl-locales musl-locales-lang
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8 -
设置后应该如下:
系统语言环境
708d64338dbb:/home# locale
LANG=en_US.UTF-8
LC_CTYPE=en_US.UTF-8
LC_NUMERIC=en_US.UTF-8
LC_TIME=en_US.UTF-8
LC_COLLATE=en_US.UTF-8
LC_MONETARY=en_US.UTF-8
LC_MESSAGES=en_US.UTF-8
LC_ALL=en_US.UTF-8jvm语言环境
708d64338dbb:/home# java -XshowSettings:properties -version 2>&1 | grep jnu.encoding
sun.jnu.encoding = UTF-8 -
执行测试代码
708d64338dbb:/home# java TestPath.java
fileName: 标签labels.csv
fileName bytes (UTF-8): [-26, -96, -121, -25, -83, -66, 108, 97, 98, 101, 108, 115, 46, 99, 115, 118]
resolved path: /home/app/csvfile/标签labels.csv
resolved bytes: [47, 104, 111, 109, 101, 47, 97, 112, 112, 47, 99, 115, 118, 102, 105, 108, 101, 47, -26, -96, -121, -25, -83, -66, 108, 97, 98, 101, 108, 115, 46, 99, 115, 118]
file.exists(): true
file.isFile(): true
Files.exists(path): true
3.2 代码层面
| API | JNI 实现类 | 路径处理 |
|---|---|---|
File.exists() |
java.io.UnixFileSystem |
走 musl realpath() 规范化,对非 ASCII 路径处理有问题 |
Files.exists() |
sun.nio.fs.UnixNativeDispatcher |
直接传字节数组给内核,不做规范化 |
-
检查文件是否存在
// 当前(有问题的) File file = filePath.toFile(); if (!file.exists() || !file.isFile()) { // 修复后 if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { -
读取文件
1. DLakeInfoServiceImpl.java 第 1437 行 --- 日志用:
java
// 改前
file.length()
// 改后
Files.size(filePath)
2. DLakeInfoServiceImpl.java 第 1439 行 --- 编码检测,需要给 FileEncodingHandler 加一个接受 Path 的重载:
java
// 改后
String detectedEncoding = String.valueOf(FileEncodingHandler.detectEncoding(filePath));
3. DLakeInfoServiceImpl.java 第 1453 行 --- 读取文件:
java
// 改前
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), detectedEncoding))
// 改后
BufferedReader br = Files.newBufferedReader(filePath, Charset.forName(detectedEncoding))