简介:一份基于WeChatFerry-Java-Client打造的插件化Java微信机器人项目,适合有一定Java基础、希望研究微信自动化消息收发与联系人管理的开发者。资源包共108个文件,约2.68MB,核心代码以57个Java源文件为主,覆盖消息收发、联系人管理、SQL执行等模块;同时包含12个C++文件、6个JAR包及若干proto、yaml、yml配置文件,对应底层SDK封装、依赖库与运行配置,目录结构清晰。目前已有435人学习下载。项目采用插件化设计,将核心功能与扩展功能解耦,可依据自身需求编写插件实现个性化回复、群管理等功能;内容预览显示其中包含send_msg、receive_msg、contact_mgmt、exec_sql等关键实现,并附有license_check.bat等辅助脚本,便于快速部署和二次开发。对关注Java应用与微信机器人实践的开发者来说,是一份有参考价值的完整示例项目。
1. 微信机器人不止发消息:WeChatFerry-JavaBot 把插件化玩明白了
做微信机器人的路子很多,但大多数方案把逻辑写死在主程序里,改一个功能就要重新编译、重启进程,维护成本高得离谱。
这个基于 WeChatFerry-Java-Client 的插件化 Java 微信机器人,思路是把每个功能做成独立插件,运行时动态加载,主框架只负责消息分发和插件管理。我第一次拆它的源码时最意外的一点是:消息从微信进程到 Java 插件,中间只隔了一个 WebSocket,链路比想象中短得多。
如果你已经熟悉 Spring Boot 那一套,想给自己的微信账号加自动回复、群管理、定时推送,又不想每次改功能都动主程序,这个资源值得花一个下午拆一遍。新手能按步骤跑通第一个插件,熟手能直接看它的事件分发和类加载器设计。
2. 插件化架构与消息流转:理解 WCF 的 Java 落地
2.1 从微信钩子到 Java 事件:WCF 的核心链路
WeChatFerry(简称 WCF)并不是一个 Java 项目,它是运行在 Windows 上的微信 Hook 库,通过注入 DLL 拦截微信内部消息,再以 WebSocket 方式对外提供接口。这个 JavaBot 项目做的事,是把 WCF 的 WebSocket 接口封装成 Java 能直接调用的客户端 SDK,并在 SDK 之上加了一层插件容器。
完整链路是这样的:
微信进程 → wcf.dll 钩子层 → WCF SDK → WebSocket(默认 127.0.0.1:7600)→ Java 客户端 → 消息分发器 → 插件实例
这条链路上,Java 项目本身不直接碰微信内部结构,所有交互都走 WebSocket 的 JSON 协议。好处是微信升级导致 Hook API 变化时,只要 WCF 服务端还能跑,Java 侧代码基本不用动;坏处是 WebSocket 是长连接,断线重连、消息积压、回调阻塞这些问题都得自己兜住。
// 连接 WCF 服务端的核心代码,端口必须和 wcf 启动配置一致 WcfClient client = new WcfClient("ws://127.0.0.1:7600"); client.connect(); // 建立长连接,内部含握手和心跳 client.registerMsgCallback(msg -> { // 全量消息都会进这个回调:文本、图片、文件、系统通知 System.out.println("收到消息: " + msg.getContent()); });这段就是最小可用的连接样例。WcfClient 封装了 WebSocket 握手、心跳维持和 JSON 序列化,connect 之后不需要额外写保活线程。registerMsgCallback 注册的是全局回调,所有消息先到这里,再由框架层做类型分发。拿到 WcfMsg 后常用的 getter 有 getContent()(消息正文)、getSender()(发送人 wxid)、getRoomId()(群聊 id,非群聊为空)、getType()(消息类型)。
提示:connect 失败时先确认 WCF 服务端是否在监听 7600,Windows 下直接
netstat -ano | findstr 7600,看不到监听就回去检查微信版本对不对。
2.2 插件的加载与生命周期:注册、卸载、热更新
插件容器是整个项目最值得看的部分。它不是在 main 方法里 new 一堆对象,而是约定一个插件接口,由容器统一扫描、加载、卸载。每个插件一个 JAR 包放 plugins 目录,启动时加载,运行时可热插拔。
public interface WcfPlugin { String getName(); // 插件全局唯一标识 void onLoad(WcfContext context); // 加载时注册指令和定时任务 boolean onMessage(WcfMsg msg, WcfContext context); // 返回 true 表示已消费 void onUnload(); // 卸载时释放资源 }接口只有四个方法,设计意图很明确。getName 是插件在容器里的唯一标识,重名插件会被拒绝加载。onLoad 里拿到的 WcfContext 是核心对象,发消息、查联系人、管理白名单的 API 都挂在它上面。onMessage 的返回值值得单独说——返回 true 表示这条消息被当前插件消费,后续插件不再收到;返回 false 则继续往后传。写插件时第一件事就是判断消息类型是不是自己关心的,再决定消费还是放行,不然两个插件会互相抢消息。
// 插件管理器核心逻辑:每个 JAR 独立 URLClassLoader,避免类冲突 public void loadPlugin(String jarPath) throws Exception { URLClassLoader loader = new URLClassLoader( new URL[] { new File(jarPath).toURI().toURL() }, getClass().getClassLoader() ); // 约定实现类名,或通过 ServiceLoader 发现 Class<?> clazz = loader.loadClass("com.bot.plugins.MyPlugin"); WcfPlugin plugin = (WcfPlugin) clazz.getDeclaredConstructor().newInstance(); plugin.onLoad(context); plugins.put(plugin.getName(), plugin); }这是插件加载的最小实现思路,实际项目里通常还会加磁盘目录监听:plugins 目录新增 JAR 自动加载,删除 JAR 自动卸载。这里先记住一个结论:同一个类名被两个不同的类加载器加载后,它们并不是同一个类,instanceof 会返回 false。热更新相关的坑,第 5 章专门讲。
2.3 消息类型与分发策略
WCF 的消息类型定义在常量表里,常见的有文本、图片、文件、语音、视频、系统通知这几类。分发器拿到消息后不是直接调插件,而是先解析出消息类型和会话维度(单聊/群聊),再按插件声明的兴趣范围做过滤分发。
// 分发器伪代码:先按类型过滤,再逐个流转 if (msg.getType() == WcfMsgType.TEXT) { for (WcfPlugin p : plugins.values()) { if (p.interest(msg)) { boolean consumed = p.onMessage(msg, context); if (consumed) break; // 消费即停,不再往后续传 } } }这样设计的好处是每个插件不用重复写 if 判断,也避免了一个插件收到所有消息导致日志噪音。实际项目里兴趣判断一般做成注解声明,比如 @MsgFilter(type = TEXT, rooms = {"群A"}),由容器在加载时解析注解生成过滤表。
2.4 断线重连与消息确认
WebSocket 长连接在 Windows 桌面环境下三天两头掉线:微信崩溃、网络切换、休眠唤醒都会断。框架里默认的重连策略是固定间隔,但固定间隔有个问题——服务端还没起来时,客户端每 5 秒重连一次,日志刷得飞快。我一般改成指数退避:第一次失败等 1 秒,第二次 2 秒,第三次 4 秒,封顶 30 秒,服务端恢复后能较快感知,日志也干净。
消息确认这块,WCF 的 WebSocket 协议是"推送不确认"模型,服务端推了就算完。这意味着客户端处理消息的速度必须跟上推送速度,处理不过来的话消息就在 TCP 缓冲区里排队,积压多了直接丢。所以队列容量的监视比写业务逻辑更重要,我习惯在框架层统计入队数和出队数,差值超过阈值就输出警告日志。
3. 快速上手:环境准备与第一个插件
3.1 环境要求与依赖引入
WCF 的定位是 Windows 桌面端 Hook,环境要求很具体,先列一张表:
| 组件 | 要求 | 说明 |
|---|---|---|
| 操作系统 | Windows 10/11 x64 | 不支持 Linux / macOS,别想着跑容器 |
| 微信 | 与 WCF 匹配的固定版本 | 版本敏感,自动更新后会失效 |
| JDK | 8 或 11 | 标准 Java,不需要新语法特性 |
| 构建工具 | Maven 3.6+ | 依赖管理和打包插件 JAR |
依赖引入就一行,坐标里的 groupId 和 artifactId 以你解压出来的 SDK 模块实际 pom 为准,这里只写占位写法:
<dependency> <groupId>com.example</groupId> <artifactId>wcf-java-client</artifactId> <version>${wcf.version}</version> </dependency>如果你拿到的资源里没有把 SDK 装进本地仓库,可以在 SDK 模块目录执行mvn install -DskipTests,把产物装进本地 Maven 仓库,再在机器人模块里按上面的坐标引用。
环境准备里最容易翻车的是微信版本。WCF 通常针对某个具体微信版本做偏移量适配,微信一旦自动升级,Hook 注入就会失败。我一般会把微信的自动更新关掉,并在启动脚本里加一步版本校验,版本对不上直接退出,别带病运行。
3.2 写第一个插件:自动回复
写插件只需要做两件事:实现 WcfPlugin 接口,打成 JAR 包丢进 plugins 目录。这里做一个最简单的文本自动回复插件:
public class EchoPlugin implements WcfPlugin { private WcfContext ctx; @Override public String getName() { return "echo"; } @Override public void onLoad(WcfContext context) { this.ctx = context; System.out.println("[echo] 插件已加载"); } @Override public boolean onMessage(WcfMsg msg, WcfContext context) { // 只处理文本消息,非文本直接放行 if (msg.getType() != WcfMsgType.TEXT) { return false; } // sendText 第一个参数是目标 wxid 或群 id ctx.sendText(msg.getSender(), "收到: " + msg.getContent()); return true; } @Override public void onUnload() { System.out.println("[echo] 插件已卸载"); } }EchoPlugin 的逻辑很直白:onLoad 里保存 context,onMessage 里判断类型,是文本就原样回一句,然后返回 true 表示这条消息处理完了。sendText 的第一个参数传 msg.getSender(),单聊场景没问题,群聊场景要传群 id 而不是发送人 wxid,这个区别新手经常搞混,发错地方容易造成骚扰。
编译和打包:
mvn clean package -DskipTests # 产物在 target/ 目录下,拷到机器人根目录的 plugins/ 文件夹 cp target/echo-plugin-1.0.jar ../bot/plugins/打包时注意 pom 里不要引入 spring-boot 那种 fat jar 插件,插件 JAR 只需要包含自己的业务类,框架类由主程序提供。打出来一个几百 KB 的小 JAR 就对了,几十 MB 的肯定有问题。
3.3 配置文件与参数说明
机器人根目录的 config 文件控制框架行为,核心参数这几项:
| 参数 | 默认值 | 作用 |
|---|---|---|
| wcf.ws.url | ws://127.0.0.1:7600 | WCF 服务端地址 |
| wcf.reconnect.interval | 5000 | 断线重连间隔毫秒 |
| wcf.plugins.dir | plugins | 插件扫描目录 |
| wcf.whitelist.enable | false | 是否启用联系人白名单 |
| wcf.msg.queue.size | 10000 | 消息队列容量 |
wcf: ws: url: ws://127.0.0.1:7600 reconnect: interval: 5000 max-retries: -1 # -1 表示无限重连 plugins: dir: plugins whitelist: enable: true list: - wxid_xxxreconnect.interval 默认 5 秒,如果服务端重启频繁可以调大,避免日志刷屏。whitelist 打开后,白名单外的联系人消息会被框架直接丢弃,不会进入任何插件,这对"消息回错人"这种事故是最后一道保险。
4. 功能扩展实战:群管、定时任务与异步处理
4.1 群聊消息管理与关键词回复
实际场景里机器人在群里待得最多。群消息的判断靠 roomId,代码写起来是这样:
@Override public boolean onMessage(WcfMsg msg, WcfContext context) { String roomId = msg.getRoomId(); if (roomId == null || roomId.isEmpty()) { return false; // 不是群消息,放行给其他插件 } String content = msg.getContent(); if (content.startsWith("/mute")) { // 禁言指令:只有管理员才能触发 if (isAdmin(msg.getSender())) { context.sendText(roomId, "本群已开启机器人管理模式"); } } return false; // 继续让后面的插件有机会处理 }注意最后返回的是 false,这让群管理插件只做自己那一小件事,不垄断消息。isAdmin 可以做成从配置或数据库读管理员 wxid 列表,判断逻辑单独抽一个方法,别写在消息回调里。关键词回复也是同一套路子,把关键词和回复内容的映射做成配置表,插件只负责查表,业务变更不用重新打包。
4.2 定时任务与异步处理
消息回调线程被长时间占用是很多机器人"越跑越慢"的根源。WCF 的消息回调跑在框架的线程池里,你如果在回调里做 HTTP 请求或写数据库,后面排队消息就全卡住了。正确姿势是把重活丢进自己的线程池:
public class TimerPlugin implements WcfPlugin { private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final ExecutorService workers = Executors.newFixedThreadPool(4); private WcfContext ctx; @Override public void onLoad(WcfContext context) { this.ctx = context; scheduler.scheduleAtFixedRate(() -> { // 每天早上九点给指定群推一条摘要 String report = fetchReport(); ctx.sendText("群wxid", report); }, 0, 24, TimeUnit.HOURS); } @Override public boolean onMessage(WcfMsg msg, WcfContext context) { if (!msg.getContent().startsWith("/report")) { return false; } workers.submit(() -> { String report = buildReport(); // 耗时的数据汇总 context.sendText(msg.getRoomId(), report); }); return true; } @Override public void onUnload() { scheduler.shutdownNow(); workers.shutdownNow(); } }两个要点。第一,定时任务和异步任务的线程池必须在 onUnload 里 shutdownNow,否则插件卸载后线程还在跑,属于典型的内存泄漏。第二,scheduleAtFixedRate 的起始延迟设 0 只是演示,真实场景建议先计算下个整点,用delay = nextHour - now作为首次延迟,避免每次都在启动时刻触发。fetchReport 和 buildReport 是占位业务逻辑,实际可以接 HTTP API,但记得把请求超时设短,默认的 30 秒会让线程池很容易被打满。
4.3 发送图片、文件与会话感知
文本之外的能力才是机器人的实用价值。发送图片用 sendImage,参数是本地文件路径,框架会转成微信能识别的格式:
boolean ok = ctx.sendImage("D:\\bot\\data\\qrcode.png", roomId); if (!ok) { // 常见原因:路径不存在、文件被占用、微信窗口被最小化到托盘 log.warn("图片发送失败, path={}", path); }sendFile 类似,但文件发送在某些微信版本上有大小限制,超过上限会静默失败,没有异常抛出,所以发送前先检查文件大小。会话感知是指同一个机器人被拉进多个群,回复要回到正确的会话。框架的 WcfMsg 里已经带 roomId,你只要保证"从哪条消息来,回哪条消息去",不要自己维护一个全局"当前群"变量,多群并发时那个变量一定是乱的。
5. 常见问题与避坑:消息丢、线程炸、Hook 失效
运行微信机器人是个体力活,最大的不可控因素全在 Windows 桌面环境。这一章把高频踩坑逐条记录,每条都是现象、原因、解决三段式。
5.1 微信升级后机器人完全没反应
现象:某天开始机器人完全没反应,日志停更,WCF 服务端起不来。
原因:微信客户端自动升级了。WCF 针对固定微信版本做内存偏移量适配,版本一变 Hook 就失效,DLL 注入不进去,WebSocket 服务根本没起来。
解决:固定微信版本并关闭自动更新。Windows 下微信升级策略可以在设置里关掉,但更稳妥的做法是下载对应版本的安装包备份,升级检测被触发时直接覆盖安装回去。启动脚本里加一段版本号校验,读微信安装目录的版本文件,跟配置的期望版本比对,不一致就直接退出。
5.2 消息偶尔收不到,重启后恢复
现象:消息偶尔丢,尤其消息多的时候,重启机器人后又能正常一阵。
原因:消息回调里做了耗时操作,阻塞了框架线程池;或者消息量超过队列容量,队列拒绝写入后消息被丢弃。这是最隐蔽的问题,因为日志里没有任何报错。
解决:回调只做判断和派发,把 IO 操作全部丢到自己的线程池。队列容量调大只是缓解,根治是让回调"快进快出"。我在生产环境里习惯给消息队列加监控计数,队列积压超过阈值就告警,比自己盯着日志强。
5.3 热更新插件后报 ClassCastException
现象:热更新插件 JAR 后,框架强转插件实例时报 ClassCastException,或者插件里的单例状态丢失。
原因:插件 JAR 被 URLClassLoader 加载过,卸载后旧类加载器没有真正释放,新 JAR 又是新的类加载器。两个加载器加载同一个插件类,它们不是同一个类,强转会失败。
解决:加载器必须和插件生命周期绑定,卸载时把插件、线程池、类加载器一并置空,并显式调用 loader.close()(JDK 8+ 支持)。如果插件的静态变量持有 context 引用,类加载器永远回收不掉。排查时用 jmap 看类加载器数量最直观,数量只增不减就是泄漏了。
5.4 发送频率一高就被限制
现象:消息能收到,但发送一快就被限制,发送接口返回成功但对方收不到。
原因:发送太快是玄学,但基本能确认短时间大量同内容消息会触发限制,表现为发送接口返回成功却无实际效果,或者直接被要求验证。
解决:发送侧加限速。我用一个简单的令牌桶,每发一条扣一个令牌,令牌不足就 sleep 随机 1-3 秒。对同内容回复做去重,同一句话在短时间内不重复发送,用内容 hash 做缓存。这些策略没有标准答案,以自己实测为准。
5.5 跑几个小时后 JVM 崩溃或微信白屏
现象:运行几个小时后 JVM 异常退出,或者微信界面白屏无响应。
原因:DLL 注入的微信进程不稳定,加上 Java 侧线程反复创建销毁,资源耗尽。常见诱因是定时任务用 new Thread 起线程且不回收。
解决:所有线程池统一在插件 onLoad 创建、onUnload 关闭;微信侧定期清理消息记录,避免内存膨胀到微信自己崩。如果崩溃高频出现,换一台干净的 Windows 环境,把输入法、安全软件这些可能注入进程的软件全卸载,Hook 类工具对环境洁癖要求很高。
6. 进阶:多实例部署与消息落库
6.1 一对多:多微信号隔离部署
一台机器跑多个机器人,核心是端口隔离。WCF 服务端启动时可以通过参数指定不同端口,Java 侧在配置里改 wcf.ws.url 指向对应端口即可。每个实例拥有独立的 plugins 目录和数据目录,进程之间互不干扰。部署时我习惯把每个实例的日志输出到独立文件,排查问题的时候不用在日志海洋里捞。
6.2 消息落库:把对话变成可检索数据
机器人的价值在数据。每条经过框架的消息,在分发前写一份到 SQLite,后面要统计活跃度、找历史上下文都方便:
private void persist(WcfMsg msg) { try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement( "INSERT INTO msg_log(room_id, sender, content, type, ts) VALUES(?,?,?,?,?)")) { ps.setString(1, msg.getRoomId()); ps.setString(2, msg.getSender()); ps.setString(3, msg.getContent()); ps.setInt(4, msg.getType()); ps.setLong(5, System.currentTimeMillis()); ps.executeUpdate(); } catch (SQLException e) { log.error("消息落库失败", e); } }落库操作要放在异步线程,不能直接写在回调里,原因第 4 章讲过。SQLite 单文件、零运维,最合适这种单机场景;并发量再上去再考虑换 MySQL,初期别过度设计。
做这块最深的体会是:微信群机器人最大的风险从来不是技术,而是你拿它干了什么。从那以后我每次部署前都强制走一遍自检清单,确认消息不越权、不泄露、不骚扰,时间长了这成了一种习惯。这套插件化框架的边界、参数和坑,希望帮到你。
本文还有配套的精品资源,点击获取