Java Swing 自定义组件库分享(十六)

文件处理工具箱

一、背景

在桌面应用中,文件处理是几乎每个项目都会遇到的需求:

  • 用户需要选择文件或目录
  • 数据需要导出为 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 + 用户目录双源)

这些工具类覆盖了桌面应用中最常见的文件操作场景,配合使用可以大幅减少重复代码。

----- 系列完结,代码见各篇文章 -----

相关推荐
沙盘客1 小时前
AFSIM 14篇 C++ 插件开发:扩展你的仿真能力
c++·后端
桦说编程1 小时前
如何对待中断异常:一个被吞掉的 InterruptedException 引发的思考
后端
赵广陆2 小时前
企业实战:Markdown图片检索
android·java·开发语言
C++、Java和Python的菜鸟2 小时前
第3章 从0开始用若依
java·开发语言
蒸蒸yyyyzwd3 小时前
cpp选手秋招准备 学习笔记day7 webserver
笔记·学习
Json____3 小时前
五金制品行业-企业官网源码
java·大数据·数据库·企业站·wwwoop.com
雪隐3 小时前
个人电脑玩AI-16让5060 Ti给你打工——5060Ti 16G 跑 MiniMax-Music-3:从下载到 60s 出歌的全流程
前端·人工智能·后端
报错小能手3 小时前
Go 语言结构 基础语法
开发语言·后端·golang
暗黑小白4 小时前
参数从哪来、何时来 —— 提取时机与平台化 Slot 管理
人工智能·后端·大模型·ai agent