Spring Boot定时任务QQ机器人:基于OneBot协议的消息推送实现

Spring Boot定时任务QQ机器人:基于OneBot协议的消息推送实现 定时任务 QQ 机器人是很多 Java 开发者在做提醒服务、群内日报、运营推送时首先想到的落地方式某个时间点一到机器人自动往指定 QQ 群发一条消息。这个需求看起来简单真正落地时却会涉及两个完全不同的技术域一个是定时调度一个是 QQ 消息通道。定时调度要回答“到点之后做什么”消息通道要回答“业务代码如何把文本变成 QQ 会话里的消息”。把这层链路拆清楚再基于 Spring Boot 写一个最小可运行案例就能很快上手也能在后面加开关、加重试、加分布式调度时不至于重写。本文面向已经掌握 Java 和 Spring Boot 基础、但还没有完整做过 QQ 机器人项目的开发者。文章会从整体架构讲起然后给出一个可以直接运行的最小工程使用 OneBot 协议兼容客户端处理 QQ 账号登录与消息收发使用 Spring 的Scheduled驱动定时任务业务服务只负责在固定时间调用机器人 HTTP API 发送群消息。学完之后你能独立搭建一个最小闭环掌握 cron 表达式的常见写法并且知道定时任务没触发、消息发送失败时应该从哪一层开始排查。1. 先理解定时任务机器人的完整链路1.1 消息从定时任务到 QQ 会话要经过哪些环节把“定时任务”和“QQ 机器人”放在一起一开始就要区分两件事任务谁来触发消息谁来发。定时触发由业务应用负责。在 Java 生态里最简单的做法是在方法上标注ScheduledSpring 容器启动时就会按照表达式注册一个调度器到点后执行方法。这个方法里可以查询数据库、组装文本、调用外部接口最后把要推送的内容交给消息发送服务。消息发送则不能直接由业务应用连 QQ 来完成。出于协议复杂度和账号安全考虑目前主流开源方案是使用一个独立的协议适配端它负责登录 QQ 账号、维护会话状态并对外提供统一的 HTTP 或 WebSocket 接口。业务应用只需要按协议约定发送请求例如调用send_group_msg接口适配端就会把消息投递到对应的 QQ 群。这个拆分的价值在于业务代码无需关心 QQ 协议细节。只要适配端的接口稳定业务侧就可以像调用普通 HTTP 服务一样完成消息推送后续更换适配实现也不影响定时任务代码。1.2 定时调度组件怎么选定时任务本身是 Java 后端非常成熟的领域选型取决于部署规模和任务复杂度。方案适用场景优点需要关注的成本SpringScheduled单机、轻量、定时任务数量少配置简单注解即可多实例会重复执行任务无持久化Quartz单机或集群需要复杂触发器、任务持久化功能强支持 misfire、持久化配置较重学习成本高XXL-Job分布式、多实例、需要可视化调度管理中心化调度自带管理界面和告警需要部署调度中心引入运维依赖对于“简单快速”这个目标第一版优先选择 SpringScheduled。它不需要额外装服务不会增加部署成本等业务规模到了多实例部署、需要统一管理执行记录的时候再迁移到分布式调度框架也不迟。1.3 最小闭环需要准备哪些元素一个能跑起来的最小闭环至少包含四个元素一个能登录 QQ 账号的协议适配端并启用了 HTTP API。一个 Spring Boot 应用包含定时任务代码和消息发送代码。一组自定义配置用于指定适配端地址、目标群号、cron 表达式。一个验证入口比如通过日志确认任务触发、通过 QQ 群消息确认结果送达。把这四部分串起来后整个执行链路就是定时表达式到点Spring 调度器调用任务方法任务方法组装消息内容调用机器人 HTTP 接口适配端将消息发送到目标 QQ 群。2. 环境准备与依赖配置2.1 基础环境检查开始写代码前先把环境确认一遍避免把时间浪费在版本不匹配上。环境项建议版本说明JDK17 或 21Spring Boot 3.x 要求 JDK 17 起若继续使用 Spring Boot 2.x 可用 JDK 8Maven3.8 以上用于依赖管理和打包Spring Boot3.2.x本文示例以 3.x 为主2.x 用法基本相同协议适配端见对应文档需要支持 OneBot HTTP API能登录 QQ 账号网络本机回环或内网互通业务应用与适配端之间能互相访问不同协议适配端的安装方式差异较大有的提供桌面客户端有的通过 Docker 运行有的需要配置 QQ 账号的扫码登录。无论选哪种先确认它能正常启动并且能在本地访问到它暴露的 HTTP 端口。2.2 创建 Spring Boot 项目并引入依赖推荐从 Spring Initializr 创建项目或者直接使用 Maven 骨架。因为消息发送要发起 HTTP 请求这里只需要引入spring-boot-starter-web它同时提供了 Web 容器和RestTemplate所需的转换器。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies如果不想引入完整 Web 容器也可以只引入spring-boot-starter并手动加上spring-web但工程上直接使用starter-web更容易排查问题后续需要对外提供管理接口时也不需要再改依赖。2.3 配置 application.yml定时机器人相关的地址、群号、cron 表达式不应该写死在代码里而是放到配置文件中。server: port: 8080 bot: base-url: http://127.0.0.1:5700 group-id: 123456789 cron: 0 30 9 * * ?这里的bot.base-url是协议适配端监听 HTTP API 的地址。常见 OneBot 实现的默认端口是 5700但不同项目可能不同这里只是一个示例要按你自己启动的适配端实际端口填写。bot.group-id是准备接收消息的 QQ 群号。bot.cron是典型表达式表示每天上午 9 点 30 分 0 秒触发一次。2.4 在启动类上开启定时任务Spring Boot 默认不会自动开启定时任务必须显式使用EnableScheduling。package com.example.qqbot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; SpringBootApplication EnableScheduling public class QqBotApplication { public static void main(String[] args) { SpringApplication.run(QqBotApplication.class, args); } }忘记这一步是最常见的“定时任务不执行”原因。加了Scheduled但启动类上缺少EnableSchedulingSpring 不会注册调度器代码也不会报错只会静默不执行。3. 编写一个最小可运行的定时任务机器人3.1 项目结构设计最小项目不需要分层太多先保证职责清晰。src/main/java/com/example/qqbot/ ├── QqBotApplication.java ├── config/ │ └── ScheduleConfig.java ├── service/ │ └── BotMessageService.java └── task/ └── DailyReportTask.javaQqBotApplication启动类和EnableScheduling入口。ScheduleConfig配置定时任务线程池。BotMessageService封装调用机器人 HTTP 接口的逻辑。DailyReportTask写定时任务拼装消息并调用服务发送。这个结构的好处是任务逻辑和消息通道分离。将来要支持按月执行、按用户群分类发送只需要替换或扩展task包里的类。3.2 封装机器人消息发送服务BotMessageService使用RestTemplate调用 OneBot 的send_group_msg接口返回boolean表示是否成功。package com.example.qqbot.service; import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; Service public class BotMessageService { private final RestTemplate restTemplate new RestTemplate(); private final ObjectMapper objectMapper new ObjectMapper(); Value(${bot.base-url}) private String baseUrl; public boolean sendGroupMessage(Long groupId, String message) { String url baseUrl /send_group_msg; MapString, Object body new HashMap(); body.put(group_id, groupId); body.put(message, message); body.put(auto_escape, true); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityMapString, Object request new HttpEntity(body, headers); try { ResponseEntityString response restTemplate.postForEntity(url, request, String.class); JsonNode root objectMapper.readTree(response.getBody()); if (ok.equals(root.path(status).asText())) { return true; } System.out.println(发送失败响应内容: response.getBody()); return false; } catch (RestClientException | com.fasterxml.jackson.core.JsonProcessingException e) { System.out.println(调用机器人接口异常: e.getMessage()); return false; } } }这里有两个关键点。第一请求体要设置Content-Type: application/json否则某些实现可能把 JSON 参数当成表单处理导致group_id解析失败。第二OneBot 标准响应中status字段为ok表示成功。不同实现的响应结构可能略有差异只要你的适配端文档声明是 OneBot 兼容接口就可以按这个方式判断。注意生产环境不要用System.out.println记录日志后面会说明如何替换成规范日志。3.3 配置定时任务线程池Spring 默认的Scheduled执行器是单线程的。如果一个任务执行时间过长会阻塞后续其他定时任务。建议在一开始就给定时任务配置一个独立线程池。package com.example.qqbot.config; import java.util.concurrent.Executors; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; Configuration public class ScheduleConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(4)); } }这里创建了一个大小为 4 的调度线程池。具体大小取决于任务数量和单次执行耗时。任务之间没有依赖关系时可以适当调大有资源竞争时避免盲目调大导致数据库或下游接口被打满。3.4 编写定时任务类假设需求是每天上午在群里发送一条日报提醒任务类可以这样写。package com.example.qqbot.task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import com.example.qqbot.service.BotMessageService; Component public class DailyReportTask { private static final Logger log LoggerFactory.getLogger(DailyReportTask.class); private final BotMessageService botMessageService; Value(${bot.group-id}) private Long groupId; public DailyReportTask(BotMessageService botMessageService) { this.botMessageService botMessageService; } Scheduled(cron ${bot.cron}, zone Asia/Shanghai) public void sendDailyReminder() { log.info(日报提醒任务开始执行目标群{}, groupId); String message 早上好请记得在 10 点前提交昨日日报。; boolean success botMessageService.sendGroupMessage(groupId, message); if (!success) { log.error(日报提醒发送失败group{}, groupId); return; } log.info(日报提醒发送成功group{}, groupId); } }这里使用Value(${bot.cron})把 cron 表达式外置避免修改执行时间需要重新编译代码。同时显式指定zone Asia/Shanghai可以避免服务器时区不统一导致任务在错误时间触发。sendDailyReminder方法内部先记录任务开始再组装消息接着调用BotMessageService最后记录结果。日志一定要到位因为定时任务没有主动触发入口出现问题时日志几乎是唯一的排查依据。3.5 运行方式与查看方式运行QqBotApplication的main方法或者使用 Maven 命令mvn spring-boot:run启动后观察控制台如果出现类似下面的日志说明 Spring 已启动Started QqBotApplication in 2.437 seconds (process running for 2.512)等到 cron 表达式中配置的时间到达后再看任务日志。4. 关键配置与参数说明4.1 cron 表达式要理解六段结构Spring 的Scheduled支持六段 cron 表达式和 Linux crontab 的五段表达式不同第一段是秒。字段位置含义取值范围允许符号第 1 位秒0-59*,-/第 2 位分0-59*,-/第 3 位时0-23*,-/第 4 位日1-31*,-?/第 5 位月1-12 或 JAN-DEC*,-/第 6 位周0-7 或 SUN-SAT*,-?/常用写法需求cron 表达式每天 9 点 30 分触发0 30 9 * * ?每个工作日 8 点触发0 0 8 ? * MON-FRI每小时整点触发0 0 * * * ?每周一 9 点触发0 0 9 ? * MON每隔 10 分钟触发0 */10 * * * ?这里最容易犯的错误是写成五段表达式例如30 9 * * *。在 Spring 中会把30当作秒9当作分导致任务每天只有每秒第 30 毫秒级触发一次而且触发时间完全不符合预期。4.2 OneBot HTTP 接口参数速查定时任务最终要通过 HTTP 接口把消息交给适配端。以 OneBot v11 的send_group_msg为例常用参数如下。参数类型必填说明group_idNumber是目标 QQ 群号messageString是要发送的消息内容auto_escapeBoolean否是否将内容作为纯文本处理默认false发送私聊消息则使用send_private_msg对应的必填参数是user_id。如果你的适配端使用 WebSocket 通信也可以从 REST 调用改为 WS 连接但最小项目中 REST 更直观也更容易用 curl 验证。用 curl 模拟协议适配端是否正常curl -X POST http://127.0.0.1:5700/send_group_msg \ -H Content-Type: application/json \ -d {group_id:123456789,message:测试消息,auto_escape:true}如果返回{ status: ok, retcode: 0, data: null }说明协议适配端正常问题大概率在业务应用侧如果返回超时或错误码问题大概率在适配端。4.3 自定义配置项的读取方式配置项建议全部集中在application.yml里类中只通过Value或ConfigurationProperties读取。若配置项较多推荐使用ConfigurationProperties绑定为一个配置类。package com.example.qqbot.config; import org.springframework.boot.context.properties.ConfigurationProperties; ConfigurationProperties(prefix bot) public class BotProperties { private String baseUrl; private Long groupId; private String cron; // getter / setter 省略 }使用配置类的最大好处是配置项有类型提示也方便在多个任务中复用。小项目中用Value足够项目变大后逐步迁移到配置类。5. 运行验证与日志排查5.1 启动顺序和检查点整个系统有两个进程协议适配端和 Spring Boot 应用。建议按以下顺序启动。启动协议适配端确认能成功登录 QQ 账号。调用 curl 命令测试send_group_msg接口确认消息链路可用。启动 Spring Boot 应用观察日志。等待 cron 时间点确认任务日志出现。在目标 QQ 群中确认收到机器人消息。如果跳过第二步直接把任务跑起来一旦群内没有消息就无法区分是适配端问题还是业务代码问题。先用 curl 验证等于把这条链路的底层先点亮。5.2 看日志确认执行链路定时任务运行后应该能在控制台看到类似下面的日志2025-01-06 09:30:00.001 INFO [scheduling-1] c.e.qqbot.task.DailyReportTask : 日报提醒任务开始执行目标群123456789 2025-01-06 09:30:00.120 INFO [scheduling-1] c.e.qqbot.task.DailyReportTask : 日报提醒发送成功group123456789日志中的[scheduling-1]表示任务是在调度线程池里执行的。如果看到的是[http-nio-8080-exec-x]或者其他线程名说明调度配置可能有变化但只要能执行问题不大。5.3 异常场景的预期表现如果适配端没有启动日志会类似调用机器人接口异常: Connection refused: connect 日报提醒发送失败group123456789如果 cron 表达式写错启动时 Spring 可能直接抛出IllegalStateException或者在日志中提示无法解析 cron 表达式。这时候要优先看启动日志而不是等到点后找原因。6. 常见问题排查6.1 定时任务到点没执行这是最典型的问题排查顺序如下。现象原因检查方式解决方式到点没有任何任务日志缺少EnableScheduling查看启动类注解加上注解到点没有任何任务日志cron 表达式解析失败看启动日志有无异常修正表达式到点但日志很晚才输出调度线程池被其他长任务占满看日志线程名和任务耗时配置独立线程池执行时间与预期相差几个小时服务器时区不对执行date -R查看时区指定zone或修改 TZ应用部署了多个实例每个实例都会执行一次检查实例数量引入分布式锁或调度框架先看有没有日志输出再判断问题在调度层还是任务内部。任务内部异常如果没有被捕获也可能表现为“看似没执行”其实异常发生在消息拼装阶段。6.2 消息发送失败任务日志显示“发送失败”时按链路从下往上排查。调用机器人接口异常: Connection refused: connect大概率是协议适配端没启动、端口不对或地址不通。先执行curl -X POST http://127.0.0.1:5700/send_group_msg ...再参考适配端日志确认 QQ 账号是否在线、是否出现登录失效。账号登录失效在协议适配端中属于常见问题需要重新登录。6.3 任务重复执行单机部署时如果任务执行时间超过触发间隔Spring 默认不会等待上一个任务结束。例如 cron 设置为0 */5 * * * ?任务运行了 8 分钟下一次触发时就会出现两个任务叠加执行。解决方式有两种在任务方法中使用状态标记确保同一时间只有一个任务在执行。使用 Quartz 或 XXL-Job 的任务调度语义控制并发。对于简单场景使用线程池并配合状态标记即可。6.4 使用 jstack 排查调度线程阻塞如果定时任务长时间不输出日志但进程还活着可以导出线程栈。jstack pid搜索scheduling-开头的线程查看当前是否卡在某个调用上。常见原因是消息服务访问外部地址时没有设置超时时间导致RestTemplate一直等待。生产环境一定要给 HTTP 客户端设置连接超时和读取超时。SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(5000); RestTemplate restTemplate new RestTemplate(factory);6.5 三处常见坑汇总坑错误写法正确做法cron 少写秒字段30 9 * * *0 30 9 * * ?RestTemplate 不设超时new RestTemplate()配置连接和读取超时启动类缺EnableScheduling只加Scheduled启动类显式开启7. 从学习到生产环境的最佳实践7.1 配置外置cron 不要在代码里写死定时任务最常见的变更需求就是改执行时间。如果 cron 写在注解里每次修改都要重新编译发布。建议全部放到配置中心或环境变量中让任务时间可以独立调整。7.2 建立任务执行记录表生产环境里定时任务失败往往不是立即暴露的。建议为每个任务记录执行时间、执行结果、失败原因和消息内容摘要。字段名类型说明task_namevarchar任务标识executed_atdatetime计划执行时间finished_atdatetime结束时间resultvarchar成功或失败error_msgtext异常信息target_groupvarchar目标群号有了这张表即使日志被清理也能追溯某条消息是否发送成功。7.3 失败重试和告警消息发送失败后最简单的策略是重试两次。重试时要设置间隔避免短时间连续调用导致适配端压力过大。重试仍然失败后应该通过其他渠道告警比如发送到运维群或者接入统一告警平台。重试逻辑不要直接写循环建议使用 Spring Retry 或封装一个带延迟的重试方法。7.4 多实例部署时必须解决重复执行问题在 Spring Cloud 架构中服务通常会部署多个实例。每个实例都会加载同一个Scheduled任务到点时都会执行一次导致重复消息。如果实例数量不多可以引入 ShedLock使用数据库锁保证只有一个实例执行。如果已经上了分布式调度中心推荐直接迁移到 XXL-Job 这类方案把执行权交给调度中心统一分配。迁移时只需要去掉Scheduled改为在调度中心配置执行器和方法业务逻辑本身可以保持不变。7.5 消息内容要有节制定时机器人最忌讳高频骚扰。不要在没有明确场景的情况下每分钟推送一次。考虑用户可接受的消息频率增加开关配置在不需要推送的时段自动跳过。7.6 发布前检查清单检查项确认内容cron 表达式是否是六段格式时间是否与预期一致时区是否指定zone服务器时区是否正确适配端是否已登录HTTP 接口是否可访问目标群号是否配置正确机器人是否在群内日志是否有开始和结束日志超时HTTP 客户端是否设置了超时时间多实例是否引入分布式锁或调度框架8. 扩展方向8.1 让用户通过群聊设置定时任务当前示例是任务完全由配置文件决定。更常见的使用方式是用户发一条“每天 10 点提醒我喝水”机器人解析后保存到数据库再动态注册定时任务。这时需要监听群消息接收到命令后回传结果。建议引入 WebSocket 事件监听在机器人收到消息时回调业务服务。8.2 定时任务持久化与动态变更使用Scheduled的任务在运行期很难动态修改。如果需要频繁变更任务可以研究 Spring 的ScheduledTaskRegistrar动态注册机制或者直接引入 Quartz让任务存储在数据库中通过管理页面修改触发时间。8.3 定时任务与外部数据源结合很多场景下机器人推送的内容不是固定文本而是来自天气接口、股票行情、数据库报表等外部数据。可以在任务方法中先查询数据再拼装消息。注意控制外部接口的调用频率避免因为数据源超时导致任务整体异常。定时任务机器人的核心价值不在机器人本身而在任务链路是否稳定可维护。先用最小方案跑通闭环再逐步补充执行记录、失败重试和分布式调度比一开始就套用重型框架更务实。希望这次梳理能帮助你把“定时任务”和“QQ 消息发送”这两部分真正连起来在遇到问题时知道该在哪一层找答案。