文件处理工具箱
一、背景
在桌面应用中,文件处理是几乎每个项目都会遇到的需求:
- 用户需要选择文件或目录
- 数据需要导出为 Excel
- 批量文件需要打包成 ZIP
- 配置文件需要以 JSON 格式存储和读取
如果每次都从头写文件选择器、Excel 导出、压缩解压等逻辑,代码会非常冗余且容易出错。
本篇介绍的文件处理工具箱包含以下四个核心类:
| 类 | 职责 |
|---|---|
MetFileUtils |
文件上传/下载/导出 Excel 的统一封装 |
ZipFileUtils |
ZIP 压缩/解压(支持密码加密) |
ExportField |
Excel 导出字段配置 |
JsonLoader |
JSON 配置文件加载与更新 |
二、ExportField --- 导出字段配置
2.1 背景
Excel 导出时,需要指定导出的字段名、列别名、列宽,有时还需要对字段值进行转换(如状态码转文字)。ExportField 就是用来封装这些配置的。 2.2 类源码
java
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.function.Function;
/**
* 导出字段配置类
* 用于配置 Excel 导出的字段信息
*
* 使用示例:
* ExportField.builder()
* .name("userName")
* .alias("姓名")
* .width(20)
* .converter(value -> value == null ? "" : value.toString())
* .build();
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class ExportField implements Serializable {
private static final long serialVersionUID = 6376510389987010773L;
/** 字段名(对应对象属性名) */
private String name;
/** 表格显示的列别名(表头) */
private String alias;
/** 列宽,默认 20(字符宽度) */
@Builder.Default
private Integer width = 20;
/** 字段值转换器(如状态码转文字) */
private Function<Object, Object> converter;
}
2.3 使用示例
java
List<ExportField> fields = Arrays.asList(
ExportField.builder()
.name("id")
.alias("编号")
.width(15)
.build(),
ExportField.builder()
.name("userName")
.alias("姓名")
.width(20)
.build(),
ExportField.builder()
.name("status")
.alias("状态")
.width(15)
.converter(v -> {
Integer status = (Integer) v;
if (status == null) return "未知";
return status == 1 ? "启用" : "禁用";
})
.build()
);
三、FileSelectExecutor --- 文件选择接口
3.1 背景
文件选择器的回调需要处理单选和多选两种场景,使用接口统一处理。
3.2 类源码
java
import java.io.File;
/**
* 文件选择执行器接口
* 用于处理文件选择后的回调
*/
public interface FileSelectExecutor {
/**
* 单文件处理
* @param file 文件
*/
void execute(File file);
/**
* 多文件处理
* @param files 文件数组
*/
void execute(File[] files);
}
四、ZipFileUtils --- 压缩解压工具
4.1 背景
Java 原生 java.util.zip 包功能有限,不支持密码加密的 ZIP。ZipFileUtils 基于 Zip4j 库封装,提供了更完整的压缩解压功能。
4.2 添加依赖
xml
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.11.5</version>
</dependency>
4.3 类源码
java
import cn.hutool.core.util.StrUtil;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.AesKeyStrength;
import net.lingala.zip4j.model.enums.EncryptionMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
/**
* 文件压缩解压工具
* 基于 Zip4j 封装,支持密码加密、文件夹压缩、流式读取
*
* 使用示例:
* // 压缩
* ZipFileUtils.zip("output.zip", file1, file2);
* // 带密码压缩
* ZipFileUtils.zipWithPassword("output.zip", "123456", "file1.txt", "file2.txt");
* // 解压
* ZipFileUtils.unzip("output.zip", "dest/");
*/
public class ZipFileUtils {
private static final Logger log = LoggerFactory.getLogger(ZipFileUtils.class);
/**
* 压缩文件或文件夹
* @param zipName 压缩包路径
* @param files 要压缩的文件
*/
public static void zip(String zipName, File... files) {
try (ZipFile zipFile = new ZipFile(zipName)) {
ZipParameters zipParameters = new ZipParameters();
for (File file : files) {
if (file.isDirectory()) {
zipFile.addFolder(file, zipParameters);
} else {
zipFile.addFile(file, zipParameters);
}
}
} catch (Exception e) {
log.error("文件压缩失败:{}", e.getMessage());
}
}
/**
* 带密码压缩文件或文件夹
* @param zipPath 压缩包路径
* @param password 压缩包密码
* @param files 要压缩的文件路径
*/
public static void zipWithPassword(String zipPath, String password, String... files) {
try (ZipFile zipFile = new ZipFile(zipPath, password.toCharArray())) {
ZipParameters zipParameters = new ZipParameters();
zipParameters.setEncryptFiles(true);
zipParameters.setEncryptionMethod(EncryptionMethod.AES);
zipParameters.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256);
for (String file : files) {
File sourceFile = new File(file);
if (sourceFile.isDirectory()) {
zipFile.addFolder(sourceFile, zipParameters);
} else {
zipFile.addFile(sourceFile, zipParameters);
}
}
} catch (Exception e) {
log.error("文件压缩失败:{}", e.getMessage());
}
}
/**
* 解压 ZIP 文件(无密码)
* @param zipPath 压缩包路径
* @param destFolder 解压目标目录
*/
public static void unzip(String zipPath, String destFolder) {
unzip(zipPath, null, destFolder);
}
/**
* 解压 ZIP 文件(带密码)
* @param zipPath 压缩包路径
* @param password 压缩包密码
* @param destFolder 解压目标目录
*/
public static void unzip(String zipPath, String password, String destFolder) {
try (ZipFile zipFile = new ZipFile(zipPath)) {
if (!zipFile.isValidZipFile()) {
log.error("提供的文件不是一个有效的 ZIP 文件");
return;
}
if (StrUtil.isNotBlank(password) && zipFile.isEncrypted()) {
zipFile.setPassword(password.toCharArray());
}
File outputFileFolder = new File(destFolder);
if (!outputFileFolder.exists()) {
outputFileFolder.mkdirs();
}
List<FileHeader> fileHeaders = zipFile.getFileHeaders();
if (fileHeaders.isEmpty()) {
return;
}
zipFile.extractAll(destFolder);
} catch (IOException e) {
log.error("解压失败:{}", e.getMessage());
}
}
/**
* 读取 ZIP 文件内容(无密码)
* @param zipFilePath 压缩包路径
* @return 文件头列表
*/
public static List<FileHeader> readZip(String zipFilePath) {
return readZip(zipFilePath, null);
}
/**
* 读取 ZIP 文件内容(带密码)
* @param zipFilePath 压缩包路径
* @param password 压缩包密码
* @return 文件头列表
*/
public static List<FileHeader> readZip(String zipFilePath, String password) {
try (ZipFile zipFile = new ZipFile(zipFilePath)) {
if (StrUtil.isNotBlank(password) && zipFile.isEncrypted()) {
zipFile.setPassword(password.toCharArray());
}
return zipFile.getFileHeaders();
} catch (IOException e) {
log.error("ZIP 操作错误: {}", e.getMessage());
return Collections.emptyList();
}
}
/**
* 读取受密码保护的 ZIP 文件并处理每个文件流
* @param zipFilePath 压缩包路径
* @param password 压缩包密码
* @param consumer 文件流处理器
*/
public static void readProtectedZip(String zipFilePath, String password, Consumer<InputStream> consumer) {
try (ZipFile zipFile = new ZipFile(zipFilePath)) {
if (StrUtil.isNotBlank(password) && zipFile.isEncrypted()) {
zipFile.setPassword(password.toCharArray());
}
List<FileHeader> fileHeaders = zipFile.getFileHeaders();
for (FileHeader fileHeader : fileHeaders) {
if (!fileHeader.isDirectory()) {
try (InputStream inputStream = zipFile.getInputStream(fileHeader)) {
consumer.accept(inputStream);
}
}
}
} catch (IOException e) {
log.error("ZIP 操作错误: {}", e.getMessage());
}
}
}
4.4 使用示例
java
// 压缩
ZipFileUtils.zip("data/backup.zip", new File("data/report.xlsx"));
// 带密码压缩
ZipFileUtils.zipWithPassword("data/backup.zip", "123456", "data/report.xlsx", "data/settings.json");
// 解压
ZipFileUtils.unzip("data/backup.zip", "data/extract/");
// 读取 ZIP 内容
List<FileHeader> headers = ZipFileUtils.readZip("data/backup.zip");
for (FileHeader header : headers) {
System.out.println(header.getFileName());
}
五、MetFileUtils --- 文件操作工具
5.1 背景
MetFileUtils 封装了 Swing 应用中最常用的文件操作:
- 文件选择器(单选/多选、后缀过滤)
- 文件下载(选择保存目录)
- Excel 导出(带字段配置和数据转换)
5.2 类源码
java
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import cn.hutool.poi.excel.StyleSet;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import java.io.File;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.util.*;
import java.util.function.Function;
/**
* 文件操作工具类
* 封装文件选择、下载、Excel 导出等功能
*/
public class MetFileUtils {
private static final Logger log = LoggerFactory.getLogger(MetFileUtils.class);
private static final Integer DECIMAL_PLACES = 6;
// ==================== 文件选择 ====================
/**
* 文件上传选择
* @param suffix 接收的文件后缀(如 ".xlsx,.xls"),空表示所有文件
* @param multipleSelect 是否多选
* @param executor 文件选择执行器
*/
public static void upload(String suffix, boolean multipleSelect, FileSelectExecutor executor) {
JFileChooser fileChooser = new JFileChooser();
FileFilter fileFilter = createFileFilter(suffix);
fileChooser.setFileFilter(fileFilter);
fileChooser.setMultiSelectionEnabled(multipleSelect);
fileChooser.setDialogTitle("选择文件");
int returnValue = fileChooser.showOpenDialog(null);
if (returnValue == JFileChooser.APPROVE_OPTION) {
if (!multipleSelect) {
executor.execute(fileChooser.getSelectedFile());
} else {
executor.execute(fileChooser.getSelectedFiles());
}
}
}
/**
* 创建文件过滤器
* @param suffix 后缀(如 ".xlsx,.xls"),空表示所有文件
* @return 文件过滤器
*/
public static FileFilter createFileFilter(String suffix) {
return new FileFilter() {
private final List<String> suffixList;
{
if (StrUtil.isBlank(suffix)) {
suffixList = Collections.emptyList();
} else {
suffixList = Arrays.asList(suffix.replaceAll("\\s", "").split(","));
}
}
@Override
public boolean accept(File file) {
if (suffixList.isEmpty()) {
return true;
}
if (file.isDirectory()) {
return true;
}
String fileName = file.getName().toLowerCase();
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex == -1 || dotIndex == fileName.length() - 1) {
return false;
}
String extension = fileName.substring(dotIndex);
return suffixList.stream().map(String::toLowerCase).anyMatch(extension::equals);
}
@Override
public String getDescription() {
if (suffixList.isEmpty()) {
return "所有文件 (*.*)";
}
return "支持的文件类型: " + String.join(",", suffixList);
}
};
}
// ==================== 文件下载 ====================
/**
* 文件下载(选择保存目录)
* @param sourceFile 源文件
* @param filename 新文件名
* @return 目标文件,取消时返回 null
*/
public static File download(File sourceFile, String filename) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
int returnVal = fileChooser.showOpenDialog(null);
if (returnVal != JFileChooser.APPROVE_OPTION) {
return null;
}
File file = fileChooser.getSelectedFile();
File destFile = new File(file.getAbsolutePath() + File.separator + filename);
FileUtil.copy(sourceFile, destFile, true);
destFile.setLastModified(System.currentTimeMillis());
return destFile;
}
/**
* 文件下载并压缩为 ZIP
* @param folder 待压缩的文件夹
* @param filename 压缩包文件名
* @return 目标目录路径,取消时返回 "-1"
*/
public static String downloadToZip(File folder, String filename) {
File[] files = folder.listFiles(File::isFile);
JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
int returnVal = fileChooser.showOpenDialog(null);
if (returnVal != JFileChooser.APPROVE_OPTION) {
return "-1";
}
File file = fileChooser.getSelectedFile();
ZipFileUtils.zip(file.getAbsolutePath() + File.separator + filename, files);
return file.getAbsolutePath();
}
// ==================== Excel 导出 ====================
/**
* Excel 导出(弹出保存对话框)
* @param dataList 数据集
* @param filename 文件名
* @param fields 导出字段配置
* @return 导出文件路径,取消时返回 null
*/
public static <T> String exportExcel(List<T> dataList, String filename, List<ExportField> fields) throws Exception {
return exportExcel(dataList, filename, fields, "yyyy-MM-dd HH:mm:ss");
}
/**
* Excel 导出(可指定日期格式)
*/
public static <T> String exportExcel(List<T> dataList, String filename,
List<ExportField> fields, String dateFormat) throws Exception {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
fileChooser.setSelectedFile(new File(filename));
if (fileChooser.showSaveDialog(null) != JFileChooser.APPROVE_OPTION) {
return null;
}
String filePath = fileChooser.getSelectedFile().getAbsolutePath();
File file = exportExcel(filePath, dataList, fields, dateFormat);
if (file.exists()) {
SwingUtilities.invokeLater(() -> MessageDialog.showSuccess("提示", "数据已导出!"));
}
return filePath;
}
/**
* 导出 Excel(直接指定文件路径)
*/
public static <T> File exportExcel(String filePath, List<T> dataList,
List<ExportField> fields) throws Exception {
return exportExcel(filePath, dataList, fields, "yyyy-MM-dd HH:mm:ss");
}
/**
* 导出 Excel(完整参数)
*/
public static <T> File exportExcel(String filePath, List<T> dataList,
List<ExportField> fields, String dateFormat) throws Exception {
File file = handleExcelFile(filePath);
try (ExcelWriter writer = createExcelWriter(file, fields, dateFormat)) {
List<Map<String, Object>> convertedData = processData(dataList, fields);
writer.write(convertedData, true);
return file;
} catch (Exception e) {
log.error("数据导出失败:{}", e.getMessage(), e);
if (file.exists()) {
Files.delete(file.toPath());
}
throw new Exception("数据导出失败");
}
}
// ==================== ExcelWriter 创建 ====================
public static ExcelWriter createExcelWriter(File file, List<ExportField> fields, String dateFormat) {
ExcelWriter writer = ExcelUtil.getWriter(file);
handleExcelWriter(writer, fields, dateFormat, DECIMAL_PLACES);
return writer;
}
private static void handleExcelWriter(ExcelWriter writer, List<ExportField> fields,
String dateFormat, Integer digit) {
writer.getSheet().createFreezePane(0, 1);
writer.setOnlyAlias(true);
for (int i = 0; i < fields.size(); i++) {
ExportField field = fields.get(i);
writer.addHeaderAlias(field.getName(), field.getAlias());
writer.setColumnWidth(i, field.getWidth());
}
if (StrUtil.isNotBlank(dateFormat)) {
writer.getStyleSet().getCellStyleForDate()
.setDataFormat(writer.getWorkbook().createDataFormat().getFormat(dateFormat));
}
String numberFormat = "0." + String.join("", Collections.nCopies(digit, "#"));
StyleSet styleSet = writer.getStyleSet();
CellStyle numberStyle = styleSet.getCellStyleForNumber();
DataFormat dataFormat = writer.getWorkbook().createDataFormat();
numberStyle.setDataFormat(dataFormat.getFormat(numberFormat));
}
public static File handleExcelFile(String filePath) {
if (filePath.endsWith(".xlsx") || filePath.endsWith(".xls")) {
return new File(filePath);
}
return new File(filePath + ".xlsx");
}
// ==================== 数据转换 ====================
private static <T> List<Map<String, Object>> processData(List<T> dataList, List<ExportField> fields) {
List<Map<String, Object>> convertedData = new ArrayList<>();
for (T data : dataList) {
Map<String, Object> row = new HashMap<>();
for (ExportField field : fields) {
Object value = getNestedValue(data, field.getName());
if (null != field.getConverter()) {
value = field.getConverter().apply(value);
}
row.put(field.getName(), value);
}
convertedData.add(row);
}
return convertedData;
}
private static Object getNestedValue(Object obj, String fieldPath) {
if (null == obj || null == fieldPath) {
return null;
}
if (!fieldPath.contains(".")) {
return BeanUtil.getProperty(obj, fieldPath);
}
try {
if (obj instanceof Map) {
return ((Map<?, ?>) obj).get(fieldPath);
}
String[] parts = fieldPath.split("\\.");
Object currentValue = obj;
for (String part : parts) {
if (currentValue instanceof Map) {
currentValue = ((Map<?, ?>) currentValue).get(part);
} else {
currentValue = BeanUtil.getProperty(currentValue, part);
}
}
return currentValue;
} catch (Exception e) {
log.warn("解析字段 [{}] 失败: {}", fieldPath, e.getMessage());
return null;
}
}
}
5.3 使用示例
java
// 文件选择(单选)
MetFileUtils.upload(".xlsx,.xls", false, new FileSelectExecutor() {
@Override
public void execute(File file) {
System.out.println("选择的文件:" + file.getAbsolutePath());
}
@Override
public void execute(File[] files) {}
});
// 文件选择(多选)
MetFileUtils.upload(".jpg,.png", true, new FileSelectExecutor() {
@Override
public void execute(File file) {}
@Override
public void execute(File[] files) {
for (File f : files) {
System.out.println(f.getAbsolutePath());
}
}
});
// Excel 导出
List<User> userList = getUserList();
List<ExportField> fields = getExportFields();
MetFileUtils.exportExcel(userList, "用户数据.xlsx", fields);
六、JsonLoader --- JSON 配置加载
6.1 背景
桌面应用通常需要保存配置信息(如窗口位置、用户偏好等)。JsonLoader 提供了统一的 JSON 配置文件加载和更新功能,支持从 resources 或用户目录加载。
6.2 类源码
java
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* JSON 配置文件加载器
* 支持从 resources 或用户目录加载 JSON 配置,支持嵌套属性访问和更新
*
* 使用示例:
* JsonLoader loader = JsonLoader.create();
* loader.loadConfig("config/user.json", "default-user.json");
* String name = loader.getString("user.name");
* Integer age = loader.getInteger("user.age");
*/
public class JsonLoader {
private static final Logger log = LoggerFactory.getLogger(JsonLoader.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
private JsonNode root = null;
private JsonNode resourceRoot = null;
private String jsonPath = null;
private Class<?> sourceClass = null;
static {
configureObjectMapper();
}
public static JsonLoader create() {
return new JsonLoader();
}
public static JsonLoader create(Class<?> sourceClass) {
JsonLoader loader = new JsonLoader();
loader.sourceClass = sourceClass;
return loader;
}
/**
* 加载配置(用户目录 + resources 兜底)
* @param path 用户目录下的路径(如 config/user.json)
* @param fileName resources 中的文件名(如 default-user.json)
*/
public void loadConfig(String path, String fileName) {
root = loadUserDir(path);
resourceRoot = loadResource(fileName);
}
/**
* 从用户目录加载配置
*/
public JsonNode loadUserDir(String path) {
if (StrUtil.isBlank(path)) {
return null;
}
try {
jsonPath = System.getProperty("user.dir") + File.separator + path;
return FileUtil.exist(jsonPath) ? MAPPER.readTree(new File(jsonPath)) : null;
} catch (Exception e) {
log.error("usr.dir 配置文件[{}]加载失败:{}", path, e.getMessage());
return null;
}
}
/**
* 从 resources 加载配置
*/
public JsonNode loadResource(String fileName) {
if (StrUtil.isBlank(fileName)) {
return null;
}
try {
if (null != sourceClass) {
return loadResourceFromClass(fileName, sourceClass);
}
String resourceContent = ResourceUtil.readUtf8Str(fileName);
return !resourceContent.isEmpty() ? MAPPER.readTree(resourceContent) : null;
} catch (Exception e) {
log.error("resources 配置文件[{}]加载失败:{}", fileName, e.getMessage());
return null;
}
}
private JsonNode loadResourceFromClass(String fileName, Class<?> clazz) throws IOException {
String resourceContent = getResourceFromJar(fileName, clazz);
return StrUtil.isNotBlank(resourceContent) ? MAPPER.readTree(resourceContent) : null;
}
public static String getResourceFromJar(String fileName, Class<?> jarMarkerClass) {
if (StrUtil.isBlank(fileName) || null == jarMarkerClass) {
return null;
}
try (InputStream inputStream = jarMarkerClass.getClassLoader().getResourceAsStream(fileName)) {
if (null == inputStream) {
return null;
}
return IoUtil.read(inputStream, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("从jar包[{}]加载资源[{}]失败:{}", jarMarkerClass.getName(), fileName, e.getMessage());
return null;
}
}
// ==================== 读取方法 ====================
/**
* 获取指定路径的值(优先从用户目录读取,fallback 到 resources)
*/
public <T> T get(String path) {
T result = get(root, path);
if (null == result) {
result = get(resourceRoot, path);
}
return result;
}
public <T> T get(String path, T defaultValue) {
T result = get(path);
return null != result ? result : defaultValue;
}
@SuppressWarnings("unchecked")
public <T> T get(JsonNode jsonNode, String path) {
String[] keys = path.split("\\.");
for (String key : keys) {
if (null == jsonNode || !jsonNode.has(key)) {
return null;
}
jsonNode = jsonNode.get(key);
}
return (T) convertJsonNode(jsonNode);
}
private Object convertJsonNode(JsonNode node) {
if (node.isNull()) return null;
if (node.isTextual()) return node.asText();
if (node.isBoolean()) return node.asBoolean();
if (node.isNumber()) {
if (node.canConvertToInt()) return node.intValue();
if (node.canConvertToLong()) return node.longValue();
if (node.isBigDecimal() || node.isFloat() || node.isDouble()) {
return node.decimalValue();
}
return node.numberValue();
}
if (node.isArray()) {
List<Object> list = new ArrayList<>();
node.forEach(element -> list.add(convertJsonNode(element)));
return list;
}
if (node.isObject()) {
Map<String, Object> map = new LinkedHashMap<>();
node.properties().forEach(entry -> map.put(entry.getKey(), convertJsonNode(entry.getValue())));
return map;
}
return null;
}
// ==================== 类型快捷方法 ====================
public String getString(String path) {
Object obj = get(path);
if (null == obj) return null;
if (obj instanceof String) return (String) obj;
if (JSONUtil.isTypeJSON(obj.toString())) return JSONUtil.toJsonStr(obj);
return obj.toString();
}
public Integer getInteger(String path) {
Object obj = get(path);
if (obj instanceof Integer) return (Integer) obj;
if (obj instanceof Number) return ((Number) obj).intValue();
return null;
}
public Long getLong(String path) {
Object obj = get(path);
if (obj instanceof Long) return (Long) obj;
if (obj instanceof Number) return ((Number) obj).longValue();
return null;
}
public Boolean getBoolean(String path) {
Object obj = get(path);
return obj instanceof Boolean ? (Boolean) obj : null;
}
public JSONObject getJsonObject(String path) {
Object obj = get(path);
return obj instanceof Map ? new JSONObject(obj) : null;
}
// ==================== 更新方法 ====================
/**
* 更新或新增 JSON 键值对
* @param keyPath 键路径(如 "user.name")
* @param newValue 新值
* @return 是否更新成功
*/
public boolean updateOrAddKey(String keyPath, Object newValue) {
try {
boolean rootUpdated = false;
if (null != root) {
rootUpdated = updateJsonNode(root, keyPath, newValue, new File(jsonPath));
}
if (null != resourceRoot) {
updateJsonNode(resourceRoot, keyPath, newValue, null);
}
return rootUpdated;
} catch (Exception e) {
log.error("更新 config.json 文件失败: {}", e.getMessage());
return false;
}
}
public boolean updateJsonNode(JsonNode jsonNode, String keyPath, Object newValue, File file) {
try {
if (!(jsonNode instanceof ObjectNode)) {
return false;
}
ObjectNode rootNode = (ObjectNode) jsonNode;
String[] keys = keyPath.split("\\.");
ObjectNode currentNode = getObjectNode(rootNode, keys);
String finalKey = keys[keys.length - 1];
setJsonValue(currentNode, finalKey, newValue);
if (null != file) {
MAPPER.writeValue(file, rootNode);
}
return true;
} catch (IOException e) {
log.error("更新 JsonNode 失败: {}", e.getMessage());
return false;
}
}
private ObjectNode getObjectNode(ObjectNode rootNode, String[] keys) {
ObjectNode currentNode = rootNode;
for (int i = 0; i < keys.length - 1; i++) {
String key = keys[i];
if (!currentNode.has(key)) {
currentNode.putObject(key);
}
JsonNode nextNode = currentNode.get(key);
if (!(nextNode instanceof ObjectNode)) {
currentNode.putObject(key);
}
currentNode = (ObjectNode) currentNode.get(key);
}
return currentNode;
}
private void setJsonValue(ObjectNode node, String key, Object value) {
if (value instanceof String) {
node.put(key, (String) value);
} else if (value instanceof Integer) {
node.put(key, (Integer) value);
} else if (value instanceof Long) {
node.put(key, (Long) value);
} else if (value instanceof Double) {
node.put(key, (Double) value);
} else if (value instanceof BigDecimal) {
node.put(key, (BigDecimal) value);
} else if (value instanceof Boolean) {
node.put(key, (Boolean) value);
} else if (value instanceof Map) {
node.set(key, MAPPER.valueToTree(value));
} else if (value instanceof List) {
node.set(key, MAPPER.valueToTree(value));
} else if (value == null) {
node.putNull(key);
} else {
node.set(key, MAPPER.valueToTree(value));
}
}
private static void configureObjectMapper() {
MAPPER.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
SimpleModule module = new SimpleModule();
module.addDeserializer(Date.class, new MultiDateDeserializer());
MAPPER.registerModule(module);
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
MAPPER.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true);
MAPPER.enable(SerializationFeature.INDENT_OUTPUT);
MAPPER.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
}
}
6.3 使用示例
java
// 加载配置
JsonLoader loader = JsonLoader.create();
loader.loadConfig("config/app.json", "default-app.json");
// 读取配置
String appName = loader.getString("app.name");
Integer version = loader.getInteger("app.version");
Boolean debug = loader.getBoolean("app.debug");
String theme = loader.getString("app.theme", "dark");
// 更新配置
loader.updateOrAddKey("app.theme", "light");
loader.updateOrAddKey("window.width", 1200);
loader.updateOrAddKey("window.height", 800);
七、总结
本篇介绍了文件处理工具箱的四个核心类:
| 类 | 核心功能 |
|---|---|
ExportField |
Excel 导出字段配置(别名、列宽、值转换) |
ZipFileUtils |
ZIP 压缩/解压(支持密码加密) |
MetFileUtils |
文件选择、下载、Excel 导出统一封装 |
JsonLoader |
JSON 配置加载(resources + 用户目录双源) |
这些工具类覆盖了桌面应用中最常见的文件操作场景,配合使用可以大幅减少重复代码。
----- 系列完结,代码见各篇文章 -----