企业微信API接口的Java SDK封装:可复用、可测试的工具类设计方法

企业微信API接口的Java SDK封装:可复用、可测试的工具类设计方法

直接在业务代码中拼接URL、手动处理Token、解析JSON响应,会导致重复逻辑多、难以维护和单元测试。本文基于wlkankan.cn.wecom.sdk包,设计模块化、依赖注入友好、支持Mock的企业微信SDK,提升开发效率与系统健壮性。

统一响应结构与异常定义

java 复制代码
package wlkankan.cn.wecom.sdk.model;

public class WeComResponse<T> {
    private int errcode;
    private String errmsg;
    private T data;

    public boolean isSuccess() {
        return errcode == 0;
    }

    // getters & setters
    public int getErrcode() { return errcode; }
    public void setErrcode(int errcode) { this.errcode = errcode; }
    public String getErrmsg() { return errmsg; }
    public void setErrmsg(String errmsg) { this.errmsg = errmsg; }
    public T getData() { return data; }
    public void setData(T data) { this.data = data; }
}
java 复制代码
package wlkankan.cn.wecom.sdk.exception;

public class WeComApiException extends RuntimeException {
    private final int errcode;

    public WeComApiException(int errcode, String message) {
        super(message);
        this.errcode = errcode;
    }

    public int getErrcode() { return errcode; }
}

Token管理器接口与实现

java 复制代码
package wlkankan.cn.wecom.sdk.token;

import org.springframework.stereotype.Component;

@Component
public class CorpAccessTokenManager implements AccessTokenProvider {
    private final TokenCache tokenCache = new TokenCache();

    @Override
    public String getAccessToken(String corpId, String secret) {
        String cacheKey = "wecom:token:" + corpId;
        String cached = tokenCache.get(cacheKey);
        if (cached != null) {
            return cached;
        }

        // 调用获取token接口
        String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpId + "&corpsecret=" + secret;
        // 使用HttpClient或RestTemplate(此处简化)
        String rawResponse = "{\"access_token\":\"mock_token_123\",\"expires_in\":7200}";
        // 实际应解析JSON
        String token = "mock_token_123";
        tokenCache.put(cacheKey, token, 7000); // 提前200秒过期
        return token;
    }
}
java 复制代码
package wlkankan.cn.wecom.sdk.token;

public interface AccessTokenProvider {
    String getAccessToken(String corpId, String secret);
}
java 复制代码
package wlkankan.cn.wecom.sdk.token;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

public class TokenCache {
    private final ConcurrentHashMap<String, CacheEntry> cache = new ConcurrentHashMap<>();

    public void put(String key, String value, long ttlSeconds) {
        cache.put(key, new CacheEntry(value, System.currentTimeMillis() + ttlSeconds * 1000));
    }

    public String get(String key) {
        CacheEntry entry = cache.get(key);
        if (entry == null) return null;
        if (System.currentTimeMillis() > entry.expireAt) {
            cache.remove(key);
            return null;
        }
        return entry.value;
    }

    private static class CacheEntry {
        final String value;
        final long expireAt;
        CacheEntry(String value, long expireAt) {
            this.value = value;
            this.expireAt = expireAt;
        }
    }
}

HTTP客户端抽象

java 复制代码
package wlkankan.cn.wecom.sdk.http;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
public class WeComHttpClient {
    private final RestTemplate restTemplate = new RestTemplate();
    private final ObjectMapper objectMapper = new ObjectMapper();

    public <T> T post(String url, Object requestBody, Class<T> responseType) {
        try {
            String json = objectMapper.writeValueAsString(requestBody);
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            HttpEntity<String> entity = new HttpEntity<>(json, headers);
            return restTemplate.postForObject(url, entity, responseType);
        } catch (Exception e) {
            throw new RuntimeException("HTTP request failed", e);
        }
    }

    public <T> T get(String url, Class<T> responseType) {
        return restTemplate.getForObject(url, responseType);
    }
}

用户服务SDK封装

java 复制代码
package wlkankan.cn.wecom.sdk.service;

import wlkankan.cn.wecom.sdk.model.WeComResponse;
import wlkankan.cn.wecom.sdk.token.AccessTokenProvider;
import wlkankan.cn.wecom.sdk.http.WeComHttpClient;
import wlkankan.cn.wecom.sdk.exception.WeComApiException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class UserService {
    @Resource
    private AccessTokenProvider tokenProvider;

    @Resource
    private WeComHttpClient httpClient;

    public WeComResponse<UserDetail> getUser(String corpId, String secret, String userId) {
        String token = tokenProvider.getAccessToken(corpId, secret);
        String url = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=" + token + "&userid=" + userId;
        WeComResponse<UserDetail> resp = httpClient.get(url, WeComResponse.class);
        if (!resp.isSuccess()) {
            throw new WeComApiException(resp.getErrcode(), resp.getErrmsg());
        }
        return resp;
    }

    public static class UserDetail {
        private String userid;
        private String name;
        private String mobile;
        // getters/setters
        public String getUserid() { return userid; }
        public void setUserid(String userid) { this.userid = userid; }
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getMobile() { return mobile; }
        public void setMobile(String mobile) { this.mobile = mobile; }
    }
}

支持Mock的单元测试

java 复制代码
package wlkankan.cn.wecom.sdk.test;

import wlkankan.cn.wecom.sdk.service.UserService;
import wlkankan.cn.wecom.sdk.token.AccessTokenProvider;
import wlkankan.cn.wecom.sdk.http.WeComHttpClient;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

public class UserServiceTest {

    @Test
    public void testGetUserSuccess() {
        AccessTokenProvider mockTokenProvider = Mockito.mock(AccessTokenProvider.class);
        WeComHttpClient mockClient = Mockito.mock(WeComHttpClient.class);

        when(mockTokenProvider.getAccessToken("corp123", "secret456"))
            .thenReturn("mock_token");

        UserService.UserDetail detail = new UserService.UserDetail();
        detail.setUserid("zhangsan");
        detail.setName("张三");

        UserService.WeComResponse<UserService.UserDetail> resp = new UserService.WeComResponse<>();
        resp.setErrcode(0);
        resp.setErrmsg("ok");
        resp.setData(detail);

        when(mockClient.get(anyString(), eq(UserService.WeComResponse.class)))
            .thenReturn(resp);

        UserService service = new UserService();
        // 注入Mock(实际项目可用Spring Test)
        service.tokenProvider = mockTokenProvider;
        service.httpClient = mockClient;

        UserService.WeComResponse<UserService.UserDetail> result = service.getUser("corp123", "secret456", "zhangsan");
        assertTrue(result.isSuccess());
        assertEquals("张三", result.getData().getName());
    }
}

配置类集成

java 复制代码
package wlkankan.cn.wecom.sdk.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = "wlkankan.cn.wecom.sdk")
public class WeComSdkAutoConfiguration {
    // 自动装配所有SDK组件
}

通过wlkankan.cn.wecom.sdk模块封装的SDK,业务层只需注入UserService等服务类,无需关心Token刷新、HTTP调用、错误重试等细节。接口设计清晰,依赖可替换,便于Mock测试,显著提升企业微信对接代码的可维护性与可靠性。

相关推荐
hanjq_code2 小时前
java使用阿里的easyExcel解决把excel每行的数据转成excel表格格式数据并打包成ZIP下载
java·开发语言·excel
路多辛2 小时前
JSONC-带注释的 JSON 详解
开发语言·json
阳光九叶草LXGZXJ2 小时前
达梦数据库-学习-43-定时备份模式和删除备份(Python+Crontab)
linux·运维·开发语言·数据库·python·学习
Grassto2 小时前
9 Go Module 依赖图是如何构建的?源码解析
开发语言·后端·golang·go module
软件开发技术深度爱好者2 小时前
JavaScript的p5.js库使用详解(上)
开发语言·javascript
独自破碎E2 小时前
包含min函数的栈
android·java·开发语言·leetcode
沛沛老爹2 小时前
基于Spring Retry实现的退避重试机制
java·开发语言·后端·spring·架构
wregjru2 小时前
【C++】2.9异常处理
开发语言·c++·算法
古城小栈2 小时前
Rust unsafe 一文全功能解析
开发语言·后端·rust