微信公众账号开发教程(java)

supreme26

贡献于2014-03-24

字数:75227 关键词:

微信公众帐号接口开发 接触微信公众帐号已经有两个多月的时间了,在这期间,除了陆续完善个人公众帐号xiaoqrobot以外,还带领团队为公司开发了两个企业应用:一个是普通类型的公众帐号,另一个是会议类型的公众帐号。经过这3个公众帐号的开发,对目前微信公众平台开放的api算是比较熟悉了,像文本消息、图文消息、音乐消息、语音消息、位置消息等全部用到过,菜单也使用过。所以,就有了写微信公众帐号开发教程的想法,将学习到的技术经验分享出来,帮助更多需要的朋友,也希望借此认识同行的朋友,共同交流,共同进步! 下面将对即将推出的微信公众帐号开发系列连载教程做简单的说明。教程主要是面向有一定Java编程基础的朋友,不打算从编程语言开始讲起,一是考虑到自己没有那么多时间和精力(要上班、装修、学车等),二是怕等我把编程语言讲完,微信公众帐号又发生了大变化,这样教程就显得有点过时,没有吸引力了,所以只能是有侧重点的介绍。至于内容方面,大概会涉及到: 1)前沿知识:微信公众帐号的分类、两种模式各自的特点和区别、开发模式的配置使用等; 2)API中各类消息的使用(我已经对api进行封装并打成了jar包,到时候会考虑分享出来); 3)微信公众帐号开发中的小技巧(如换行、通过代码发送表情、屏幕飘雪花、表情的接收识别、在Android和iOS上表现不一致等等); 4)与业务系统对接的方法(链接、短信等,除了技术讲解还会做一定的分析对比); 5)微信公众平台上常见功能的开发(如像小黄鸡那样的人机对话、天气预报、精确的定位及百度地图的使用、音乐搜索、语音识别解析等) 当然,具体写出来的内容肯定不止这些,但一定会包含以上介绍的所有内容。 我也不知道多久能写完这些内容,当然是越快越好,我会尽全力的。希望正在看博文的你通过微信关注xiaoqrobot或者在博客留言支持,给我动力,谢谢! 开发xiaoqrobot就是为了学习微信公众帐号开发,将api开放出来的各类消息都体验了。虽然现在看来有点大杂烩的意思,但还是比较实用的,一款生活、娱乐的好帮手,目前已有370多关注者。周边搜索功能定位比较准确(解决了纠偏问题,能精确到十米范围),平时出门在外搜美食、ATM机、厕所、超市等再方便不过了,还提供路线导航;聊天唠嗑功能是我自己开发的,后面的连载教程很多内容都会从中抽取出来,下面是主界面截图,对系列连载教程有所期待的朋友很建议关注体验下,做的不好的地方也请多提意见,除了技术本身外,体验也是我比较重视关注的。 2. 微信公众帐号开发教程第2篇-微信公众帐号的类型(普通和会议) 个人公众帐号与企业公众帐号 记得在两个月前,我在微信官方开发群里问个人公众帐号与企业公众帐号有什么区别的时候,还被人笑话过,没有人愿意告知,也许是这个问题问的太过于简单了吧。我想一定也还有不少朋友在刚接触时,也搞不清楚这一点。其实,在注册微信公众帐号时,是不区分个人帐号与企业帐号的,它们需要填写的注册资料是一样的,这个区别仅仅是帐号申请成功后在使用用途上的区别罢了。然而,在注册公众帐号时的确有个类型可以选择,但并不是选择个人帐号与企业帐号,那有些什么类型可以选择呢?这也正是今天我想讲的主题,请继续往下看。 注册时可选择的两种帐号类型 微信公众帐号注册的最后一步是填写“公众号信息”,最后一个选项是选择“类型”,它有二个值可供选择“普通公众帐号类型”和“公众会议帐号”。当我们选择“公众会议号”时,下方会出现醒目的红字“ 提醒:会议号是有一定时间限制的公众帐号,过期后将无法登录使用。”,如下图所示。 那注册时到底应该选择哪个类型呢?这就需要我们对两种类型有一定的了解才好做出判断。下面将主要通过介绍公众会议帐号与普通公众帐号的区别来进行说明。 公众帐号与普通帐号的区别 在注册好的公会会议帐号的“设置”一栏里,可以看到“会议号设置”项,如下图所示: 其实会议帐号与普通帐号的区别在“会议号设置”里就能全部体现出来,它们的区别有以下三点: 1)有效时间 普通帐号是创建后永久有效的,而会议帐号的有效期只有一个月,一个月后帐号就失效了。帐号失效后登录微信公众平台时,会提示“该公众会议号已经过期,无法再登录使用”,如下图所示: 帐号失效后已关注了会议帐号的用户继续使用时,会提示“该公众帐号已过期,无法下发消息”,但如果是有菜单权限的会议帐号,仍然可以通过菜单获取信息,帐号过期后菜单的响应没有被禁止,如下图所示: 从上图可以看到,会议帐号过期后,无法再通过文本获取消息,但点击菜单是可以继续使用的,图中的图文消息“峰会概况”就是点击菜单后返回的。 2)关注权限 普通帐号任何人都可以关注,没有权限限制。会议帐号是可以设置关注权限的,分为两种:任何人都可以关注和需要通过验证才可以关注,不进行此项设置时默认是前者。如果设置为需要通过验证才可以关注,就有点类似于微信添加朋友时的验证一样,只不过这里的验证问题是可以设置的,并且如果你设置的验证消息是类似于询问用户身份的,例如“请问您的真实姓名叫什么?”,你还可以勾选“将验证消息作为备注名”,这样就很好辩认所有关注了会议帐号的人。 3)参与人相互可见 普通帐号的关注者之间是不可见的,而会议帐号的关注者之间是相互可见的,这是什么意思呢?在会议号设置里,如果勾选了“参与人相互可见”,那么在关注了该会议帐号后,能够在帐号详细资料里看到多了一项“与会者”,点击它将会显示所有关注了该会议帐号的微信号列表,并且点击某个参与人还可以查看详细资料、申请加为朋友等。 这是会议帐号比较给力的一个功能,方便参加会议的人相互认识。 以上三点是会议帐号的特点,也是与普通帐号的区别。可以看出,会议帐号是在普通帐号功能的基础上增加了帐号有效时间限制(一个月)、关注权限和关注者相互可见三个功能。 其实,微信目前对会议帐号的支持还远远不够。比如像会议主题、时间、地点等会议的常规属性设置都不支持,还有会议通常都会有的签到、互动、投票等环节也没有任何体现,更没有考虑到周期性的会议,希望微信后期的版本对这块的支持力度更大。 3. 微信公众帐号开发教程第3篇-开发模式启用及接口配置 编辑模式与开发模式 微信公众帐号申请成功后,要想接收处理用户的请求,就必须要在“高级功能”里进行配置,点击“高级功能”,将看到如下界面: 从上图中可以看到,高级功能包含两种模式:编辑模式和开发模式,并且这两种模式是互斥关系,即两种模式不能同时开启。那两种模式有什么区别呢?作为开发人员到底要开启哪一种呢? 编辑模式:主要针对非编程人员及信息发布类公众帐号使用。开启该模式后,可以方便地通过界面配置“自定义菜单”和“自动回复的消息”。 开发模式:主要针对具备开发能力的人使用。开启该模式后,能够使用微信公众平台开放的接口,通过编程方式实现自定义菜单的创建、用户消息的接收/处理/响应。这种模式更加灵活,建议有开发能力的公司或个人都采用该模式。 启用开发模式(上) 微信公众帐号注册完成后,默认开启的是编辑模式。那么该如何开启开发模式呢?操作步骤如下: 1)点击进入编辑模式,将右上角的编辑模式开关由“开启”切换到“关闭”,如下图所示: 2)点击高级功能进入到开发模式,将右上角的开发模式开关由“关闭”切换到“开启”,但在切换时会遇到如下提示: 提示需要我们先成为开发者,才能开启开发模式。那就先点击下图所示的“成为开发者”按钮: 如果提示资料不全,那就先补齐资料再回来继续操作。需要补全的资料有公众帐号头像、描述和运营地区。 待资料补全后,再次点击“成为开发者”,这时将看到接口配置信息界面,如下图所示: 这里需要填写URL和Token两个值。URL指的是能够接收处理微信服务器发送的GET/POST请求的地址,并且是已经存在的,现在就能够在浏览器访问到的地址,这就要求我们先把公众帐号后台处理程序开发好(至少应该完成了对GET请求的处理)并部署在公网服务器上。Token后面会详细说明。 也就是说要完成接口配置,只需要先完成微信服务器的GET请求处理就可以?是的。 那这是为什么呢?因为这是微信公众平台接口中定义的。具体请参考API文档-消息接口-消息接口指南中的网址接入部分。点此进入。 上面写的很清楚,其实你只要能理解上面在说什么就OK了,至于怎么编写相关代码,我已经帮你完成了,请继续往下看。 创建公众帐号后台接口程序 创建一个Java Web工程,并新建一个能够处理请求的Servlet,命名任意,我在这里将其命名为org.liufeng.course.servlet.CoreServlet,代码如下: [java] view plaincopyprint? 1. package org.liufeng.course.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.liufeng.course.util.SignUtil;   12.    13. /**  14.  * 核心请求处理类  15.  *   16.  * @author liufeng  17.  * @date 2013-05-18  18.  */   19. public class CoreServlet extends HttpServlet {   20.     private static final long serialVersionUID = 4440739483644821986L;   21.    22.     /**  23.      * 确认请求来自微信服务器  24.      */   25.     public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {   26.         // 微信加密签名    27.         String signature = request.getParameter("signature");   28.         // 时间戳    29.         String timestamp = request.getParameter("timestamp");   30.         // 随机数    31.         String nonce = request.getParameter("nonce");   32.         // 随机字符串    33.         String echostr = request.getParameter("echostr");   34.    35.         PrintWriter out = response.getWriter();   36.         // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败    37.         if (SignUtil.checkSignature(signature, timestamp, nonce)) {   38.             out.print(echostr);   39.         }   40.         out.close();   41.         out = null;   42.     }   43.    44.     /**  45.      * 处理微信服务器发来的消息  46.      */   47.     public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {   48.         // TODO 消息的接收、处理、响应    49.     }   50.    51. }   package org.liufeng.course.servlet; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.liufeng.course.util.SignUtil; /** * 核心请求处理类 * * @author liufeng * @date 2013-05-18 */ public class CoreServlet extends HttpServlet { private static final long serialVersionUID = 4440739483644821986L; /** * 确认请求来自微信服务器 */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 微信加密签名 String signature = request.getParameter("signature"); // 时间戳 String timestamp = request.getParameter("timestamp"); // 随机数 String nonce = request.getParameter("nonce"); // 随机字符串 String echostr = request.getParameter("echostr"); PrintWriter out = response.getWriter(); // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败 if (SignUtil.checkSignature(signature, timestamp, nonce)) { out.print(echostr); } out.close(); out = null; } /** * 处理微信服务器发来的消息 */ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO 消息的接收、处理、响应 } } 可以看到,代码中只完成了doGet方法,它的作用正是确认请求是否来自于微信服务器;而doPost方法不是我们这次要讲的内容,并且完成接口配置也不需要管doPost方法,就先空在那里。 在doGet方法中调用了org.liufeng.course.util.SignUtil.checkSignature方法,SignUtil.java的实现如下: [java] view plaincopyprint? 1. package org.liufeng.course.util;   2.    3. import java.security.MessageDigest;   4. import java.security.NoSuchAlgorithmException;   5. import java.util.Arrays;   6.    7. /**  8.  * 请求校验工具类  9.  *   10.  * @author liufeng  11.  * @date 2013-05-18  12.  */   13. public class SignUtil {   14.     // 与接口配置信息中的Token要一致    15.     private static String token = "weixinCourse";   16.    17.     /**  18.      * 验证签名  19.      *   20.      * @param signature  21.      * @param timestamp  22.      * @param nonce  23.      * @return  24.      */   25.     public static boolean checkSignature(String signature, String timestamp, String nonce) {   26.         String[] arr = new String[] { token, timestamp, nonce };   27.         // 将token、timestamp、nonce三个参数进行字典序排序    28.         Arrays.sort(arr);   29.         StringBuilder content = new StringBuilder();   30.         for (int i = 0; i < arr.length; i++) {   31.             content.append(arr[i]);   32.         }   33.         MessageDigest md = null;   34.         String tmpStr = null;   35.    36.         try {   37.             md = MessageDigest.getInstance("SHA-1");   38.             // 将三个参数字符串拼接成一个字符串进行sha1加密    39.             byte[] digest = md.digest(content.toString().getBytes());   40.             tmpStr = byteToStr(digest);   41.         } catch (NoSuchAlgorithmException e) {   42.             e.printStackTrace();   43.         }   44.    45.         content = null;   46.         // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信    47.         return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;   48.     }   49.    50.     /**  51.      * 将字节数组转换为十六进制字符串  52.      *   53.      * @param byteArray  54.      * @return  55.      */   56.     private static String byteToStr(byte[] byteArray) {   57.         String strDigest = "";   58.         for (int i = 0; i < byteArray.length; i++) {   59.             strDigest += byteToHexStr(byteArray[i]);   60.         }   61.         return strDigest;   62.     }   63.    64.     /**  65.      * 将字节转换为十六进制字符串  66.      *   67.      * @param mByte  68.      * @return  69.      */   70.     private static String byteToHexStr(byte mByte) {   71.         char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };   72.         char[] tempArr = new char[2];   73.         tempArr[0] = Digit[(mByte >>> 4) & 0X0F];   74.         tempArr[1] = Digit[mByte & 0X0F];   75.    76.         String s = new String(tempArr);   77.         return s;   78.     }   79. }   package org.liufeng.course.util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; /** * 请求校验工具类 * * @author liufeng * @date 2013-05-18 */ public class SignUtil { // 与接口配置信息中的Token要一致 private static String token = "weixinCourse"; /** * 验证签名 * * @param signature * @param timestamp * @param nonce * @return */ public static boolean checkSignature(String signature, String timestamp, String nonce) { String[] arr = new String[] { token, timestamp, nonce }; // 将token、timestamp、nonce三个参数进行字典序排序 Arrays.sort(arr); StringBuilder content = new StringBuilder(); for (int i = 0; i < arr.length; i++) { content.append(arr[i]); } MessageDigest md = null; String tmpStr = null; try { md = MessageDigest.getInstance("SHA-1"); // 将三个参数字符串拼接成一个字符串进行sha1加密 byte[] digest = md.digest(content.toString().getBytes()); tmpStr = byteToStr(digest); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } content = null; // 将sha1加密后的字符串可与signature对比,标识该请求来源于微信 return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false; } /** * 将字节数组转换为十六进制字符串 * * @param byteArray * @return */ private static String byteToStr(byte[] byteArray) { String strDigest = ""; for (int i = 0; i < byteArray.length; i++) { strDigest += byteToHexStr(byteArray[i]); } return strDigest; } /** * 将字节转换为十六进制字符串 * * @param mByte * @return */ private static String byteToHexStr(byte mByte) { char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; char[] tempArr = new char[2]; tempArr[0] = Digit[(mByte >>> 4) & 0X0F]; tempArr[1] = Digit[mByte & 0X0F]; String s = new String(tempArr); return s; } } 这里唯一需要注意的就是SignUtil类中的成员变量token,这里赋予什么值,在接口配置信息中的Token就要填写什么值,两边保持一致即可,没有其他要求,建议用项目名称、公司名称缩写等,我在这里用的是项目名称weixinCourse。 最后再来看一下web.xml中,CoreServlet是怎么配置的,web.xml中的配置代码如下: [html] view plaincopyprint? 1.    2.    6.        7.         coreServlet   8.            9.             org.liufeng.course.servlet.CoreServlet   10.            11.        12.    13.        14.        15.         coreServlet   16.         /coreServlet   17.        18.    19.        20.         index.jsp   21.        22.    coreServlet org.liufeng.course.servlet.CoreServlet coreServlet /coreServlet index.jsp 到这里,所有编码都完成了,就是这么简单。接下来就是将工程发布到公网服务器上,如果没有公网服务器环境,可以去了解下BAE、SAE或阿里云。发布到服务器上后,我们在浏览器里访问CoreServlet,如果看到如下界面就表示我们的代码没有问题: 啊,代码都报空指针异常了还说证明没问题?那当然了,因为直接在地址栏访问coreServlet,就相当于提交的是GET请求,而我们什么参数都没有传,在验证的时候当然会报空指针异常。 接下来,把coreServlet的访问路径拷贝下来,再回到微信公众平台的接入配置信息界面,将coreServlet的访问路径粘贴到URL中,并将SignUtil类中指定的token值weixinCourse填入到Token中,填写后的结果如下图所示: 我在写这篇教程的时候是使用的BAE环境,如果想学习微信公众帐号开发又没有公网服务器环境的,建议可以试试,注册使用都很方便,如果有问题我们还可以交流。 接着点击“提交”,如果程序写的没问题,并且URL、Token都填写正确,可以在页面最上方看到“提交成功”的提示,并会再次跳转到开发模式设置界面,而且能够看到“你已成为开发者”的提示,如下图所示: 启用开发模式(下) 这个时候就已经成为开发者了,百般周折啊,哈哈,到这里还没有完哦,还有最后一步工作就是将开发模式开启。将右上角的开发模式开关由“关闭”切换到“开启”,如下图所示: 到这里,接口配置、开发模式的开启就都完成了,本章节的内容也就讲到这里。接下来要章节要讲的就是如何接收、处理、响应由微信服务器转发的用户发送给公众帐号的消息,也就是完成CoreServlet中doPost方法的编写。 4. 微信公众帐号开发教程第4篇-消息及消息处理工具的封装 工欲善其事必先利其器!本篇内容主要讲解如何将微信公众平台定义的消息及消息相关的操作封装成工具类,方面后期的使用。这里需要明确的是消息其实是由用户发给你的公众帐号的,消息先被微信平台接收到,然后微信平台会将该消息转给你在开发模式接口配置中指定的URL地址。 微信公众平台消息接口 要接收微信平台发送的消息,我们需要先熟悉微信公众平台API中消息接口部分,点此进入,点击后将进入到消息接口指南部分,如下图所示: 在上图左侧可以看到微信公众平台目前开放的接口有三种:消息接口、通用接口和自定义菜单接口。通用接口和自定义菜单接口只有拿到内测资格才能调用,而内测资格的申请也已经关闭了,我们只有期待将来某一天微信会对大众用户开放吧,所以没有内测资格的用户就不要再浪费时间在这两个接口上,只需要用好消息接口就可以了。 消息推送和消息回复 下面将主要介绍消息接口。对于消息的接收、响应我们只需要关注上图中的“4 消息推送”和“5 消息回复”就足够了。 我们先来了解接口中的“消息推送”指的是什么,点击“4 消息推送”,可以看到接口中的“消息推送”指的是“当普通用户向公众帐号发消息时,微信服务器将POST该消息到填写的URL上”,即这里定义的是用户能够发送哪些类型的消息、消息有哪些字段、消息被微信服务器以什么方式转发给我们的公众帐号后台。 消息推送中定义了我们将会接收到的消息类型有5种:文本消息、图片消息、地理位置消息、链接消息和事件推送,其实语音消息我们也能够接收到的,只不过拿不到具体的语音文件而以(需要内测资格才能够获取语音文件)。 接口中的“消息回复”定义了我们能回复给用户的消息类型、消息字段和消息格式,微信公众平台的接口指南中是这样描述的: 上面说到我们能回复给用户的消息有5种,但目前在开发模式下能回复的消息只有3种:文本消息、音乐消息和图文消息,而语音消息和视频消息目前只能在编辑模式下使用。 消息的封装 接下来要做的就是将消息推送(请求)、消息回复(响应)中定义的消息进行封装,建立与之对应的Java类(Java是一门面向对象的编程语言,封装后使用起来更方便),下面的请求消息是指消息推送中定义的消息,响应消息指消息回复中定义的消息。 请求消息的基类 把消息推送中定义的所有消息都有的字段提取出来,封装成一个基类,这些公有的字段包括:ToUserName(开发者微信号)、FromUserName(发送方帐号,OPEN_ID)、CreateTime(消息的创建时间)、MsgType(消息类型)、MsgId(消息ID),封装后基类org.liufeng.course.message.req.BaseMessage的代码如下: [java] view plaincopyprint? 1. package org.liufeng.course.message.req;   2.    3. /**  4.  * 消息基类(普通用户 -> 公众帐号)  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class BaseMessage {   10.     // 开发者微信号    11.     private String ToUserName;   12.     // 发送方帐号(一个OpenID)    13.     private String FromUserName;   14.     // 消息创建时间 (整型)    15.     private long CreateTime;   16.     // 消息类型(text/image/location/link)    17.     private String MsgType;   18.     // 消息id,64位整型    19.     private long MsgId;   20.    21.     public String getToUserName() {   22.         return ToUserName;   23.     }   24.    25.     public void setToUserName(String toUserName) {   26.         ToUserName = toUserName;   27.     }   28.    29.     public String getFromUserName() {   30.         return FromUserName;   31.     }   32.    33.     public void setFromUserName(String fromUserName) {   34.         FromUserName = fromUserName;   35.     }   36.    37.     public long getCreateTime() {   38.         return CreateTime;   39.     }   40.    41.     public void setCreateTime(long createTime) {   42.         CreateTime = createTime;   43.     }   44.    45.     public String getMsgType() {   46.         return MsgType;   47.     }   48.    49.     public void setMsgType(String msgType) {   50.         MsgType = msgType;   51.     }   52.    53.     public long getMsgId() {   54.         return MsgId;   55.     }   56.    57.     public void setMsgId(long msgId) {   58.         MsgId = msgId;   59.     }   60. }   package org.liufeng.course.message.req; /** * 消息基类(普通用户 -> 公众帐号) * * @author liufeng * @date 2013-05-19 */ public class BaseMessage { // 开发者微信号 private String ToUserName; // 发送方帐号(一个OpenID) private String FromUserName; // 消息创建时间 (整型) private long CreateTime; // 消息类型(text/image/location/link) private String MsgType; // 消息id,64位整型 private long MsgId; public String getToUserName() { return ToUserName; } public void setToUserName(String toUserName) { ToUserName = toUserName; } public String getFromUserName() { return FromUserName; } public void setFromUserName(String fromUserName) { FromUserName = fromUserName; } public long getCreateTime() { return CreateTime; } public void setCreateTime(long createTime) { CreateTime = createTime; } public String getMsgType() { return MsgType; } public void setMsgType(String msgType) { MsgType = msgType; } public long getMsgId() { return MsgId; } public void setMsgId(long msgId) { MsgId = msgId; } } 请求消息之文本消息 [java] view plaincopyprint? 1. package org.liufeng.course.message.req;   2.    3. /**  4.  * 文本消息  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class TextMessage extends BaseMessage {   10.     // 消息内容    11.     private String Content;   12.    13.     public String getContent() {   14.         return Content;   15.     }   16.    17.     public void setContent(String content) {   18.         Content = content;   19.     }   20. }   package org.liufeng.course.message.req; /** * 文本消息 * * @author liufeng * @date 2013-05-19 */ public class TextMessage extends BaseMessage { // 消息内容 private String Content; public String getContent() { return Content; } public void setContent(String content) { Content = content; } } 请求消息之图片消息 [java] view plaincopyprint? 1. package org.liufeng.course.message.req;   2.    3. /**  4.  * 图片消息  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class ImageMessage extends BaseMessage {   10.     // 图片链接    11.     private String PicUrl;   12.    13.     public String getPicUrl() {   14.         return PicUrl;   15.     }   16.    17.     public void setPicUrl(String picUrl) {   18.         PicUrl = picUrl;   19.     }   20. }   package org.liufeng.course.message.req; /** * 图片消息 * * @author liufeng * @date 2013-05-19 */ public class ImageMessage extends BaseMessage { // 图片链接 private String PicUrl; public String getPicUrl() { return PicUrl; } public void setPicUrl(String picUrl) { PicUrl = picUrl; } } 请求消息之地理位置消息 [java] view plaincopyprint? 1. package org.liufeng.course.message.req;   2.    3. /**  4.  * 地理位置消息  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class LocationMessage extends BaseMessage {   10.     // 地理位置维度    11.     private String Location_X;   12.     // 地理位置经度    13.     private String Location_Y;   14.     // 地图缩放大小    15.     private String Scale;   16.     // 地理位置信息    17.     private String Label;   18.    19.     public String getLocation_X() {   20.         return Location_X;   21.     }   22.    23.     public void setLocation_X(String location_X) {   24.         Location_X = location_X;   25.     }   26.    27.     public String getLocation_Y() {   28.         return Location_Y;   29.     }   30.    31.     public void setLocation_Y(String location_Y) {   32.         Location_Y = location_Y;   33.     }   34.    35.     public String getScale() {   36.         return Scale;   37.     }   38.    39.     public void setScale(String scale) {   40.         Scale = scale;   41.     }   42.    43.     public String getLabel() {   44.         return Label;   45.     }   46.    47.     public void setLabel(String label) {   48.         Label = label;   49.     }   50. }   package org.liufeng.course.message.req; /** * 地理位置消息 * * @author liufeng * @date 2013-05-19 */ public class LocationMessage extends BaseMessage { // 地理位置维度 private String Location_X; // 地理位置经度 private String Location_Y; // 地图缩放大小 private String Scale; // 地理位置信息 private String Label; public String getLocation_X() { return Location_X; } public void setLocation_X(String location_X) { Location_X = location_X; } public String getLocation_Y() { return Location_Y; } public void setLocation_Y(String location_Y) { Location_Y = location_Y; } public String getScale() { return Scale; } public void setScale(String scale) { Scale = scale; } public String getLabel() { return Label; } public void setLabel(String label) { Label = label; } } 请求消息之链接消息 [java] view plaincopyprint? 1. package org.liufeng.course.message.req;   2.    3. /**  4.  * 链接消息  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class LinkMessage extends BaseMessage {   10.     // 消息标题    11.     private String Title;   12.     // 消息描述    13.     private String Description;   14.     // 消息链接    15.     private String Url;   16.    17.     public String getTitle() {   18.         return Title;   19.     }   20.    21.     public void setTitle(String title) {   22.         Title = title;   23.     }   24.    25.     public String getDescription() {   26.         return Description;   27.     }   28.    29.     public void setDescription(String description) {   30.         Description = description;   31.     }   32.    33.     public String getUrl() {   34.         return Url;   35.     }   36.    37.     public void setUrl(String url) {   38.         Url = url;   39.     }   40. }   package org.liufeng.course.message.req; /** * 链接消息 * * @author liufeng * @date 2013-05-19 */ public class LinkMessage extends BaseMessage { // 消息标题 private String Title; // 消息描述 private String Description; // 消息链接 private String Url; public String getTitle() { return Title; } public void setTitle(String title) { Title = title; } public String getDescription() { return Description; } public void setDescription(String description) { Description = description; } public String getUrl() { return Url; } public void setUrl(String url) { Url = url; } } 请求消息之语音消息 [java] view plaincopyprint? 1. package org.liufeng.course.message.req;   2.    3. /**  4.  * 音频消息  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class VoiceMessage extends BaseMessage {   10.     // 媒体ID    11.     private String MediaId;   12.     // 语音格式    13.     private String Format;   14.    15.     public String getMediaId() {   16.         return MediaId;   17.     }   18.    19.     public void setMediaId(String mediaId) {   20.         MediaId = mediaId;   21.     }   22.    23.     public String getFormat() {   24.         return Format;   25.     }   26.    27.     public void setFormat(String format) {   28.         Format = format;   29.     }   30. }   package org.liufeng.course.message.req; /** * 音频消息 * * @author liufeng * @date 2013-05-19 */ public class VoiceMessage extends BaseMessage { // 媒体ID private String MediaId; // 语音格式 private String Format; public String getMediaId() { return MediaId; } public void setMediaId(String mediaId) { MediaId = mediaId; } public String getFormat() { return Format; } public void setFormat(String format) { Format = format; } } 响应消息的基类 同样,把消息回复中定义的所有消息都有的字段提取出来,封装成一个基类,这些公有的字段包括:ToUserName(接收方帐号,用户的OPEN_ID)、FromUserName(开发者的微信号)、CreateTime(消息的创建时间)、MsgType(消息类型)、FuncFlag(消息的星标标识),封装后基类org.liufeng.course.message.resp.BaseMessage的代码如下: [java] view plaincopyprint? 1. package org.liufeng.course.message.resp;   2.    3. /**  4.  * 消息基类(公众帐号 -> 普通用户)  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class BaseMessage {   10.     // 接收方帐号(收到的OpenID)    11.     private String ToUserName;   12.     // 开发者微信号    13.     private String FromUserName;   14.     // 消息创建时间 (整型)    15.     private long CreateTime;   16.     // 消息类型(text/music/news)    17.     private String MsgType;   18.     // 位0x0001被标志时,星标刚收到的消息    19.     private int FuncFlag;   20.    21.     public String getToUserName() {   22.         return ToUserName;   23.     }   24.    25.     public void setToUserName(String toUserName) {   26.         ToUserName = toUserName;   27.     }   28.    29.     public String getFromUserName() {   30.         return FromUserName;   31.     }   32.    33.     public void setFromUserName(String fromUserName) {   34.         FromUserName = fromUserName;   35.     }   36.    37.     public long getCreateTime() {   38.         return CreateTime;   39.     }   40.    41.     public void setCreateTime(long createTime) {   42.         CreateTime = createTime;   43.     }   44.    45.     public String getMsgType() {   46.         return MsgType;   47.     }   48.    49.     public void setMsgType(String msgType) {   50.         MsgType = msgType;   51.     }   52.    53.     public int getFuncFlag() {   54.         return FuncFlag;   55.     }   56.    57.     public void setFuncFlag(int funcFlag) {   58.         FuncFlag = funcFlag;   59.     }   60. }   package org.liufeng.course.message.resp; /** * 消息基类(公众帐号 -> 普通用户) * * @author liufeng * @date 2013-05-19 */ public class BaseMessage { // 接收方帐号(收到的OpenID) private String ToUserName; // 开发者微信号 private String FromUserName; // 消息创建时间 (整型) private long CreateTime; // 消息类型(text/music/news) private String MsgType; // 位0x0001被标志时,星标刚收到的消息 private int FuncFlag; public String getToUserName() { return ToUserName; } public void setToUserName(String toUserName) { ToUserName = toUserName; } public String getFromUserName() { return FromUserName; } public void setFromUserName(String fromUserName) { FromUserName = fromUserName; } public long getCreateTime() { return CreateTime; } public void setCreateTime(long createTime) { CreateTime = createTime; } public String getMsgType() { return MsgType; } public void setMsgType(String msgType) { MsgType = msgType; } public int getFuncFlag() { return FuncFlag; } public void setFuncFlag(int funcFlag) { FuncFlag = funcFlag; } } 响应消息之文本消息 [java] view plaincopyprint? 1. package org.liufeng.course.message.resp;   2.    3. /**  4.  * 文本消息  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class TextMessage extends BaseMessage {   10.     // 回复的消息内容    11.     private String Content;   12.    13.     public String getContent() {   14.         return Content;   15.     }   16.    17.     public void setContent(String content) {   18.         Content = content;   19.     }   20. }   package org.liufeng.course.message.resp; /** * 文本消息 * * @author liufeng * @date 2013-05-19 */ public class TextMessage extends BaseMessage { // 回复的消息内容 private String Content; public String getContent() { return Content; } public void setContent(String content) { Content = content; } } 响应消息之音乐消息 [java] view plaincopyprint? 1. package org.liufeng.course.message.resp;   2.    3. /**  4.  * 音乐消息  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class MusicMessage extends BaseMessage {   10.     // 音乐    11.     private Music Music;   12.    13.     public Music getMusic() {   14.         return Music;   15.     }   16.    17.     public void setMusic(Music music) {   18.         Music = music;   19.     }   20. }   package org.liufeng.course.message.resp; /** * 音乐消息 * * @author liufeng * @date 2013-05-19 */ public class MusicMessage extends BaseMessage { // 音乐 private Music Music; public Music getMusic() { return Music; } public void setMusic(Music music) { Music = music; } } 音乐消息中Music类的定义 [java] view plaincopyprint? 1. package org.liufeng.course.message.resp;   2.    3. /**  4.  * 音乐model  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class Music {   10.     // 音乐名称    11.     private String Title;   12.     // 音乐描述    13.     private String Description;   14.     // 音乐链接    15.     private String MusicUrl;   16.     // 高质量音乐链接,WIFI环境优先使用该链接播放音乐    17.     private String HQMusicUrl;   18.    19.     public String getTitle() {   20.         return Title;   21.     }   22.    23.     public void setTitle(String title) {   24.         Title = title;   25.     }   26.    27.     public String getDescription() {   28.         return Description;   29.     }   30.    31.     public void setDescription(String description) {   32.         Description = description;   33.     }   34.    35.     public String getMusicUrl() {   36.         return MusicUrl;   37.     }   38.    39.     public void setMusicUrl(String musicUrl) {   40.         MusicUrl = musicUrl;   41.     }   42.    43.     public String getHQMusicUrl() {   44.         return HQMusicUrl;   45.     }   46.    47.     public void setHQMusicUrl(String musicUrl) {   48.         HQMusicUrl = musicUrl;   49.     }   50.    51. }   package org.liufeng.course.message.resp; /** * 音乐model * * @author liufeng * @date 2013-05-19 */ public class Music { // 音乐名称 private String Title; // 音乐描述 private String Description; // 音乐链接 private String MusicUrl; // 高质量音乐链接,WIFI环境优先使用该链接播放音乐 private String HQMusicUrl; public String getTitle() { return Title; } public void setTitle(String title) { Title = title; } public String getDescription() { return Description; } public void setDescription(String description) { Description = description; } public String getMusicUrl() { return MusicUrl; } public void setMusicUrl(String musicUrl) { MusicUrl = musicUrl; } public String getHQMusicUrl() { return HQMusicUrl; } public void setHQMusicUrl(String musicUrl) { HQMusicUrl = musicUrl; } } 响应消息之图文消息 [java] view plaincopyprint? 1. package org.liufeng.course.message.resp;   2.    3. import java.util.List;   4.    5. /**  6.  * 文本消息  7.  *   8.  * @author liufeng  9.  * @date 2013-05-19  10.  */   11. public class NewsMessage extends BaseMessage {   12.     // 图文消息个数,限制为10条以内    13.     private int ArticleCount;   14.     // 多条图文消息信息,默认第一个item为大图    15.     private List
 Articles;   16.    17.     public int getArticleCount() {   18.         return ArticleCount;   19.     }   20.    21.     public void setArticleCount(int articleCount) {   22.         ArticleCount = articleCount;   23.     }   24.    25.     public List
 getArticles() {   26.         return Articles;   27.     }   28.    29.     public void setArticles(List
 articles) {   30.         Articles = articles;   31.     }   32. }   package org.liufeng.course.message.resp; import java.util.List; /** * 文本消息 * * @author liufeng * @date 2013-05-19 */ public class NewsMessage extends BaseMessage { // 图文消息个数,限制为10条以内 private int ArticleCount; // 多条图文消息信息,默认第一个item为大图 private List
Articles; public int getArticleCount() { return ArticleCount; } public void setArticleCount(int articleCount) { ArticleCount = articleCount; } public List
getArticles() { return Articles; } public void setArticles(List
articles) { Articles = articles; } } 图文消息中Article类的定义 [java] view plaincopyprint? 1. package org.liufeng.course.message.resp;   2.    3. /**  4.  * 图文model  5.  *   6.  * @author liufeng  7.  * @date 2013-05-19  8.  */   9. public class Article {   10.     // 图文消息名称    11.     private String Title;   12.     // 图文消息描述    13.     private String Description;   14.     // 图片链接,支持JPG、PNG格式,较好的效果为大图640*320,小图80*80,限制图片链接的域名需要与开发者填写的基本资料中的Url一致    15.     private String PicUrl;   16.     // 点击图文消息跳转链接    17.     private String Url;   18.    19.     public String getTitle() {   20.         return Title;   21.     }   22.    23.     public void setTitle(String title) {   24.         Title = title;   25.     }   26.    27.     public String getDescription() {   28.         return null == Description ? "" : Description;   29.     }   30.    31.     public void setDescription(String description) {   32.         Description = description;   33.     }   34.    35.     public String getPicUrl() {   36.         return null == PicUrl ? "" : PicUrl;   37.     }   38.    39.     public void setPicUrl(String picUrl) {   40.         PicUrl = picUrl;   41.     }   42.    43.     public String getUrl() {   44.         return null == Url ? "" : Url;   45.     }   46.    47.     public void setUrl(String url) {   48.         Url = url;   49.     }   50.    51. }   package org.liufeng.course.message.resp; /** * 图文model * * @author liufeng * @date 2013-05-19 */ public class Article { // 图文消息名称 private String Title; // 图文消息描述 private String Description; // 图片链接,支持JPG、PNG格式,较好的效果为大图640*320,小图80*80,限制图片链接的域名需要与开发者填写的基本资料中的Url一致 private String PicUrl; // 点击图文消息跳转链接 private String Url; public String getTitle() { return Title; } public void setTitle(String title) { Title = title; } public String getDescription() { return null == Description ? "" : Description; } public void setDescription(String description) { Description = description; } public String getPicUrl() { return null == PicUrl ? "" : PicUrl; } public void setPicUrl(String picUrl) { PicUrl = picUrl; } public String getUrl() { return null == Url ? "" : Url; } public void setUrl(String url) { Url = url; } } 全部消息封装完成后,Eclipse工程中关于消息部分的结构应该与下图保持一致,如果不一致的(类名、属性名称不一致的)请检查后调整一致,因为后面的章节还要介绍如何将微信开发中通用的类方法、与业务无关的工具类封装打成jar包,以后再做微信项目只需要引入该jar包即可,这种工作做一次就可以了。 如何解析请求消息? 接下来解决请求消息的解析问题。微信服务器会将用户的请求通过doPost方法发送给我们,让我们再来回顾下上一章节已经写好的doPost方法的定义: [java] view plaincopyprint? 1. /**   2.     * 处理微信服务器发来的消息   3.     */     4.    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {     5.        // TODO 消息的接收、处理、响应      6.    }     /** * 处理微信服务器发来的消息 */ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // TODO 消息的接收、处理、响应 } doPost方法有两个参数,request中封装了请求相关的所有内容,可以从request中取出微信服务器发来的消息;而通过response我们可以对接收到的消息进行响应,即发送消息。 那么如何解析请求消息的问题也就转化为如何从request中得到微信服务器发送给我们的xml格式的消息了。这里我们借助于开源框架dom4j去解析xml(这里使用的是dom4j-1.6.1.jar),然后将解析得到的结果存入HashMap,解析请求消息的方法如下: [java] view plaincopyprint? 1. /**  2.  * 解析微信发来的请求(XML)  3.  *   4.  * @param request  5.  * @return  6.  * @throws Exception  7.  */   8. @SuppressWarnings("unchecked")   9. public static Map parseXml(HttpServletRequest request) throws Exception {   10.     // 将解析结果存储在HashMap中    11.     Map map = new HashMap();   12.    13.     // 从request中取得输入流    14.     InputStream inputStream = request.getInputStream();   15.     // 读取输入流    16.     SAXReader reader = new SAXReader();   17.     Document document = reader.read(inputStream);   18.     // 得到xml根元素    19.     Element root = document.getRootElement();   20.     // 得到根元素的所有子节点    21.     List elementList = root.elements();   22.    23.     // 遍历所有子节点    24.     for (Element e : elementList)   25.         map.put(e.getName(), e.getText());   26.    27.     // 释放资源    28.     inputStream.close();   29.     inputStream = null;   30.    31.     return map;   32. }   /** * 解析微信发来的请求(XML) * * @param request * @return * @throws Exception */ @SuppressWarnings("unchecked") public static Map parseXml(HttpServletRequest request) throws Exception { // 将解析结果存储在HashMap中 Map map = new HashMap(); // 从request中取得输入流 InputStream inputStream = request.getInputStream(); // 读取输入流 SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); // 得到xml根元素 Element root = document.getRootElement(); // 得到根元素的所有子节点 List elementList = root.elements(); // 遍历所有子节点 for (Element e : elementList) map.put(e.getName(), e.getText()); // 释放资源 inputStream.close(); inputStream = null; return map; } 如何将响应消息转换成xml返回? 我们先前已经将响应消息封装成了Java类,方便我们在代码中使用。那么,请求接收成功、处理完成后,该如何将消息返回呢?这里就涉及到如何将响应消息转换成xml返回的问题,这里我们将采用开源框架xstream来实现Java类到xml的转换(这里使用的是xstream-1.3.1.jar),代码如下: [java] view plaincopyprint? 1. /**  2.  * 文本消息对象转换成xml  3.  *   4.  * @param textMessage 文本消息对象  5.  * @return xml  6.  */   7. public static String textMessageToXml(TextMessage textMessage) {   8.     xstream.alias("xml", textMessage.getClass());   9.     return xstream.toXML(textMessage);   10. }   11.    12. /**  13.  * 音乐消息对象转换成xml  14.  *   15.  * @param musicMessage 音乐消息对象  16.  * @return xml  17.  */   18. public static String musicMessageToXml(MusicMessage musicMessage) {   19.     xstream.alias("xml", musicMessage.getClass());   20.     return xstream.toXML(musicMessage);   21. }   22.    23. /**  24.  * 图文消息对象转换成xml  25.  *   26.  * @param newsMessage 图文消息对象  27.  * @return xml  28.  */   29. public static String newsMessageToXml(NewsMessage newsMessage) {   30.     xstream.alias("xml", newsMessage.getClass());   31.     xstream.alias("item", new Article().getClass());   32.     return xstream.toXML(newsMessage);   33. }   34.    35. /**  36.  * 扩展xstream,使其支持CDATA块  37.  *   38.  * @date 2013-05-19  39.  */   40. private static XStream xstream = new XStream(new XppDriver() {   41.     public HierarchicalStreamWriter createWriter(Writer out) {   42.         return new PrettyPrintWriter(out) {   43.             // 对所有xml节点的转换都增加CDATA标记    44.             boolean cdata = true;   45.    46.             @SuppressWarnings("unchecked")   47.             public void startNode(String name, Class clazz) {   48.                 super.startNode(name, clazz);   49.             }   50.    51.             protected void writeText(QuickWriter writer, String text) {   52.                 if (cdata) {   53.                     writer.write("");   56.                 } else {   57.                     writer.write(text);   58.                 }   59.             }   60.         };   61.     }   62. });   /** * 文本消息对象转换成xml * * @param textMessage 文本消息对象 * @return xml */ public static String textMessageToXml(TextMessage textMessage) { xstream.alias("xml", textMessage.getClass()); return xstream.toXML(textMessage); } /** * 音乐消息对象转换成xml * * @param musicMessage 音乐消息对象 * @return xml */ public static String musicMessageToXml(MusicMessage musicMessage) { xstream.alias("xml", musicMessage.getClass()); return xstream.toXML(musicMessage); } /** * 图文消息对象转换成xml * * @param newsMessage 图文消息对象 * @return xml */ public static String newsMessageToXml(NewsMessage newsMessage) { xstream.alias("xml", newsMessage.getClass()); xstream.alias("item", new Article().getClass()); return xstream.toXML(newsMessage); } /** * 扩展xstream,使其支持CDATA块 * * @date 2013-05-19 */ private static XStream xstream = new XStream(new XppDriver() { public HierarchicalStreamWriter createWriter(Writer out) { return new PrettyPrintWriter(out) { // 对所有xml节点的转换都增加CDATA标记 boolean cdata = true; @SuppressWarnings("unchecked") public void startNode(String name, Class clazz) { super.startNode(name, clazz); } protected void writeText(QuickWriter writer, String text) { if (cdata) { writer.write(""); } else { writer.write(text); } } }; } }); 说明:由于xstream框架本身并不支持CDATA块的生成,40~62行代码是对xtream做了扩展,使其支持在生成xml各元素值时添加CDATA块。 消息处理工具的封装 知道怎么解析请求消息,也知道如何将响应消息转化成xml了,接下来就是将消息相关的处理方法全部封装到工具类MessageUtil中,该类的完整代码如下: [java] view plaincopyprint? 1. package org.liufeng.course.util;   2.    3. import java.io.InputStream;   4. import java.io.Writer;   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.Element;   13. import org.dom4j.io.SAXReader;   14. import org.liufeng.course.message.resp.Article;   15. import org.liufeng.course.message.resp.MusicMessage;   16. import org.liufeng.course.message.resp.NewsMessage;   17. import org.liufeng.course.message.resp.TextMessage;   18.    19. import com.thoughtworks.xstream.XStream;   20. import com.thoughtworks.xstream.core.util.QuickWriter;   21. import com.thoughtworks.xstream.io.HierarchicalStreamWriter;   22. import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;   23. import com.thoughtworks.xstream.io.xml.XppDriver;   24.    25. /**  26.  * 消息工具类  27.  *   28.  * @author liufeng  29.  * @date 2013-05-19  30.  */   31. public class MessageUtil {   32.    33.     /**  34.      * 返回消息类型:文本  35.      */   36.     public static final String RESP_MESSAGE_TYPE_TEXT = "text";   37.    38.     /**  39.      * 返回消息类型:音乐  40.      */   41.     public static final String RESP_MESSAGE_TYPE_MUSIC = "music";   42.    43.     /**  44.      * 返回消息类型:图文  45.      */   46.     public static final String RESP_MESSAGE_TYPE_NEWS = "news";   47.    48.     /**  49.      * 请求消息类型:文本  50.      */   51.     public static final String REQ_MESSAGE_TYPE_TEXT = "text";   52.    53.     /**  54.      * 请求消息类型:图片  55.      */   56.     public static final String REQ_MESSAGE_TYPE_IMAGE = "image";   57.    58.     /**  59.      * 请求消息类型:链接  60.      */   61.     public static final String REQ_MESSAGE_TYPE_LINK = "link";   62.    63.     /**  64.      * 请求消息类型:地理位置  65.      */   66.     public static final String REQ_MESSAGE_TYPE_LOCATION = "location";   67.    68.     /**  69.      * 请求消息类型:音频  70.      */   71.     public static final String REQ_MESSAGE_TYPE_VOICE = "voice";   72.    73.     /**  74.      * 请求消息类型:推送  75.      */   76.     public static final String REQ_MESSAGE_TYPE_EVENT = "event";   77.    78.     /**  79.      * 事件类型:subscribe(订阅)  80.      */   81.     public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";   82.    83.     /**  84.      * 事件类型:unsubscribe(取消订阅)  85.      */   86.     public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";   87.    88.     /**  89.      * 事件类型:CLICK(自定义菜单点击事件)  90.      */   91.     public static final String EVENT_TYPE_CLICK = "CLICK";   92.    93.     /**  94.      * 解析微信发来的请求(XML)  95.      *   96.      * @param request  97.      * @return  98.      * @throws Exception  99.      */   100.     @SuppressWarnings("unchecked")   101.     public static Map parseXml(HttpServletRequest request) throws Exception {   102.         // 将解析结果存储在HashMap中    103.         Map map = new HashMap();   104.    105.         // 从request中取得输入流    106.         InputStream inputStream = request.getInputStream();   107.         // 读取输入流    108.         SAXReader reader = new SAXReader();   109.         Document document = reader.read(inputStream);   110.         // 得到xml根元素    111.         Element root = document.getRootElement();   112.         // 得到根元素的所有子节点    113.         List elementList = root.elements();   114.    115.         // 遍历所有子节点    116.         for (Element e : elementList)   117.             map.put(e.getName(), e.getText());   118.    119.         // 释放资源    120.         inputStream.close();   121.         inputStream = null;   122.    123.         return map;   124.     }   125.    126.     /**  127.      * 文本消息对象转换成xml  128.      *   129.      * @param textMessage 文本消息对象  130.      * @return xml  131.      */   132.     public static String textMessageToXml(TextMessage textMessage) {   133.         xstream.alias("xml", textMessage.getClass());   134.         return xstream.toXML(textMessage);   135.     }   136.    137.     /**  138.      * 音乐消息对象转换成xml  139.      *   140.      * @param musicMessage 音乐消息对象  141.      * @return xml  142.      */   143.     public static String musicMessageToXml(MusicMessage musicMessage) {   144.         xstream.alias("xml", musicMessage.getClass());   145.         return xstream.toXML(musicMessage);   146.     }   147.    148.     /**  149.      * 图文消息对象转换成xml  150.      *   151.      * @param newsMessage 图文消息对象  152.      * @return xml  153.      */   154.     public static String newsMessageToXml(NewsMessage newsMessage) {   155.         xstream.alias("xml", newsMessage.getClass());   156.         xstream.alias("item", new Article().getClass());   157.         return xstream.toXML(newsMessage);   158.     }   159.    160.     /**  161.      * 扩展xstream,使其支持CDATA块  162.      *   163.      * @date 2013-05-19  164.      */   165.     private static XStream xstream = new XStream(new XppDriver() {   166.         public HierarchicalStreamWriter createWriter(Writer out) {   167.             return new PrettyPrintWriter(out) {   168.                 // 对所有xml节点的转换都增加CDATA标记    169.                 boolean cdata = true;   170.    171.                 @SuppressWarnings("unchecked")   172.                 public void startNode(String name, Class clazz) {   173.                     super.startNode(name, clazz);   174.                 }   175.    176.                 protected void writeText(QuickWriter writer, String text) {   177.                     if (cdata) {   178.                         writer.write("");   181.                     } else {   182.                         writer.write(text);   183.                     }   184.                 }   185.             };   186.         }   187.     });   188. }   package org.liufeng.course.util; import java.io.InputStream; import java.io.Writer; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.liufeng.course.message.resp.Article; import org.liufeng.course.message.resp.MusicMessage; import org.liufeng.course.message.resp.NewsMessage; import org.liufeng.course.message.resp.TextMessage; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.core.util.QuickWriter; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.xml.PrettyPrintWriter; import com.thoughtworks.xstream.io.xml.XppDriver; /** * 消息工具类 * * @author liufeng * @date 2013-05-19 */ public class MessageUtil { /** * 返回消息类型:文本 */ public static final String RESP_MESSAGE_TYPE_TEXT = "text"; /** * 返回消息类型:音乐 */ public static final String RESP_MESSAGE_TYPE_MUSIC = "music"; /** * 返回消息类型:图文 */ public static final String RESP_MESSAGE_TYPE_NEWS = "news"; /** * 请求消息类型:文本 */ public static final String REQ_MESSAGE_TYPE_TEXT = "text"; /** * 请求消息类型:图片 */ public static final String REQ_MESSAGE_TYPE_IMAGE = "image"; /** * 请求消息类型:链接 */ public static final String REQ_MESSAGE_TYPE_LINK = "link"; /** * 请求消息类型:地理位置 */ public static final String REQ_MESSAGE_TYPE_LOCATION = "location"; /** * 请求消息类型:音频 */ public static final String REQ_MESSAGE_TYPE_VOICE = "voice"; /** * 请求消息类型:推送 */ public static final String REQ_MESSAGE_TYPE_EVENT = "event"; /** * 事件类型:subscribe(订阅) */ public static final String EVENT_TYPE_SUBSCRIBE = "subscribe"; /** * 事件类型:unsubscribe(取消订阅) */ public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe"; /** * 事件类型:CLICK(自定义菜单点击事件) */ public static final String EVENT_TYPE_CLICK = "CLICK"; /** * 解析微信发来的请求(XML) * * @param request * @return * @throws Exception */ @SuppressWarnings("unchecked") public static Map parseXml(HttpServletRequest request) throws Exception { // 将解析结果存储在HashMap中 Map map = new HashMap(); // 从request中取得输入流 InputStream inputStream = request.getInputStream(); // 读取输入流 SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); // 得到xml根元素 Element root = document.getRootElement(); // 得到根元素的所有子节点 List elementList = root.elements(); // 遍历所有子节点 for (Element e : elementList) map.put(e.getName(), e.getText()); // 释放资源 inputStream.close(); inputStream = null; return map; } /** * 文本消息对象转换成xml * * @param textMessage 文本消息对象 * @return xml */ public static String textMessageToXml(TextMessage textMessage) { xstream.alias("xml", textMessage.getClass()); return xstream.toXML(textMessage); } /** * 音乐消息对象转换成xml * * @param musicMessage 音乐消息对象 * @return xml */ public static String musicMessageToXml(MusicMessage musicMessage) { xstream.alias("xml", musicMessage.getClass()); return xstream.toXML(musicMessage); } /** * 图文消息对象转换成xml * * @param newsMessage 图文消息对象 * @return xml */ public static String newsMessageToXml(NewsMessage newsMessage) { xstream.alias("xml", newsMessage.getClass()); xstream.alias("item", new Article().getClass()); return xstream.toXML(newsMessage); } /** * 扩展xstream,使其支持CDATA块 * * @date 2013-05-19 */ private static XStream xstream = new XStream(new XppDriver() { public HierarchicalStreamWriter createWriter(Writer out) { return new PrettyPrintWriter(out) { // 对所有xml节点的转换都增加CDATA标记 boolean cdata = true; @SuppressWarnings("unchecked") public void startNode(String name, Class clazz) { super.startNode(name, clazz); } protected void writeText(QuickWriter writer, String text) { if (cdata) { writer.write(""); } else { writer.write(text); } } }; } }); } OK,到这里关于消息及消息处理工具的封装就讲到这里,其实就是对请求消息/响应消息建立了与之对应的Java类、对xml消息进行解析、将响应消息的Java对象转换成xml。下一篇讲会介绍如何利用上面封装好的工具识别用户发送的消息类型,并做出正确的响应。 5. 微信公众帐号开发教程第5篇-各种消息的接收与响应 《这些年,我们读过的技术经典图书》主题有奖征文        专访李铁军:从医生到金山首席安全专家的转变      独一无二的职位:开源社区经理 [029] 微信公众帐号开发教程第5篇-各种消息的接收与响应 分类: 微信公众平台 2013-05-21 13:56 7197人阅读 评论(67) 收藏 举报 微信公众平台开发Java事件推送消息关注自定义菜单 前一篇文章里我们已经把微信公众平台接口中消息及相关操作都进行了封装,本章节将主要介绍如何接收微信服务器发送的消息并做出响应。 明确在哪接收消息 从微信公众平台接口消息指南中可以了解到,当用户向公众帐号发消息时,微信服务器会将消息通过POST方式提交给我们在接口配置信息中填写的URL,而我们就需要在URL所指向的请求处理类CoreServlet的doPost方法中接收消息、处理消息和响应消息。 接收、处理、响应消息 下面先来看我已经写好的CoreServlet的完整代码: [java] view plaincopyprint? 1. package org.liufeng.course.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.liufeng.course.service.CoreService;   12. import org.liufeng.course.util.SignUtil;   13.    14. /**  15.  * 核心请求处理类  16.  *   17.  * @author liufeng  18.  * @date 2013-05-18  19.  */   20. public class CoreServlet extends HttpServlet {   21.     private static final long serialVersionUID = 4440739483644821986L;   22.    23.     /**  24.      * 确认请求来自微信服务器  25.      */   26.     public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {   27.         // 微信加密签名    28.         String signature = request.getParameter("signature");   29.         // 时间戳    30.         String timestamp = request.getParameter("timestamp");   31.         // 随机数    32.         String nonce = request.getParameter("nonce");   33.         // 随机字符串    34.         String echostr = request.getParameter("echostr");   35.    36.         PrintWriter out = response.getWriter();   37.         // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败    38.         if (SignUtil.checkSignature(signature, timestamp, nonce)) {   39.             out.print(echostr);   40.         }   41.         out.close();   42.         out = null;   43.     }   44.    45.     /**  46.      * 处理微信服务器发来的消息  47.      */   48.     public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {   49.         // 将请求、响应的编码均设置为UTF-8(防止中文乱码)    50.         request.setCharacterEncoding("UTF-8");   51.         response.setCharacterEncoding("UTF-8");   52.    53.         // 调用核心业务类接收消息、处理消息    54.         String respMessage = CoreService.processRequest(request);   55.            56.         // 响应消息    57.         PrintWriter out = response.getWriter();   58.         out.print(respMessage);   59.         out.close();   60.     }   61.    62. }   package org.liufeng.course.servlet; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.liufeng.course.service.CoreService; import org.liufeng.course.util.SignUtil; /** * 核心请求处理类 * * @author liufeng * @date 2013-05-18 */ public class CoreServlet extends HttpServlet { private static final long serialVersionUID = 4440739483644821986L; /** * 确认请求来自微信服务器 */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 微信加密签名 String signature = request.getParameter("signature"); // 时间戳 String timestamp = request.getParameter("timestamp"); // 随机数 String nonce = request.getParameter("nonce"); // 随机字符串 String echostr = request.getParameter("echostr"); PrintWriter out = response.getWriter(); // 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败 if (SignUtil.checkSignature(signature, timestamp, nonce)) { out.print(echostr); } out.close(); out = null; } /** * 处理微信服务器发来的消息 */ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 将请求、响应的编码均设置为UTF-8(防止中文乱码) request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); // 调用核心业务类接收消息、处理消息 String respMessage = CoreService.processRequest(request); // 响应消息 PrintWriter out = response.getWriter(); out.print(respMessage); out.close(); } } 代码说明: 1)第51行代码:微信服务器POST消息时用的是UTF-8编码,在接收时也要用同样的编码,否则中文会乱码; 2)第52行代码:在响应消息(回复消息给用户)时,也将编码方式设置为UTF-8,原理同上; 3)第54行代码:调用CoreService类的processRequest方法接收、处理消息,并得到处理结果; 4)第57~59行:调用response.getWriter().write()方法将消息的处理结果返回给用户 从doPost方法的实现可以看到,它是通过调用CoreService类的processRequest方法接收、处理消息的,这样做的目的是为了解耦,即业务相关的操作都不在Servlet里处理,而是完全交由业务核心类CoreService去做。下面来看CoreService类的代码实现: [java] view plaincopyprint? 1. package org.liufeng.course.service;   2.    3. import java.util.Date;   4. import java.util.Map;   5. import javax.servlet.http.HttpServletRequest;   6. import org.liufeng.course.message.resp.TextMessage;   7. import org.liufeng.course.util.MessageUtil;   8.    9. /**  10.  * 核心服务类  11.  *   12.  * @author liufeng  13.  * @date 2013-05-20  14.  */   15. public class CoreService {   16.     /**  17.      * 处理微信发来的请求  18.      *   19.      * @param request  20.      * @return  21.      */   22.     public static String processRequest(HttpServletRequest request) {   23.         String respMessage = null;   24.         try {   25.             // 默认返回的文本消息内容    26.             String respContent = "请求处理异常,请稍候尝试!";   27.    28.             // xml请求解析    29.             Map requestMap = MessageUtil.parseXml(request);   30.    31.             // 发送方帐号(open_id)    32.             String fromUserName = requestMap.get("FromUserName");   33.             // 公众帐号    34.             String toUserName = requestMap.get("ToUserName");   35.             // 消息类型    36.             String msgType = requestMap.get("MsgType");   37.    38.             // 回复文本消息    39.             TextMessage textMessage = new TextMessage();   40.             textMessage.setToUserName(fromUserName);   41.             textMessage.setFromUserName(toUserName);   42.             textMessage.setCreateTime(new Date().getTime());   43.             textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);   44.             textMessage.setFuncFlag(0);   45.    46.             // 文本消息    47.             if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {   48.                 respContent = "您发送的是文本消息!";   49.             }   50.             // 图片消息    51.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) {   52.                 respContent = "您发送的是图片消息!";   53.             }   54.             // 地理位置消息    55.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) {   56.                 respContent = "您发送的是地理位置消息!";   57.             }   58.             // 链接消息    59.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) {   60.                 respContent = "您发送的是链接消息!";   61.             }   62.             // 音频消息    63.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) {   64.                 respContent = "您发送的是音频消息!";   65.             }   66.             // 事件推送    67.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {   68.                 // 事件类型    69.                 String eventType = requestMap.get("Event");   70.                 // 订阅    71.                 if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) {   72.                     respContent = "谢谢您的关注!";   73.                 }   74.                 // 取消订阅    75.                 else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {   76.                     // TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息    77.                 }   78.                 // 自定义菜单点击事件    79.                 else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {   80.                     // TODO 自定义菜单权没有开放,暂不处理该类消息    81.                 }   82.             }   83.    84.             textMessage.setContent(respContent);   85.             respMessage = MessageUtil.textMessageToXml(textMessage);   86.         } catch (Exception e) {   87.             e.printStackTrace();   88.         }   89.    90.         return respMessage;   91.     }   92. }   package org.liufeng.course.service; import java.util.Date; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.liufeng.course.message.resp.TextMessage; import org.liufeng.course.util.MessageUtil; /** * 核心服务类 * * @author liufeng * @date 2013-05-20 */ public class CoreService { /** * 处理微信发来的请求 * * @param request * @return */ public static String processRequest(HttpServletRequest request) { String respMessage = null; try { // 默认返回的文本消息内容 String respContent = "请求处理异常,请稍候尝试!"; // xml请求解析 Map requestMap = MessageUtil.parseXml(request); // 发送方帐号(open_id) String fromUserName = requestMap.get("FromUserName"); // 公众帐号 String toUserName = requestMap.get("ToUserName"); // 消息类型 String msgType = requestMap.get("MsgType"); // 回复文本消息 TextMessage textMessage = new TextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(new Date().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); textMessage.setFuncFlag(0); // 文本消息 if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { respContent = "您发送的是文本消息!"; } // 图片消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) { respContent = "您发送的是图片消息!"; } // 地理位置消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) { respContent = "您发送的是地理位置消息!"; } // 链接消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) { respContent = "您发送的是链接消息!"; } // 音频消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) { respContent = "您发送的是音频消息!"; } // 事件推送 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) { // 事件类型 String eventType = requestMap.get("Event"); // 订阅 if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) { respContent = "谢谢您的关注!"; } // 取消订阅 else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) { // TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息 } // 自定义菜单点击事件 else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) { // TODO 自定义菜单权没有开放,暂不处理该类消息 } } textMessage.setContent(respContent); respMessage = MessageUtil.textMessageToXml(textMessage); } catch (Exception e) { e.printStackTrace(); } return respMessage; } } 代码说明: 1)第29行:调用消息工具类MessageUtil解析微信发来的xml格式的消息,解析的结果放在HashMap里; 2)32~36行:从HashMap中取出消息中的字段; 3)39-44、84行:组装要返回的文本消息对象; 4)47~82行:演示了如何接收微信发送的各类型的消息,根据MsgType判断属于哪种类型的消息; 5)85行:调用消息工具类MessageUtil将要返回的文本消息对象TextMessage转化成xml格式的字符串; 关于事件推送(关注、取消关注、菜单点击) 对于消息类型的判断,像文本消息、图片消息、地理位置消息、链接消息和语音消息都比较好理解,有很多刚接触的朋友搞不懂事件推送消息有什么用,或者不清楚该如何判断用户关注的消息。那我们就专门来看下事件推送,下图是官方消息接口文档中关于事件推送的说明: 这里我们只要关心两个参数:MsgType和Event。当MsgType=event时,就表示这是一条事件推送消息;而Event表示事件类型,包括订阅、取消订阅和自定义菜单点击事件。也就是说,无论用户是关注了公众帐号、取消对公众帐号的关注,还是在使用公众帐号的菜单,微信服务器都会发送一条MsgType=event的消息给我们,而至于具体这条消息表示关注、取消关注,还是菜单的点击事件,就需要通过Event的值来判断了。(注意区分Event和event) 连载五篇教程总结 经过5篇的讲解,已经把开发模式启用,接口配置,消息相关工具类的封装,消息的接收与响应全部讲解完了,而且贴上了完整的源代码,相信有一定Java基础的朋友可以看的明白,能够通过系列文章基本掌握微信公众平台开发的相关技术知识。下面我把目前项目的完整结构贴出,方便大家对照: 6. 微信公众帐号开发教程第6篇-文本消息的内容长度限制揭秘 相信不少朋友都遇到过这样的问题:当发送的文本消息内容过长时,微信将不做任何响应。那么到底微信允许的文本消息的最大长度是多少呢?我们又该如何计算文本的长度呢?为什么还有些人反应微信好像支持的文本消息最大长度在1300多呢?这篇文章会彻底解除大家的疑问。 接口文档中对消息长度限制为2048 可以看到,接口文档中写的很明确:回复的消息内容长度不超过2048字节。那为什么很多人测试反应消息内容长度在1300多字节时,微信就不响应了呢?我想这问题应该在这部分人没有搞清楚到底该如何计算文本的字节数。 如何正确计算文本所占字节数 计算文本(字符串)所占字节数,大家第一个想到的应该就是String类的getBytes()方法,该方法返回的是字符串对应的字节数组,再计算数组的length就能够得到字符串所占字节数。例如: [java] view plaincopyprint? 1. public static void main(String []args)  {   2.     // 运行结果:4    3.     System.out.println("柳峰".getBytes().length);   4. }   public static void main(String []args) { // 运行结果:4 System.out.println("柳峰".getBytes().length); } 上面的示例中计算了两个中文所占的字节数为4,即一个汉字占2个字节。真的是这样吗?其实我们忽略了一个问题:对于不同的编码方式,中文所占的字节数也不一样!这到底要怎么呢?在上面的例子中,我们并没有指定编码方式,那么会使用操作系统所默认的编码方式。先来看我得出的三条结论: 1)如果上面的例子运行在默认编码方式为ISO8859-1的操作系统平台上,计算结果是2; 2)如果上面的例子运行在默认编码方式为gb2312或gbk的操作系统平台上,计算结果是4; 3)如果上面的例子运行在默认编码方式为utf-8的操作系统平台上,计算结果是6; 如果真的是这样,是不是意味着String.getBytes()方法在我们的系统平台上默认采用的是gb2312或gbk编码方式呢?我们再来看一个例子: [java] view plaincopyprint? 1. public static void main(String []args) throws UnsupportedEncodingException  {   2.     // 运行结果:2    3.     System.out.println("柳峰".getBytes("ISO8859-1").length);   4.     // 运行结果:4    5.     System.out.println("柳峰".getBytes("GB2312").length);   6.     // 运行结果:4    7.     System.out.println("柳峰".getBytes("GBK").length);   8.     // 运行结果:6    9.     System.out.println("柳峰".getBytes("UTF-8").length);   10. }   public static void main(String []args) throws UnsupportedEncodingException { // 运行结果:2 System.out.println("柳峰".getBytes("ISO8859-1").length); // 运行结果:4 System.out.println("柳峰".getBytes("GB2312").length); // 运行结果:4 System.out.println("柳峰".getBytes("GBK").length); // 运行结果:6 System.out.println("柳峰".getBytes("UTF-8").length); } 这个例子是不是很好地证明了我上面给出的三条结论呢?也就是说采用ISO8859-1编码方式时,一个中/英文都只占一个字节;采用GB2312或GBK编码方式时,一个中文占两个字节;而采用UTF-8编码方式时,一个中文占三个字节。 微信平台采用的编码方式及字符串所占字节数的计算 那么,在向微信服务器返回消息时,该采用什么编码方式呢?当然是UTF-8,因为我们已经在doPost方法里采用了如下代码来避免中文乱码了: [java] view plaincopyprint? 1. // 将请求、响应的编码均设置为UTF-8(防止中文乱码)    2. request.setCharacterEncoding("UTF-8");   3. response.setCharacterEncoding("UTF-8");   // 将请求、响应的编码均设置为UTF-8(防止中文乱码) request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); 为了验证我所说了,我写了个例子来测试: [java] view plaincopyprint? 1. private static String getMsgContent() {   2.     StringBuffer buffer = new StringBuffer();   3.     // 每行70个汉字,共682个汉字加1个英文的感叹号    4.     buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");   5.     buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");   6.     buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");   7.     buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");   8.     buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");   9.     buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");   10.     buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");   11.     buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");   12.     buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你");   13.     buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵!");   14.     return buffer.toString();   15. }   16.    17. public static void main(String []args) throws Exception  {   18.     // 采用gb2312编码方式时占1365个字节    19.     System.out.println(getMsgContent().getBytes("gb2312").length);   20.     // 采用utf-8编码方式时占2047个字节    21.     System.out.println(getMsgContent().getBytes("utf-8").length);   22. }   private static String getMsgContent() { StringBuffer buffer = new StringBuffer(); // 每行70个汉字,共682个汉字加1个英文的感叹号 buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你"); buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你"); buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你"); buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你"); buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你"); buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你"); buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你"); buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你"); buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵你的手走过风风雨雨有什么困难我都陪你"); buffer.append("不知道什么时候开始喜欢这里每个夜里都会来这里看你你长得多么美丽叫我不能不看你看不到你我就迷失了自己好想牵!"); return buffer.toString(); } public static void main(String []args) throws Exception { // 采用gb2312编码方式时占1365个字节 System.out.println(getMsgContent().getBytes("gb2312").length); // 采用utf-8编码方式时占2047个字节 System.out.println(getMsgContent().getBytes("utf-8").length); } getMsgContent()方法返回的内容正是微信的文本消息最长能够支持的,即采用UTF-8编码方式时,文本消息内容最多支持2047个字节,也就是微信公众平台接口文档里所说的回复的消息内容长度不超过2048字节,即使是等于2048字节也不行,你可以试着将getMsgContent()方法里的内容多加一个英文符号,这个时候微信就不响应了。 同时,我们也发现,如果采用gb2312编码方式来计算getMsgContent()方法返回的文本所占字节数的结果是1365,这就是为什么很多朋友都说微信的文本消息最大长度好像只支持1300多字节,并不是接口文档中所说的2048字节,其实是忽略了编码方式,只是简单的使用了String类的getBytes()方法而不是getBytes("utf-8")方法去计算所占字节数。 Java中utf-8编码方式时所占字节数的计算方法封装 [java] view plaincopyprint? 1. /**  2.  * 计算采用utf-8编码方式时字符串所占字节数  3.  *   4.  * @param content  5.  * @return  6.  */   7. public static int getByteSize(String content) {   8.     int size = 0;   9.     if (null != content) {   10.         try {   11.             // 汉字采用utf-8编码时占3个字节    12.             size = content.getBytes("utf-8").length;   13.         } catch (UnsupportedEncodingException e) {   14.             e.printStackTrace();   15.         }   16.     }   17.     return size;   18. }   /** * 计算采用utf-8编码方式时字符串所占字节数 * * @param content * @return */ public static int getByteSize(String content) { int size = 0; if (null != content) { try { // 汉字采用utf-8编码时占3个字节 size = content.getBytes("utf-8").length; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } return size; } 好了,本章节的内容就讲到这里,我想大家通过这篇文章所学到的应该不仅仅是2047这个数字,还应该对字符编码方式有一个新的认识。 7. 微信公众帐号开发教程第7篇-文本消息中换行符的使用 本篇文章主要介绍在文本消息中使用换行符的好处以及如何使用换行符。 最近一个月虽然抽不出时间写博客,但却一直在认真答复大家提出的问题。收到这么多的回复、关注和答谢,还是蛮有成就感的,让我觉得做这件事越来越有意义,更加坚定了我继续写下去的决心。经过前面六篇文章的讲解,相信在看文章的你,已经掌握了微信公众帐号的基础开发知识(基于Java),如框架搭建、API封装、消息接收与回复等;接下来的系列文章将专注于讲解公众帐号开发中的技巧及实用功能的开发(如天气查询、周边搜索、人机对话等)。 使用换行的好处及示例 使用换行的好处无非就是让信息的呈现更加整齐、美观和直观,适当的在文本消息中使用换行符,会让人看了之后感觉很舒服、清晰、明了。下面是公众帐号xiaoqrobot的主菜单示例,就是合理地使用了换行符,看上去是不是很直观、清爽呢?(什么?觉得很丑?呃,那就算是我自恋吧...) 你可以试想一下,如果这个文本菜单没有使用一个换行符,那会长什么样? 如何在文本消息中使用换行符? 在微信公众帐号的文本消息中,换行符仍然是“\n”,下面就通过代码来讲解xiaoqrobot的文本菜单是如何实现的? [java] view plaincopyprint? 1. /**  2.  * xiaoqrobot的主菜单  3.  *   4.  * @return  5.  */   6. public static String getMainMenu() {   7.     StringBuffer buffer = new StringBuffer();   8.     buffer.append("您好,我是小q,请回复数字选择服务:").append("\n\n");   9.     buffer.append("1  天气预报").append("\n");   10.     buffer.append("2  公交查询").append("\n");   11.     buffer.append("3  周边搜索").append("\n");   12.     buffer.append("4  歌曲点播").append("\n");   13.     buffer.append("5  经典游戏").append("\n");   14.     buffer.append("6  美女电台").append("\n");   15.     buffer.append("7  人脸识别").append("\n");   16.     buffer.append("8  聊天唠嗑").append("\n\n");   17.     buffer.append("回复“?”显示此帮助菜单");   18.     return buffer.toString();   19. }   /** * xiaoqrobot的主菜单 * * @return */ public static String getMainMenu() { StringBuffer buffer = new StringBuffer(); buffer.append("您好,我是小q,请回复数字选择服务:").append("\n\n"); buffer.append("1 天气预报").append("\n"); buffer.append("2 公交查询").append("\n"); buffer.append("3 周边搜索").append("\n"); buffer.append("4 歌曲点播").append("\n"); buffer.append("5 经典游戏").append("\n"); buffer.append("6 美女电台").append("\n"); buffer.append("7 人脸识别").append("\n"); buffer.append("8 聊天唠嗑").append("\n\n"); buffer.append("回复“?”显示此帮助菜单"); return buffer.toString(); } 怎么样,实现起来是不是很简单呢? 1)9-16行就是菜单项,菜单项之间都是用一个换行符分隔; 2)第8行、第16号末尾都使用了两个换行符,这样可以把菜单项与其他内容分隔开,更有层次感,看上去也会舒服、直观一点。 可能细心的朋友已经发现了:在截图上,“周边搜索”和“美女电台”后边都有一个“礼物”表情,而代码中并没有看到,这是我专门去掉的,因为我打算后面专门用一篇文章把QQ表情的发送、处理、接收讲清楚。 细节决定成败! 8. 微信公众帐号开发教程第8篇-文本消息中使用网页超链接 本文主要介绍网页超链接的作用以及如何在文本消息中使用网页超链接。 网页超链接的作用 我想但凡是熟悉HTML的朋友,对超链接一定不会陌生。而今天我们要讨论和使用的只是超链接中的其中一种---网页超链接,即使用HTML中的标签将某段文字链接到其他网页上去,示例如下: [html] view plaincopyprint? 1. 柳峰的博客   柳峰的博客 上面是一段标准的HTML代码,实现了一个网页超链接,即将“柳峰的博客”5个字链接到了博客主页URL,当“柳峰的博客”5个字时,会打开http://blog.csdn.net/lyq8479所指向的网页。 如何在文本消息中使用网页超链接 其实,不知道如何在文本消息中使用网页超链接的开发者几乎100%都熟悉HTML,特别是对HTML中的标签再熟悉不过了。那到底在微信公众帐号的文本消息中使用超链接有什么特别之处呢?为什么如此多的朋友都曾经在这个问题上栽过跟头?我们先来看在微信中两种错误使用超链接的方法: 错误用法1(a标签的href属性值未被引号引起): [html] view plaincopyprint? 1. 柳峰的博客   柳峰的博客 错误用法2(a标签的href属性值被单引号引起): [html] view plaincopyprint? 1. 柳峰的博客   柳峰的博客 在做Web开发时,以上两种写法都是可以的,但是放在微信公众帐号的文本消息中,这两种写法都是错误的,网页超链接并不会起作用,而且在Android手机上还会将HTML代码原样显示出来,如下图所示: Android手机上的效果: iPhone手机上的效果: 可以看出,在微信上,HTML的a标签属性值不用引号引起,或者使用单引号引起,都是错误的写法(在iPhone上,a标签属性href的值用单引号是正常的)。正确的用法是将a标签href属性的值用双引号引起,代码如下: [html] view plaincopyprint? 1. 柳峰的博客   柳峰的博客 这样在Android和iPhone手机上,都可以正确显示超链接,并且点击该超链接,会使用微信内置浏览器打开http://blog.csdn.net/lyq8479。 提示:在测试微信公众帐号时,不要只是在自己的手机上测试通过就认为完全没问题了,因为目前微信公众帐号上有好几处在Android和iOS平台上表现不一致 9. 微信公众帐号开发教程第9篇-QQ表情的发送与接收 我想大家对QQ表情一定不会陌生,一个个小头像极大丰富了聊天的乐趣,使得聊天不再是简单的文字叙述,还能够配上喜、怒、哀、乐等表达人物心情的小图片。本文重点要介绍的内容就是如何在微信公众平台使用QQ表情,即在微信公众帐号开发模式下,如何发送QQ表情给用户,以及如何识别用户发来的是QQ表情。 QQ表情代码表 首先需要明确的是:QQ表情虽然呈现为一张张动态的表情图片,但在微信公众平台的消息接口中却是属于文本消息;也就是说当用户向公众帐号发送QQ表情时,公众帐号后台程序接收到的消息类型MsgType的值为text。只要上面这点能理解了,下面的工作就好开展了。 对于QQ表情,发送的是文本消息,而呈现出来却是表情图片,那么每一个QQ表情图片一定会有与之相对应的表情代码。下面是我已经整理好的微信公众帐号中使用的QQ表情代码对照表: 上面一共列出了105个QQ表情,每个表情都给出了与之相对应的文字代码与符号代码(也许这两种叫法并不恰当),至于这两种代码怎么来的以及如何使用,下面马上会讲到。 用户向公众帐号发送QQ表情 在微信上使用公众帐号时,如何发送QQ表情,我想这个很少有人不会的。在输入框旁边有一个笑脸的图片按钮,点击它将会弹出表情选择界面,可选择的表情依次为“QQ表情”、“符号表情”和“动画表情”。当我们点击选择了某个QQ表情后,发现在输入框中会显示该表情的文字代码,这里是用一对中括号引起的,如下图所示: 其实,当我们很熟悉要使用QQ表情的文字代码时,也可以直接在输入框中输入表情的代码,而不需要弹出表情选择框。如下图所示: 从上图可以看出,在输入框中输入“[呲牙]”、“/呲牙”和“/::D”这三种代码的作用一样,都是发送呲牙的QQ表情。这个时候,大家再回过头去看文章最开始的QQ表情代码对照表,就明白是怎么回事了。 公众帐号向用户发送QQ表情 与用户向公众帐号发送QQ表情一样,在开发模式下,公众帐号也可以用同样的表情代码(文字代码或符号代码)向用户回复QQ表情。代码片段如下: [java] view plaincopyprint? 1. // 文本消息    2. if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {   3.     // 回复文本消息    4.     TextMessage textMessage = new TextMessage();   5.     textMessage.setToUserName(fromUserName);   6.     textMessage.setFromUserName(toUserName);   7.     textMessage.setCreateTime(new Date().getTime());   8.     textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);   9.     textMessage.setFuncFlag(0);   10.     textMessage.setContent("[难过] /难过 /::(");   11.        12.     // 文本消息对象转换成xml字符串    13.     respMessage = MessageUtil.textMessageToXml(textMessage);   14. }   // 文本消息 if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { // 回复文本消息 TextMessage textMessage = new TextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(new Date().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); textMessage.setFuncFlag(0); textMessage.setContent("[难过] /难过 /::("); // 文本消息对象转换成xml字符串 respMessage = MessageUtil.textMessageToXml(textMessage); } 上面代码片段的作用是:判断发送的消息类型,如果是文本消息(MsgType=text),则回复三个难过的QQ表情给用户。可以看出,不管是用户发给公众帐号,还是公众帐号发给用户,都可以使用QQ表情的文字代码(如:[难过]  /难过)和符号代码(如 /::()。 公众帐号识别用户发送的QQ表情 在掌握了如何发送QQ表情后,我们再来看看公众帐号如何识别用户发送的是QQ表情。这是什么意思呢?当用户向公众帐号发送一个QQ表情,在后台程序中接收到的会是什么值,我们又怎么知道这个值就是一个QQ表情。 其实,只要做个简单的测试,比如:将接收到的文本消息输出到日志中(可以用log4j或者System.out.print),不难发现:向公众帐号发送一个QQ表情,在后台程序中接收到的是QQ表情的符号代码。 下面是我简单封装的一个方法,通过正则表达式实现的,用于判断用户发送的是否是单个QQ表情。 [java] view plaincopyprint? 1. /**  2.  * 判断是否是QQ表情  3.  *   4.  * @param content  5.  * @return  6.  */   7. public static boolean isQqFace(String content) {   8.     boolean result = false;   9.    10.     // 判断QQ表情的正则表达式    11.     String qqfaceRegex = "/::\\)|/::~|/::B|/::\\||/:8-\\)|/::<|/::$|/::X|/::Z|/::'\\(|/::-\\||/::@|/::P|/::D|/::O|/::\\(|/::\\+|/:--b|/::Q|/::T|/:,@P|/:,@-D|/::d|/:,@o|/::g|/:\\|-\\)|/::!|/::L|/::>|/::,@|/:,@f|/::-S|/:\\?|/:,@x|/:,@@|/::8|/:,@!|/:!!!|/:xx|/:bye|/:wipe|/:dig|/:handclap|/:&-\\(|/:B-\\)|/:<@|/:@>|/::-O|/:>-\\||/:P-\\(|/::'\\||/:X-\\)|/::\\*|/:@x|/:8\\*|/:pd|/:|/:beer|/:basketb|/:oo|/:coffee|/:eat|/:pig|/:rose|/:fade|/:showlove|/:heart|/:break|/:cake|/:li|/:bome|/:kn|/:footb|/:ladybug|/:shit|/:moon|/:sun|/:gift|/:hug|/:strong|/:weak|/:share|/:v|/:@\\)|/:jj|/:@@|/:bad|/:lvu|/:no|/:ok|/:love|/:|/:jump|/:shake|/:|/:circle|/:kotow|/:turn|/:skip|/:oY|/:#-0|/:hiphot|/:kiss|/:<&|/:&>";   12.     Pattern p = Pattern.compile(qqfaceRegex);   13.     Matcher m = p.matcher(content);   14.     if (m.matches()) {   15.         result = true;   16.     }   17.     return result;   18. }   /** * 判断是否是QQ表情 * * @param content * @return */ public static boolean isQqFace(String content) { boolean result = false; // 判断QQ表情的正则表达式 String qqfaceRegex = "/::\\)|/::~|/::B|/::\\||/:8-\\)|/::<|/::$|/::X|/::Z|/::'\\(|/::-\\||/::@|/::P|/::D|/::O|/::\\(|/::\\+|/:--b|/::Q|/::T|/:,@P|/:,@-D|/::d|/:,@o|/::g|/:\\|-\\)|/::!|/::L|/::>|/::,@|/:,@f|/::-S|/:\\?|/:,@x|/:,@@|/::8|/:,@!|/:!!!|/:xx|/:bye|/:wipe|/:dig|/:handclap|/:&-\\(|/:B-\\)|/:<@|/:@>|/::-O|/:>-\\||/:P-\\(|/::'\\||/:X-\\)|/::\\*|/:@x|/:8\\*|/:pd|/:|/:beer|/:basketb|/:oo|/:coffee|/:eat|/:pig|/:rose|/:fade|/:showlove|/:heart|/:break|/:cake|/:li|/:bome|/:kn|/:footb|/:ladybug|/:shit|/:moon|/:sun|/:gift|/:hug|/:strong|/:weak|/:share|/:v|/:@\\)|/:jj|/:@@|/:bad|/:lvu|/:no|/:ok|/:love|/:|/:jump|/:shake|/:|/:circle|/:kotow|/:turn|/:skip|/:oY|/:#-0|/:hiphot|/:kiss|/:<&|/:&>"; Pattern p = Pattern.compile(qqfaceRegex); Matcher m = p.matcher(content); if (m.matches()) { result = true; } return result; } 下面是方法的使用,实现了这样一个简单的功能:用户发什么QQ表情给公众帐号,公众帐号就回复什么QQ表情给用户(xiaoqrobot就是这么做的)。实现代码如下: [java] view plaincopyprint? 1. // 文本消息    2. if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {   3.     // 文本消息内容    4.     String content = requestMap.get("Content");   5.        6.     // 判断用户发送的是否是单个QQ表情    7.     if(XiaoqUtil.isQqFace(content)) {   8.         // 回复文本消息    9.         TextMessage textMessage = new TextMessage();   10.         textMessage.setToUserName(fromUserName);   11.         textMessage.setFromUserName(toUserName);   12.         textMessage.setCreateTime(new Date().getTime());   13.         textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);   14.         textMessage.setFuncFlag(0);   15.         // 用户发什么QQ表情,就返回什么QQ表情    16.         textMessage.setContent(content);   17.            18.         // 将文本消息对象转换成xml字符串    19.         respMessage = MessageUtil.textMessageToXml(textMessage);   20.     }   21. }   // 文本消息 if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { // 文本消息内容 String content = requestMap.get("Content"); // 判断用户发送的是否是单个QQ表情 if(XiaoqUtil.isQqFace(content)) { // 回复文本消息 TextMessage textMessage = new TextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(new Date().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); textMessage.setFuncFlag(0); // 用户发什么QQ表情,就返回什么QQ表情 textMessage.setContent(content); // 将文本消息对象转换成xml字符串 respMessage = MessageUtil.textMessageToXml(textMessage); } } 好了,关于微信公众帐号中QQ表情的使用就介绍这么多。其实,我并不希望初学者上来只是简单拷贝我贴出的代码,实现了自己想要的功能就完事了,更希望初学的朋友能够通过此文章学会一种思考问题和解决问题的方法。 10. 微信公众帐号开发教程第10篇-解析接口中的消息创建时间CreateTime 从微信公众平台的消息接口指南中可以看出,每种类型的消息定义中,都包含有CreateTime参数,它表示消息的创建时间,如下图所示: 上图是消息接口指南中4.1-文本消息的定义。注意CreateTime的描述:消息创建时间(整型),重点在于这是一个整型的时间,而不是我们大家所熟悉的类似于"yyyy-MM-dd HH:mm:ss"的标准格式时间。本文主要想介绍的就是微信消息接口中定义的整型消息创建时间CreateTime的含义,以及如何将CreateTime转换成我们所熟悉的时间格式。   整型CreateTime的含义 消息接口中定义的消息创建时间CreateTime,它表示1970年1月1日0时0分0秒至消息创建时所间隔的秒数,注意是间隔的秒数,不是毫秒数!   整型CreateTime的转换 在Java中,我们也经常会通过下面两种方式获取long类型的时间,先上代码: [java] view plaincopyprint? 1. /**  2.  * 演示Java中常用的获取long类型时间的两种方式  3.  */   4. public static void main(String[] args) {   5.     long longTime1 = System.currentTimeMillis();   6.     // 1373206143378    7.     System.out.println(longTime1);   8.    9.     long longTime2 = new java.util.Date().getTime();   10.     // 1373206143381    11.     System.out.println(longTime2);   12. }   /** * 演示Java中常用的获取long类型时间的两种方式 */ public static void main(String[] args) { long longTime1 = System.currentTimeMillis(); // 1373206143378 System.out.println(longTime1); long longTime2 = new java.util.Date().getTime(); // 1373206143381 System.out.println(longTime2); } 上面两种获取long类型时间的方法是等价的,获取到的结果表示当时时间距离1970年1月1日0时0分0秒0毫秒的毫秒数,注意这里是毫秒数!那么这里获取到的long类型的时间如何转换成标准格式的时间呢?方法如下: [java] view plaincopyprint? 1. /**  2.  * 演示Java中常用的获取long类型时间的两种方式  3.  */   4. public static void main(String[] args) {   5.     // 当前时间(距离1970年1月1日0时0分0秒0毫秒的毫秒数)    6.     long longTime = 1373206143378L;   7.        8.     String stdFormatTime = formatTime(longTime);   9.     // 输出:2013-07-07 22:09:03    10.     System.out.println(stdFormatTime);   11. }   12.    13. /**  14.  * 将long类型的时间转换成标准格式(yyyy-MM-dd HH:mm:ss)  15.  *   16.  * @param longTime  17.  * @return  18.  */   19. public static String formatTime(long longTime) {   20.     DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");   21.     return format.format(new Date(longTime));   22. }   /** * 演示Java中常用的获取long类型时间的两种方式 */ public static void main(String[] args) { // 当前时间(距离1970年1月1日0时0分0秒0毫秒的毫秒数) long longTime = 1373206143378L; String stdFormatTime = formatTime(longTime); // 输出:2013-07-07 22:09:03 System.out.println(stdFormatTime); } /** * 将long类型的时间转换成标准格式(yyyy-MM-dd HH:mm:ss) * * @param longTime * @return */ public static String formatTime(long longTime) { DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return format.format(new Date(longTime)); } 上面演示了将一个long类型的时间转换成标准格式的时间,只是简单的运用了SimpleDateFormat类,比较好懂的。那么再回到今天的主题上来,如何将CreateTime转换成标准格式的时间。 微信消息接口中的CreateTime表示距离1970年的秒数,而System.currentTimeMillis()表示距离1970年的毫秒数,它们之间的换算就相当于:1秒=1000毫秒,即将CreateTime乘以1000,就变成了距离1970年的毫秒数了,就可以使用上面的formatTime()方法来处理了,是不是很简单呢? 下面,我还是单另封装一个方法,用于将微信消息中的整型的消息创建时间CreateTime转换成标准格式的时间,如下: [java] view plaincopyprint? 1. /**  2.  * 将微信消息中的CreateTime转换成标准格式的时间(yyyy-MM-dd HH:mm:ss)  3.  *   4.  * @param createTime 消息创建时间  5.  * @return  6.  */   7. public static String formatTime(String createTime) {   8.     // 将微信传入的CreateTime转换成long类型,再乘以1000    9.     long msgCreateTime = Long.parseLong(createTime) * 1000L;   10.     DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");   11.     return format.format(new Date(msgCreateTime));   12. }   11. 微信公众帐号开发教程第11篇-符号表情的发送(上) 题外话(可以略过) 相信这篇文章已经让大家等的太久了,不是我故弄玄虚、吊大家胃口,而是写一篇文章真的需要花太多的时间。也许一篇文章,你们花3-5分钟就看完了、就学会掌握了,而我却要花2-3个小时的时间来完成,也许只有用心写过文章的人才能体会,希望大家能够相互体谅!   也曾经有人对我说,我写的东西太初级,都是入门级的东西。好吧,我承认众口难调,很难满足所有的读者,再加上我自己也只是个新手,一个4月前才听说微信公众平台这个词的初学者,谢谢你们以不同方式对我的激励,我会更加努力的!   第9篇文章介绍了QQ表情的发送与接收。在此之后,很多朋友问我如何发emoji表情(微信上叫符号表情),也就让我有了写这篇文章的决心。在此之前,我在网上进行了大量的搜索,发现根本没有介绍这方面的文章,并且在微信公众帐号开发官方交流群里提问,也少有人知道该如何发送emoji表情。今天,就让我们一起来揭开它的神秘面纱!   文章概要 本文重点介绍如何在微信公众帐号开发模式下,通过程序代码向用户发送符号表情。至于如何识别用户发送的是符号表情,就不在此讲解了,留给大家一点学习思考的空间。我只是给大家一个提示:用户向公众帐号发送符号表情,其实也是一条文本消息,这与QQ表现是一样的,即然是文本消息,将接收的符号表情内容打印到日志,不就知道每个表情对应的文本了吗?呵呵,当然也没有这么简单,并不是像其他文本消息,这里需要对接收到符号表情消息先做编码的转换。好了,就提示这么多。   认识符号表情 在公众帐号的主交互界面,窗口底部的输入框旁边有一个笑脸的图片按钮,点击它将会弹出表情选择界面,可选择的表情依次为“QQ表情”、“符号表情”和“动画表情”,我们选择“符号表情”,将会看到如下图所示界面: 可以持看出,相比QQ表情,符号表情要更加实用。为什么这么说呢?因为QQ表情大都是脸部表情,而符号表情除了脸部表情外,还有很多与生活息息相关的表情,例如:动物、花朵、树木、电视、电话、电脑、吉它、球类、交通工具等等。如果能在消息中使用符号表情,会不会显得更加生动、有趣呢? 再来看看小q机器人中使用符号表情的效果,先上两张图:    左边截图是小q机器人的主菜单,在Q友圈文字旁边的那个表情就是符号表情,是一女一男两人小朋友,示意着在Q友圈里可以结识到更多的朋友,不要想歪了,^_^。右边截图是人脸识别功能的使用指南,里面的“相机”、“鬼脸”也是符号表情,这样看上去是不是更加有趣味性呢?如果是纯文本,一定会显得太单调、太枯燥了。   Emoji表情的分类 Emoji表情有很多种版本,包括Unified、DoCoMo、KDDI、Softbank和Google,而且不同版本的表情代码也不一样,更可恶的是:不同的手机操作系统、甚至是同一操作系统的不同版本所支持的emoji表情又不一样。所以,完美主义者可以止步了,因为目前emoji表情并不能保证在所有终端上都能正常使用。 庆幸的是,我已经在超过10余部终端上测试过emoji表情的使用,这其中包括iPhone 4S、iPhone 5、Android 2.2、Android 4.0+、Win8、iPad2,只有极个别终端上显示不出来或显示为一个小方格,所以并没有什么太大的影响,也就可以放心使用了!   Emoji表情代码表之Unified版本 上面介绍的几种版本的emoji表情,都是通过unicode编码来表示的。换言之,不同版本的emoji表情对应的unicode编码值也不一样。本篇文章,我先给出Unified版本emoji表情的代码表,如下图所示:   公众帐号如何向用户发送emoji表情 上面已经给出了emoji表情的unified unicode代码对照表,那么这些代码要如何使用,才能发送出对应的emoji表情呢?如果你只是简单的像使用QQ表情代码那样,直接在文本消息的Content里写emoji表情代码,一定是会原样显示的。 这里需要用到一个Java方法做转换处理,方法的代码如下: [java] view plaincopyprint? 1. /**  2.  * emoji表情转换(hex -> utf-16)  3.  *   4.  * @param hexEmoji  5.  * @return  6.  */   7. public static String emoji(int hexEmoji) {   8.     return String.valueOf(Character.toChars(hexEmoji));   9. }   /** * emoji表情转换(hex -> utf-16) * * @param hexEmoji * @return */ public static String emoji(int hexEmoji) { return String.valueOf(Character.toChars(hexEmoji)); } 方法说明:例如,“自行车”的unicode编码值为U+1F6B2,如果我们要在程序代码中使用“自行车”这个emoji表情,需要这样使用: [java] view plaincopyprint? 1. String bike = String.valueOf(Character.toChars(0x1F6B2));   String bike = String.valueOf(Character.toChars(0x1F6B2)); 其实前面那个emoji()方法就是对上面这行代码做了个简单的封装而以。现在知道如何使用emoji表情代码了吧,其实就是将代码表中的U+替换为0x,再调用emoji方法进行转换,将转换后的结果放在文本消息的Content中,返回给用户就会显示emoji表情了。 下面,我给出一个使用emoji表情的完整示例,如下: [java] view plaincopyprint? 1. package org.liufeng.course.service;   2.    3. import java.util.Date;   4. import java.util.Map;   5.    6. import javax.servlet.http.HttpServletRequest;   7.    8. import org.liufeng.course.message.resp.TextMessage;   9. import org.liufeng.course.util.MessageUtil;   10.    11. /**  12.  * 核心服务类  13.  *   14.  * @author liufeng  15.  * @date 2013-05-20  16.  */   17. public class CoreService {   18.     /**  19.      * 处理微信发来的请求  20.      *   21.      * @param request  22.      * @return  23.      */   24.     public static String processRequest(HttpServletRequest request) {   25.         String respMessage = null;   26.         try {   27.             // xml请求解析    28.             Map requestMap = MessageUtil.parseXml(request);   29.    30.             // 发送方帐号(open_id)    31.             String fromUserName = requestMap.get("FromUserName");   32.             // 公众帐号    33.             String toUserName = requestMap.get("ToUserName");   34.    35.             // 回复文本消息    36.             TextMessage textMessage = new TextMessage();   37.             textMessage.setToUserName(fromUserName);   38.             textMessage.setFromUserName(toUserName);   39.             textMessage.setCreateTime(new Date().getTime());   40.             textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);   41.             textMessage.setFuncFlag(0);   42.             textMessage.setContent("自行车" + emoji(0x1F6B2) + " 男性" + emoji(0x1F6B9) + " 钱袋" + emoji(0x1F4B0));   43.             respMessage = MessageUtil.textMessageToXml(textMessage);   44.         } catch (Exception e) {   45.             e.printStackTrace();   46.         }   47.    48.         return respMessage;   49.     }   50.    51.     /**  52.      * emoji表情转换(hex -> utf-16)  53.      *   54.      * @param hexEmoji  55.      * @return  56.      */   57.     public static String emoji(int hexEmoji) {   58.         return String.valueOf(Character.toChars(hexEmoji));   59.     }   60. }   package org.liufeng.course.service; import java.util.Date; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.liufeng.course.message.resp.TextMessage; import org.liufeng.course.util.MessageUtil; /** * 核心服务类 * * @author liufeng * @date 2013-05-20 */ public class CoreService { /** * 处理微信发来的请求 * * @param request * @return */ public static String processRequest(HttpServletRequest request) { String respMessage = null; try { // xml请求解析 Map requestMap = MessageUtil.parseXml(request); // 发送方帐号(open_id) String fromUserName = requestMap.get("FromUserName"); // 公众帐号 String toUserName = requestMap.get("ToUserName"); // 回复文本消息 TextMessage textMessage = new TextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(new Date().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); textMessage.setFuncFlag(0); textMessage.setContent("自行车" + emoji(0x1F6B2) + " 男性" + emoji(0x1F6B9) + " 钱袋" + emoji(0x1F4B0)); respMessage = MessageUtil.textMessageToXml(textMessage); } catch (Exception e) { e.printStackTrace(); } return respMessage; } /** * emoji表情转换(hex -> utf-16) * * @param hexEmoji * @return */ public static String emoji(int hexEmoji) { return String.valueOf(Character.toChars(hexEmoji)); } } 上面代码的作用是:不管用户发送什么类型的消息,都返回包含三个emoji表情的文本消息。如果不明白CoreService类怎么回事,请查看本系列教程的第5篇,或者你只需要认真看第42行代码,就知道怎么样把emoji表情代码放在文本消息的Content中了。最后再来看下运行效果截图: 本篇文章要讲的内容就至此结束了,但关于emoji表情的讲解还没有结束,为什么这么说呢?请仔细看本篇文章的第二张截图,也就是小q机器人的文本菜单,里面用到的emoji表情在本文给出的emoji代码表里根本找不到(微信上的emoji表情与代码表中完全一致),那这个emoji表情又是如何发送的呢,请听下回分解! 12. 微信公众帐号开发教程第12篇-符号表情的发送(下) 引言及文章概要 第11篇文章给出了Unified版本的符号表情(emoji表情)代码表,并且介绍了如何在微信公众帐号开发模式下发送emoji表情,还在文章结尾出,卖了个关子:“小q机器人中使用的一些符号表情,在微信的符号表情选择栏里根本找不到,并且在上篇文章给出的符号表情代码表(Unified版)中也没有,那这些表情是如何发送的呢?”如下面两张图所示的符号表情“情侣”和“公共汽车”。         本文主要介绍以下内容:1)如何在微信上使用更多的符号表情(即如何发送在微信符号表情选择栏中不存在的emoji表情);2)给出SoftBank版符号表情的代码对照表;3)介绍及演示如何发送SoftBank版本的符号表情。让大家彻底玩转微信公众帐号的emoji表情!   如何在微信上使用更多的符号表情 我们先来看下,作为一个微信用户,如何向好友或微信公众帐号发送一些微信符号表情选择栏中没有列出的符号表情。例如:小q机器人中使用的“情侣”、“公共汽车”两个符号表情,如果我想在与朋友微信聊天时使用,该怎么办呢?请先看下面的两张截图:         可以看出,当我们在输入框中输入“情侣”的全拼“qinglv”、“公共汽车”的全拼“gonggongqiche”时,输入法的文本提示列表中就会自动显示对应的符号表情,怎么样,是不是很容易呢?这类表情还有很多,例如:马桶、厕所、取款机等。 说明:笔者使用的是iPhone 4S手机系统自带的输入法做的测试,如果你用的是安卓、或者是第三方输入法,那就另当别论了。   Emoji表情代码表之SoftBank版本 上篇文章讲过,emoji表情有很多种版本,其中包括Unified、DoCoMo、KDDI、Softbank和Google,并且不同版本用于表示同一符号表情的Unicode代码也不相同。本篇文章,给出SoftBank(日本软银集团)版本的emoji表情代码表(网上一般称之为SB Unicode,指的就是它),如下图所示:   公众帐号如何向用户发送SoftBank版本的符号表情 在微信公众帐号开发模式下,发送SoftBank版的符号表情要比发送Unified版的符号表情简单的多,直接将符号表情对应的SoftBank Unicode值写在程序代码中返回给用户即可,无需做任何处理。 下面,我给出一个发送SoftBank版符号表情的示例,代码如下: [java] view plaincopyprint? 1. package org.liufeng.course.service;   2.    3. import java.util.Date;   4. import java.util.Map;   5.    6. import javax.servlet.http.HttpServletRequest;   7.    8. import org.liufeng.course.message.resp.TextMessage;   9. import org.liufeng.course.util.MessageUtil;   10.    11. /**  12.  * 核心服务类  13.  *   14.  * @author liufeng  15.  * @date 2013-07-21  16.  */   17. public class CoreService {   18.     /**  19.      * 处理微信发来的请求  20.      *   21.      * @param request  22.      * @return  23.      */   24.     public static String processRequest(HttpServletRequest request) {   25.         String respMessage = null;   26.         try {   27.             // xml请求解析    28.             Map requestMap = MessageUtil.parseXml(request);   29.    30.             // 发送方帐号(open_id)    31.             String fromUserName = requestMap.get("FromUserName");   32.             // 公众帐号    33.             String toUserName = requestMap.get("ToUserName");   34.    35.             // 回复文本消息    36.             TextMessage textMessage = new TextMessage();   37.             textMessage.setToUserName(fromUserName);   38.             textMessage.setFromUserName(toUserName);   39.             textMessage.setCreateTime(new Date().getTime());   40.             textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);   41.             textMessage.setFuncFlag(0);   42.             textMessage.setContent("自行车\ue136 男人\ue138 钱袋\ue12f 情侣\ue428 公共汽车\ue159");   43.             respMessage = MessageUtil.textMessageToXml(textMessage);   44.         } catch (Exception e) {   45.             e.printStackTrace();   46.         }   47.    48.         return respMessage;   49.     }   50. }   package org.liufeng.course.service; import java.util.Date; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.liufeng.course.message.resp.TextMessage; import org.liufeng.course.util.MessageUtil; /** * 核心服务类 * * @author liufeng * @date 2013-07-21 */ public class CoreService { /** * 处理微信发来的请求 * * @param request * @return */ public static String processRequest(HttpServletRequest request) { String respMessage = null; try { // xml请求解析 Map requestMap = MessageUtil.parseXml(request); // 发送方帐号(open_id) String fromUserName = requestMap.get("FromUserName"); // 公众帐号 String toUserName = requestMap.get("ToUserName"); // 回复文本消息 TextMessage textMessage = new TextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(new Date().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); textMessage.setFuncFlag(0); textMessage.setContent("自行车\ue136 男人\ue138 钱袋\ue12f 情侣\ue428 公共汽车\ue159"); respMessage = MessageUtil.textMessageToXml(textMessage); } catch (Exception e) { e.printStackTrace(); } return respMessage; } } 上面代码的作用是:不管用户发送什么类型的消息,都返回包含5个emoji表情的文本消息。如果不明白CoreService类怎么回事,请查看本系列教程的第5篇,或者你只需要认真看第42行代码,就知道怎么样把SoftBank版emoji表情代码放在文本消息的Content中了。最后再来看下运行效果截图: 说明:每一个符号表情都有与之对应的Unified unicode、Softbank unicode代码,并不是说“情侣”、“公共汽车”这类在微信的符号表情栏中找不到的emoji表情只能通过本文的方式发送,只要你拿到与之对应的Unified unicode代码,一样可以使用上篇文章所讲的方法发送这类符号表情。   好了,关于微信公众帐号向用户发送符号表情的讲解就此结束了,相信有些朋友看完教程已经开始在帐号中使用符号表情了。其实,我更希望大家在拷贝我粘出的Unified版、SoftBank版符号表情代码表的同时,也能去了解下符号表情各种版本、Unicode编码及增补码的相关知识,不断拓展自己的知识面,触类旁通,这样才能真正地把我讲解的知识变成你自己的,才能做到以不变应万变。 13. 微信公众帐号开发教程第13篇-图文消息全攻略 引言及内容概要 已经有几位读者抱怨“柳峰只用到文本消息作为示例,从来不提图文消息,都不知道图文消息该如何使用”,好吧,我错了,原本以为把基础API封装完、框架搭建好,再给出一个文本消息的使用示例,大家就能够照猫画虎的,或许是因为我的绘画功底太差,画出的那只猫本来就不像猫吧…… 本篇主要介绍微信公众帐号开发中图文消息的使用,以及图文消息的几种表现形式。标题取名为“图文消息全攻略”,这绝对不是标题党,是想借此机会把大家对图文消息相关的问题、疑虑、障碍全部清除掉。   图文消息的主要参数说明 通过微信官方的消息接口指南,可以看到对图文消息的参数介绍,如下图所示: 从图中可以了解到: 1)图文消息的个数限制为10,也就是图中ArticleCount的值(图文消息的个数,限制在10条以内); 2)对于多图文消息,第一条图文的图片显示为大图,其他图文的图片显示为小图; 3)第一条图文的图片大小建议为640*320,其他图文的图片大小建议为80*80; 好了,了解这些,再结合第4篇文章所讲的消息及消息处理工具的封装,想要回复图文消息给用户也就不是什么难事了。   图文消息的多种表现形式 下面直接通过代码演示图文消息最主要的五种表现形式的用法,源代码如下: [java] view plaincopyprint? 1. package org.liufeng.course.service;   2.    3. import java.util.ArrayList;   4. import java.util.Date;   5. import java.util.List;   6. import java.util.Map;   7.    8. import javax.servlet.http.HttpServletRequest;   9.    10. import org.liufeng.course.message.resp.Article;   11. import org.liufeng.course.message.resp.NewsMessage;   12. import org.liufeng.course.message.resp.TextMessage;   13. import org.liufeng.course.util.MessageUtil;   14.    15. /**  16.  * 核心服务类  17.  *   18.  * @author liufeng  19.  * @date 2013-07-25  20.  */   21. public class CoreService {   22.     /**  23.      * 处理微信发来的请求  24.      *   25.      * @param request  26.      * @return  27.      */   28.     public static String processRequest(HttpServletRequest request) {   29.         String respMessage = null;   30.         try {   31.             // xml请求解析    32.             Map requestMap = MessageUtil.parseXml(request);   33.    34.             // 发送方帐号(open_id)    35.             String fromUserName = requestMap.get("FromUserName");   36.             // 公众帐号    37.             String toUserName = requestMap.get("ToUserName");   38.             // 消息类型    39.             String msgType = requestMap.get("MsgType");   40.    41.             // 默认回复此文本消息    42.             TextMessage textMessage = new TextMessage();   43.             textMessage.setToUserName(fromUserName);   44.             textMessage.setFromUserName(toUserName);   45.             textMessage.setCreateTime(new Date().getTime());   46.             textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);   47.             textMessage.setFuncFlag(0);   48.             // 由于href属性值必须用双引号引起,这与字符串本身的双引号冲突,所以要转义    49.             textMessage.setContent("欢迎访问柳峰的博客!");   50.             // 将文本消息对象转换成xml字符串    51.             respMessage = MessageUtil.textMessageToXml(textMessage);   52.    53.             // 文本消息    54.             if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {   55.                 // 接收用户发送的文本消息内容    56.                 String content = requestMap.get("Content");   57.    58.                 // 创建图文消息    59.                 NewsMessage newsMessage = new NewsMessage();   60.                 newsMessage.setToUserName(fromUserName);   61.                 newsMessage.setFromUserName(toUserName);   62.                 newsMessage.setCreateTime(new Date().getTime());   63.                 newsMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_NEWS);   64.                 newsMessage.setFuncFlag(0);   65.    66.                 List
 articleList = new ArrayList
();   67.                 // 单图文消息    68.                 if ("1".equals(content)) {   69.                     Article article = new Article();   70.                     article.setTitle("微信公众帐号开发教程Java版");   71.                     article.setDescription("柳峰,80后,微信公众帐号开发经验4个月。为帮助初学者入门,特推出此系列教程,也希望借此机会认识更多同行!");   72.                     article.setPicUrl("http://0.xiaoqrobot.duapp.com/images/avatar_liufeng.jpg");   73.                     article.setUrl("http://blog.csdn.net/lyq8479");   74.                     articleList.add(article);   75.                     // 设置图文消息个数    76.                     newsMessage.setArticleCount(articleList.size());   77.                     // 设置图文消息包含的图文集合    78.                     newsMessage.setArticles(articleList);   79.                     // 将图文消息对象转换成xml字符串    80.                     respMessage = MessageUtil.newsMessageToXml(newsMessage);   81.                 }   82.                 // 单图文消息---不含图片    83.                 else if ("2".equals(content)) {   84.                     Article article = new Article();   85.                     article.setTitle("微信公众帐号开发教程Java版");   86.                     // 图文消息中可以使用QQ表情、符号表情    87.                     article.setDescription("柳峰,80后," + emoji(0x1F6B9)   88.                             + ",微信公众帐号开发经验4个月。为帮助初学者入门,特推出此系列连载教程,也希望借此机会认识更多同行!\n\n目前已推出教程共12篇,包括接口配置、消息封装、框架搭建、QQ表情发送、符号表情发送等。\n\n后期还计划推出一些实用功能的开发讲解,例如:天气预报、周边搜索、聊天功能等。");   89.                     // 将图片置为空    90.                     article.setPicUrl("");   91.                     article.setUrl("http://blog.csdn.net/lyq8479");   92.                     articleList.add(article);   93.                     newsMessage.setArticleCount(articleList.size());   94.                     newsMessage.setArticles(articleList);   95.                     respMessage = MessageUtil.newsMessageToXml(newsMessage);   96.                 }   97.                 // 多图文消息    98.                 else if ("3".equals(content)) {   99.                     Article article1 = new Article();   100.                     article1.setTitle("微信公众帐号开发教程\n引言");   101.                     article1.setDescription("");   102.                     article1.setPicUrl("http://0.xiaoqrobot.duapp.com/images/avatar_liufeng.jpg");   103.                     article1.setUrl("http://blog.csdn.net/lyq8479/article/details/8937622");   104.    105.                     Article article2 = new Article();   106.                     article2.setTitle("第2篇\n微信公众帐号的类型");   107.                     article2.setDescription("");   108.                     article2.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");   109.                     article2.setUrl("http://blog.csdn.net/lyq8479/article/details/8941577");   110.    111.                     Article article3 = new Article();   112.                     article3.setTitle("第3篇\n开发模式启用及接口配置");   113.                     article3.setDescription("");   114.                     article3.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");   115.                     article3.setUrl("http://blog.csdn.net/lyq8479/article/details/8944988");   116.    117.                     articleList.add(article1);   118.                     articleList.add(article2);   119.                     articleList.add(article3);   120.                     newsMessage.setArticleCount(articleList.size());   121.                     newsMessage.setArticles(articleList);   122.                     respMessage = MessageUtil.newsMessageToXml(newsMessage);   123.                 }   124.                 // 多图文消息---首条消息不含图片    125.                 else if ("4".equals(content)) {   126.                     Article article1 = new Article();   127.                     article1.setTitle("微信公众帐号开发教程Java版");   128.                     article1.setDescription("");   129.                     // 将图片置为空    130.                     article1.setPicUrl("");   131.                     article1.setUrl("http://blog.csdn.net/lyq8479");   132.    133.                     Article article2 = new Article();   134.                     article2.setTitle("第4篇\n消息及消息处理工具的封装");   135.                     article2.setDescription("");   136.                     article2.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");   137.                     article2.setUrl("http://blog.csdn.net/lyq8479/article/details/8949088");   138.    139.                     Article article3 = new Article();   140.                     article3.setTitle("第5篇\n各种消息的接收与响应");   141.                     article3.setDescription("");   142.                     article3.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");   143.                     article3.setUrl("http://blog.csdn.net/lyq8479/article/details/8952173");   144.    145.                     Article article4 = new Article();   146.                     article4.setTitle("第6篇\n文本消息的内容长度限制揭秘");   147.                     article4.setDescription("");   148.                     article4.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");   149.                     article4.setUrl("http://blog.csdn.net/lyq8479/article/details/8967824");   150.    151.                     articleList.add(article1);   152.                     articleList.add(article2);   153.                     articleList.add(article3);   154.                     articleList.add(article4);   155.                     newsMessage.setArticleCount(articleList.size());   156.                     newsMessage.setArticles(articleList);   157.                     respMessage = MessageUtil.newsMessageToXml(newsMessage);   158.                 }   159.                 // 多图文消息---最后一条消息不含图片    160.                 else if ("5".equals(content)) {   161.                     Article article1 = new Article();   162.                     article1.setTitle("第7篇\n文本消息中换行符的使用");   163.                     article1.setDescription("");   164.                     article1.setPicUrl("http://0.xiaoqrobot.duapp.com/images/avatar_liufeng.jpg");   165.                     article1.setUrl("http://blog.csdn.net/lyq8479/article/details/9141467");   166.    167.                     Article article2 = new Article();   168.                     article2.setTitle("第8篇\n文本消息中使用网页超链接");   169.                     article2.setDescription("");   170.                     article2.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg");   171.                     article2.setUrl("http://blog.csdn.net/lyq8479/article/details/9157455");   172.    173.                     Article article3 = new Article();   174.                     article3.setTitle("如果觉得文章对你有所帮助,请通过博客留言或关注微信公众帐号xiaoqrobot来支持柳峰!");   175.                     article3.setDescription("");   176.                     // 将图片置为空    177.                     article3.setPicUrl("");   178.                     article3.setUrl("http://blog.csdn.net/lyq8479");   179.    180.                     articleList.add(article1);   181.                     articleList.add(article2);   182.                     articleList.add(article3);   183.                     newsMessage.setArticleCount(articleList.size());   184.                     newsMessage.setArticles(articleList);   185.                     respMessage = MessageUtil.newsMessageToXml(newsMessage);   186.                 }   187.             }   188.         } catch (Exception e) {   189.             e.printStackTrace();   190.         }   191.         return respMessage;   192.     }   193.    194.     /**  195.      * emoji表情转换(hex -> utf-16)  196.      *   197.      * @param hexEmoji  198.      * @return  199.      */   200.     public static String emoji(int hexEmoji) {   201.         return String.valueOf(Character.toChars(hexEmoji));   202.     }   203. }   package org.liufeng.course.service; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.liufeng.course.message.resp.Article; import org.liufeng.course.message.resp.NewsMessage; import org.liufeng.course.message.resp.TextMessage; import org.liufeng.course.util.MessageUtil; /** * 核心服务类 * * @author liufeng * @date 2013-07-25 */ public class CoreService { /** * 处理微信发来的请求 * * @param request * @return */ public static String processRequest(HttpServletRequest request) { String respMessage = null; try { // xml请求解析 Map requestMap = MessageUtil.parseXml(request); // 发送方帐号(open_id) String fromUserName = requestMap.get("FromUserName"); // 公众帐号 String toUserName = requestMap.get("ToUserName"); // 消息类型 String msgType = requestMap.get("MsgType"); // 默认回复此文本消息 TextMessage textMessage = new TextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(new Date().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); textMessage.setFuncFlag(0); // 由于href属性值必须用双引号引起,这与字符串本身的双引号冲突,所以要转义 textMessage.setContent("欢迎访问柳峰的博客!"); // 将文本消息对象转换成xml字符串 respMessage = MessageUtil.textMessageToXml(textMessage); // 文本消息 if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { // 接收用户发送的文本消息内容 String content = requestMap.get("Content"); // 创建图文消息 NewsMessage newsMessage = new NewsMessage(); newsMessage.setToUserName(fromUserName); newsMessage.setFromUserName(toUserName); newsMessage.setCreateTime(new Date().getTime()); newsMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_NEWS); newsMessage.setFuncFlag(0); List
articleList = new ArrayList
(); // 单图文消息 if ("1".equals(content)) { Article article = new Article(); article.setTitle("微信公众帐号开发教程Java版"); article.setDescription("柳峰,80后,微信公众帐号开发经验4个月。为帮助初学者入门,特推出此系列教程,也希望借此机会认识更多同行!"); article.setPicUrl("http://0.xiaoqrobot.duapp.com/images/avatar_liufeng.jpg"); article.setUrl("http://blog.csdn.net/lyq8479"); articleList.add(article); // 设置图文消息个数 newsMessage.setArticleCount(articleList.size()); // 设置图文消息包含的图文集合 newsMessage.setArticles(articleList); // 将图文消息对象转换成xml字符串 respMessage = MessageUtil.newsMessageToXml(newsMessage); } // 单图文消息---不含图片 else if ("2".equals(content)) { Article article = new Article(); article.setTitle("微信公众帐号开发教程Java版"); // 图文消息中可以使用QQ表情、符号表情 article.setDescription("柳峰,80后," + emoji(0x1F6B9) + ",微信公众帐号开发经验4个月。为帮助初学者入门,特推出此系列连载教程,也希望借此机会认识更多同行!\n\n目前已推出教程共12篇,包括接口配置、消息封装、框架搭建、QQ表情发送、符号表情发送等。\n\n后期还计划推出一些实用功能的开发讲解,例如:天气预报、周边搜索、聊天功能等。"); // 将图片置为空 article.setPicUrl(""); article.setUrl("http://blog.csdn.net/lyq8479"); articleList.add(article); newsMessage.setArticleCount(articleList.size()); newsMessage.setArticles(articleList); respMessage = MessageUtil.newsMessageToXml(newsMessage); } // 多图文消息 else if ("3".equals(content)) { Article article1 = new Article(); article1.setTitle("微信公众帐号开发教程\n引言"); article1.setDescription(""); article1.setPicUrl("http://0.xiaoqrobot.duapp.com/images/avatar_liufeng.jpg"); article1.setUrl("http://blog.csdn.net/lyq8479/article/details/8937622"); Article article2 = new Article(); article2.setTitle("第2篇\n微信公众帐号的类型"); article2.setDescription(""); article2.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg"); article2.setUrl("http://blog.csdn.net/lyq8479/article/details/8941577"); Article article3 = new Article(); article3.setTitle("第3篇\n开发模式启用及接口配置"); article3.setDescription(""); article3.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg"); article3.setUrl("http://blog.csdn.net/lyq8479/article/details/8944988"); articleList.add(article1); articleList.add(article2); articleList.add(article3); newsMessage.setArticleCount(articleList.size()); newsMessage.setArticles(articleList); respMessage = MessageUtil.newsMessageToXml(newsMessage); } // 多图文消息---首条消息不含图片 else if ("4".equals(content)) { Article article1 = new Article(); article1.setTitle("微信公众帐号开发教程Java版"); article1.setDescription(""); // 将图片置为空 article1.setPicUrl(""); article1.setUrl("http://blog.csdn.net/lyq8479"); Article article2 = new Article(); article2.setTitle("第4篇\n消息及消息处理工具的封装"); article2.setDescription(""); article2.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg"); article2.setUrl("http://blog.csdn.net/lyq8479/article/details/8949088"); Article article3 = new Article(); article3.setTitle("第5篇\n各种消息的接收与响应"); article3.setDescription(""); article3.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg"); article3.setUrl("http://blog.csdn.net/lyq8479/article/details/8952173"); Article article4 = new Article(); article4.setTitle("第6篇\n文本消息的内容长度限制揭秘"); article4.setDescription(""); article4.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg"); article4.setUrl("http://blog.csdn.net/lyq8479/article/details/8967824"); articleList.add(article1); articleList.add(article2); articleList.add(article3); articleList.add(article4); newsMessage.setArticleCount(articleList.size()); newsMessage.setArticles(articleList); respMessage = MessageUtil.newsMessageToXml(newsMessage); } // 多图文消息---最后一条消息不含图片 else if ("5".equals(content)) { Article article1 = new Article(); article1.setTitle("第7篇\n文本消息中换行符的使用"); article1.setDescription(""); article1.setPicUrl("http://0.xiaoqrobot.duapp.com/images/avatar_liufeng.jpg"); article1.setUrl("http://blog.csdn.net/lyq8479/article/details/9141467"); Article article2 = new Article(); article2.setTitle("第8篇\n文本消息中使用网页超链接"); article2.setDescription(""); article2.setPicUrl("http://avatar.csdn.net/1/4/A/1_lyq8479.jpg"); article2.setUrl("http://blog.csdn.net/lyq8479/article/details/9157455"); Article article3 = new Article(); article3.setTitle("如果觉得文章对你有所帮助,请通过博客留言或关注微信公众帐号xiaoqrobot来支持柳峰!"); article3.setDescription(""); // 将图片置为空 article3.setPicUrl(""); article3.setUrl("http://blog.csdn.net/lyq8479"); articleList.add(article1); articleList.add(article2); articleList.add(article3); newsMessage.setArticleCount(articleList.size()); newsMessage.setArticles(articleList); respMessage = MessageUtil.newsMessageToXml(newsMessage); } } } catch (Exception e) { e.printStackTrace(); } return respMessage; } /** * emoji表情转换(hex -> utf-16) * * @param hexEmoji * @return */ public static String emoji(int hexEmoji) { return String.valueOf(Character.toChars(hexEmoji)); } } 如果不明白CoreService类放在什么位置,该如何使用,请查看本系列教程的第5篇。上面代码实现的功能是当用户发送数字1-5时,分别回复五种不同表现形式的图文消息给用户,如下: a)用户发送1,回复单图文消息。参考代码68~81行,运行效果如下:   b)用户发送2,回复单图文消息---不含图片。参考代码82~96行,运行效果如下: 说明:图文消息的标题、描述是可以包含QQ表情、符号表情的。   c)用户发送3,回复多图文消息。参考代码97~123行,运行效果如下: 说明:对于多图文消息,描述不会被显示,可以在标题使用换行符,使得显示更加美观。   d)用户发送4,回复多图文消息---首条消息不含图片。参考代码124~158行,运行效果如下:   e)用户发送5,回复多图文消息---最后一条消息不含图片。参考代码159~186行,运行效果如下: 可以看出,图文消息有着丰富的内容及多样化的表现形式,希望大家能够根据各自的特点及实际使用需要,合理地运用。   最后,根据实践经验,我对图文消息做一个使用总结: 1)一定要给图文消息的Url属性赋值。不管是单图文,还是多图文,或者是不含图片的图文,都有可能会被用户点击。如果Url为空,用户点击后将会打开一个空白页面,这给用户的体验是非常差的; 2)只有单图文的描述才会显示,多图文的描述不会被显示; 3)图文消息的标题、描述中可以使用QQ表情和符号表情。合理地运用表情符号,会使得消息更加生动; 4)图文消息的标题、描述中可以使用换行符。合理地使用换行符,会使得内容结构更加清晰; 5)图文消息的标题、描述中不支持超文本链接(html的标签)。不只是技术上实现不了,就连逻辑上也说不通,因为一条图文消息的任何位置被点击,都将调用微信内置的浏览器打开Url,如果标题、描述里再放几个超链接,不知道点击该打开哪个页面。真搞不懂为什么有好几个同学都在问这个问题,难道设计成多图文不好吗? 6)图文消息的链接、图片链接可以使用外部域名下的资源,如本例中:柳峰的头像、博文的链接,都是指向CSDN网站的资源。在网上,甚至是微信官方交流群里,认为图文消息的Url、PicUrl不可以使用外链的大有人在,不知道这谣言从哪开始的,实践是检验真理的唯一标准! 7)使用指定大小的图片。第一条图文的图片大小建议为640*320,其他图文的图片大小建议为80*80。如果使用的图片太大,加载慢,而且耗流量;如果使用的图片太小,显示后会被拉伸,失真了很难看。 8)每条图文消息的图文建议控制在1-4条。这样在绝大多数终端上一屏能够显示完,用户扫一眼就能大概了解消息的主要内容,这样最有可能促使用户去点击并阅读 14. 微信公众帐号开发教程第14篇-自定义菜单的创建及菜单事件响应 微信5.0发布 2013年8月5日,伴随着微信5.0 iPhone版的发布,公众平台也进行了重要的更新,主要包括: 1)运营主体为组织,可选择成为服务号或者订阅号; 2)服务号可以申请自定义菜单; 3)使用QQ登录的公众号,可以升级为邮箱登录; 4)使用邮箱登录的公众号,可以修改登录邮箱; 5)编辑图文消息可选填作者; 6)群发消息可以同步到腾讯微博。 其中,大家议论最多的当属前两条,就是关于帐号类型和自定义菜单的更新,我这里做几点补充说明: 1)目前公众号类型分为两种:服务号和订阅号,8月5日平台更新后所有的帐号默认为订阅号,有一次转换成服务号的机会; 2)服务号主要面向企业、政府和其他组织,而订阅号主要面向媒体和个人; 3)只有服务号可以申请自定义菜单,订阅号不能申请; 4)服务号每月只能群发一条消息,而订阅号每天能群发一条消息。 平台更新后,让很多人纠结的是自定义菜单和每天群发一条消息不可兼得,对此,我不想过多评论。   引言及内容概要 在微信5.0以前,自定义菜单是作为一种内测资格使用的,只有少数公众帐号拥有菜单,因此出现很多企业为了弄到菜单不惜重金求购。现如今,一大批帐号从订阅号转为服务号,很多都是奔着自定义菜单去的。而且,经测试发现,微信最近的审核放松很多,只要申请服务号、自定义菜单的基本都成功了,根本不管填写的资料真伪。不知道以后微信会不会翻脸,要求补全企业资料,那将会是一种给小孩一颗糖吃再把他打哭的感觉。。。 自定义菜单是申请到了,到底该怎么创建、怎么使用呢?最近几天不管是微信官方交流群,还是在我博客留言里,都能够看到不少开发者都在为这个发愁。本篇文章就为大家解决这个难题。   自定义菜单的创建步骤 1、找到AppId和AppSecret。自定义菜单申请成功后,在“高级功能”-“开发模式”-“接口配置信息”的最后两项就是; 2、根据AppId和AppSecret,以https get方式获取访问特殊接口所必须的凭证access_token; 3、根据access_token,将json格式的菜单数据通过https post方式提交。   分析创建菜单的难点 原来创建菜单这么简单,三步就能搞定?跟把大象放冰箱差不多。呵呵,当然没有这么简单,那我们一步步来看,到底难在哪里? 首先,第1步肯定都没有问题,只要成功申请了自定义菜单,一定能拿到AppId和AppSecret这两个值。 再来看第2步,由于是get方式获取access_token,很多人直接把拼好的url放在浏览器里执行,access_token就拿到了。抛开是不是用编程方式实现的来说,这真是个好办法,显然大家在第二步上也没有问题。 最后再看第3步,拼装json格式的菜单数据,虽然繁锁一点,但基本上也都没有什么问题的,因为官方给了个例子,照猫画虎就行了。那问题一定就出现在https post提交上了。 结论:不知道如何创建自定义菜单的朋友,大都可以归为以下三种情况: 1)根本不看或者没看懂公众平台API文档中关于“通用接口”、“自定义菜单接口”和“使用限制”部分的说明; 2)不知道如何发起HTTPS请求(平时的http请求,直接使用HttpUrlConnection就可以轻松搞定,但https请求要复杂一点); 3)不知道如何通过POST方式提交json格式的菜单数据。 正在看文章的你,不知道是属于哪一种,或者几种情况都有,不妨留言说出来,也可以做个调查。不管属于哪一种情况,既然看到了这篇文章,相信一定会让你弄明白的。   解读通用接口文档---凭证的获取 我们先来看通用接口文档的简介部分,如下图所示。 通俗点讲,这段简介可以这么理解:公众平台还有很多特殊的接口,像自定义菜单的创建、语音文件的获取、主动发送消息等,如果开发者想通过HTTP请求访问这些特殊接口,就必须要有访问凭证,也就是access_token。 那么,又该如何获取接口访问凭证access_token呢?让我们继续往下看。 图中已经表达的很清楚了,获取access_token是通过GET方式访问如下链接: [java] view plaincopyprint? 1. https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET   https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET 链接中有三个参数,分别是grant_type、appid和secret。根据图中的参数说明,grant_type传固定值client_credential,而appid和secret就是申请完自定义菜单后微信分配给我们的。 请求发送成功后,微信服务器会返回一个json串,包含access_token和expires_in两个元素。其中,access_token就是我们最终需要的凭证,而expires_in是凭证的有效期,单位是秒,7200秒也就是2个小时。这就意味着, 不是每次访问特殊接口,都需要重新获取一次access_token,只要access_token还在有效期内,就一直可以使用。   解读自定义菜单接口文档 还是一样,先来看看自定义菜单接口的简介部分,如下图所示。 从图中我们能够获取到以下信息: 1)拿到凭证access_token后,我们能对菜单执行三种操作:创建、查询和删除; 2)自定义菜单目前只支持click一种事件,即用户点击后回复某种类型的消息;不能够实现点击菜单项直接打开页面(type=view未开放,目前只是微生活有); 3)由于微信客户端缓存的原因,菜单创建后并不会立即在微信上显示出来,需要过24小时。在测试菜单创建时,可以通过取消关注后,再关注的方式达到立即看菜单的目的。   继续往下看,就是关于菜单怎么创建的介绍了,如下图所示。 其实就是向地址https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN以POST方式提交一个JSON格式的菜单字符串。 后面,关于参数说明的部分我就不一一贴图说明了,把重点说一下: 1)自定义菜单是一个3x5结构的,即菜单最多只能有二级,一级菜单最多只能有3个,每个一级菜单下最多可以有5个二级菜单项; 2)菜单项都有一个key值。当用户点击某个菜单项时,微信会将该菜单项的key值以事件推送的方式发送给我们的后台处理程序。 关于菜单的查询、创建我就不提了,这两个接口使用的频率非常小,一般都用不上。如果需要,再按照我上面提供的思路也不难理解。   解读API文档之使用限制 很多小伙伴看到这张图就开始疑惑了:怎么菜单还限制使用次数,用户量越来越大的时候,根本不够用啊。看清楚,这个限制是针对接口调用的,也就是针对开发者的,和用户数、使用次数半点关系也没有。 就先拿获取凭证接口来说吧,限制一天只能调用200次。还记得前面提到过access_token是有有效期的,并且有效期为两小时,也就是获取一次access_token后的两小时内,都可以继续使用,那么理想情况一天24小时内,是不是只需要获取12次就够了?难道200次还不够用? 再来看下菜单创建接口限制一天只能调用100次。我就这么解释吧,菜单创建一次后,只要你不切换模式(指的是在编辑模式和开发模式间切换)、不调用删除接口,这个菜单会永远存在的。谁没事干,一天要创建100次菜单,就算是测试,测个10次8次足够了吧? 菜单的查询和删除接口的限制我就不解释了,至今为止这二个接口我都没使用过一次。就算有这样的使用需求,一天这么多次的调用,完全足够了。   封装通用的请求方法 读到这里,就默认大家已经掌握了上面讲到的所有关于自定义菜单的理论知识,下面就进入代码实战讲解的部分。 先前我们了解到,创建菜单需要调用二个接口,并且都是https请求,而非http。如果要封装一个通用的请求方法,该方法至少需要具备以下能力: 1)支持HTTPS请求; 2)支持GET、POST两种方式; 3)支持参数提交,也支持无参数的情况; 对于https请求,我们需要一个证书信任管理器,这个管理器类需要自己定义,但需要实现X509TrustManager接口,代码如下: [java] view plaincopyprint? 1. package org.liufeng.weixin.util;   2.    3. import java.security.cert.CertificateException;   4. import java.security.cert.X509Certificate;   5.    6. import javax.net.ssl.X509TrustManager;   7.    8. /**  9.  * 证书信任管理器(用于https请求)  10.  *   11.  * @author liufeng  12.  * @date 2013-08-08  13.  */   14. public class MyX509TrustManager implements X509TrustManager {   15.    16.     public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {   17.     }   18.    19.     public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {   20.     }   21.    22.     public X509Certificate[] getAcceptedIssuers() {   23.         return null;   24.     }   25. }   package org.liufeng.weixin.util; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.X509TrustManager; /** * 证书信任管理器(用于https请求) * * @author liufeng * @date 2013-08-08 */ public class MyX509TrustManager implements X509TrustManager { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } public X509Certificate[] getAcceptedIssuers() { return null; } } 这个证书管理器的作用就是让它信任我们指定的证书,上面的代码意味着信任所有证书,不管是否权威机构颁发。 证书有了,通用的https请求方法就不难实现了,实现代码如下: [java] view plaincopyprint? 1. package org.liufeng.weixin.util;   2.    3. import java.io.BufferedReader;   4. import java.io.InputStream;   5. import java.io.InputStreamReader;   6. import java.io.OutputStream;   7. import java.net.ConnectException;   8. import java.net.URL;   9.    10. import javax.net.ssl.HttpsURLConnection;   11. import javax.net.ssl.SSLContext;   12. import javax.net.ssl.SSLSocketFactory;   13. import javax.net.ssl.TrustManager;   14.    15. import net.sf.json.JSONObject;   16.    17. import org.slf4j.Logger;   18. import org.slf4j.LoggerFactory;   19.    20. /**  21.  * 公众平台通用接口工具类  22.  *   23.  * @author liuyq  24.  * @date 2013-08-09  25.  */   26. public class WeixinUtil {   27.     private static Logger log = LoggerFactory.getLogger(WeixinUtil.class);   28.    29.     /**  30.      * 发起https请求并获取结果  31.      *   32.      * @param requestUrl 请求地址  33.      * @param requestMethod 请求方式(GET、POST)  34.      * @param outputStr 提交的数据  35.      * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)  36.      */   37.     public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) {   38.         JSONObject jsonObject = null;   39.         StringBuffer buffer = new StringBuffer();   40.         try {   41.             // 创建SSLContext对象,并使用我们指定的信任管理器初始化    42.             TrustManager[] tm = { new MyX509TrustManager() };   43.             SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");   44.             sslContext.init(null, tm, new java.security.SecureRandom());   45.             // 从上述SSLContext对象中得到SSLSocketFactory对象    46.             SSLSocketFactory ssf = sslContext.getSocketFactory();   47.    48.             URL url = new URL(requestUrl);   49.             HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection();   50.             httpUrlConn.setSSLSocketFactory(ssf);   51.    52.             httpUrlConn.setDoOutput(true);   53.             httpUrlConn.setDoInput(true);   54.             httpUrlConn.setUseCaches(false);   55.             // 设置请求方式(GET/POST)    56.             httpUrlConn.setRequestMethod(requestMethod);   57.    58.             if ("GET".equalsIgnoreCase(requestMethod))   59.                 httpUrlConn.connect();   60.    61.             // 当有数据需要提交时    62.             if (null != outputStr) {   63.                 OutputStream outputStream = httpUrlConn.getOutputStream();   64.                 // 注意编码格式,防止中文乱码    65.                 outputStream.write(outputStr.getBytes("UTF-8"));   66.                 outputStream.close();   67.             }   68.    69.             // 将返回的输入流转换成字符串    70.             InputStream inputStream = httpUrlConn.getInputStream();   71.             InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");   72.             BufferedReader bufferedReader = new BufferedReader(inputStreamReader);   73.    74.             String str = null;   75.             while ((str = bufferedReader.readLine()) != null) {   76.                 buffer.append(str);   77.             }   78.             bufferedReader.close();   79.             inputStreamReader.close();   80.             // 释放资源    81.             inputStream.close();   82.             inputStream = null;   83.             httpUrlConn.disconnect();   84.             jsonObject = JSONObject.fromObject(buffer.toString());   85.         } catch (ConnectException ce) {   86.             log.error("Weixin server connection timed out.");   87.         } catch (Exception e) {   88.             log.error("https request error:{}", e);   89.         }   90.         return jsonObject;   91.     }   92. }   package org.liufeng.weixin.util; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.ConnectException; import java.net.URL; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import net.sf.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 公众平台通用接口工具类 * * @author liuyq * @date 2013-08-09 */ public class WeixinUtil { private static Logger log = LoggerFactory.getLogger(WeixinUtil.class); /** * 发起https请求并获取结果 * * @param requestUrl 请求地址 * @param requestMethod 请求方式(GET、POST) * @param outputStr 提交的数据 * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值) */ public static JSONObject httpRequest(String requestUrl, String requestMethod, String outputStr) { JSONObject jsonObject = null; StringBuffer buffer = new StringBuffer(); try { // 创建SSLContext对象,并使用我们指定的信任管理器初始化 TrustManager[] tm = { new MyX509TrustManager() }; SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE"); sslContext.init(null, tm, new java.security.SecureRandom()); // 从上述SSLContext对象中得到SSLSocketFactory对象 SSLSocketFactory ssf = sslContext.getSocketFactory(); URL url = new URL(requestUrl); HttpsURLConnection httpUrlConn = (HttpsURLConnection) url.openConnection(); httpUrlConn.setSSLSocketFactory(ssf); httpUrlConn.setDoOutput(true); httpUrlConn.setDoInput(true); httpUrlConn.setUseCaches(false); // 设置请求方式(GET/POST) httpUrlConn.setRequestMethod(requestMethod); if ("GET".equalsIgnoreCase(requestMethod)) httpUrlConn.connect(); // 当有数据需要提交时 if (null != outputStr) { OutputStream outputStream = httpUrlConn.getOutputStream(); // 注意编码格式,防止中文乱码 outputStream.write(outputStr.getBytes("UTF-8")); outputStream.close(); } // 将返回的输入流转换成字符串 InputStream inputStream = httpUrlConn.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8"); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String str = null; while ((str = bufferedReader.readLine()) != null) { buffer.append(str); } bufferedReader.close(); inputStreamReader.close(); // 释放资源 inputStream.close(); inputStream = null; httpUrlConn.disconnect(); jsonObject = JSONObject.fromObject(buffer.toString()); } catch (ConnectException ce) { log.error("Weixin server connection timed out."); } catch (Exception e) { log.error("https request error:{}", e); } return jsonObject; } }  代码说明: 1)41~50行:解决https请求的问题,很多人问题就出在这里; 2)55~59行:兼容GET、POST两种方式; 3)61~67行:兼容有数据提交、无数据提交两种情况,也有相当一部分人不知道如何POST提交数据;   Pojo类的封装 在获取凭证创建菜单前,我们还需要封装一些pojo,这会让我们的代码更美观,有条理。 首先是调用获取凭证接口后,微信服务器会返回json格式的数据:{"access_token":"ACCESS_TOKEN","expires_in":7200},我们将其封装为一个AccessToken对象,对象有二个属性:token和expiresIn,代码如下: [java] view plaincopyprint? 1. package org.liufeng.weixin.pojo;   2.    3. /**  4.  * 微信通用接口凭证  5.  *   6.  * @author liufeng  7.  * @date 2013-08-08  8.  */   9. public class AccessToken {   10.     // 获取到的凭证    11.     private String token;   12.     // 凭证有效时间,单位:秒    13.     private int expiresIn;   14.    15.     public String getToken() {   16.         return token;   17.     }   18.    19.     public void setToken(String token) {   20.         this.token = token;   21.     }   22.    23.     public int getExpiresIn() {   24.         return expiresIn;   25.     }   26.    27.     public void setExpiresIn(int expiresIn) {   28.         this.expiresIn = expiresIn;   29.     }   30. }   package org.liufeng.weixin.pojo; /** * 微信通用接口凭证 * * @author liufeng * @date 2013-08-08 */ public class AccessToken { // 获取到的凭证 private String token; // 凭证有效时间,单位:秒 private int expiresIn; public String getToken() { return token; } public void setToken(String token) { this.token = token; } public int getExpiresIn() { return expiresIn; } public void setExpiresIn(int expiresIn) { this.expiresIn = expiresIn; } } 接下来是对菜单结构的封装。因为我们是采用面向对象的编程方式,最终提交的json格式菜单数据就应该是由对象直接转换得到,而不是在程序代码中拼一大堆json数据。菜单结构封装的依据是公众平台API文档中给出的那一段json格式的菜单结构,如下所示: [java] view plaincopyprint? 1. {   2.     "button":[   3.     {      4.          "type":"click",   5.          "name":"今日歌曲",   6.          "key":"V1001_TODAY_MUSIC"   7.      },   8.      {   9.           "type":"click",   10.           "name":"歌手简介",   11.           "key":"V1001_TODAY_SINGER"   12.      },   13.      {   14.           "name":"菜单",   15.           "sub_button":[   16.            {   17.               "type":"click",   18.               "name":"hello word",   19.               "key":"V1001_HELLO_WORLD"   20.            },   21.            {   22.               "type":"click",   23.               "name":"赞一下我们",   24.               "key":"V1001_GOOD"   25.            }]   26.       }]   27. }   { "button":[ { "type":"click", "name":"今日歌曲", "key":"V1001_TODAY_MUSIC" }, { "type":"click", "name":"歌手简介", "key":"V1001_TODAY_SINGER" }, { "name":"菜单", "sub_button":[ { "type":"click", "name":"hello word", "key":"V1001_HELLO_WORLD" }, { "type":"click", "name":"赞一下我们", "key":"V1001_GOOD" }] }] } 首先是菜单项的基类,所有一级菜单、二级菜单都共有一个相同的属性,那就是name。菜单项基类的封装代码如下: [java] view plaincopyprint? 1. package org.liufeng.weixin.pojo;   2.    3. /**  4.  * 按钮的基类  5.  *   6.  * @author liufeng  7.  * @date 2013-08-08  8.  */   9. public class Button {   10.     private String name;   11.    12.     public String getName() {   13.         return name;   14.     }   15.    16.     public void setName(String name) {   17.         this.name = name;   18.     }   19. }   package org.liufeng.weixin.pojo; /** * 按钮的基类 * * @author liufeng * @date 2013-08-08 */ public class Button { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } 接着是子菜单项的封装。这里对子菜单是这样定义的:没有子菜单的菜单项,有可能是二级菜单项,也有可能是不含二级菜单的一级菜单。这类子菜单项一定会包含三个属性:type、name和key,封装的代码如下: [java] view plaincopyprint? 1. package org.liufeng.weixin.pojo;   2.    3. /**  4.  * 普通按钮(子按钮)  5.  *   6.  * @author liufeng  7.  * @date 2013-08-08  8.  */   9. public class CommonButton extends Button {   10.     private String type;   11.     private String key;   12.    13.     public String getType() {   14.         return type;   15.     }   16.    17.     public void setType(String type) {   18.         this.type = type;   19.     }   20.    21.     public String getKey() {   22.         return key;   23.     }   24.    25.     public void setKey(String key) {   26.         this.key = key;   27.     }   28. }   package org.liufeng.weixin.pojo; /** * 普通按钮(子按钮) * * @author liufeng * @date 2013-08-08 */ public class CommonButton extends Button { private String type; private String key; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } } 再往下是父菜单项的封装。对父菜单项的定义:包含有二级菜单项的一级菜单。这类菜单项包含有二个属性:name和sub_button,而sub_button以是一个子菜单项数组。父菜单项的封装代码如下: [java] view plaincopyprint? 1. package org.liufeng.weixin.pojo;   2.    3. /**  4.  * 复杂按钮(父按钮)  5.  *   6.  * @author liufeng  7.  * @date 2013-08-08  8.  */   9. public class ComplexButton extends Button {   10.     private Button[] sub_button;   11.    12.     public Button[] getSub_button() {   13.         return sub_button;   14.     }   15.    16.     public void setSub_button(Button[] sub_button) {   17.         this.sub_button = sub_button;   18.     }   19. }   package org.liufeng.weixin.pojo; /** * 复杂按钮(父按钮) * * @author liufeng * @date 2013-08-08 */ public class ComplexButton extends Button { private Button[] sub_button; public Button[] getSub_button() { return sub_button; } public void setSub_button(Button[] sub_button) { this.sub_button = sub_button; } } 最后是整个菜单对象的封装,菜单对象包含多个菜单项(最多只能有3个),这些菜单项即可以是子菜单项(不含二级菜单的一级菜单),也可以是父菜单项(包含二级菜单的菜单项),如果能明白上面所讲的,再来看封装后的代码就很容易理解了: [java] view plaincopyprint? 1. package org.liufeng.weixin.pojo;   2.    3. /**  4.  * 菜单  5.  *   6.  * @author liufeng  7.  * @date 2013-08-08  8.  */   9. public class Menu {   10.     private Button[] button;   11.    12.     public Button[] getButton() {   13.         return button;   14.     }   15.    16.     public void setButton(Button[] button) {   17.         this.button = button;   18.     }   19. }   package org.liufeng.weixin.pojo; /** * 菜单 * * @author liufeng * @date 2013-08-08 */ public class Menu { private Button[] button; public Button[] getButton() { return button; } public void setButton(Button[] button) { this.button = button; } } 关于POJO类的封装就介绍完了。   凭证access_token的获取方法 继续在先前通用请求方法的类WeixinUtil.java中加入以下代码,用于获取接口访问凭证: [java] view plaincopyprint? 1. // 获取access_token的接口地址(GET) 限200(次/天)    2. public final static String access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";   3.    4. /**  5.  * 获取access_token  6.  *   7.  * @param appid 凭证  8.  * @param appsecret 密钥  9.  * @return  10.  */   11. public static AccessToken getAccessToken(String appid, String appsecret) {   12.     AccessToken accessToken = null;   13.    14.     String requestUrl = access_token_url.replace("APPID", appid).replace("APPSECRET", appsecret);   15.     JSONObject jsonObject = httpRequest(requestUrl, "GET", null);   16.     // 如果请求成功    17.     if (null != jsonObject) {   18.         try {   19.             accessToken = new AccessToken();   20.             accessToken.setToken(jsonObject.getString("access_token"));   21.             accessToken.setExpiresIn(jsonObject.getInt("expires_in"));   22.         } catch (JSONException e) {   23.             accessToken = null;   24.             // 获取token失败    25.             log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));   26.         }   27.     }   28.     return accessToken;   29. }   // 获取access_token的接口地址(GET) 限200(次/天) public final static String access_token_url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET"; /** * 获取access_token * * @param appid 凭证 * @param appsecret 密钥 * @return */ public static AccessToken getAccessToken(String appid, String appsecret) { AccessToken accessToken = null; String requestUrl = access_token_url.replace("APPID", appid).replace("APPSECRET", appsecret); JSONObject jsonObject = httpRequest(requestUrl, "GET", null); // 如果请求成功 if (null != jsonObject) { try { accessToken = new AccessToken(); accessToken.setToken(jsonObject.getString("access_token")); accessToken.setExpiresIn(jsonObject.getInt("expires_in")); } catch (JSONException e) { accessToken = null; // 获取token失败 log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg")); } } return accessToken; } 自定义菜单的创建方法 继续在先前通用请求方法的类WeixinUtil.java中加入以下代码,用于创建自定义菜单: [java] view plaincopyprint? 1. // 菜单创建(POST) 限100(次/天)    2. public static String menu_create_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN";   3.    4. /**  5.  * 创建菜单  6.  *   7.  * @param menu 菜单实例  8.  * @param accessToken 有效的access_token  9.  * @return 0表示成功,其他值表示失败  10.  */   11. public static int createMenu(Menu menu, String accessToken) {   12.     int result = 0;   13.    14.     // 拼装创建菜单的url    15.     String url = menu_create_url.replace("ACCESS_TOKEN", accessToken);   16.     // 将菜单对象转换成json字符串    17.     String jsonMenu = JSONObject.fromObject(menu).toString();   18.     // 调用接口创建菜单    19.     JSONObject jsonObject = httpRequest(url, "POST", jsonMenu);   20.    21.     if (null != jsonObject) {   22.         if (0 != jsonObject.getInt("errcode")) {   23.             result = jsonObject.getInt("errcode");   24.             log.error("创建菜单失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));   25.         }   26.     }   27.    28.     return result;   29. }   // 菜单创建(POST) 限100(次/天) public static String menu_create_url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN"; /** * 创建菜单 * * @param menu 菜单实例 * @param accessToken 有效的access_token * @return 0表示成功,其他值表示失败 */ public static int createMenu(Menu menu, String accessToken) { int result = 0; // 拼装创建菜单的url String url = menu_create_url.replace("ACCESS_TOKEN", accessToken); // 将菜单对象转换成json字符串 String jsonMenu = JSONObject.fromObject(menu).toString(); // 调用接口创建菜单 JSONObject jsonObject = httpRequest(url, "POST", jsonMenu); if (null != jsonObject) { if (0 != jsonObject.getInt("errcode")) { result = jsonObject.getInt("errcode"); log.error("创建菜单失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg")); } } return result; }   调用封装的方法创建自定义菜单 [java] view plaincopyprint? 1. package org.liufeng.weixin.main;   2.    3. import org.liufeng.weixin.pojo.AccessToken;   4. import org.liufeng.weixin.pojo.Button;   5. import org.liufeng.weixin.pojo.CommonButton;   6. import org.liufeng.weixin.pojo.ComplexButton;   7. import org.liufeng.weixin.pojo.Menu;   8. import org.liufeng.weixin.util.WeixinUtil;   9. import org.slf4j.Logger;   10. import org.slf4j.LoggerFactory;   11.    12. /**  13.  * 菜单管理器类  14.  *   15.  * @author liufeng  16.  * @date 2013-08-08  17.  */   18. public class MenuManager {   19.     private static Logger log = LoggerFactory.getLogger(MenuManager.class);   20.    21.     public static void main(String[] args) {   22.         // 第三方用户唯一凭证    23.         String appId = "000000000000000000";   24.         // 第三方用户唯一凭证密钥    25.         String appSecret = "00000000000000000000000000000000";   26.    27.         // 调用接口获取access_token    28.         AccessToken at = WeixinUtil.getAccessToken(appId, appSecret);   29.    30.         if (null != at) {   31.             // 调用接口创建菜单    32.             int result = WeixinUtil.createMenu(getMenu(), at.getToken());   33.    34.             // 判断菜单创建结果    35.             if (0 == result)   36.                 log.info("菜单创建成功!");   37.             else   38.                 log.info("菜单创建失败,错误码:" + result);   39.         }   40.     }   41.    42.     /**  43.      * 组装菜单数据  44.      *   45.      * @return  46.      */   47.     private static Menu getMenu() {   48.         CommonButton btn11 = new CommonButton();   49.         btn11.setName("天气预报");   50.         btn11.setType("click");   51.         btn11.setKey("11");   52.    53.         CommonButton btn12 = new CommonButton();   54.         btn12.setName("公交查询");   55.         btn12.setType("click");   56.         btn12.setKey("12");   57.    58.         CommonButton btn13 = new CommonButton();   59.         btn13.setName("周边搜索");   60.         btn13.setType("click");   61.         btn13.setKey("13");   62.    63.         CommonButton btn14 = new CommonButton();   64.         btn14.setName("历史上的今天");   65.         btn14.setType("click");   66.         btn14.setKey("14");   67.    68.         CommonButton btn21 = new CommonButton();   69.         btn21.setName("歌曲点播");   70.         btn21.setType("click");   71.         btn21.setKey("21");   72.    73.         CommonButton btn22 = new CommonButton();   74.         btn22.setName("经典游戏");   75.         btn22.setType("click");   76.         btn22.setKey("22");   77.    78.         CommonButton btn23 = new CommonButton();   79.         btn23.setName("美女电台");   80.         btn23.setType("click");   81.         btn23.setKey("23");   82.    83.         CommonButton btn24 = new CommonButton();   84.         btn24.setName("人脸识别");   85.         btn24.setType("click");   86.         btn24.setKey("24");   87.    88.         CommonButton btn25 = new CommonButton();   89.         btn25.setName("聊天唠嗑");   90.         btn25.setType("click");   91.         btn25.setKey("25");   92.    93.         CommonButton btn31 = new CommonButton();   94.         btn31.setName("Q友圈");   95.         btn31.setType("click");   96.         btn31.setKey("31");   97.    98.         CommonButton btn32 = new CommonButton();   99.         btn32.setName("电影排行榜");   100.         btn32.setType("click");   101.         btn32.setKey("32");   102.    103.         CommonButton btn33 = new CommonButton();   104.         btn33.setName("幽默笑话");   105.         btn33.setType("click");   106.         btn33.setKey("33");   107.    108.         ComplexButton mainBtn1 = new ComplexButton();   109.         mainBtn1.setName("生活助手");   110.         mainBtn1.setSub_button(new CommonButton[] { btn11, btn12, btn13, btn14 });   111.    112.         ComplexButton mainBtn2 = new ComplexButton();   113.         mainBtn2.setName("休闲驿站");   114.         mainBtn2.setSub_button(new CommonButton[] { btn21, btn22, btn23, btn24, btn25 });   115.    116.         ComplexButton mainBtn3 = new ComplexButton();   117.         mainBtn3.setName("更多体验");   118.         mainBtn3.setSub_button(new CommonButton[] { btn31, btn32, btn33 });   119.    120.         /**  121.          * 这是公众号xiaoqrobot目前的菜单结构,每个一级菜单都有二级菜单项
  122.          *   123.          * 在某个一级菜单下没有二级菜单的情况,menu该如何定义呢?
  124.          * 比如,第三个一级菜单项不是“更多体验”,而直接是“幽默笑话”,那么menu应该这样定义:
  125.          * menu.setButton(new Button[] { mainBtn1, mainBtn2, btn33 });  126.          */   127.         Menu menu = new Menu();   128.         menu.setButton(new Button[] { mainBtn1, mainBtn2, mainBtn3 });   129.    130.         return menu;   131.     }   132. }   package org.liufeng.weixin.main; import org.liufeng.weixin.pojo.AccessToken; import org.liufeng.weixin.pojo.Button; import org.liufeng.weixin.pojo.CommonButton; import org.liufeng.weixin.pojo.ComplexButton; import org.liufeng.weixin.pojo.Menu; import org.liufeng.weixin.util.WeixinUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 菜单管理器类 * * @author liufeng * @date 2013-08-08 */ public class MenuManager { private static Logger log = LoggerFactory.getLogger(MenuManager.class); public static void main(String[] args) { // 第三方用户唯一凭证 String appId = "000000000000000000"; // 第三方用户唯一凭证密钥 String appSecret = "00000000000000000000000000000000"; // 调用接口获取access_token AccessToken at = WeixinUtil.getAccessToken(appId, appSecret); if (null != at) { // 调用接口创建菜单 int result = WeixinUtil.createMenu(getMenu(), at.getToken()); // 判断菜单创建结果 if (0 == result) log.info("菜单创建成功!"); else log.info("菜单创建失败,错误码:" + result); } } /** * 组装菜单数据 * * @return */ private static Menu getMenu() { CommonButton btn11 = new CommonButton(); btn11.setName("天气预报"); btn11.setType("click"); btn11.setKey("11"); CommonButton btn12 = new CommonButton(); btn12.setName("公交查询"); btn12.setType("click"); btn12.setKey("12"); CommonButton btn13 = new CommonButton(); btn13.setName("周边搜索"); btn13.setType("click"); btn13.setKey("13"); CommonButton btn14 = new CommonButton(); btn14.setName("历史上的今天"); btn14.setType("click"); btn14.setKey("14"); CommonButton btn21 = new CommonButton(); btn21.setName("歌曲点播"); btn21.setType("click"); btn21.setKey("21"); CommonButton btn22 = new CommonButton(); btn22.setName("经典游戏"); btn22.setType("click"); btn22.setKey("22"); CommonButton btn23 = new CommonButton(); btn23.setName("美女电台"); btn23.setType("click"); btn23.setKey("23"); CommonButton btn24 = new CommonButton(); btn24.setName("人脸识别"); btn24.setType("click"); btn24.setKey("24"); CommonButton btn25 = new CommonButton(); btn25.setName("聊天唠嗑"); btn25.setType("click"); btn25.setKey("25"); CommonButton btn31 = new CommonButton(); btn31.setName("Q友圈"); btn31.setType("click"); btn31.setKey("31"); CommonButton btn32 = new CommonButton(); btn32.setName("电影排行榜"); btn32.setType("click"); btn32.setKey("32"); CommonButton btn33 = new CommonButton(); btn33.setName("幽默笑话"); btn33.setType("click"); btn33.setKey("33"); ComplexButton mainBtn1 = new ComplexButton(); mainBtn1.setName("生活助手"); mainBtn1.setSub_button(new CommonButton[] { btn11, btn12, btn13, btn14 }); ComplexButton mainBtn2 = new ComplexButton(); mainBtn2.setName("休闲驿站"); mainBtn2.setSub_button(new CommonButton[] { btn21, btn22, btn23, btn24, btn25 }); ComplexButton mainBtn3 = new ComplexButton(); mainBtn3.setName("更多体验"); mainBtn3.setSub_button(new CommonButton[] { btn31, btn32, btn33 }); /** * 这是公众号xiaoqrobot目前的菜单结构,每个一级菜单都有二级菜单项
* * 在某个一级菜单下没有二级菜单的情况,menu该如何定义呢?
* 比如,第三个一级菜单项不是“更多体验”,而直接是“幽默笑话”,那么menu应该这样定义:
* menu.setButton(new Button[] { mainBtn1, mainBtn2, btn33 }); */ Menu menu = new Menu(); menu.setButton(new Button[] { mainBtn1, mainBtn2, mainBtn3 }); return menu; } } 注意:在运行以上代码时,需要将appId和appSecret换成你自己公众号的。   整个工程的结构 为了保证文章的完整独立性和可读性,我是新建了一个Java Project(Java web工程也可以,没有太大关系),没有在前几篇文章所讲到的weixinCourse工程中添加代码。如果需要,读者可以自己实现将菜单创建的代码移到自己已有的工程中去。 图中所有Java文件的源代码都在文章中贴出并进行了说明,图中使用到的jar也是Java开发中通用的jar包,很容易在网上下载到。 工程中引入的jar包主要分为两类: 1)第一类是json开发工具包,用于Java对象和Json字符串之间的转换;json开发工具包一共有3个jar:ezmorph-1.0.6.jar,json-lib-2.2.3-jdk13.jar和morph-1.1.1.jar。 2)第二类是slf4j日志工具包,用于记录系统运行所产生的日志,日志可以输出到控制台或文件中。 整个工程中,唯一没有讲到的是src下的log4j.properties的配置,也把它贴出来,方便大家参考,这样才是一个完整的工程源码。log4j.properties文件的内容如下: [java] view plaincopyprint? 1. log4j.rootLogger=info,console,file   2.    3. log4j.appender.console=org.apache.log4j.ConsoleAppender   4. log4j.appender.console.layout=org.apache.log4j.PatternLayout   5. log4j.appender.console.layout.ConversionPattern=[%-5p] %m%n   6.    7. log4j.appender.file=org.apache.log4j.DailyRollingFileAppender   8. log4j.appender.file.DatePattern='-'yyyy-MM-dd   9. log4j.appender.file.File=./logs/weixinmpmenu.log   10. log4j.appender.file.Append=true   11. log4j.appender.file.layout=org.apache.log4j.PatternLayout   12. log4j.appender.file.layout.ConversionPattern=[%-5p] %d %37c %3x - %m%n   log4j.rootLogger=info,console,file log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=[%-5p] %m%n log4j.appender.file=org.apache.log4j.DailyRollingFileAppender log4j.appender.file.DatePattern='-'yyyy-MM-dd log4j.appender.file.File=./logs/weixinmpmenu.log log4j.appender.file.Append=true log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=[%-5p] %d %37c %3x - %m%n   如何响应菜单点击事件 自定义菜单的创建工作已经完成,那么该如何接收和响应菜单的点击事件呢,也就是说在公众帐号后台处理程序中,如何识别用户点击的是哪个菜单,以及做出响应。这部分内容其实在教程的第5篇各种消息的接收与响应中已经讲解清楚了。 来看一下第一篇教程weixinCourse项目中的CoreService类要怎么改写,才能接收响应菜单点击事件,该类修改后的完整代码如下: [java] view plaincopyprint? 1. package org.liufeng.course.service;   2.    3. import java.util.Date;   4. import java.util.Map;   5.    6. import javax.servlet.http.HttpServletRequest;   7.    8. import org.liufeng.course.message.resp.TextMessage;   9. import org.liufeng.course.util.MessageUtil;   10.    11. /**  12.  * 核心服务类  13.  *   14.  * @author liufeng  15.  * @date 2013-05-20  16.  */   17. public class CoreService {   18.     /**  19.      * 处理微信发来的请求  20.      *   21.      * @param request  22.      * @return  23.      */   24.     public static String processRequest(HttpServletRequest request) {   25.         String respMessage = null;   26.         try {   27.             // 默认返回的文本消息内容    28.             String respContent = "请求处理异常,请稍候尝试!";   29.    30.             // xml请求解析    31.             Map requestMap = MessageUtil.parseXml(request);   32.    33.             // 发送方帐号(open_id)    34.             String fromUserName = requestMap.get("FromUserName");   35.             // 公众帐号    36.             String toUserName = requestMap.get("ToUserName");   37.             // 消息类型    38.             String msgType = requestMap.get("MsgType");   39.    40.             // 回复文本消息    41.             TextMessage textMessage = new TextMessage();   42.             textMessage.setToUserName(fromUserName);   43.             textMessage.setFromUserName(toUserName);   44.             textMessage.setCreateTime(new Date().getTime());   45.             textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT);   46.             textMessage.setFuncFlag(0);   47.    48.             // 文本消息    49.             if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) {   50.                 respContent = "您发送的是文本消息!";   51.             }   52.             // 图片消息    53.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) {   54.                 respContent = "您发送的是图片消息!";   55.             }   56.             // 地理位置消息    57.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) {   58.                 respContent = "您发送的是地理位置消息!";   59.             }   60.             // 链接消息    61.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) {   62.                 respContent = "您发送的是链接消息!";   63.             }   64.             // 音频消息    65.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) {   66.                 respContent = "您发送的是音频消息!";   67.             }   68.             // 事件推送    69.             else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) {   70.                 // 事件类型    71.                 String eventType = requestMap.get("Event");   72.                 // 订阅    73.                 if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) {   74.                     respContent = "谢谢您的关注!";   75.                 }   76.                 // 取消订阅    77.                 else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) {   78.                     // TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息    79.                 }   80.                 // 自定义菜单点击事件    81.                 else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) {   82.                     // 事件KEY值,与创建自定义菜单时指定的KEY值对应    83.                     String eventKey = requestMap.get("EventKey");   84.    85.                     if (eventKey.equals("11")) {   86.                         respContent = "天气预报菜单项被点击!";   87.                     } else if (eventKey.equals("12")) {   88.                         respContent = "公交查询菜单项被点击!";   89.                     } else if (eventKey.equals("13")) {   90.                         respContent = "周边搜索菜单项被点击!";   91.                     } else if (eventKey.equals("14")) {   92.                         respContent = "历史上的今天菜单项被点击!";   93.                     } else if (eventKey.equals("21")) {   94.                         respContent = "歌曲点播菜单项被点击!";   95.                     } else if (eventKey.equals("22")) {   96.                         respContent = "经典游戏菜单项被点击!";   97.                     } else if (eventKey.equals("23")) {   98.                         respContent = "美女电台菜单项被点击!";   99.                     } else if (eventKey.equals("24")) {   100.                         respContent = "人脸识别菜单项被点击!";   101.                     } else if (eventKey.equals("25")) {   102.                         respContent = "聊天唠嗑菜单项被点击!";   103.                     } else if (eventKey.equals("31")) {   104.                         respContent = "Q友圈菜单项被点击!";   105.                     } else if (eventKey.equals("32")) {   106.                         respContent = "电影排行榜菜单项被点击!";   107.                     } else if (eventKey.equals("33")) {   108.                         respContent = "幽默笑话菜单项被点击!";   109.                     }   110.                 }   111.             }   112.    113.             textMessage.setContent(respContent);   114.             respMessage = MessageUtil.textMessageToXml(textMessage);   115.         } catch (Exception e) {   116.             e.printStackTrace();   117.         }   118.    119.         return respMessage;   120.     }   121. }   package org.liufeng.course.service; import java.util.Date; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.liufeng.course.message.resp.TextMessage; import org.liufeng.course.util.MessageUtil; /** * 核心服务类 * * @author liufeng * @date 2013-05-20 */ public class CoreService { /** * 处理微信发来的请求 * * @param request * @return */ public static String processRequest(HttpServletRequest request) { String respMessage = null; try { // 默认返回的文本消息内容 String respContent = "请求处理异常,请稍候尝试!"; // xml请求解析 Map requestMap = MessageUtil.parseXml(request); // 发送方帐号(open_id) String fromUserName = requestMap.get("FromUserName"); // 公众帐号 String toUserName = requestMap.get("ToUserName"); // 消息类型 String msgType = requestMap.get("MsgType"); // 回复文本消息 TextMessage textMessage = new TextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(new Date().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); textMessage.setFuncFlag(0); // 文本消息 if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { respContent = "您发送的是文本消息!"; } // 图片消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) { respContent = "您发送的是图片消息!"; } // 地理位置消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) { respContent = "您发送的是地理位置消息!"; } // 链接消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) { respContent = "您发送的是链接消息!"; } // 音频消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) { respContent = "您发送的是音频消息!"; } // 事件推送 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) { // 事件类型 String eventType = requestMap.get("Event"); // 订阅 if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) { respContent = "谢谢您的关注!"; } // 取消订阅 else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) { // TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息 } // 自定义菜单点击事件 else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) { // 事件KEY值,与创建自定义菜单时指定的KEY值对应 String eventKey = requestMap.get("EventKey"); if (eventKey.equals("11")) { respContent = "天气预报菜单项被点击!"; } else if (eventKey.equals("12")) { respContent = "公交查询菜单项被点击!"; } else if (eventKey.equals("13")) { respContent = "周边搜索菜单项被点击!"; } else if (eventKey.equals("14")) { respContent = "历史上的今天菜单项被点击!"; } else if (eventKey.equals("21")) { respContent = "歌曲点播菜单项被点击!"; } else if (eventKey.equals("22")) { respContent = "经典游戏菜单项被点击!"; } else if (eventKey.equals("23")) { respContent = "美女电台菜单项被点击!"; } else if (eventKey.equals("24")) { respContent = "人脸识别菜单项被点击!"; } else if (eventKey.equals("25")) { respContent = "聊天唠嗑菜单项被点击!"; } else if (eventKey.equals("31")) { respContent = "Q友圈菜单项被点击!"; } else if (eventKey.equals("32")) { respContent = "电影排行榜菜单项被点击!"; } else if (eventKey.equals("33")) { respContent = "幽默笑话菜单项被点击!"; } } } textMessage.setContent(respContent); respMessage = MessageUtil.textMessageToXml(textMessage); } catch (Exception e) { e.printStackTrace(); } return respMessage; } } 代码说明: 1)第69行、第81行这两行代码说明了如何判断菜单的点击事件。当消息类型MsgType=event,并且Event=CLICK时,就表示是自定义菜单点击事件; 2)第83行是判断具体点击的是哪个菜单项,根据菜单的key值来判断; 3)第85~109行表示当用户点击某个菜单项后,具体返回什么消息,我只是做个简单示例,统一返回文本消息,读者可以根据实际需要来灵活处理。   总结 到这里关于自定义菜单的创建、菜单事件的判断和处理响应就全部介绍完了。我只希望看过文章的人不要只是拷贝代码,如果是这样,我完全不用花这么多的时间来写这篇文章,直接把工程放在下载区多简单。另外,网上是有很多工具,让你填入appid,appsecret和菜单结构,提交就能创建菜单,请慎用!因为appid和appsecret一旦告诉别人,你的公众号的菜单控制权就在别人手上了,总会有别有用心的人出来搞点事的。

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享文档获得金币 ]
12 人已下载

下载文档

相关文档