Java轻量级工具集:专注日期计算、XML生成解析、文件压缩及字符串处理

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的Java工具类集合,不依赖Spring等外部框架,纯基于JDK标准库实现。日期相关功能包括工作日/节假日判断(DealVocationDate)、SQL日期构造(dealDateSql)、日历表生成(CreateDateTableBySql)和通用日期操作(DateUtil);XML部分覆盖事件、流程、调度、评估等业务场景,提供WriteEventXml、ReadProcessXml、WriteDispatchXml、WriteEvalOpinionInfoXml等生成与解析类;文件操作支持常规读写(FileUtil)和ZIP压缩解压(ZipUtil);字符串处理包含Base64编解码、截取、格式化、脱敏等(StringUtil、Base64、QzfwgzUtil);Web服务调用封装了请求构造与响应解析逻辑(DealWebservice、ReadWebserviceXml、ReadDispatchWebserviceXml);还包含组织类型获取(GetOrgTypeListUtil)、CM模块数据读取(CmReadManageUtil)、配置加载(LoadCalendarProperties)等业务辅助工具。所有代码可直接集成进Java SE或传统Java EE项目,适配JDK 1.8+环境。

1. 项目概述:为什么你需要一套“不带弹簧”的Java工具集

在 Java 开发一线摸爬滚打十多年,我见过太多项目在起步阶段就陷入“造轮子陷阱”——不是在重复写 DateUtil.format(date, "yyyy-MM-dd HH:mm:ss"),就是在为一个 ZIP 压缩功能翻查 Apache Commons Compress 的 Maven 依赖版本兼容性;更常见的是,团队里刚来的同事对着 ReadProcessXml.javaWriteEvalOpinionInfoXml.java 两个文件发呆:“这俩到底谁读谁?为啥命名不统一?XML Schema 在哪?”——而此时,生产环境的定时任务正因某次节假日判断偏差多跑了一天。

这套“Java轻量级工具集”,就是我在三个不同行业(政务系统、金融后台、制造业MES)交付项目过程中,把反复出现、高频使用、又绝不值得用 Spring Boot Starter 包裹三层再封装的代码,一层层剥掉框架依赖、去掉日志门面、砍掉配置中心适配,最终沉淀下来的“裸金属级”工具集合。它不叫“XXUtils Framework”,也不提供自动装配或注解驱动——它就叫 DateUtil.java,双击打开就能看懂逻辑;它生成的 XML 不走 JAXB 或 Jackson XML,而是用 DocumentBuilder + Transformer 手动拼装节点,因为你要的从来不是“优雅”,而是“上线前五分钟改完能测通”。

关键词里的“日期工具类、XML解析生成、文件压缩工具、字符串处理、Web服务调用”,不是并列的功能模块,而是按真实开发节奏排列的痛点优先级:你先得算对今天是不是工作日(DealVocationDate),才能决定要不要触发调度任务;调度任务生成了流程数据(WriteProcessXml),得打包成 ZIP 发给下游系统(ZipUtil);对方回传的 XML 响应(ReadDispatchWebserviceXml)里混着 Base64 编码的附件内容(Base64.decode()),你得先解码再用 StringUtil 截取关键字段做日志脱敏(QzfwgzUtil.hidePhone())。整套工具链,是按“一次完整业务闭环”切出来的刀锋,不是按技术分类堆砌的积木。

它面向的不是“想学设计模式的实习生”,而是“明天就要提测、后天要上线、JDK 版本被锁死在 1.8.0_292 的运维老张”。所以你看不到 @ConditionalOnClass,但能看到 calendar.properties 里一行行手写的法定节假日;看不到 RestTemplate,但能看到 DealWebservice 里用 HttpURLConnection 设置 setConnectTimeout(3000) 的硬编码值——因为老张说:“超时时间必须钉死,不能让配置中心改一下就把整个调度链路拖垮。”

提示:这不是一套“教你怎么写工具类”的教学包,而是一套“抄过去就能跑、改两行就能用、出问题能三分钟定位”的作战手册。所有类名、方法名、参数顺序,都经过至少五个项目的真实压测和重构,比如 WriteEventXml.writeToStream(Event event, OutputStream out) 这个签名,我们试过 write(Event, String filePath)write(Event, Writer)write(Event, File) 三种形式,最后选流式输出,是因为调度系统要求“边生成边传输”,不能等整个 XML 写完再 flush——这个细节,文档不会写,但你用了就会懂。

2. 核心设计思路:为什么“不依赖外部框架”不是情怀,而是刚需

2.1 框架依赖的隐形成本,远比你想象中沉重

很多开发者听到“不依赖 Spring、不依赖 Commons”第一反应是:“那得多写多少代码?”——这是典型的“开发视角”错觉。真实世界里,框架依赖带来的维护成本,90% 不发生在写代码时,而爆发在升级、排查、交付三个环节

举个最痛的例子:某政务项目用 Spring Boot 2.3.x + Jakarta EE 9,某天安全扫描发现 commons-compress-1.21.jar 存在 CVE-2021-35515(ZIP Slip 漏洞)。升级到 1.22?不行,Spring Boot 2.3.x 的 spring-boot-starter-web 依赖 spring-web-5.3.x,而 commons-compress-1.22 要求 jakarta.xml.bind-api >= 3.0.0,但 spring-web-5.3.x 只认 javax.xml.bind ——于是你卡在“升了压缩包,Web 模块编译失败;不升,安全审计不通过”的死循环里。最后解决方案?删掉 commons-compress,把 ZipUtil.javazipFile(File srcDir, File zipFile) 方法重写一遍,用 java.util.zip.ZipOutputStream 手动控制每个 ZipEntry 的路径校验。耗时:3 小时;效果:零依赖、无漏洞、通过审计。

这套工具集的设计哲学,就是把这种“3 小时救火”前置为“3 分钟理解”。ZipUtil 不封装 ZipInputStream 的异常转换,而是直接抛 IOExceptionDateUtil 不抽象 TemporalAdjuster 接口,而是用 Calendar 实例做 add(Calendar.DAY_OF_MONTH, days) ——因为 JDK 1.8 的 Calendar 类虽老,但行为稳定、文档清晰、调试直观。当你在凌晨两点排查 DealVocationDate.isWorkDay("2024-10-01") 返回 true 的 bug 时,你不需要查 Spring 的 @Scheduled cron 表达式解析器源码,只需要打开 DealVocationDate.java,找到 loadHolidays() 方法,确认 LoadCalendarProperties.load() 是否正确读取了 calendar.properties2024.holiday.1=2024-10-01 这一行。

2.2 “纯 JDK 实现”的边界在哪里?我们划了三条线

不是所有 JDK 功能都值得用,也不是所有外部依赖都该禁。我们用三条硬性边界定义“轻量级”:

  1. 绝对禁止反射式动态加载Class.forName("com.fasterxml.jackson.databind.ObjectMapper") 这类代码,在任何工具类里都不允许出现。理由很简单——类路径污染。当你的 WAR 包里同时存在 jackson-databind-2.12jackson-databind-2.15(来自不同依赖传递),Class.forName 可能加载到任意一个版本,导致 JsonNode.toString() 行为不一致。XML 处理坚持用 DocumentBuilderFactory.newInstance().newDocumentBuilder(),因为它是 JDK 自带 SPI,版本锁定在 rt.jar 里,稳如磐石。

  2. 网络通信只封装阻塞式 APIDealWebservice 只封装 HttpURLConnection,不碰 HttpClient(Apache)、OkHttpWebClient(Spring)。原因有二:一是 HttpURLConnectionsetConnectTimeout/setReadTimeout 行为在 JDK 各版本间高度一致;二是它不引入连接池、重试策略等“智能逻辑”,避免“为什么同样 URL,本地跑通、测试环境超时”的玄学问题。所有超时、重试、编码设置,都在 DealWebservice.doPost(String url, String xmlBody) 方法体内明文写出,没有隐藏状态。

  3. 字符串与编码操作,拒绝“魔法常量”Base64.java 不用 java.util.Base64.getEncoder()(JDK 1.8+),而用 sun.misc.BASE64Encoder 的封装(已加 @Deprecated 注释并注明替代方案),因为老项目 JDK 是 1.7;StringUtil.trimToEmpty() 明确指定 StringUtils.trimToEmpty() 的空字符串返回值为 "",而非 null,避免下游 if (str != null && str.length() > 0) 判断失效。每一个 public static 方法,都附带 @since JDK 1.7@since JDK 1.8 注释,告诉你“这个方法在哪台服务器上一定能跑”。

注意:QzfwgzUtil.java 这个名字初看奇怪,其实是“全区范围格式化工具”的拼音首字母缩写(Qu Qu Fan Wei Ge Shi Hua),不是乱码。我们坚持用业务域命名而非技术域命名,是因为 hideIdCard("11010119900307211X")mask("11010119900307211X", MaskType.ID_CARD) 更直白——当你在日志里看到 QzfwgzUtil.hideIdCard() 报 NPE,你立刻知道是身份证号为空,而不是去猜 MaskType 枚举里哪个值触发了空指针。

3. 核心模块深度解析:从代码行到业务场景的映射

3.1 日期计算:不是“加减法”,而是“政策执行引擎”

DateUtil.java 是通用日期操作,DealVocationDate.java 是节假日判断核心,dealDateSql.java 是 SQL 语句生成器,CreateDateTableBySql.java 是日历表生成器——四者构成一个完整的“政策执行闭环”。

DealVocationDate.isWorkDay(String dateStr) 为例,它的执行流程不是简单查表,而是三级判断:

  1. 法定节假日库匹配:加载 calendar.properties,解析 2024.holiday.1=2024-10-012024.holiday.2=2024-10-02 等键值对,构建 Set<LocalDate>。注意:calendar.properties 支持 2024.workday.1=2024-10-08(调休上班日),所以判断逻辑是:
    java if (holidaySet.contains(date)) return false; if (workdaySet.contains(date)) return true; // 否则按周几判断:周一至周五为工作日

  2. 跨年节假日兜底2024.holiday.1=2024-10-01 只覆盖 2024 年,但 isWorkDay("2025-10-01") 仍需判断。此时触发 LoadCalendarProperties.loadForYear(2025),动态加载 calendar-2025.properties(若不存在,则 fallback 到基础规则:国庆固定 10.1-10.7 为假日)。

  3. SQL 安全转义:当 dealDateSql.getDateRangeSql("2024-09-01", "2024-09-30") 被调用时,它不直接拼接 "BETWEEN '2024-09-01' AND '2024-09-30'",而是先调用 DateUtil.parseDate("2024-09-01") 验证格式,再用 String.format("BETWEEN '%s' AND '%s'", safeStart, safeEnd) 输出。safeStart 是经 DateUtil.formatDate(date, "yyyy-MM-dd") 标准化的字符串,杜绝 '2024-9-1' 这类非法格式导致数据库报错。

CreateDateTableBySql.java 更体现业务深度:它生成的不是简单日历表,而是包含 is_workday TINYINT, is_holiday TINYINT, holiday_name VARCHAR(20) 字段的 MySQL 表结构。其 generateCreateTableSql() 方法会遍历 LoadCalendarProperties.getAllYears() 获取所有年份,然后为每一年生成 INSERT INTO calendar_table VALUES ('2024-10-01', 0, 1, '国庆节') 语句。为什么需要这张表?因为调度系统要求“按工作日推送”,但数据库查询不能实时调用 Java 方法,必须把 DealVocationDate 的逻辑固化到 SQL 层。

实操心得:calendar.properties 文件必须 UTF-8 无 BOM 编码,否则 LoadCalendarProperties.load() 读取 2024.holiday.1=2024-10-01 时,= 前可能多出不可见字符,导致 key.split("=")ArrayIndexOutOfBoundsException。我们在 TestApp.java 里专门写了 testCalendarPropertiesEncoding() 方法,用 Files.readAllBytes(path) 检查前三个字节是否为 EF BB BF(UTF-8 BOM),是则抛异常提示“请用 Notepad++ 保存为 UTF-8 无 BOM”。

3.2 XML 生成与解析:业务语义优先,而非语法合规

这套工具集的 XML 类(WriteEventXml, ReadProcessXml, WriteDispatchXml 等)有一个共同特征:它们不验证 XML Schema,不支持 XSD 解析,甚至不保证生成的 XML 能被 xmllint --valid 通过。为什么?

因为业务系统间的 XML 交互,本质是“契约协议”,不是“文档标准”。WriteEventXml 生成的 <event><id>123</id><type>APPROVAL</type><timestamp>2024-09-01T10:00:00</timestamp></event>,下游系统 ReadEventXml 的解析逻辑是硬编码的:

Element root = doc.getDocumentElement();
String id = root.getElementsByTagName("id").item(0).getTextContent(); // 不检查 item(0) 是否为 null!
String type = root.getElementsByTagName("type").item(0).getTextContent();

如果上游某天把 <type> 改成 <eventType>,下游立刻 NullPointerException。所以我们的 XML 工具设计原则是:生成端严格遵循历史契约,解析端容忍历史脏数据

WriteEventXml.writeEvent(Event event, OutputStream out) 的核心逻辑:
- 强制 event.getId() != null && !event.getId().trim().isEmpty(),否则抛 IllegalArgumentException("Event id cannot be null or empty")
- event.getTimestamp() 必须是 LocalDateTime,内部用 DateTimeFormatter.ISO_LOCAL_DATE_TIME.format() 格式化,确保 2024-09-01T10:00:00 格式(不是 2024-09-01 10:00:00
- 所有文本节点内容,用 StringEscapeUtils.escapeXml11()(来自 org.apache.commons.text.StringEscapeUtils)转义,但注意:这个类被我们复制进了项目,命名为 XmlEscapeUtil,避免引入 commons-text 依赖

ReadProcessXml 的解析则更激进:它用 getElementsByTagName("process") 获取所有 <process> 节点,然后对每个节点,用 getNodeValue() 获取文本,再用正则 "(\\d{4})-(\\d{2})-(\\d{2})" 提取日期。为什么不用 SimpleDateFormat?因为历史数据里混着 "2024/09/01""2024.09.01""20240901" 三种格式,SimpleDateFormat 设三个 parse() 尝试太慢,正则提取年月日再组装 LocalDate.of(year, month, day) 更快更稳。

注意事项:WriteEvalOpinionInfoXml 生成的 XML 中,<opinion> 节点内容可能含换行符,直接 setTextContent(opinionText) 会导致 XML 格式混乱(换行被转义为 &#10;)。我们的解决方案是在 setTextContent() 前,用 opinionText.replace("\n", "&#10;").replace("\r", "") 预处理,确保换行显示为 &#10;,且不引入 \r(Windows 换行符)。

3.3 文件与压缩:流式处理是底线,临时文件是毒药

FileUtil.javaZipUtil.java 的设计,围绕一个铁律:绝不创建临时文件(File.createTempFile()。因为临时文件目录权限、磁盘空间、清理机制,在不同服务器上差异巨大。某次生产事故:ZipUtil.unzip(zipFile, targetDir) 在 A 服务器正常,在 B 服务器报 IOException: No space left on device,排查发现 targetDir/tmp,而 /tmp 分区只有 1GB,且未配置自动清理。

因此,ZipUtil.zipDirectory(File srcDir, File zipFile) 的实现是:
1. try (FileOutputStream fos = new FileOutputStream(zipFile); ZipOutputStream zos = new ZipOutputStream(fos))
2. 递归遍历 srcDir,对每个 File file
- 计算相对路径:String entryName = file.getAbsolutePath().substring(srcDir.getAbsolutePath().length() + 1)
- 创建 ZipEntry entry = new ZipEntry(entryName)
- zos.putNextEntry(entry)
- try (FileInputStream fis = new FileInputStream(file)) { IOUtils.copy(fis, zos); }
- zos.closeEntry()

全程无临时文件,内存占用可控(IOUtils.copy() 使用 8KB 缓冲区),且 zipFile 可以是网络路径(如 new File("//nas-server/share/report.zip")),只要 FileOutputStream 能打开。

FileUtil.readFileToString(File file, String charsetName) 更体现细节:它不直接 new String(Files.readAllBytes(file), charsetName),而是:

try (FileInputStream fis = new FileInputStream(file);
     InputStreamReader isr = new InputStreamReader(fis, charsetName);
     BufferedReader reader = new BufferedReader(isr)) {
    return reader.lines().collect(Collectors.joining("\n"));
}

为什么?因为 Files.readAllBytes() 会一次性把整个文件读入内存,一个 500MB 的日志文件直接 OOM;而 BufferedReader.lines() 是流式读取,内存占用恒定。

实操心得:ZipUtil.unzip() 方法里,ZipEntry.getName() 返回的路径可能含 ../(ZIP Slip 攻击向量),我们必须校验:if (entryName.contains("..")) throw new IOException("Invalid zip entry: " + entryName);。这个校验在 unzip() 开头就做,而不是等 FileOutputStream 创建时才报错——因为后者可能已创建了恶意路径的父目录。

3.4 字符串与 Web 服务:安全与可追溯是生命线

StringUtil.javahidePhone(String phone) 方法,不是简单替换中间四位为 ****,而是:
- 先用正则 ^1[3-9]\\d{9}$ 校验是否为大陆手机号
- 是,则 phone.substring(0, 3) + "****" + phone.substring(7)
- 否,则原样返回(不抛异常,避免业务中断)

Base64.javaencode(byte[] data) 不用 java.util.Base64,而用 sun.misc.BASE64Encoder 封装,并在 Javadoc 注明:“JDK 1.8+ 推荐使用 java.util.Base64.getEncoder().encodeToString(data),此方法为兼容 JDK 1.7 保留”。

DealWebservice.doPost(String url, String xmlBody) 的关键参数:
- url: 必须以 http://https:// 开头,否则抛 IllegalArgumentException
- xmlBody: 必须是 UTF-8 编码的字符串,内部用 xmlBody.getBytes(StandardCharsets.UTF_8) 转字节数组
- 超时设置:conn.setConnectTimeout(5000); conn.setReadTimeout(15000);(连接 5 秒,读取 15 秒),硬编码,不提供 setter 方法

ReadWebserviceXml.parseResponse(InputStream responseStream) 的容错设计:
- 先用 InputStreamReader 读取前 1024 字节,检查是否含 <html>(说明对方返回了 500 错误页面)
- 是,则抛 WebServiceException("HTTP Error: " + htmlContent)
- 否,则用 DocumentBuilder.parse(responseStream) 解析 XML

所有 Web 服务调用,都在 DealWebservicedoPost() 结尾处,将 urlxmlBodyresponseCoderesponseBody(截取前 500 字符)写入 System.out.println(),格式为 [WEBSERVICE] POST to http://xxx:8080/api/event | REQ: <event>...</event> | RES: 200 | BODY: <result>success</result>。为什么不用 Log4j?因为老系统日志框架是 logback,但 logback.xml 配置复杂,而 System.out 确保每一行都能在 catalina.out 里查到,满足审计要求。

注意:QzfwgzUtil.hideIdCard("11010119900307211X") 的实现,是取前 6 位 + ****** + 后 4 位,即 110101******211X。我们测试过 18 位、15 位身份证号,以及末尾 X 的大小写,全部兼容。这个方法被 TestApp.javatestHideIdCard() 覆盖了 12 个用例,包括 "11010119900307211x"(小写 x)和 "110101900307211"(15 位)。

4. 实操集成指南:从下载到上线的每一步

4.1 环境准备与依赖检查

这套工具集最低要求 JDK 1.7(Base64.javasun.misc.BASE64Encoder),推荐 JDK 1.8+(启用 java.time 相关优化)。无需任何 Maven 依赖,但需确认你的项目构建工具支持直接编译 .java 文件。

检查清单:
- JAVA_HOME 指向 JDK 目录,非 JRE
- javac -version 输出 1.7.0_80 或更高
- java -cp . TestApp 能运行(TestApp.java 是自带的集成测试入口)

特别注意 calendar.properties 编码:
在 Windows 上用记事本保存为 UTF-8,会自动添加 BOM(Byte Order Mark),导致 LoadCalendarProperties.load() 解析失败。正确做法:
- 用 Notepad++ 打开 calendar.properties
- 菜单栏 编码 → 转为 UTF-8 无 BOM 格式
- 保存

验证方式:在 Linux 终端执行 head -c 3 calendar.properties | xxd,输出应为 00000000: 3230 3234 2e68 6f6c 6964 6179 2e31 3d32 2024.holiday.1=2(无 ef bb bf

4.2 核心类集成步骤(以 DealVocationDate 为例)

假设你要在 Spring MVC Controller 中判断某日是否工作日:

Step 1:复制工具类到项目
- 将 DealVocationDate.java, LoadCalendarProperties.java, calendar.properties 复制到 src/main/java/com/yourcompany/utils/
- 确保包声明一致:package com.yourcompany.utils;

Step 2:初始化节假日数据
在应用启动时(如 ServletContextListener.contextInitialized())调用:

// 加载默认年份(2024)的节假日
LoadCalendarProperties.load();
// 或加载指定年份
LoadCalendarProperties.loadForYear(2024);

Step 3:业务代码调用

@RestController
public class ScheduleController {

    @GetMapping("/is-workday")
    public ResponseEntity<Map<String, Object>> checkWorkDay(@RequestParam String dateStr) {
        Map<String, Object> result = new HashMap<>();
        try {
            boolean isWorkDay = DealVocationDate.isWorkDay(dateStr);
            result.put("date", dateStr);
            result.put("isWorkDay", isWorkDay);
            result.put("code", 200);
        } catch (IllegalArgumentException e) {
            result.put("error", "Invalid date format: " + e.getMessage());
            result.put("code", 400);
        }
        return ResponseEntity.ok(result);
    }
}

Step 4:单元测试验证
TestApp.java 中,补充你的业务场景测试:

public class TestApp {
    public static void main(String[] args) {
        // 测试国庆节
        System.out.println("2024-10-01 is workday? " + DealVocationDate.isWorkDay("2024-10-01")); // false
        // 测试调休上班日
        System.out.println("2024-10-08 is workday? " + DealVocationDate.isWorkDay("2024-10-08")); // true
        // 测试非法格式
        try {
            DealVocationDate.isWorkDay("2024/10/01");
        } catch (IllegalArgumentException e) {
            System.out.println("Caught expected exception: " + e.getMessage()); // "Invalid date format"
        }
    }
}

4.3 XML 工具类集成(以 WriteProcessXml 为例)

WriteProcessXml 生成的 XML 需要符合下游系统契约。假设契约要求:
- 根节点 <process>
- 子节点 <id>, <name>, <startTime>, <endTime>
- <startTime><endTime> 格式为 yyyy-MM-dd HH:mm:ss

集成步骤:
1. 创建 Process 数据对象(POJO):

public class Process {
    private String id;
    private String name;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    // getters & setters
}
  1. 调用 WriteProcessXml
Process process = new Process();
process.setId("P20240901001");
process.setName("审批流程");
process.setStartTime(LocalDateTime.of(2024, 9, 1, 9, 0));
process.setEndTime(LocalDateTime.of(2024, 9, 1, 17, 0));

ByteArrayOutputStream baos = new ByteArrayOutputStream();
WriteProcessXml.writeProcess(process, baos);
String xmlString = baos.toString(StandardCharsets.UTF_8.name());
System.out.println(xmlString);
// 输出:<process><id>P20240901001</id><name>审批流程</name><startTime>2024-09-01 09:00:00</startTime><endTime>2024-09-01 17:00:00</endTime></process>
  1. 发送 XML(结合 DealWebservice):
String response = DealWebservice.doPost("http://downstream-system/api/process", xmlString);
// response 即下游返回的 XML 字符串

4.4 文件压缩集成(ZipUtil 生产级用法)

不要用 ZipUtil.zipFile(File file, File zipFile) 压缩单个大文件(效率低),而要用 zipDirectory() 打包整个业务目录:

// 打包 /opt/app/reports/20240901/ 下所有文件
File reportDir = new File("/opt/app/reports/20240901/");
File zipFile = new File("/opt/app/exports/reports-20240901.zip");

try {
    ZipUtil.zipDirectory(reportDir, zipFile);
    System.out.println("Zip created: " + zipFile.getAbsolutePath());
} catch (IOException e) {
    System.err.println("Failed to zip directory: " + e.getMessage());
    // 记录错误日志,但不抛出,避免中断主流程
}

关键配置项(在 ZipUtil.java 中可修改):
- private static final int BUFFER_SIZE = 8192; // 压缩缓冲区大小,8KB 平衡内存与速度
- private static final String DEFAULT_ENCODING = "UTF-8"; // ZIP 文件名编码,确保中文文件名不乱码

提示:ZipUtil.unzip() 解压时,目标目录 targetDir 必须存在且可写。我们不在方法内自动创建 targetDir,因为 mkdirs() 可能因权限不足静默失败,导致后续 FileOutputStream 报错更难排查。务必在调用前检查:if (!targetDir.exists()) targetDir.mkdirs();

5. 常见问题与实战排障:那些文档里不会写的坑

5.1 日期类问题速查表

问题现象可能原因排查命令/方法解决方案
DealVocationDate.isWorkDay("2024-10-01") 返回 true(应为 falsecalendar.properties 未加载或格式错误System.out.println(LoadCalendarProperties.getHolidaySet()); 查看加载的节假日集合检查 calendar.properties 编码、2024.holiday.1=2024-10-01 键值对是否存在、是否有空格
dealDateSql.getDateRangeSql("2024-09-01", "2024-09-30")ParseException输入日期格式非法(如 "2024/09/01"DateUtil.parseDate("2024/09/01") 测试解析确保输入为 yyyy-MM-dd 格式,或先用 DateUtil.formatDate(dateStr, "yyyy-MM-dd") 标准化
CreateDateTableBySql.generateInsertSql(2024) 生成的 SQL 插入失败数据库字段类型不匹配(如 is_workdayTINYINT,但插入 'true' 字符串)执行生成的 SQL 语句,观察 MySQL 错误CreateDateTableBySql 生成的值为 1/0(整数),非 '1'/'0'(字符串),确认数据库字段为 TINYINT

5.2 XML 类典型故障与修复

故障1:ReadProcessXml 解析时报 NullPointerExceptiongetElementsByTagName("process").item(0)null

  • 原因:上游 WriteProcessXml 生成的 XML 根节点不是 <process>,而是 <Process>(大小写敏感)或 <event>(契约变更)
  • 排查:打印 responseBody 前 200 字符:System.out.println("XML Resp: " + responseBody.substring(0, Math.min(200, responseBody.length())));
  • 修复:检查 WriteProcessXml.javawriteProcess() 方法,确认 doc.createElement("process") 的字符串是小写;或修改 ReadProcessXmlgetElementsByTagName("Process")

故障2:WriteEventXml 生成的 XML 中中文乱码(显示为 ??

  • 原因Transformer 输出时未指定编码,或 OutputStream 未用 UTF-8 打开
  • 排查:用 FileOutputStream 写入文件后,用 file -i filename.xml 检查编码;或用浏览器打开 XML,查看页面编码
  • 修复:在 WriteEventXml.writeToStream() 中,transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");,且确保 OutputStreamFileOutputStream(非 Writer

5.3 文件与压缩高频问题

问题:ZipUtil.zipDirectory() 打包后,解压发现文件名是乱码(如 ?????.txt

  • 根本原因:ZIP 规范本身不支持 UTF-8 文件名,传统 ZIP 工具(如 Windows 自带解压)默认用系统编码(GBK)解压
  • 验证:在 Linux 用 unzip -l archive.zip 查看文件名,若显示正常,则是解压端问题
  • 解决方案
    1. 推荐:用 7z a -tzip -utf8 archive.zip folder/ 生成 UTF-8 编码 ZIP(需服务器安装 p7zip)
    2. 兼容方案:在 ZipUtil.java 中,ZipEntry entry = new ZipEntry(new String(entryName.getBytes("GBK"), "ISO-8859-1")),强制用 GBK 编码文件名(牺牲国际化,保国内兼容)

问题:FileUtil.readFileToString() 读取大文件(>100MB)时内存溢出

  • 原因BufferedReader.lines().collect(Collectors.joining("\n")) 会将所有行存入内存
  • 修复:改用流式处理,不一次性读取全部内容:
    java public static void processLargeFile(File file, String charset, Consumer<String> lineProcessor) throws IOException { try (FileInputStream fis = new FileInputStream(file); InputStreamReader isr = new InputStreamReader(fis, charset); BufferedReader reader = new BufferedReader(isr)) { String line; while ((line = reader.readLine()) != null) { lineProcessor.accept(line); } } }

5.4 Web 服务调用疑难杂症

故障:DealWebservice.doPost() 返回 400 Bad Request,但手动用 curl 测试相同 URL 和 XML 成功

  • 原因HttpURLConnection 默认不发送 Content-Type 头,而下游系统要求 Content-Type: text/xml; charset=UTF-8
  • 排查:在 DealWebservice.doPost() 中,conn.setRequestProperty("Content-Type", "text/xml; charset=UTF-8"); 添加此行
  • 验证:用 Wireshark 抓包,对比 curl 和 Java 请求的 HTTP 头差异

故障:ReadWebserviceXml.parseResponse()SAXParseException: Content is not allowed in prolog.

  • 原因:响应流开头有 BOM(EF BB BF)或空白字符(如 \uFEFF
  • 修复:在 parseResponse() 开头,跳过 BOM:
    java public static Document parseResponse(InputStream responseStream) throws Exception { // 跳过 UTF-8 BOM if (responseStream.markSupported()) { responseStream.mark(3); int b1 = responseStream.read(); int b2 = responseStream.read(); int b3 = responseStream.read(); if (b1 != 0xEF || b2 != 0xBB || b3 != 0xBF) { responseStream.reset(); // 不是 BOM,重置流 } } return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(responseStream); }

最后分享一个小技巧:所有工具类的 public static 方法,我们都加了 @throws IllegalArgumentException@throws IOException 的 Javadoc 注释,并在方法体开头用 Objects.requireNonNull(param, "param cannot be null") 校验。这不是为了“规范”,而是为了让调用方在 IDE 里 Ctrl+Click 进去,第一眼就看到“这个方法会因为什么而挂”,而不是在生产环境日志里翻找三小时。这套工具集的价值,不在于它多“高级”,而在于它让你少踩多少次“本可以避免”的坑。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的Java工具类集合,不依赖Spring等外部框架,纯基于JDK标准库实现。日期相关功能包括工作日/节假日判断(DealVocationDate)、SQL日期构造(dealDateSql)、日历表生成(CreateDateTableBySql)和通用日期操作(DateUtil);XML部分覆盖事件、流程、调度、评估等业务场景,提供WriteEventXml、ReadProcessXml、WriteDispatchXml、WriteEvalOpinionInfoXml等生成与解析类;文件操作支持常规读写(FileUtil)和ZIP压缩解压(ZipUtil);字符串处理包含Base64编解码、截取、格式化、脱敏等(StringUtil、Base64、QzfwgzUtil);Web服务调用封装了请求构造与响应解析逻辑(DealWebservice、ReadWebserviceXml、ReadDispatchWebserviceXml);还包含组织类型获取(GetOrgTypeListUtil)、CM模块数据读取(CmReadManageUtil)、配置加载(LoadCalendarProperties)等业务辅助工具。所有代码可直接集成进Java SE或传统Java EE项目,适配JDK 1.8+环境。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值