ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Java节假日API工具包:告别写死清单,自动更新调休与放假数据

Java节假日API工具包:告别写死清单,自动更新调休与放假数据 简介一份基于Java实现的中国法定节假日查询API源码包面向需要快速集成节假日判断、工作日计算或排班系统的后端开发者。项目通过定时任务自动更新节假日数据并持久化到数据库降低了对第三方公共接口的持续依赖对外只暴露一个简洁接口支持按年、按月、按日返回对应的节假日详情调用方便、响应高效。压缩包体积仅102KB共包含25个文件其中12个Java文件覆盖接口层、服务层与更新逻辑3个SQL脚本提供建表及初始化数据2个XML和2个properties文件完成相关框架配置另有说明文档和许可证便于阅读与合规使用。该资源已有7532人学习浏览开源免费个人维护但结构完整。学习后可获得自动同步数据的调度实现、数据库表设计、接口封装方法以及可直接运行的示例工程尤其适合在中小企业或个人项目中快速落地节假日判断功能。1. 中国法定节假日 API别再写死节假日清单了这个 Java 工具包帮你自动更新很多做排班、运营日历、薪资计算的同学应该都有过这样的经历每年年底拿到下一年的放假安排第一件事就是打开 Excel 手工录入节假日、调休和补班日期。这种写死清单的方式不仅容易漏而且一旦官方调整比如临时补假又要重新改代码。我最近拆了一个名为 holiday 的 Java 工具包它的定位就是「获取中国法定节假日 API自动更新」。核心思路是把节假日数据从官方公告中解析出来封装成 REST API同时保留手动覆盖和离线兜底的机制。对于 Java 后端开发者、做企业应用集成的人这个资源能省掉你维护日历数据的一堆麻烦。2. 把节假日做成接口数据从哪来自动更新怎么跑2.1 数据源与更新机制官方公告的解析与转换这个工具包的核心数据来源是官方每年发布的节假日安排公告。它本质上是一个 PDF 或者网页里面写了「元旦、春节、清明节…」的休假日期和调休上班日期。直接拿公告里的文字去判断是否放假经常会出现格式不统一、标点混乱的问题所以工具包的做法是先人工维护一份「年份 → 解析规则」的映射再通过定时任务去抓取转换成统一的结构化数据。自动更新跑起来是这样一个流程启动时检查本地数据版本如果本地数据已经覆盖当前年份就不请求外部源如果发现新的一年安排已经发布就触发一次更新。更新方式不是实时轮询而是用定时任务比如每周一凌晨执行一次。因为节假日安排一年才变几次过度频繁的请求没有任何意义反而容易把自己变成耗资源的黑匣子。我一般会在项目里这样用Scheduled(cron 0 0 2 * * MON) // 每周一凌晨2点检查一次 public void autoRefreshHolidayData() { boolean updated holidayDataRefresher.refreshIfNeeded(); if (updated) { log.info(节假日数据已更新当前版本{}, holidayDataRefresher.getVersion()); } else { log.debug(节假日数据未变化跳过本次更新); } }这段代码的逻辑很直白refreshIfNeeded()内部会先比较远端版本号和本地版本号只有版本号不一致才拉取新的数据拉取成功后写入本地存储文件或数据库然后更新内存中的缓存。cron表达式里的0 0 2 * * MON表示每周一凌晨 2 点执行选择凌晨是为了避开业务高峰也避免和运维的定时任务撞车。如果你希望更激进可以改成0 0 */3 * * *每三小时一次但我不建议这么做因为节假日数据源并不会有这么高的更新频率。这里有个容易忽略的细节版本号不能只依赖文件修改时间因为某些文件系统的时间戳精度不够而且如果外部源把同一版本的数据重新发布修改时间会变但内容没变。工具包里用的是「年.批次」格式比如2025.1表示 2025 年的第一批数据每次解析成功后会生成version字段存进数据文件比对时只认这个字段不认时间戳。这算是被坑过才补上的设计之前我就是因为时间戳不一致每周都触发一次全量拉取白白浪费流量。2.2 核心模型节假日、调休日、工作日怎么表达节假日数据光有「是不是放假」是不够的还要区分「正常休息的法定节假日」和「调休补班的周末」。工具包里用了一个很简单的模型public class Holiday { private LocalDate date; // 具体日期 private int type; // 0工作日, 1法定节假日, 2调休上班日 private String name; // 节日名称如“春节”、“国庆节” private String description; // 原始公告描述留作排查 public boolean isHoliday() { return this.type 1; } public boolean isWorkday() { return this.type 0 || this.type 2; } }type字段是整个模型的灵魂。很多刚接手的开发者以为只要标记isHoliday布尔值就够了结果在计算「今天是否上班」时把调休上班日搞反了。注意type 2表示虽然是周末但需要上班这种情况在放假安排里非常常见春节前一周的周日国庆后的周六。所以判断上班日必须把type 2也加进去否则你的薪资系统和考勤系统就会在调休日翻车。举个例子2025 年春节前的那个周日在很多年份里都是调休上班日。如果你用isWeekend()去判断休息那天的数据就被判成了休息日考勤系统就会认为员工没有缺勤实际上大部分人都在上班绩效算错工资也算错。所以我在代码评审时看到有人直接写if (date.getDayOfWeek() DayOfWeek.SATURDAY || date.getDayOfWeek() DayOfWeek.SUNDAY)就会特别谨慎必须让他加上节假日类型的判断。2.3 一个最小可运行的查询示例最简单的用法是直接调用工具包的查询接口不用自己搭 HTTP 服务直接在代码里嵌入HolidayService holidayService new HolidayService(); LocalDate date LocalDate.of(2025, 10, 1); OptionalHoliday holiday holidayService.find(date); if (holiday.isPresent()) { System.out.println(holiday.get().getName() 类型 holiday.get().getType()); } else { System.out.println(未配置的日期默认按工作日处理); }这里HolidayService是工具包的核心门面它会先去本地缓存里查查不到再查离线数据文件。Optional返回的意义是数据文件中没有这个日期的记录说明它不是节假日也不是调休日应当按普通工作日处理。我在实际项目里会建议把else分支明确返回Workday而不是让调用方去猜否则容易出现空指针。从这段最小示例可以看到工具包把数据获取、解析、缓存都封装好了开发人员只需要面向HolidayService编程。但如果你要把它做成一个可供多个服务共享的 API就得看下一章的接入方式。3. 接入 Java 工程Spring Boot 自动配置与手动调用两手准备3.1 引入依赖与配置项如果项目是 Spring Boot 体系这个工具包可以作为一个 starter 被自动装配。基本原理是在spring.factories里声明自动配置类把你需要的HolidayService、HolidayDataRefresher注册成 Bean。你只需要在application.yml里加几行配置holiday: api-endpoint: http://your-holiday-api.example/v1 # 远端数据接口真实的工具包中会替换为实际服务地址 client-token: ${HOLIDAY_CLIENT_TOKEN} # 访问凭证建议放到环境变量 refresh-cron: 0 0 2 * * MON # 数据刷新频率 offline-data-file: classpath:holiday-data.json # 内置离线数据文件 cache-expire-hours: 24 # 内存缓存过期时间这些配置项里api-endpoint和client-token是访问远端数据源的凭证offline-data-file是内置的兜底数据即使断网也能查询到最近一两年的节假日refresh-cron用来覆盖默认的刷新频率。要注意的是client-token不要硬编码在配置文件里否则机密信息容易泄露进代码仓库这也是一个常见的血泪教训。自动配置类做的事情很简单就是读取HolidayProperties然后创建HolidayService和HolidayDataRefresher两个 BeanBean public HolidayService holidayService(HolidayProperties properties) { HolidayDataStore dataStore new FileBackedHolidayDataStore(properties.getOfflineDataFile()); return new HolidayService(dataStore, properties.getCacheExpireHours()); } Bean public HolidayDataRefresher holidayDataRefresher(HolidayService holidayService, HolidayProperties properties) { return new HolidayDataRefresher(holidayService, properties.getApiEndpoint(), properties.getClientToken()); }这个配置化的好处是开发环境可以指向一个 mock 地址生产环境通过环境变量注入真实地址代码里完全不用感知。而且FileBackedHolidayDataStore保证了离线数据文件是只读的正式刷新会写入另外的数据目录避免把 jar 包里面的文件搞乱。我在实际项目里还会加一个spring.profiles.active区分环境每个环境用不同的client-token这样数据访问的权限边界很清楚开发环境只能读mock数据生产环境才能写真实数据。3.2 手动调用与 HTTP 客户端封装有些老项目没有引入 Spring Boot或者内部网络隔离不允许服务直接外连。这时候你可以不用自动配置直接把这个工具包当成普通库来用或者自己封装一个 HTTP 客户端调用别人发布的节假日 API。手动调用并不复杂HttpClient client HttpClient.newHttpClient(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(http://your-holiday-api.example/v1/holiday?date2025-10-01)) .header(Authorization, Bearer getToken()) .timeout(Duration.ofSeconds(5)) .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() 200) { HolidayResult result JsonUtil.fromJson(response.body(), HolidayResult.class); System.out.println(result.isHoliday()); }这段代码有两点值得注意一是构建HttpRequest时给定了 5 秒超时不能让一个需要快速响应的业务请求卡在外部服务上二是对返回结果做了 JSON 反序列化而不是直接拿字符串去匹配。工具包里通常会附带一个HolidayResultDTO字段和前面提到的Holiday模型对应。如果外部接口返回的字段名和本地模型不一致可以在JsonUtil层做一次映射别把接口的字段名泄露到业务代码里。如果项目里用了 Feign还可以把这段封装成一个 FeignClient方式也更简洁。但需要注意 Feign 默认的请求超时只有 1 秒对节假日接口这种低频数据不够用要单独调大ribbon.ReadTimeout或feign.client.config.remoteService.readTimeout。这个坑特别常见我见过同事用 Feign 调外部数据源接口响应慢一点就开始报SocketTimeoutException折腾了好久才定位到是超时配置没调。3.3 返回值字段说明与缓存策略拿到接口返回的数据后一个合格的实现应该统一转换成下面的格式字段类型说明dateLocalDate日期格式 yyyy-MM-ddtypeint0 工作日1 法定节假日2 调休上班日nameString节假日名称工作日为WORKDAYdescriptionString原始文本方便排查数据溯源这个字段设计很实用。比如你需要给运营系统做「今天是否推送营销内容」的判断只看type 0就可以了如果是薪资系统则要看type 2是否补班。接口返回的数据最好不要直接放到调用方的内存里因为多个业务方会重复请求导致外部服务压力变大。工具包里建议用 Caffeine 做本地缓存CacheLocalDate, Holiday localCache Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(24, TimeUnit.HOURS) .build();maximumSize(1000)对一年的日期数据365 条绰绰有余expireAfterWrite设置为 24 小时可以保证数据不是永久旧。这里有个细节如果你是自建外部 API需要在接口层做一次同样的缓存否则每天上千万次请求砸到数据源上迟早出问题。缓存之后实际对数据源的真实请求量会降到每天几十次完全够用。另外建议在缓存中同时存储version字段。这样当业务方反馈某个日期不对时你可以快速确认当前缓存的是哪一批数据而不是想当然认为是最新的。我在生产环境里加过一个简单的监控每隔一小时读取缓存 version 和当前 data 目录 version不一致就告警这样数据更新异常能第一时间发现。4. 避坑指南节假日接口落地时常见的几个翻车现场4.1 时区不对日期差一天现象用接口查询2025-05-01返回结果是2025-04-30的节假日数据或者服务端打印日志显示日期正常但收到前端请求后多传了一天。原因前端传来的日期字符串没有指定时区JVM 默认时区被配置成了 UTCLocalDate本身不携带时区但在解析2025-05-01T00:00:00Z时会被 UTC 下推导成前一天的日期。解决在接口入口显式指定时区。最简单的办法是在 JVM 启动参数里加上-Duser.timezoneAsia/Shanghai或者在解析日期时使用LocalDate.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE)不要用带Instant的反序列化。工具包里所有日期字段都强制使用LocalDate避开Date和Instant就是为了杜绝这类时区偏移。4.2 把调休上班日当成了节假日现象系统里有用户在周日请假考勤系统自动按休息日处理但实际当天是调休上班日结果漏记了缺勤。原因判断逻辑只写了if (type 1) return false没有考虑type 2的补班场景。很多开发者一开始会下意识地认为「周末 休息日」而调休上班日恰好打破了这一常识。解决所有涉及「是否上班」的判断统一走工具包提供的方法而不是自己去组合条件。工具包的方法逻辑是这样的type 0 || type 2视为工作日出勤。如果你要在自己的代码里判断也请把type 2显式加入。我见过最离谱的翻车是把type 2当成普通节假日写了一年直到月底对账才发现所有调休那天都不算工资。4.3 自动更新失败时静默吞异常现象数据源接口偶发 500定时任务捕获异常后只打了一条 debug 日志半个月后官方发布了新的调休方案系统里还是旧数据导致后面半个月的排班全部算错。原因写定时任务的同事为了省事在整个方法外面加了个大try-catch异常被吞掉没有告警。自动更新是后台任务失败时往往没人注意到。解决更新失败必须抛异常或者至少打 error 日志并且必要时接入邮件/企业微信告警。我一般会把刷新逻辑拆成两步第一步检查版本第二步拉数据并替换。第三步替换失败时回滚到旧版本这样即使失败也只是暂时没有新数据不会把本地数据文件写坏。另外每次成功更新后记录版本号方便排查问题。4.4 并发访问高却忘了缓存把外部接口打爆现象线上服务第一次接入节假日 API 后QPS 突然飙升外部数据源响应变慢甚至返回 429业务方超时。原因一个请求进来内部调用了三次节假日查询三次都没命中缓存全部打到外部接口上叠加多个业务方流量放大了几十倍。解决参考 3.3 的 Caffeine 方案在本地做好 24 小时缓存。同时还要注意缓存穿透的情况如果有人在循环里查询一个不存在的日期每次都返回空也可以把空结果缓存起来。工具包里提供了一个blackList小机制专门记录「确认不存在」的日期避免刷无效请求。4.5 跨年切换时用了旧版本数据现象每年 12 月 31 日晚上系统里显示 1 月 1 日是工作日没有识别出元旦放假或者 1 月 2 日调休上班日被误判成休息日。原因工具包的数据文件是「年份 批次」管理但调用方查询某个日期时没有校验这个日期是否属于当前已加载的数据版本。比如缓存里还是 2024 年的数据查询 2025 年 1 月 1 日自然查不到于是默认返回工作日。没有一条逻辑去识别「跨年数据缺失」。解决在HolidayService里加一个「年度边界检查」查询日期若晚于已加载数据文件的最大年份就自动尝试触发一次增量更新更新失败时至少输出警告日志并返回一个UNKNOWN类型而不是默默按工作日处理。工具包新版本里加了withYearFallback策略本地没有某一年数据时会先检查有没有兄弟服务可以转发都没有再去远端拉最后才返回默认工作日。5. 进阶离线数据兜底与一年数据的验证技巧5.1 内置数据文件和手动覆盖机制即使自动更新再稳定也要准备离线兜底。工具包默认带一份holiday-data.json里面维护了近三年的公开数据。你可以直接打开看它的结构{ version: 2025.1, updatedAt: 2025-01-10, data: [ { date: 2025-01-01, type: 1, name: 元旦 }, { date: 2025-01-26, type: 2, name: 春节调休上班 } ] }当外部数据源不可用时HolidayService会回落到这个文件。需要注意的是工具包不会自动覆盖这个内置文件而是把新拉到的数据写到一个data/external-holiday.json或者数据库中。这样做的原因是避免每次升级 jar 包都把官方新数据丢掉。手动覆盖时你只需要把当前年份的数据写到外部数据文件服务会优先读取外部数据本地内置数据作为最后兜底。5.2 用已知年份的放假安排验证接口准确性拿到任何一套节假日数据我的习惯是先验证再接入。不要相信它自称是「自动更新」就放心用每年都有数据源把节日名称写错或者漏掉调休。验证方法很简单选一个已经过完的年份比如 2024 年把当年放假安排摘出来逐个和工具包的返回值对比。ListLocalDate knownHolidays List.of( LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 10), LocalDate.of(2024, 10, 1) ); for (LocalDate date : knownHolidays) { Holiday holiday holidayService.find(date).orElse(null); if (holiday null || holiday.getType() ! 1) { throw new IllegalStateException(验证失败 date); } }这段代码只验证了三个关键点实际你可以写成遍历全年每一个周末、每个已知补班日的数据集。我的经验是至少要准备三组节假日、调休上班日、普通周末每组各挑几个代表日期。这样能一次性发现模型层面的问题。5.3 我的一个强制习惯上线前全量跑一遍自检接手这个工具包后我给自己立了一个规矩凡是接节假日相关功能的项目上线前必须跑一遍全量自检。所谓全量自检就是遍历下一年的每一天把工具包返回的类型和一份人工维护的「可信答案」对比任何不一致都直接阻断发布。这份可信答案可以放在测试资源里由组长或产品人工维护不作为业务逻辑的一部分。自检代码其实不复杂LocalDate start LocalDate.of(2026, 1, 1); LocalDate end LocalDate.of(2026, 12, 31); for (LocalDate d start; !d.isAfter(end); d d.plusDays(1)) { int expected trustedAnswer.getType(d); int actual service.getType(d); if (expected ! actual) { throw new IllegalStateException(日期 d 类型不一致expected expected , actual actual); } }从那以后我每次接这类功能都会强制走一遍全量自检宁可多花十分钟换一个放心。这个习惯救了我好几次比如有一次工具包新版本数据源字段映射反了全量自检直接拦住了发布。希望这个思路也能帮到你拿着这个 holiday 资源包接入之前先把它当作一个「数据准确性审计工具」而不是一个黑匣子直接信任。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表