微信公眾平台是騰訊為了讓用戶申請和管理微信公眾賬號而推出的一個web平台。微信公眾賬號的種類可以分為3種,並且一旦選定不可更改。按照功能的限制從小到大依次為:訂閱號、服務號、企業號。個人只能注冊訂閱號。注冊地址:https://mp.weixin.qq.com/。
開發環境的准備
- 微信公眾號
- 外網映射工具(開發調試)
與微信的對接的URL應該滿足以下的條件:
- 在公網上能夠訪問
- 只支持80端口
映射工具有很多,例如花生殼,ngrok可以將內網映射到公網上面,這樣就可以使用公網訪問本機的網絡服務。下載鏈接: http://pan.baidu.com/s/1i3u26St 密碼: v4e8(里面有簡明的教程)。
微信公眾號的數據交互原理
我們的主要開發就是微信公眾號服務器的開發。
開發模式的接入
進入微信公眾號平台之后進入開發者中心,在開發者中心中找到開發者文檔,在新手指南中有接入的相關步驟。依據接入文檔有以下的實現:
1 package org.gpf.servlet; 2 3 import java.io.IOException; 4 import java.io.PrintWriter; 5 6 import javax.servlet.ServletException; 7 import javax.servlet.http.HttpServlet; 8 import javax.servlet.http.HttpServletRequest; 9 import javax.servlet.http.HttpServletResponse; 10 11 import org.gpf.util.CheckUtil; 12 /** 13 * 接收微信服務器發送的4個參數並返回echostr 14 */ 15 public class WeixinServlet extends HttpServlet { 16 17 public void doGet(HttpServletRequest request, HttpServletResponse response) 18 throws ServletException, IOException { 19 20 // 接收微信服務器以Get請求發送的4個參數 21 String signature = request.getParameter("signature"); 22 String timestamp = request.getParameter("timestamp"); 23 String nonce = request.getParameter("nonce"); 24 String echostr = request.getParameter("echostr"); 25 26 PrintWriter out = response.getWriter(); 27 if (CheckUtil.checkSignature(signature, timestamp, nonce)) { 28 out.print(echostr); // 校驗通過,原樣返回echostr參數內容 29 } 30 } 31 32 public void doPost(HttpServletRequest request, HttpServletResponse response) 33 throws ServletException, IOException { 34 35 doGet(request, response); 36 } 37 38 }
校驗工具類:
1 package org.gpf.util; 2 3 import java.util.Arrays; 4 5 import org.apache.commons.codec.digest.DigestUtils; 6 7 /** 8 * 校驗的工具類 9 */ 10 public class CheckUtil { 11 12 private static final String token = "weixin"; 13 public static boolean checkSignature(String signature,String timestamp,String nonce){ 14 15 String[] arr = new String[] { token, timestamp, nonce }; 16 17 // 排序 18 Arrays.sort(arr); 19 // 生成字符串 20 StringBuilder content = new StringBuilder(); 21 for (int i = 0; i < arr.length; i++) { 22 content.append(arr[i]); 23 } 24 25 // sha1加密 26 String temp = getSHA1String(content.toString()); 27 28 return temp.equals(signature); // 與微信傳遞過來的簽名進行比較 29 } 30 31 private static String getSHA1String(String data){ 32 return DigestUtils.sha1Hex(data); // 使用commons codec生成sha1字符串 33 } 34 }
Servlet配置:
1 servlet> 2 <servlet-name>WeixinServlet</servlet-name> 3 <servlet-class>org.gpf.servlet.WeixinServlet</servlet-class> 4 </servlet> 5 6 <servlet-mapping> 7 <servlet-name>WeixinServlet</servlet-name> 8 <url-pattern>/wx.do</url-pattern> 9 </servlet-mapping>
接下來通過映射工具將本地的服務器映射到公網,從公網訪問Servlet。
開發模式和編輯模式是互斥的,如果啟動了開發模式,則自定義菜單和自動回復將失效!
消息的接收和響應
參照文檔,當普通微信用戶向公眾賬號發消息時,微信服務器將POST消息的XML數據包到開發者填寫的URL上。所以我們需要更改我們的Servlet中的doPost方法,因為微信服務器與我們的服務器之間是通過XML傳遞數據的,因此我們需要實現消息實體與XML之間的互相轉換。可以采用第三方jar包XStream完成。
處理微信服務器與本機服務器進行交互的Servlet:
1 package org.gpf.servlet; 2 3 import java.io.IOException; 4 import java.io.PrintWriter; 5 import java.util.Date; 6 import java.util.Map; 7 8 import javax.servlet.ServletException; 9 import javax.servlet.http.HttpServlet; 10 import javax.servlet.http.HttpServletRequest; 11 import javax.servlet.http.HttpServletResponse; 12 13 import org.dom4j.DocumentException; 14 import org.gpf.po.TextMeaasge; 15 import org.gpf.util.CheckUtil; 16 import org.gpf.util.MessageUtil; 17 /** 18 * 微信消息的接收和響應 19 */ 20 public class WeixinServlet extends HttpServlet { 21 22 /** 23 * 接收微信服務器發送的4個參數並返回echostr 24 */ 25 public void doGet(HttpServletRequest request, HttpServletResponse response) 26 throws ServletException, IOException { 27 28 // 接收微信服務器以Get請求發送的4個參數 29 String signature = request.getParameter("signature"); 30 String timestamp = request.getParameter("timestamp"); 31 String nonce = request.getParameter("nonce"); 32 String echostr = request.getParameter("echostr"); 33 34 PrintWriter out = response.getWriter(); 35 if (CheckUtil.checkSignature(signature, timestamp, nonce)) { 36 out.print(echostr); // 校驗通過,原樣返回echostr參數內容 37 } 38 } 39 40 /** 41 * 接收並處理微信客戶端發送的請求 42 */ 43 public void doPost(HttpServletRequest request, HttpServletResponse response) 44 throws ServletException, IOException { 45 46 request.setCharacterEncoding("utf-8"); 47 response.setContentType("text/xml;charset=utf-8"); 48 PrintWriter out = response.getWriter(); 49 try { 50 Map<String, String> map = MessageUtil.xmlToMap(request); 51 String toUserName = map.get("ToUserName"); 52 String fromUserName = map.get("FromUserName"); 53 String msgType = map.get("MsgType"); 54 String content = map.get("Content"); 55 56 String message = null; 57 if ("text".equals(msgType)) { // 對文本消息進行處理 58 TextMeaasge text = new TextMeaasge(); 59 text.setFromUserName(toUserName); // 發送和回復是反向的 60 text.setToUserName(fromUserName); 61 text.setMsgType("text"); 62 text.setCreateTime(new Date().getTime()); 63 text.setContent("你發送的消息是:" + content); 64 message = MessageUtil.textMessageToXML(text); 65 System.out.println(message); 66 } 67 out.print(message); // 將回應發送給微信服務器 68 } catch (DocumentException e) { 69 e.printStackTrace(); 70 }finally{ 71 out.close(); 72 } 73 } 74 75 }
按照微信的接口文檔編寫的文本消息實體類:

1 package org.gpf.po; 2 /** 3 * 按照微信的接入文檔編寫的微信文本消息實體 4 */ 5 public class TextMeaasge { 6 7 private String ToUserName; 8 private String FromUserName; 9 private long CreateTime; 10 private String MsgType; 11 private String Content; 12 private String MsgId; 13 14 public TextMeaasge() { 15 16 } 17 18 public String getToUserName() { 19 return ToUserName; 20 } 21 public void setToUserName(String toUserName) { 22 ToUserName = toUserName; 23 } 24 public String getFromUserName() { 25 return FromUserName; 26 } 27 public void setFromUserName(String fromUserName) { 28 FromUserName = fromUserName; 29 } 30 public long getCreateTime() { 31 return CreateTime; 32 } 33 public void setCreateTime(long createTime) { 34 CreateTime = createTime; 35 } 36 public String getMsgType() { 37 return MsgType; 38 } 39 public void setMsgType(String msgType) { 40 MsgType = msgType; 41 } 42 public String getContent() { 43 return Content; 44 } 45 public void setContent(String content) { 46 Content = content; 47 } 48 public String getMsgId() { 49 return MsgId; 50 } 51 public void setMsgId(String msgId) { 52 MsgId = msgId; 53 } 54 }
實現消息轉換的工具類
1 package org.gpf.util; 2 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.util.HashMap; 6 import java.util.List; 7 import java.util.Map; 8 9 import javax.servlet.http.HttpServletRequest; 10 11 import org.dom4j.Document; 12 import org.dom4j.DocumentException; 13 import org.dom4j.Element; 14 import org.dom4j.io.SAXReader; 15 import org.gpf.po.TextMeaasge; 16 17 import com.thoughtworks.xstream.XStream; 18 19 /** 20 * 實現消息的格式轉換(Map類型和XML的互轉) 21 */ 22 public class MessageUtil { 23 24 /** 25 * 將XML轉換成Map集合 26 */ 27 public static Map<String, String>xmlToMap(HttpServletRequest request) throws IOException, DocumentException{ 28 29 Map<String, String> map = new HashMap<String, String>(); 30 SAXReader reader = new SAXReader(); // 使用dom4j解析xml 31 InputStream ins = request.getInputStream(); // 從request中獲取輸入流 32 Document doc = reader.read(ins); 33 34 Element root = doc.getRootElement(); // 獲取根元素 35 List<Element> list = root.elements(); // 獲取所有節點 36 37 for (Element e : list) { 38 map.put(e.getName(), e.getText()); 39 System.out.println(e.getName() + "--->" + e.getText()); 40 } 41 ins.close(); 42 return map; 43 } 44 45 /** 46 * 將文本消息對象轉換成XML 47 */ 48 public static String textMessageToXML(TextMeaasge textMessage){ 49 50 XStream xstream = new XStream(); // 使用XStream將實體類的實例轉換成xml格式 51 xstream.alias("xml", textMessage.getClass()); // 將xml的默認根節點替換成“xml” 52 return xstream.toXML(textMessage); 53 54 } 55 56 }
Xtream下載地址:http://xstream.codehaus.org/download.html。dom4j下載地址:http://git.oschina.net/gaopengfei/Java_XML/raw/master/lib/dom4j-1.6.1.jar
天氣預報項目的github地址:https://github.com/gao-pengfei/WeChat.git