简介:一套开箱即用的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.java 和 WriteEvalOpinionInfoXml.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.java 里 zipFile(File srcDir, File zipFile) 方法重写一遍,用 java.util.zip.ZipOutputStream 手动控制每个 ZipEntry 的路径校验。耗时:3 小时;效果:零依赖、无漏洞、通过审计。
这套工具集的设计哲学,就是把这种“3 小时救火”前置为“3 分钟理解”。ZipUtil 不封装 ZipInputStream 的异常转换,而是直接抛 IOException;DateUtil 不抽象 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.properties 中 2024.holiday.1=2024-10-01 这一行。
2.2 “纯 JDK 实现”的边界在哪里?我们划了三条线
不是所有 JDK 功能都值得用,也不是所有外部依赖都该禁。我们用三条硬性边界定义“轻量级”:
-
绝对禁止反射式动态加载:
Class.forName("com.fasterxml.jackson.databind.ObjectMapper")这类代码,在任何工具类里都不允许出现。理由很简单——类路径污染。当你的 WAR 包里同时存在jackson-databind-2.12和jackson-databind-2.15(来自不同依赖传递),Class.forName可能加载到任意一个版本,导致JsonNode.toString()行为不一致。XML 处理坚持用DocumentBuilderFactory.newInstance().newDocumentBuilder(),因为它是 JDK 自带 SPI,版本锁定在rt.jar里,稳如磐石。 -
网络通信只封装阻塞式 API:
DealWebservice只封装HttpURLConnection,不碰HttpClient(Apache)、OkHttp或WebClient(Spring)。原因有二:一是HttpURLConnection的setConnectTimeout/setReadTimeout行为在 JDK 各版本间高度一致;二是它不引入连接池、重试策略等“智能逻辑”,避免“为什么同样 URL,本地跑通、测试环境超时”的玄学问题。所有超时、重试、编码设置,都在DealWebservice.doPost(String url, String xmlBody)方法体内明文写出,没有隐藏状态。 -
字符串与编码操作,拒绝“魔法常量”:
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) 为例,它的执行流程不是简单查表,而是三级判断:
-
法定节假日库匹配:加载
calendar.properties,解析2024.holiday.1=2024-10-01、2024.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; // 否则按周几判断:周一至周五为工作日 -
跨年节假日兜底:
2024.holiday.1=2024-10-01只覆盖 2024 年,但isWorkDay("2025-10-01")仍需判断。此时触发LoadCalendarProperties.loadForYear(2025),动态加载calendar-2025.properties(若不存在,则 fallback 到基础规则:国庆固定 10.1-10.7 为假日)。 -
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 格式混乱(换行被转义为 )。我们的解决方案是在setTextContent()前,用opinionText.replace("\n", " ").replace("\r", "")预处理,确保换行显示为 ,且不引入\r(Windows 换行符)。
3.3 文件与压缩:流式处理是底线,临时文件是毒药
FileUtil.java 和 ZipUtil.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.java 的 hidePhone(String phone) 方法,不是简单替换中间四位为 ****,而是:
- 先用正则 ^1[3-9]\\d{9}$ 校验是否为大陆手机号
- 是,则 phone.substring(0, 3) + "****" + phone.substring(7)
- 否,则原样返回(不抛异常,避免业务中断)
Base64.java 的 encode(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 服务调用,都在 DealWebservice 的 doPost() 结尾处,将 url、xmlBody、responseCode、responseBody(截取前 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.java的testHideIdCard()覆盖了 12 个用例,包括"11010119900307211x"(小写 x)和"110101900307211"(15 位)。
4. 实操集成指南:从下载到上线的每一步
4.1 环境准备与依赖检查
这套工具集最低要求 JDK 1.7(Base64.java 用 sun.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
}
- 调用
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>
- 发送 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(应为 false) | calendar.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_workday 是 TINYINT,但插入 'true' 字符串) | 执行生成的 SQL 语句,观察 MySQL 错误 | CreateDateTableBySql 生成的值为 1/0(整数),非 '1'/'0'(字符串),确认数据库字段为 TINYINT |
5.2 XML 类典型故障与修复
故障1:ReadProcessXml 解析时报 NullPointerException,getElementsByTagName("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.java的writeProcess()方法,确认doc.createElement("process")的字符串是小写;或修改ReadProcessXml的getElementsByTagName("Process")
故障2:WriteEventXml 生成的 XML 中中文乱码(显示为 ??)
- 原因:
Transformer输出时未指定编码,或OutputStream未用 UTF-8 打开 - 排查:用
FileOutputStream写入文件后,用file -i filename.xml检查编码;或用浏览器打开 XML,查看页面编码 - 修复:在
WriteEventXml.writeToStream()中,transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");,且确保OutputStream是FileOutputStream(非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 进去,第一眼就看到“这个方法会因为什么而挂”,而不是在生产环境日志里翻找三小时。这套工具集的价值,不在于它多“高级”,而在于它让你少踩多少次“本可以避免”的坑。
简介:一套开箱即用的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+环境。

2225

被折叠的 条评论
为什么被折叠?



