企业微信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测试,显著提升企业微信对接代码的可维护性与可靠性。

相关推荐
淮北49412 分钟前
pip虚拟环境包的问题
开发语言·python·pip
千寻技术帮15 分钟前
10333_基于SpringBoot的家电进存销系统
java·spring boot·后端·源码·项目·家电进存销
dear_bi_MyOnly15 分钟前
【多线程——线程状态与安全】
java·开发语言·数据结构·后端·中间件·java-ee·intellij-idea
常年游走在bug的边缘16 分钟前
掌握JavaScript作用域:从函数作用域到块级作用域的演进与实践
开发语言·前端·javascript
jiaguangqingpanda21 分钟前
Day36-20260204
java·开发语言
ctyshr27 分钟前
C++编译期数学计算
开发语言·c++·算法
tb_first28 分钟前
万字超详细苍穹外卖学习笔记4
java·spring boot·笔记·学习·spring·mybatis
打码的猿30 分钟前
Qt对话框不锁死主程序的方法
开发语言·qt
努力写代码的熊大44 分钟前
c++异常和智能指针
java·开发语言·c++
山岚的运维笔记1 小时前
SQL Server笔记 -- 第15章:INSERT INTO
java·数据库·笔记·sql·microsoft·sqlserver