
之前写过 Java 收发卡片消息Webhook 机器人那条路这篇补另一半怎么封装一个正经的Java 客户端把 token 管理和 sendText 跑通。企微这类平台没有官方 Java SDK第三方包质量参差自己封装百来行反而最稳——出错行为完全可控字段和文档一一对应。技术选型与两个前提依赖只引两个OkHttp 做请求Jackson 做 JSON。开始前确认两件事实例凭证AppIDwe_xxxxx和 App Tokeneyk_xxxxx在 Eyun 官网开通实例后拿到token 有效期 7200 秒必须缓存复用提前 300 秒刷新——每次请求都拉 token 会直接撞限流客户端骨架线程安全的 token 管理Java 多线程环境下token 刷新最怕并发——10 个线程同时发现过期同时去刷直接触发刷新频率限制。用双检锁解决public class WecomClient { private final String base; private final String appId; private final OkHttpClient http new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS).build(); private final ObjectMapper mapper new ObjectMapper(); private volatile String token; // volatile 保证可见性 private volatile long tokenExpireAt; public WecomClient(String base, String appId, String token) { this.base base.replaceAll(/$, ); this.appId appId; this.token token; this.tokenExpireAt System.currentTimeMillis() 6900_000L; // 提前300秒 } private synchronized void refreshTokenIfNeeded() { if (System.currentTimeMillis() tokenExpireAt) return; // 双检锁 // 调刷新接口成功后更新 token 和 tokenExpireAt // 刷新失败抛 ApiException由调用方决定重试 } public JsonNode post(String path, ObjectNode body) throws Exception { refreshTokenIfNeeded(); body.put(appid, appId); // appid 每次调用必带 Request req new Request.Builder() .url(base path) .header(Authorization, Bearer token) .post(RequestBody.create(mapper.writeValueAsBytes(body), MediaType.parse(application/json))) .build(); JsonNode resp mapper.readTree(http.newCall(req).execute().body().string()); int code resp.get(code).asInt(-999); if (code 0) return resp.get(data); throw new ApiException(resp); // 统一异常见下文 } }volatile synchronized 双检锁保证高并发下只有一个线程真正去刷 token其余线程直接用新值。sendTextJava 处理 int64 反而最省心企微的 int64 字段externalUserId、conversationId、roomId要求 JSON number 类型。Java 的long天然就是 numberJackson 默认序列化 long 不带引号——这点比脚本语言还省心。唯一的大坑是用了String接这些字段序列化出来就带引号接口直接返回业务错误。ObjectNode body mapper.createObjectNode(); body.put(conversationId, 123456789012345678L); // long千万别写成 String body.put(content, 你好这是一条测试消息); try { JsonNode data client.post(/wx-api/api/message/sendText, body); System.out.println(发送成功, msgId data.get(msgId).asText()); } catch (ApiException e) { // 两套 code 体系在异常里已分好类 // 数字 -1 业务失败看 message 字段 // 字符串 40001/-301002 网关层问题token/限流/签名 log.error(发送失败 code{} msg{}, e.getCode(), e.getMessage()); }统一异常把两套 code 体系关进一个类返回封套是{code, data, message, detail, time}但 code 可能是数字0成功、-1业务失败也可能是字符串40001token 过期、-301002限流。建议ApiException里直接按类型分类数字 code 归业务异常字符串 code 归网关异常上层按类别决定重试还是告警别在业务代码里到处if (code.equals(40001))。跑通验证顺序先调contact/getSyncList拉一条客户数据确认 token 能用再用返回的externalUserId发一条 sendText手机收到即通。我上次带新人对接他卡了俩小时的原因是把数据库里的varchar主键直接塞给了conversationId——一个Long.parseLong的事。接口字段细节以 Eyun 开发文档为准。这套客户端骨架也就百来行却把 token 竞态、int64、异常分类三个最容易翻车的地方都兜住了后续加发文件、发卡片就是往里加方法的事。