之前写过 Java 收发卡片消息(Webhook 机器人那条路),这篇补另一半:怎么封装一个正经的Java 客户端,把 token 管理和 sendText 跑通。企微这类平台没有官方 Java SDK,第三方包质量参差,自己封装百来行反而最稳——出错行为完全可控,字段和文档一一对应。
技术选型与两个前提
依赖只引两个:OkHttp 做请求,Jackson 做 JSON。开始前确认两件事:
实例凭证:AppID(
we_xxxxx)和 App Token(eyk_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,其余线程直接用新值。
sendText:Java 处理 int64 反而最省心
企微的 int64 字段(externalUserId、conversationId、roomId)要求 JSON number 类型。Java 的long天然就是 number,Jackson 默认序列化 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业务失败)也可能是字符串("40001"token 过期、"-301002"限流)。建议ApiException里直接按类型分类:数字 code 归业务异常,字符串 code 归网关异常,上层按类别决定重试还是告警,别在业务代码里到处if (code.equals("40001"))。
跑通验证顺序
先调contact/getSyncList拉一条客户数据确认 token 能用,再用返回的externalUserId发一条 sendText,手机收到即通。我上次带新人对接,他卡了俩小时的原因是把数据库里的varchar主键直接塞给了conversationId——一个Long.parseLong的事。
接口字段细节以 Eyun 开发文档为准。这套客户端骨架也就百来行,却把 token 竞态、int64、异常分类三个最容易翻车的地方都兜住了,后续加发文件、发卡片就是往里加方法的事。