From cb3f1051219ebd3d177d414efe4e0fb260f998e2 Mon Sep 17 00:00:00 2001 From: yuanqixun Date: Thu, 14 May 2020 14:14:31 +0800 Subject: [PATCH] =?UTF-8?q?:new:=20#1562=E4=BC=81=E4=B8=9A=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E8=87=AA=E7=A0=94=E5=BA=94=E7=94=A8=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?Redisson=E7=9A=84=E5=AD=98=E5=82=A8=E5=AE=9E=E7=8E=B0=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=88=86=E5=B8=83=E5=BC=8F=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 企业微信自研应用,增加Redisson的存储实现,支持分布式锁 * 增加redisson的依赖。 Co-authored-by: 袁启勋 --- weixin-java-cp/pom.xml | 6 +- .../weixin/cp/api/impl/WxCpServiceImpl.java | 113 +++++++++++++ .../weixin/cp/config/WxCpConfigStorage.java | 7 + .../cp/config/impl/WxCpDefaultConfigImpl.java | 20 +++ .../cp/config/impl/WxCpRedisConfigImpl.java | 17 ++ .../config/impl/WxCpRedissonConfigImpl.java | 154 ++++++++++++++++++ .../config/impl/WxMaRedissonConfigImpl.java | 3 - 7 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedissonConfigImpl.java diff --git a/weixin-java-cp/pom.xml b/weixin-java-cp/pom.xml index d7d69f48c..3f5a0742e 100644 --- a/weixin-java-cp/pom.xml +++ b/weixin-java-cp/pom.xml @@ -38,7 +38,11 @@ org.slf4j slf4j-api - + + + org.redisson + redisson + org.testng testng diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java index a7538c6ea..016c880fd 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java @@ -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; + /** *
  *  默认接口实现类,使用apache httpclient实现
  * Created by Binary Wang on 2017-5-27.
  * 
+ *
+ * 增加分布式锁(基于WxCpConfigStorage实现)的支持
+ * Updated by yuanqixun on 2020-05-13
+ * 
+ * * * @author Binary Wang */ 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(); + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java index c6ccf893b..19c04f4be 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java @@ -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(); /** diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java index 9f182180a..bd407af6d 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java @@ -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; diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java index 5b48f547f..eb22bc4c7 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java @@ -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; /** *
@@ -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()) {
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedissonConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedissonConfigImpl.java
new file mode 100644
index 000000000..b17857859
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedissonConfigImpl.java
@@ -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;
+  }
+
+}
diff --git a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/impl/WxMaRedissonConfigImpl.java b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/impl/WxMaRedissonConfigImpl.java
index ef5facb82..c45ad5094 100644
--- a/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/impl/WxMaRedissonConfigImpl.java
+++ b/weixin-java-miniapp/src/main/java/cn/binarywang/wx/miniapp/config/impl/WxMaRedissonConfigImpl.java
@@ -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 的前缀,可为空
    */