微信公众平台是腾讯为了让用户申请和管理微信公众账号而推出的一个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