* 企业微信自研应用,增加Redisson的存储实现,支持分布式锁 * 增加redisson的依赖。 Co-authored-by: 袁启勋 <xun.yuan@infoship.cn>dev1
@@ -38,7 +38,11 @@ | |||
<groupId>org.slf4j</groupId> | |||
<artifactId>slf4j-api</artifactId> | |||
</dependency> | |||
<!--分布式锁支持--> | |||
<dependency> | |||
<groupId>org.redisson</groupId> | |||
<artifactId>redisson</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.testng</groupId> | |||
<artifactId>testng</artifactId> | |||
@@ -1,12 +1,125 @@ | |||
package me.chanjar.weixin.cp.api.impl; | |||
import com.google.gson.JsonObject; | |||
import com.google.gson.JsonParser; | |||
import me.chanjar.weixin.common.WxType; | |||
import me.chanjar.weixin.common.bean.WxAccessToken; | |||
import me.chanjar.weixin.common.error.WxError; | |||
import me.chanjar.weixin.common.error.WxErrorException; | |||
import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; | |||
import org.apache.http.client.config.RequestConfig; | |||
import org.apache.http.client.methods.CloseableHttpResponse; | |||
import org.apache.http.client.methods.HttpGet; | |||
import org.apache.http.impl.client.BasicResponseHandler; | |||
import org.apache.http.impl.client.CloseableHttpClient; | |||
import java.io.IOException; | |||
import java.util.concurrent.locks.Lock; | |||
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_AGENT_CONFIG_TICKET; | |||
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_JSAPI_TICKET; | |||
/** | |||
* <pre> | |||
* 默认接口实现类,使用apache httpclient实现 | |||
* Created by Binary Wang on 2017-5-27. | |||
* </pre> | |||
* <pre> | |||
* 增加分布式锁(基于WxCpConfigStorage实现)的支持 | |||
* Updated by yuanqixun on 2020-05-13 | |||
* </pre> | |||
* | |||
* | |||
* @author <a href="https://github.com/binarywang">Binary Wang</a> | |||
*/ | |||
public class WxCpServiceImpl extends WxCpServiceApacheHttpClientImpl { | |||
@Override | |||
public String getAccessToken(boolean forceRefresh) throws WxErrorException { | |||
if (!getWxCpConfigStorage().isAccessTokenExpired() && !forceRefresh) { | |||
return getWxCpConfigStorage().getAccessToken(); | |||
} | |||
Lock lock = getWxCpConfigStorage().getAccessTokenLock(); | |||
lock.lock(); | |||
try { | |||
// 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 | |||
if (!getWxCpConfigStorage().isAccessTokenExpired() && !forceRefresh) { | |||
return getWxCpConfigStorage().getAccessToken(); | |||
} | |||
String url = String.format(getWxCpConfigStorage().getApiUrl(WxCpApiPathConsts.GET_TOKEN), this.configStorage.getCorpId(), this.configStorage.getCorpSecret()); | |||
try { | |||
HttpGet httpGet = new HttpGet(url); | |||
if (getRequestHttpProxy() != null) { | |||
RequestConfig config = RequestConfig.custom() | |||
.setProxy(getRequestHttpProxy()).build(); | |||
httpGet.setConfig(config); | |||
} | |||
String resultContent; | |||
try (CloseableHttpClient httpClient = getRequestHttpClient(); | |||
CloseableHttpResponse response = httpClient.execute(httpGet)) { | |||
resultContent = new BasicResponseHandler().handleResponse(response); | |||
} finally { | |||
httpGet.releaseConnection(); | |||
} | |||
WxError error = WxError.fromJson(resultContent, WxType.CP); | |||
if (error.getErrorCode() != 0) { | |||
throw new WxErrorException(error); | |||
} | |||
WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); | |||
getWxCpConfigStorage().updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
} catch (IOException e) { | |||
throw new RuntimeException(e); | |||
} | |||
} finally { | |||
lock.unlock(); | |||
} | |||
return getWxCpConfigStorage().getAccessToken(); | |||
} | |||
@Override | |||
public String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException { | |||
if (forceRefresh) { | |||
getWxCpConfigStorage().expireAgentJsapiTicket(); | |||
} | |||
if (getWxCpConfigStorage().isAgentJsapiTicketExpired()) { | |||
Lock lock = getWxCpConfigStorage().getAgentJsapiTicketLock(); | |||
lock.lock(); | |||
try { | |||
// 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 | |||
if (getWxCpConfigStorage().isAgentJsapiTicketExpired()) { | |||
String responseContent = this.get(getWxCpConfigStorage().getApiUrl(GET_AGENT_CONFIG_TICKET), null); | |||
JsonObject jsonObject = new JsonParser().parse(responseContent).getAsJsonObject(); | |||
getWxCpConfigStorage().updateAgentJsapiTicket(jsonObject.get("ticket").getAsString(), | |||
jsonObject.get("expires_in").getAsInt()); | |||
} | |||
} finally { | |||
lock.unlock(); | |||
} | |||
} | |||
return getWxCpConfigStorage().getAgentJsapiTicket(); | |||
} | |||
@Override | |||
public String getJsapiTicket(boolean forceRefresh) throws WxErrorException { | |||
if (forceRefresh) { | |||
getWxCpConfigStorage().expireJsapiTicket(); | |||
} | |||
if (getWxCpConfigStorage().isJsapiTicketExpired()) { | |||
Lock lock = getWxCpConfigStorage().getJsapiTicketLock(); | |||
lock.lock(); | |||
try { | |||
// 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 | |||
if (getWxCpConfigStorage().isJsapiTicketExpired()) { | |||
String responseContent = this.get(getWxCpConfigStorage().getApiUrl(GET_JSAPI_TICKET), null); | |||
JsonObject tmpJsonObject = new JsonParser().parse(responseContent).getAsJsonObject(); | |||
getWxCpConfigStorage().updateJsapiTicket(tmpJsonObject.get("ticket").getAsString(), | |||
tmpJsonObject.get("expires_in").getAsInt()); | |||
} | |||
} finally { | |||
lock.unlock(); | |||
} | |||
} | |||
return getWxCpConfigStorage().getJsapiTicket(); | |||
} | |||
} |
@@ -4,6 +4,7 @@ import me.chanjar.weixin.common.bean.WxAccessToken; | |||
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; | |||
import java.io.File; | |||
import java.util.concurrent.locks.Lock; | |||
/** | |||
* 微信客户端配置存储. | |||
@@ -28,6 +29,8 @@ public interface WxCpConfigStorage { | |||
String getAccessToken(); | |||
Lock getAccessTokenLock(); | |||
boolean isAccessTokenExpired(); | |||
/** | |||
@@ -41,6 +44,8 @@ public interface WxCpConfigStorage { | |||
String getJsapiTicket(); | |||
Lock getJsapiTicketLock(); | |||
boolean isJsapiTicketExpired(); | |||
/** | |||
@@ -55,6 +60,8 @@ public interface WxCpConfigStorage { | |||
String getAgentJsapiTicket(); | |||
Lock getAgentJsapiTicketLock(); | |||
boolean isAgentJsapiTicketExpired(); | |||
/** | |||
@@ -8,6 +8,8 @@ import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; | |||
import java.io.File; | |||
import java.io.Serializable; | |||
import java.util.concurrent.locks.Lock; | |||
import java.util.concurrent.locks.ReentrantLock; | |||
/** | |||
* 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化. | |||
@@ -22,6 +24,7 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | |||
private volatile String token; | |||
protected volatile String accessToken; | |||
protected Lock accessTokenLock = new ReentrantLock(); | |||
private volatile String aesKey; | |||
protected volatile Integer agentId; | |||
private volatile long expiresTime; | |||
@@ -34,9 +37,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | |||
private volatile String httpProxyPassword; | |||
private volatile String jsapiTicket; | |||
protected Lock jsapiTicketLock = new ReentrantLock(); | |||
private volatile long jsapiTicketExpiresTime; | |||
private volatile String agentJsapiTicket; | |||
protected Lock agentJsapiTicketLock = new ReentrantLock(); | |||
private volatile long agentJsapiTicketExpiresTime; | |||
private volatile File tmpDirFile; | |||
@@ -63,6 +68,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | |||
return this.accessToken; | |||
} | |||
@Override | |||
public Lock getAccessTokenLock() { | |||
return this.accessTokenLock; | |||
} | |||
public void setAccessToken(String accessToken) { | |||
this.accessToken = accessToken; | |||
} | |||
@@ -93,6 +103,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | |||
return this.jsapiTicket; | |||
} | |||
@Override | |||
public Lock getJsapiTicketLock() { | |||
return this.jsapiTicketLock; | |||
} | |||
public void setJsapiTicket(String jsapiTicket) { | |||
this.jsapiTicket = jsapiTicket; | |||
} | |||
@@ -122,6 +137,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | |||
return this.agentJsapiTicket; | |||
} | |||
@Override | |||
public Lock getAgentJsapiTicketLock() { | |||
return this.agentJsapiTicketLock; | |||
} | |||
@Override | |||
public boolean isAgentJsapiTicketExpired() { | |||
return System.currentTimeMillis() > this.agentJsapiTicketExpiresTime; | |||
@@ -9,6 +9,8 @@ import redis.clients.jedis.JedisPool; | |||
import redis.clients.jedis.JedisPoolConfig; | |||
import java.io.File; | |||
import java.util.concurrent.locks.Lock; | |||
import java.util.concurrent.locks.ReentrantLock; | |||
/** | |||
* <pre> | |||
@@ -89,6 +91,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { | |||
} | |||
} | |||
@Override | |||
public Lock getAccessTokenLock() { | |||
return new ReentrantLock(); | |||
} | |||
@Override | |||
public boolean isAccessTokenExpired() { | |||
try (Jedis jedis = this.jedisPool.getResource()) { | |||
@@ -132,6 +139,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { | |||
} | |||
} | |||
@Override | |||
public Lock getJsapiTicketLock() { | |||
return new ReentrantLock(); | |||
} | |||
@Override | |||
public boolean isJsapiTicketExpired() { | |||
try (Jedis jedis = this.jedisPool.getResource()) { | |||
@@ -170,6 +182,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { | |||
} | |||
} | |||
@Override | |||
public Lock getAgentJsapiTicketLock() { | |||
return new ReentrantLock(); | |||
} | |||
@Override | |||
public boolean isAgentJsapiTicketExpired() { | |||
try (Jedis jedis = this.jedisPool.getResource()) { | |||
@@ -0,0 +1,154 @@ | |||
package me.chanjar.weixin.cp.config.impl; | |||
import lombok.NonNull; | |||
import me.chanjar.weixin.common.bean.WxAccessToken; | |||
import me.chanjar.weixin.common.redis.RedissonWxRedisOps; | |||
import me.chanjar.weixin.common.redis.WxRedisOps; | |||
import org.apache.commons.lang3.StringUtils; | |||
import org.redisson.api.RedissonClient; | |||
import java.util.concurrent.TimeUnit; | |||
import java.util.concurrent.locks.Lock; | |||
/** | |||
* 基于Redisson的实现 | |||
* | |||
* @author yuanqixun | |||
* @date 2020/5/13 | |||
*/ | |||
public class WxCpRedissonConfigImpl extends WxCpDefaultConfigImpl { | |||
protected final static String LOCK_KEY = "wechat_cp_lock:"; | |||
protected final static String CP_ACCESS_TOKEN_KEY = "wechat_cp_access_token_key:"; | |||
protected final static String CP_JSAPI_TICKET_KEY = "wechat_cp_jsapi_ticket_key:"; | |||
protected final static String CP_AGENT_JSAPI_TICKET_KEY = "wechat_cp_agent_jsapi_ticket_key:"; | |||
/** | |||
* redis 存储的 key 的前缀,可为空 | |||
*/ | |||
protected String keyPrefix; | |||
protected String accessTokenKey; | |||
protected String jsapiTicketKey; | |||
protected String agentJsapiTicketKey; | |||
protected String lockKey; | |||
private final WxRedisOps redisOps; | |||
public WxCpRedissonConfigImpl(@NonNull RedissonClient redissonClient, String keyPrefix) { | |||
this(new RedissonWxRedisOps(redissonClient), keyPrefix); | |||
} | |||
public WxCpRedissonConfigImpl(@NonNull RedissonClient redissonClient) { | |||
this(redissonClient, null); | |||
} | |||
private WxCpRedissonConfigImpl(@NonNull WxRedisOps redisOps, String keyPrefix) { | |||
this.redisOps = redisOps; | |||
this.keyPrefix = keyPrefix; | |||
} | |||
/** | |||
* 设置企业微信自研应用ID(整数),同时初始化相关的redis key,注意要先调用setCorpId,再调用setAgentId | |||
* | |||
* @param agentId | |||
*/ | |||
@Override | |||
public void setAgentId(Integer agentId) { | |||
super.setAgentId(agentId); | |||
String ukey = getCorpId().concat(":").concat(String.valueOf(agentId)); | |||
String prefix = StringUtils.isBlank(keyPrefix) ? "" : | |||
(StringUtils.endsWith(keyPrefix, ":") ? keyPrefix : (keyPrefix + ":")); | |||
lockKey = prefix + LOCK_KEY.concat(ukey); | |||
accessTokenKey = prefix + CP_ACCESS_TOKEN_KEY.concat(ukey); | |||
jsapiTicketKey = prefix + CP_JSAPI_TICKET_KEY.concat(ukey); | |||
agentJsapiTicketKey = prefix + CP_AGENT_JSAPI_TICKET_KEY.concat(ukey); | |||
} | |||
protected Lock getLockByKey(String key) { | |||
return redisOps.getLock(key); | |||
} | |||
@Override | |||
public Lock getAccessTokenLock() { | |||
return getLockByKey(this.lockKey.concat(":").concat("accessToken")); | |||
} | |||
@Override | |||
public Lock getAgentJsapiTicketLock() { | |||
return getLockByKey(this.lockKey.concat(":").concat("agentJsapiTicket")); | |||
} | |||
@Override | |||
public Lock getJsapiTicketLock() { | |||
return getLockByKey(this.lockKey.concat(":").concat("jsapiTicket")); | |||
} | |||
@Override | |||
public String getAccessToken() { | |||
return redisOps.getValue(this.accessTokenKey); | |||
} | |||
@Override | |||
public boolean isAccessTokenExpired() { | |||
Long expire = redisOps.getExpire(this.accessTokenKey); | |||
return expire == null || expire < 2; | |||
} | |||
@Override | |||
public void updateAccessToken(WxAccessToken accessToken) { | |||
redisOps.setValue(this.accessTokenKey, accessToken.getAccessToken(), accessToken.getExpiresIn(), TimeUnit.SECONDS); | |||
} | |||
@Override | |||
public void updateAccessToken(String accessToken, int expiresInSeconds) { | |||
redisOps.setValue(this.accessTokenKey, accessToken, expiresInSeconds, TimeUnit.SECONDS); | |||
} | |||
@Override | |||
public void expireAccessToken() { | |||
redisOps.expire(this.accessTokenKey, 0, TimeUnit.SECONDS); | |||
} | |||
@Override | |||
public String getJsapiTicket() { | |||
return redisOps.getValue(this.jsapiTicketKey); | |||
} | |||
@Override | |||
public boolean isJsapiTicketExpired() { | |||
Long expire = redisOps.getExpire(this.jsapiTicketKey); | |||
return expire == null || expire < 2; | |||
} | |||
@Override | |||
public void expireJsapiTicket() { | |||
redisOps.expire(this.jsapiTicketKey, 0, TimeUnit.SECONDS); | |||
} | |||
@Override | |||
public void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) { | |||
redisOps.setValue(this.jsapiTicketKey, jsapiTicket, expiresInSeconds, TimeUnit.SECONDS); | |||
} | |||
@Override | |||
public void expireAgentJsapiTicket() { | |||
redisOps.expire(this.agentJsapiTicketKey, 0, TimeUnit.SECONDS); | |||
} | |||
@Override | |||
public void updateAgentJsapiTicket(String agentJsapiTicket, int expiresInSeconds) { | |||
redisOps.setValue(this.agentJsapiTicketKey, agentJsapiTicket, expiresInSeconds, TimeUnit.SECONDS); | |||
} | |||
@Override | |||
public String getAgentJsapiTicket() { | |||
return redisOps.getValue(this.agentJsapiTicketKey); | |||
} | |||
@Override | |||
public boolean isAgentJsapiTicketExpired() { | |||
Long expire = redisOps.getExpire(this.agentJsapiTicketKey); | |||
return expire == null || expire < 2; | |||
} | |||
} |
@@ -23,9 +23,6 @@ public class WxMaRedissonConfigImpl extends WxMaDefaultConfigImpl { | |||
protected final static String MA_JSAPI_TICKET_KEY = "wechat_ma_jsapi_ticket_key:"; | |||
protected final static String MA_CARD_API_TICKET_KEY = "wechat_ma_card_api_ticket_key:"; | |||
/** | |||
* redis 存储的 key 的前缀,可为空 | |||
*/ | |||
/** | |||
* redis 存储的 key 的前缀,可为空 | |||
*/ | |||