* 企业微信自研应用,增加Redisson的存储实现,支持分布式锁 * 增加redisson的依赖。 Co-authored-by: 袁启勋 <xun.yuan@infoship.cn>dev1
| @@ -38,7 +38,11 @@ | |||||
| <groupId>org.slf4j</groupId> | <groupId>org.slf4j</groupId> | ||||
| <artifactId>slf4j-api</artifactId> | <artifactId>slf4j-api</artifactId> | ||||
| </dependency> | </dependency> | ||||
| <!--分布式锁支持--> | |||||
| <dependency> | |||||
| <groupId>org.redisson</groupId> | |||||
| <artifactId>redisson</artifactId> | |||||
| </dependency> | |||||
| <dependency> | <dependency> | ||||
| <groupId>org.testng</groupId> | <groupId>org.testng</groupId> | ||||
| <artifactId>testng</artifactId> | <artifactId>testng</artifactId> | ||||
| @@ -1,12 +1,125 @@ | |||||
| package me.chanjar.weixin.cp.api.impl; | 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> | * <pre> | ||||
| * 默认接口实现类,使用apache httpclient实现 | * 默认接口实现类,使用apache httpclient实现 | ||||
| * Created by Binary Wang on 2017-5-27. | * Created by Binary Wang on 2017-5-27. | ||||
| * </pre> | * </pre> | ||||
| * <pre> | |||||
| * 增加分布式锁(基于WxCpConfigStorage实现)的支持 | |||||
| * Updated by yuanqixun on 2020-05-13 | |||||
| * </pre> | |||||
| * | |||||
| * | * | ||||
| * @author <a href="https://github.com/binarywang">Binary Wang</a> | * @author <a href="https://github.com/binarywang">Binary Wang</a> | ||||
| */ | */ | ||||
| public class WxCpServiceImpl extends WxCpServiceApacheHttpClientImpl { | 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 me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; | ||||
| import java.io.File; | import java.io.File; | ||||
| import java.util.concurrent.locks.Lock; | |||||
| /** | /** | ||||
| * 微信客户端配置存储. | * 微信客户端配置存储. | ||||
| @@ -28,6 +29,8 @@ public interface WxCpConfigStorage { | |||||
| String getAccessToken(); | String getAccessToken(); | ||||
| Lock getAccessTokenLock(); | |||||
| boolean isAccessTokenExpired(); | boolean isAccessTokenExpired(); | ||||
| /** | /** | ||||
| @@ -41,6 +44,8 @@ public interface WxCpConfigStorage { | |||||
| String getJsapiTicket(); | String getJsapiTicket(); | ||||
| Lock getJsapiTicketLock(); | |||||
| boolean isJsapiTicketExpired(); | boolean isJsapiTicketExpired(); | ||||
| /** | /** | ||||
| @@ -55,6 +60,8 @@ public interface WxCpConfigStorage { | |||||
| String getAgentJsapiTicket(); | String getAgentJsapiTicket(); | ||||
| Lock getAgentJsapiTicketLock(); | |||||
| boolean isAgentJsapiTicketExpired(); | boolean isAgentJsapiTicketExpired(); | ||||
| /** | /** | ||||
| @@ -8,6 +8,8 @@ import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.Serializable; | import java.io.Serializable; | ||||
| import java.util.concurrent.locks.Lock; | |||||
| import java.util.concurrent.locks.ReentrantLock; | |||||
| /** | /** | ||||
| * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化. | * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化. | ||||
| @@ -22,6 +24,7 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | |||||
| private volatile String token; | private volatile String token; | ||||
| protected volatile String accessToken; | protected volatile String accessToken; | ||||
| protected Lock accessTokenLock = new ReentrantLock(); | |||||
| private volatile String aesKey; | private volatile String aesKey; | ||||
| protected volatile Integer agentId; | protected volatile Integer agentId; | ||||
| private volatile long expiresTime; | private volatile long expiresTime; | ||||
| @@ -34,9 +37,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | |||||
| private volatile String httpProxyPassword; | private volatile String httpProxyPassword; | ||||
| private volatile String jsapiTicket; | private volatile String jsapiTicket; | ||||
| protected Lock jsapiTicketLock = new ReentrantLock(); | |||||
| private volatile long jsapiTicketExpiresTime; | private volatile long jsapiTicketExpiresTime; | ||||
| private volatile String agentJsapiTicket; | private volatile String agentJsapiTicket; | ||||
| protected Lock agentJsapiTicketLock = new ReentrantLock(); | |||||
| private volatile long agentJsapiTicketExpiresTime; | private volatile long agentJsapiTicketExpiresTime; | ||||
| private volatile File tmpDirFile; | private volatile File tmpDirFile; | ||||
| @@ -63,6 +68,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | |||||
| return this.accessToken; | return this.accessToken; | ||||
| } | } | ||||
| @Override | |||||
| public Lock getAccessTokenLock() { | |||||
| return this.accessTokenLock; | |||||
| } | |||||
| public void setAccessToken(String accessToken) { | public void setAccessToken(String accessToken) { | ||||
| this.accessToken = accessToken; | this.accessToken = accessToken; | ||||
| } | } | ||||
| @@ -93,6 +103,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | |||||
| return this.jsapiTicket; | return this.jsapiTicket; | ||||
| } | } | ||||
| @Override | |||||
| public Lock getJsapiTicketLock() { | |||||
| return this.jsapiTicketLock; | |||||
| } | |||||
| public void setJsapiTicket(String jsapiTicket) { | public void setJsapiTicket(String jsapiTicket) { | ||||
| this.jsapiTicket = jsapiTicket; | this.jsapiTicket = jsapiTicket; | ||||
| } | } | ||||
| @@ -122,6 +137,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | |||||
| return this.agentJsapiTicket; | return this.agentJsapiTicket; | ||||
| } | } | ||||
| @Override | |||||
| public Lock getAgentJsapiTicketLock() { | |||||
| return this.agentJsapiTicketLock; | |||||
| } | |||||
| @Override | @Override | ||||
| public boolean isAgentJsapiTicketExpired() { | public boolean isAgentJsapiTicketExpired() { | ||||
| return System.currentTimeMillis() > this.agentJsapiTicketExpiresTime; | return System.currentTimeMillis() > this.agentJsapiTicketExpiresTime; | ||||
| @@ -9,6 +9,8 @@ import redis.clients.jedis.JedisPool; | |||||
| import redis.clients.jedis.JedisPoolConfig; | import redis.clients.jedis.JedisPoolConfig; | ||||
| import java.io.File; | import java.io.File; | ||||
| import java.util.concurrent.locks.Lock; | |||||
| import java.util.concurrent.locks.ReentrantLock; | |||||
| /** | /** | ||||
| * <pre> | * <pre> | ||||
| @@ -89,6 +91,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { | |||||
| } | } | ||||
| } | } | ||||
| @Override | |||||
| public Lock getAccessTokenLock() { | |||||
| return new ReentrantLock(); | |||||
| } | |||||
| @Override | @Override | ||||
| public boolean isAccessTokenExpired() { | public boolean isAccessTokenExpired() { | ||||
| try (Jedis jedis = this.jedisPool.getResource()) { | try (Jedis jedis = this.jedisPool.getResource()) { | ||||
| @@ -132,6 +139,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { | |||||
| } | } | ||||
| } | } | ||||
| @Override | |||||
| public Lock getJsapiTicketLock() { | |||||
| return new ReentrantLock(); | |||||
| } | |||||
| @Override | @Override | ||||
| public boolean isJsapiTicketExpired() { | public boolean isJsapiTicketExpired() { | ||||
| try (Jedis jedis = this.jedisPool.getResource()) { | try (Jedis jedis = this.jedisPool.getResource()) { | ||||
| @@ -170,6 +182,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage { | |||||
| } | } | ||||
| } | } | ||||
| @Override | |||||
| public Lock getAgentJsapiTicketLock() { | |||||
| return new ReentrantLock(); | |||||
| } | |||||
| @Override | @Override | ||||
| public boolean isAgentJsapiTicketExpired() { | public boolean isAgentJsapiTicketExpired() { | ||||
| try (Jedis jedis = this.jedisPool.getResource()) { | 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_JSAPI_TICKET_KEY = "wechat_ma_jsapi_ticket_key:"; | ||||
| protected final static String MA_CARD_API_TICKET_KEY = "wechat_ma_card_api_ticket_key:"; | protected final static String MA_CARD_API_TICKET_KEY = "wechat_ma_card_api_ticket_key:"; | ||||
| /** | |||||
| * redis 存储的 key 的前缀,可为空 | |||||
| */ | |||||
| /** | /** | ||||
| * redis 存储的 key 的前缀,可为空 | * redis 存储的 key 的前缀,可为空 | ||||
| */ | */ | ||||