整个项目的结果采用Maven聚合工程,方便以后小程序、微信支付、开放平台的Api的封装扩展。
注意:分布式环境下,accessToken获取和更新尽量放在一台中控服务器上,一定要避免各个业务逻辑点各自刷新accessToken, 这样会反复导致accessToken重复刷新,过期问题。比如有这样一个业务场景:我们部署了10台服务器,每台服务器都可以刷新accessToken。 当每个点accessToken过期时,并发量过来,有5台服务器同时发现redis过期,去刷新accessToken,这样会导致token反复过期。
如果不采用中控服务器,可以在发现accessToken过期时,采用分布式锁,保证只有一个业务点去刷新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);
}
}
}
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;
}
}