wechat-java 微信开发Java SDK,支持包括公众号、小程序、微信支付、开放平台的后端开发

对微信公众号相关Api进行封装,方便以后复用

整个项目的结果采用Maven聚合工程,方便以后小程序、微信支付、开放平台的Api的封装扩展。
注意:分布式环境下,accessToken获取和更新尽量放在一台中控服务器上,一定要避免各个业务逻辑点各自刷新accessToken, 这样会反复导致accessToken重复刷新,过期问题。比如有这样一个业务场景:我们部署了10台服务器,每台服务器都可以刷新accessToken。 当每个点accessToken过期时,并发量过来,有5台服务器同时发现redis过期,去刷新accessToken,这样会导致token反复过期。
如果不采用中控服务器,可以在发现accessToken过期时,采用分布式锁,保证只有一个业务点去刷新accessToken。
wechat-java 微信开发Java SDK,支持包括公众号、小程序、微信支付、开放平台的后端开发_第1张图片
项目地址

使用, 比如获取accessToken

	@Test
    public void testAccessTokenInMemory() {
        //基于内存
        WechatPSInMemoryConfigImpl config = new WechatPSInMemoryConfigImpl();
        config.setAppId("你的appid");
        config.setSecret("你的secret");
        WechatPSServiceImpl psService = new WechatPSServiceImpl();
        psService.setWechatPSConfigStorage(config);
        String accessToken = psService.getAccessToken();
        Assert.assertNotNull(accessToken);
    }

    @Test
    public void testAccessTokenInRedis() {
        //基于redis,实现分布式锁
        ...
        WechatPSInRedisConfigImpl config = new WechatPSInRedisConfigImpl(jedisPool);
         //WechatPSAdServiceImpl config = new WechatPSAdServiceImpl(jedisPool);
        config.setAppId("你的appid");
        config.setSecret("你的secret");
        WechatPSServiceImpl psService = new WechatPSServiceImpl();
        psService.setWechatPSConfigStorage(config);
        String accessToken = psService.getAccessToken();
        Assert.assertNotNull(accessToken);
    }

*需要重点关注获取accessToken,要考虑多线程线程安全和分布式
*基于Redis的微信配置provider,需要根据自己实现情况重写

/**
 * Created by wujie on 2019/1/13.
 * 基于Redis的微信配置provider. 调用者可以根据自己的实际情况进行重写
 */
@Slf4j
public class WechatPSInRedisConfigImpl extends WechatPSInMemoryConfigImpl {

    protected static final String ACCESS_TOKEN_KEY = "wechat:accesstoken:";
    protected static final String LOCK_ACCESS_TOKEN_KEY = "wechat:accesstokenlock:";
    protected static final int LOCKTIMEOUT = 1; //分布式锁的超时时间暂停为1秒,具体视业务进行调整

    //使用连接池保证线程安全.
    //单节点
    protected final JedisPool jedisPool;

    private String accessTokenKey;
    private String accessTokenLockKey;

    public WechatPSInRedisConfigImpl(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

   //该项目支持多个公众号,所以每个公众号需要生成独有的存储key来区分
    @Override
    public void setAppId(String appId) {
        super.setAppId(appId);
        this.accessTokenKey = ACCESS_TOKEN_KEY.concat(appId);
        this.accessTokenLockKey = LOCK_ACCESS_TOKEN_KEY.concat(appId);
    }

    @Override
    public String getAccessToken() {
        String accessToken = null;
        Jedis jedis = null;
        //注意:这里不要乱用jdk7的try-with-resources,try-with-resources会在结束后自动调用close方法
        //但前提是:括号里的资源实现类必须实现AutoCloseable或Closeable接口
        try {
            jedis = this.jedisPool.getResource();
            accessToken = jedis.get(this.accessTokenKey);
        } catch (Exception e) {
           log.error("[redis异常]读取失败", e);
        } finally {
            jedis.close();
        }
        return accessToken;
    }

    @Override
    public boolean isAccessTokenExpired() {
        Long result = null;
        Jedis jedis = null;
        try {
            jedis = this.jedisPool.getResource();
            result = jedis.ttl(accessTokenKey);
        } catch (Exception e) {
            log.error("[redis异常]读取失败", e);
        } finally {
            jedis.close();
        }
        return result != null && result < 0;
    }

    @Override
    public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
        //分布式锁
        RedisLockUtil lock = RedisLockUtil.getInstance(this.jedisPool);
        //抢到锁才能更改
        if(lock.lock(this.accessTokenLockKey, LOCKTIMEOUT)) {
            Jedis jedis = null;
            try {
                jedis = this.jedisPool.getResource();
                jedis.setex(this.accessTokenKey, expiresInSeconds - 200, accessToken);
                lock.unlock(this.accessTokenLockKey); //释放锁
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                jedis.close();
            }
        }
    }

    @Override
    public void expireAccessToken() {
        try (Jedis jedis = this.jedisPool.getResource()) {
            jedis.expire(this.accessTokenKey, 0);
        }
    }

}

redis分布式锁,采用双重防死锁

public class RedisLockUtil {
    private static RedisLockUtil mInstance = null;

    private final JedisPool jedisPool;

    private RedisLockUtil(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    public static RedisLockUtil getInstance(JedisPool jedisPool) {
        if(mInstance == null) {
            synchronized (RedisLockUtil.class) {
                if(mInstance == null) {
                    mInstance = new RedisLockUtil(jedisPool);
                }
            }
        }
        return mInstance;
    }

    /**
     * 加锁
     * 为了应对高并发,采用双重防死锁
     * @param key
     * @param lockTimeout 超时时间
     * (key, value) value:当前时间 + 超时时间
     * @return
     */
    public boolean lock(String key, int lockTimeout) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            Long setnxResult = jedis.setnx(key, String.valueOf(System.currentTimeMillis() + lockTimeout));
            if (setnxResult != null && setnxResult.intValue() == 1) {
                //为了防止死锁,设置个存活时间
                jedis.expire(key, lockTimeout);
                return true;
            }
            //未获取到锁,判断时间戳,看是否可以重置并获取到锁
            String lockValueStr = jedis.get(key);
            if (StringUtils.isNotBlank(lockValueStr) && System.currentTimeMillis() > Long.parseLong(lockValueStr)) {
                //旧锁已经过期
                //增强校验,重新赋值
                String getSetResult = jedis.getSet(key, String.valueOf(System.currentTimeMillis() + lockTimeout));
                //获取上一个锁的时间
                if (getSetResult == null || StringUtils.equals(lockValueStr, getSetResult)) {
                    //getSetResult == null,说明另外一个进程已经释放了锁
                    //lockValueStr与getSetResult相等,说明锁未被其他进程获取到
                    return true;
                }
            }
        } catch (Exception e) {
            log.error("[redis异常]", e);
        } finally {
            jedis.close();
        }
        return false;
    }

    /**
     * 解锁
     * @param key
     */
    public void unlock(String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            jedis.del(key);
        } catch (Exception e) {
            log.error("【redie分布式】解锁异常, {}", e);
        } finally {
            jedis.close();
        }
    }

//    public static void main(String[] args) {
//        //加锁
//        String productId = "123";
//        int TIMEOUT = 5000; //超时时间
//        long time = System.currentTimeMillis() + TIMEOUT;
//        RedisLock redisLock = new RedisLock();
//        if(!redisLock.lock(productId, String.valueOf(time))) {
//            throw new BusinessException(101, "哎哟喂,人太多了,换个姿势再试试");
//        }
//
    //todo 业务处理进行一系列操作...
//
//        //解锁
//        redisLock.unlock(productId, String.valueOf(time));
//    }
}
/**
 * Created by wujie on 2019/1/12.
 * 微信客户端配置存储
 * accessToken一般有效期为2个小时,一定要注意多线程并发时accessToken过期问题,以及集群或者分布式accessToken过期问题
 */
public interface WechatPSConfigStorage {
    
    String getAccessToken();

    Lock getAccessTokenLock();

    boolean isAccessTokenExpired();

    /**
     * 强制将access token过期掉
     */
    void expireAccessToken();

    /**
     * 应该是线程安全的
     * @param accessToken 要更新的WechatAccessToken对象
     */
    void updateAccessToken(WechatAccessToken accessToken);

    /**
     * 应该是线程安全的
     * @param accessToken      新的accessToken值
     * @param expiresInSeconds 过期时间,以秒为单位
     */
    void updateAccessToken(String accessToken, int expiresInSeconds);

    String getJsapiTicket();

    Lock getJsapiTicketLock();

    boolean isJsapiTicketExpired();

    /**
     * 强制将jsapi ticket过期掉
     */
    void expireJsapiTicket();

    /**
     * 应该是线程安全的
     * @param jsapiTicket      新的jsapi ticket值
     * @param expiresInSeconds 过期时间,以秒为单位
     */
    void updateJsapiTicket(String jsapiTicket, int expiresInSeconds);

    String getAppId();

    String getSecret();

    long getExpiresTime();
}
/**
 * Created wujie hp on 2019/1/12.
 * 基于内存的微信配置provider,只适用于单体
 * 在集群或者分布式生产环境中,需要将这些配置持久化
 */

public class WechatPSInMemoryConfigImpl implements WechatPSConfigStorage {

    protected volatile String appId;
    protected volatile String secret;
    protected volatile String accessToken;
    protected volatile long expiresTime; //过期时间:获取到accessToken的当前时间 + 返回json中的过期时间
    protected volatile String oauth2redirectUri;
    protected volatile String jsapiTicket;
    protected volatile long jsapiTicketExpiresTime;

    protected Lock accessTokenLock = new ReentrantLock(true); //公平锁
    protected Lock jsapiTicketLock = new ReentrantLock(true);

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    @Override
    public String getAccessToken() {
        return this.accessToken;
    }

    @Override
    public Lock getAccessTokenLock() {
        return this.accessTokenLock;
    }

    @Override
    public boolean isAccessTokenExpired() {
        return System.currentTimeMillis() > this.expiresTime;
    }

    @Override
    public void expireAccessToken() {
        this.expiresTime = 0;
    }

    //一定要保证线程安全
    @Override
    public synchronized void updateAccessToken(WechatAccessToken accessToken) {
        updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
    }

    //一定要保证线程安全
    @Override
    public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
        this.accessToken = accessToken;
        //这里考虑性能,预留个200秒,即在accessToken真实过期200秒之前就让它过期
        this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
    }

    public void setJsapiTicket(String jsapiTicket) {
        this.jsapiTicket = jsapiTicket;
    }

    @Override
    public String getJsapiTicket() {
        return this.jsapiTicket;
    }

    @Override
    public Lock getJsapiTicketLock() {
        return this.jsapiTicketLock;
    }

    @Override
    public boolean isJsapiTicketExpired() {
        return System.currentTimeMillis() > this.jsapiTicketExpiresTime;
    }

    @Override
    public void expireJsapiTicket() {
        this.jsapiTicketExpiresTime = 0;
    }

    //一定要保证线程安全
    @Override
    public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) {
        this.jsapiTicket = jsapiTicket;
        // 预留200秒的时间
        this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
    }

    @Override
    public String getAppId() {
        return this.appId;
    }

    public void setAppId(String appId) {
        this.appId = appId;
    }

    @Override
    public String getSecret() {
        return this.secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    @Override
    public long getExpiresTime() {
        return this.expiresTime;
    }

    public void setExpiresTime(long expiresTime) {
        this.expiresTime = expiresTime;
    }
}
/**
 * Created by wujie on 2019/1/11.
 * 微信公众号API的Service
 */
public interface WechatPSService {
    /**
     * 获取微信服务器IP地址
     */
    String GET_CALLBACK_IP_URL = "https://api.weixin.qq.com/cgi-bin/getcallbackip";

    /**
     * 获取access_token
     */
    String GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";

    /**
     * 获得jsapi_ticket
     */
    String GET_JSAPI_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi";

    /**
     * 长链接转短链接接口
     * 生成二维码的时候比较有用
     */
    String SHORTURL_API_URL = "https://api.weixin.qq.com/cgi-bin/shorturl";

    /**
     * 语义查询接口
     * 通过语义接口,接收用户发送的自然语言请求,让系统理解用户的说话内容。
     */
    String SEMANTIC_SEMPROXY_SEARCH_URL = "https://api.weixin.qq.com/semantic/semproxy/search";

    /**
     * 第三方使用网站应用授权登录的url
     */
    String QRCONNECT_URL = "https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect";

    /**
     * 获取公众号的自动回复规则
     */
    String GET_CURRENT_AUTOREPLY_INFO_URL = "https://api.weixin.qq.com/cgi-bin/get_current_autoreply_info";

    /**
     * 公众号调用或第三方平台帮公众号调用对公众号的所有api调用(包括第三方帮其调用)次数进行清零
     */
    String CLEAR_QUOTA_URL = "https://api.weixin.qq.com/cgi-bin/clear_quota";

    /**
     * 
     * 验证消息的确来自微信服务器
     * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421135319&token=&lang=zh_CN
     * 
*/
boolean checkSignature(String timestamp, String nonce, String signature); /** * 获取access_token, 不强制刷新access_token * @see #getAccessToken(boolean) */ String getAccessToken() throws WechatErrorException; /** *
     * 获取access_token,本方法线程安全
     * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
     * 另:本service的所有方法都会在access_token过期时调用此方法
     * 程序员在非必要情况下尽量不要主动调用此方法
     * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183&token=&lang=zh_CN
     * 
* @param forceRefresh 强制刷新 */
String getAccessToken(boolean forceRefresh) throws WechatErrorException; /** * 获得jsapi_ticket,不强制刷新jsapi_ticket * @see #getJsapiTicket(boolean) */ String getJsapiTicket() throws WechatErrorException; /** *
     * 获得jsapi_ticket
     * 获得时会检查jsapiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干
     * 详情请见:http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
     * 
* @param forceRefresh 强制刷新 */
String getJsapiTicket(boolean forceRefresh) throws WechatErrorException; /** *
     * 创建调用jsapi时所需要的签名
     * 详情请见:http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115&token=&lang=zh_CN
     * 
*/
WechatJsapiSignature createJsapiSignature(String url) throws WechatErrorException; }
/**
 * Created by wujie on 2019/1/12.
 * 微信公众号服务
 */
@Slf4j
public class WechatPSServiceImpl implements WechatPSService {

    private OkHttpClient httpClient;
    private WechatPSConfigStorage wechatPSConfigStorage;

    public WechatPSConfigStorage getWechatPSConfigStorage() {
        return wechatPSConfigStorage;
    }

    public void setWechatPSConfigStorage(WechatPSConfigStorage wechatPSConfigStorage) {
        this.wechatPSConfigStorage = wechatPSConfigStorage;
        initHttp();
    }

    @Override
    public boolean checkSignature(String timestamp, String nonce, String signature) {
        return false;
    }

    @Override
    public String getAccessToken() throws WechatErrorException {
        return getAccessToken(false);
    }

	//单机
    @Override
    public String getAccessToken(boolean forceRefresh) throws WechatErrorException {
        Lock lock = getWechatPSConfigStorage().getAccessTokenLock();
        try {
            lock.lock();

            if (getWechatPSConfigStorage().isAccessTokenExpired() || forceRefresh) {
                String url = String.format(WechatPSService.GET_ACCESS_TOKEN_URL,
                        getWechatPSConfigStorage().getAppId(), getWechatPSConfigStorage().getSecret());

                Request request = new Request.Builder().url(url).get().build();
                Response response = getRequestHttpClient().newCall(request).execute();
                String resultContent = response.body().string();
                //不知道返回类型,有可能是错误,有可能是正常返回
                WechatError error = WechatError.fromJson(resultContent, WechatType.WECHAT_PUBLIC_SUBSCRPTION);
                if (error != null && error.getErrcode() != 0) {
                    //0:是成功 非0是失败
                    throw new WechatErrorException(error);
                }
                WechatAccessToken accessToken = WechatAccessToken.fromJson(resultContent);
                getWechatPSConfigStorage().updateAccessToken(accessToken.getAccessToken(),
                        accessToken.getExpiresIn());
            }
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        } finally {
            lock.unlock();
        }
        return getWechatPSConfigStorage().getAccessToken();
    }

	//分布式
    @Override
    public String getAccessToken(boolean forceRefresh) throws WechatErrorException {
        Lock lock = getWechatPSConfigStorage().getAccessTokenLock();
        try {
            lock.lock();

            if (getWechatPSConfigStorage().isAccessTokenExpired() || forceRefresh) {
                if(getWechatPSConfigStorage().getAccessTokenRedisLock()) {
                    //拿到分布式锁,去刷新accessToken
                    String url = String.format(WechatPSService.GET_ACCESS_TOKEN_URL,
                            getWechatPSConfigStorage().getAppId(), getWechatPSConfigStorage().getSecret());

                    Request request = new Request.Builder().url(url).get().build();
                    Response response = getRequestHttpClient().newCall(request).execute();
                    String resultContent = response.body().string();
                    //不知道返回类型,有可能是错误,有可能是正常返回
                    WechatError error = WechatError.fromJson(resultContent, WechatType.WECHAT_PUBLIC_SUBSCRPTION);
                    if (error != null && error.getErrcode() != 0) {
                        //0:是成功 非0是失败
                        throw new WechatErrorException(error);
                    }
                    WechatAccessToken accessToken = WechatAccessToken.fromJson(resultContent);
                    getWechatPSConfigStorage().updateAccessToken(accessToken.getAccessToken(),
                            accessToken.getExpiresIn());

                    //解锁
                    getWechatPSConfigStorage().unlock();
                }

            }

        } catch (IOException e) {
            log.error(e.getMessage(), e);
        } finally {
            lock.unlock();
        }

        return getWechatPSConfigStorage().getAccessToken();
    }

	private static final int DEFAULT_TIMEOUT = 30; //默认超时时间30秒

    public OkHttpClient getRequestHttpClient() {
        return httpClient;
    }

    private void initHttp() {
        log.debug("initHttp...");
        HttpLoggingInterceptor logInterceptor = new HttpLoggingInterceptor();
        logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        httpClient = new OkHttpClient.Builder()
                .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                .readTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                .writeTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
                .addInterceptor(logInterceptor)
                .build();
    }
    
    @Override
    public String getJsapiTicket() throws WechatErrorException {
        return null;
    }

    @Override
    public String getJsapiTicket(boolean forceRefresh) throws WechatErrorException {
        return null;
    }

    @Override
    public WechatJsapiSignature createJsapiSignature(String url) throws WechatErrorException {
        return null;
    }

}

你可能感兴趣的:(java)