Background:
access_token is the globally unique ticket of the public account. The public account needs to use access_token when calling each interface. Developers need to store it properly. At least 512 characters of space must be reserved for access_token storage. The validity period of access_token is currently 2 hours and needs to be refreshed regularly. Repeated acquisition will cause the last access_token to become invalid.
1、为了保密appsecrect,第三方需要一个access_token获取和刷新的中控服务器。而其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则会造成access_token覆盖而影响业务; 2、目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器对外输出的依然是老access_token,此时公众平台后台会保证在刷新短时间内,新老access_token都可用,这保证了第三方业务的平滑过渡; 3、access_token的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新access_token的接口,这样便于业务服务器在API调用获知access_token已超时的情况下,可以触发access_token的刷新流程。 简单起见,使用一个随servlet容器一起启动的servlet来实现获取access_token的功能,具体为:因为该servlet随着web容器而启动,在该servlet的init方法中触发一个线程来获得access_token,该线程是一个无线循环的线程,每隔2个小时刷新一次access_token。相关代码如下: :
public class InitServlet extends HttpServlet { private static final long serialVersionUID = 1L; public void init(ServletConfig config) throws ServletException { new Thread(new AccessTokenThread()).start(); } }
2) Thread code :
public class AccessTokenThread implements Runnable { public static AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token if(token != null){ accessToken = token; }else{ System.out.println("get access_token failed------------------------------"); } }catch(IOException e){ e.printStackTrace(); } try{ if(null != accessToken){ Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒 }else{ Thread.sleep(60 * 1000); // 如果access_token为null,60秒后再获取 } }catch(InterruptedException e){ try{ Thread.sleep(60 * 1000); }catch(InterruptedException e1){ e1.printStackTrace(); } } } } }
3) AccessToken code:
public class AccessToken { private String access_token; private long expire_in; // access_token有效时间,单位为妙 public String getAccess_token() { return access_token; } public void setAccess_token(String access_token) { this.access_token = access_token; } public long getExpire_in() { return expire_in; } public void setExpire_in(long expire_in) { this.expire_in = expire_in; } }
## 4) servlet configuration in web.xml
<servlet> <servlet-name>initServlet</servlet-name> <servlet-class>com.sinaapp.wx.servlet.InitServlet</servlet-class> <load-on-startup>0</load-on-startup> </servlet>
Leading to multi-thread concurrency issues:
1) There seems to be no problem with the above implementation, but if you think about it carefully, the accessToken in the AccessTokenThread class , it has the problem of concurrent access. It is only updated every 2 hours by AccessTokenThread, but there will be many threads to read it. It is a typical scenario of more reading and less writing, and only one thread writes. Since there are concurrent reads and writes, there must be a problem with the above code. The easiest way to think of is to use synchronized to handle it:public class AccessTokenThread implements Runnable { private static AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token if(token != null){ AccessTokenThread.setAccessToken(token); }else{ System.out.println("get access_token failed"); } }catch(IOException e){ e.printStackTrace(); } try{ if(null != accessToken){ Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒 }else{ Thread.sleep(60 * 1000); // 如果access_token为null,60秒后再获取 } }catch(InterruptedException e){ try{ Thread.sleep(60 * 1000); }catch(InterruptedException e1){ e1.printStackTrace(); } } } } public synchronized static AccessToken getAccessToken() { return accessToken; } private synchronized static void setAccessToken(AccessToken accessToken) { AccessTokenThread2.accessToken = accessToken; } }
2) Solution 1:
We let the AccessTokenThread.getAccessToken() method return a copy of the accessToken object, so that other The thread cannot modify the accessToken in the AccessTokenThread class. Modify the AccessTokenThread.getAccessToken() method as follows:public synchronized static AccessToken getAccessToken() { AccessToken at = new AccessToken(); at.setAccess_token(accessToken.getAccess_token()); at.setExpire_in(accessToken.getExpire_in()); return at; }
3) Solution 2:
Since we should not let the AccessToken object be modified, then why don’t we define accessToken as a "Immutable objects"? The relevant modifications are as follows:public class AccessToken { private final String access_token; private final long expire_in; // access_token有效时间,单位为妙 public AccessToken(String access_token, long expire_in) { this.access_token = access_token; this.expire_in = expire_in; } public String getAccess_token() { return access_token; } public long getExpire_in() { return expire_in; } }
4) Solution 3:
Is there any other better, more perfect, more efficient method? Let's analyze it. In solution two, AccessTokenUtil.freshAccessToken() returns an immutable object, and then calls the private AccessTokenThread.setAccessToken(AccessToken accessToken) method to assign the value. What role does synchronized synchronization play in this method? Because the object is immutable and only one thread can call the setAccessToken method, synchronized here does not play a "mutual exclusion" role (because only one thread can modify it), but only plays a role in ensuring "visibility". Make the modification visible to other threads, that is, let other threads access the latest accessToken object. To ensure "visibility", volatile can be used, so synchronized here should not be necessary. We use volatile to replace it. The relevant modification code is as follows:public class AccessTokenThread implements Runnable { private static volatile AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token if(token != null){ AccessTokenThread2.setAccessToken(token); }else{ System.out.println("get access_token failed"); } }catch(IOException e){ e.printStackTrace(); } try{ if(null != accessToken){ Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒 }else{ Thread.sleep(60 * 1000); // 如果access_token为null,60秒后再获取 } }catch(InterruptedException e){ try{ Thread.sleep(60 * 1000); }catch(InterruptedException e1){ e1.printStackTrace(); } } } } private static void setAccessToken(AccessToken accessToken) { AccessTokenThread2.accessToken = accessToken; } public static AccessToken getAccessToken() { return accessToken; } }
public class AccessTokenThread implements Runnable { private static volatile AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token if(token != null){ accessToken = token; }else{ System.out.println("get access_token failed"); } }catch(IOException e){ e.printStackTrace(); } try{ if(null != accessToken){ Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒 }else{ Thread.sleep(60 * 1000); // 如果access_token为null,60秒后再获取 } }catch(InterruptedException e){ try{ Thread.sleep(60 * 1000); }catch(InterruptedException e1){ e1.printStackTrace(); } } } } public static AccessToken getAccessToken() { return accessToken; } }
public class AccessTokenThread implements Runnable { public static volatile AccessToken accessToken; @Override public void run() { while(true) { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token if(token != null){ accessToken = token; }else{ System.out.println("get access_token failed"); } }catch(IOException e){ e.printStackTrace(); } try{ if(null != accessToken){ Thread.sleep((accessToken.getExpire_in() - 200) * 1000); // 休眠7000秒 }else{ Thread.sleep(60 * 1000); // 如果access_token为null,60秒后再获取 } }catch(InterruptedException e){ try{ Thread.sleep(60 * 1000); }catch(InterruptedException e1){ e1.printStackTrace(); } } } } }
accesToken变成了public,可以直接是一个AccessTokenThread.accessToken来访问。但是为了后期维护,最好还是不要改成public.
其实这个问题的关键是:在多线程并发访问的环境中如何正确的发布一个共享对象。
其实我们也可以使用Executors.newScheduledThreadPool来搞定:
public class InitServlet2 extends HttpServlet { private static final long serialVersionUID = 1L; public void init(ServletConfig config) throws ServletException { ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); executor.scheduleAtFixedRate(new AccessTokenRunnable(), 0, 7200-200, TimeUnit.SECONDS); } }
public class AccessTokenRunnable implements Runnable { private static volatile AccessToken accessToken; @Override public void run() { try{ AccessToken token = AccessTokenUtil.freshAccessToken(); // 从微信服务器刷新access_token if(token != null){ accessToken = token; }else{ System.out.println("get access_token failed"); } }catch(IOException e){ e.printStackTrace(); } } public static AccessToken getAccessToken() { while(accessToken == null){ try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } return accessToken; } }
获取accessToken方式变成了:AccessTokenRunnable.getAccessToken();
更多由获取微信access_token引出的Java多线程并发问题相关文章请关注PHP中文网!