柳峰博主的专栏(http://blog.csdn.net/column/details/wechatmp.html)和王信平博主的专栏(http://www.cnblogs.com/wangshuo1/)对微信公众号开发已经做了比较详尽的阐述,基本上照搬,就可以做出第一个微信公众号后台应用。但是在『照搬』的过程中,发现有些地方总是觉得别扭,由着完美主义者的性格使然,对以下这几个地方做个小优化吧。
一、区别消息和响应
原来的消息类(用户发给后台)和响应类(后台回给用户)有这些:
我改成了这样的:
我把『消息Message』和『响应Response』在命名上分开了。这样在编程时不会总是想着要去区别,这个『Message』究竟是用户发给后台的『消息』呢,还是后台回给用户的『响应』。
二、给消息和响应添加构造函数
原来消息和响应都只是使用缺省构造函数(详见http://blog.csdn.net/lyq8479/article/details/8949088),实际上是把这两个类当做了『结构体』来看待。基于面向对象的思想,还是觉得需要有构造函数。
2.1 BaseMessage类的构造函数
1 public BaseMessage(String toUserName, String fromUserName, String createTime, String msgType, String msgId){ 2 this.ToUserName=toUserName; 3 this.FromUserName=fromUserName; 4 this.CreateTime=Long.parseLong(createTime); 5 this.MsgType=msgType; 6 this.MsgId=Long.parseLong(msgId); 7 } 8 9 public BaseMessage(Map<String, String> mapMessage){ 10 try { 11 // 开发者微信号 12 String toUserName = mapMessage.get("ToUserName"); 13 // 发送方帐号(open_id) 14 String fromUserName = mapMessage.get("FromUserName"); 15 // 消息类型 16 String msgType = mapMessage.get("MsgType"); 17 // 建立时间 18 String createTime = mapMessage.get("CreateTime"); 19 // 消息ID 20 String msgId = mapMessage.get("MsgId"); 21 22 this.ToUserName = toUserName; 23 this.FromUserName = fromUserName; 24 this.CreateTime = Long.parseLong(createTime); 25 this.MsgType = msgType; 26 this.MsgId = Long.parseLong(msgId); 27 } 28 catch (Exception e) { 29 e.printStackTrace(); 30 } 31 }
2.2 ImageMessage类的构造函数
1 public ImageMessage(String toUserName, 2 String fromUserName, 3 String createTime, 4 String msgType, 5 String msgId, 6 String picUrl) { 7 super(toUserName,fromUserName,createTime,msgType,msgId); 8 assert (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)); 9 this.PicUrl=picUrl; 10 } 11 12 public ImageMessage(Map<String, String> mapMessage){ 13 super(mapMessage); 14 assert (this.getMsgType().equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)); 15 this.PicUrl=mapMessage.get("PicUrl"); 16 }
2.3 TextMessage类的构造函数
1 public TextMessage(String toUserName, 2 String fromUserName, 3 String createTime, 4 String msgType, 5 String msgId, 6 String content) { 7 super(toUserName,fromUserName,createTime,msgType,msgId); 8 assert (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)); 9 this.Content=content; 10 11 } 12 13 public TextMessage(Map<String, String> mapMessage){ 14 super(mapMessage); 15 assert (this.getMsgType().equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)); 16 this.Content=mapMessage.get("Content"); 17 }
这里只列举了消息类的基类、图片消息、文本消息三个构造函数,其余的位置消息、声音消息、短视频、视频、链接消息的构造函数类似。
2.4 BaseResponse类的构造函数(这个有点意思)
1 public BaseResponse(BaseMessage baseMessage){ 2 this.ToUserName=baseMessage.getFromUserName(); //返回的目标是当初的发送源 3 this.FromUserName=baseMessage.getToUserName(); //发送的来源是当初的发送目标 4 this.MsgType= MessageUtil.RESP_MESSAGE_TYPE_TEXT; // 默认返回消息为文本 5 this.CreateTime=new Date().getTime(); //默认为当前时间 6 this.FuncFlag=0; //默认为非星标 7 } 8 9 public BaseResponse(String toUserName,String fromUserName, String msgType){ 10 this.ToUserName=toUserName; 11 this.FromUserName=fromUserName; 12 this.MsgType=msgType; 13 this.CreateTime=new Date().getTime(); //默认为当前时间 14 this.FuncFlag=0; //默认为非星标 15 }
响应类的基类的构造函数,我使用了消息类的基类作为参数,是因为响应消息的返回目标,就是当初的发送源(用户),响应消息的发送源,其实就是当初的发送目标(微信后台程序)。
2.5 TextResponse类的构造函数
1 public TextResponse(String toUserName,String fromUserName, String msgType,String content){ 2 super(toUserName,fromUserName,msgType); 3 this.Content=content; 4 } 5 public TextResponse(BaseMessage baseMessage, 6 String content){ 7 super(baseMessage); 8 this.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); 9 this.Content=content; 10 }
2.6 Article类的构造函数
1 public Article(String title, String description, String picUrl, String url) { 2 this.Title=title; 3 this.Description=description; 4 this.PicUrl=picUrl; 5 this.Url=url; 6 }
2.7 NewsResponse类的构造函数
1 public class NewsResponse extends BaseResponse { 2 // 图文消息个数,限制为10条以内 3 private int ArticleCount; 4 // 多条图文消息信息,默认第一个item为大图 5 private List<Article> Articles; 6 7 public NewsResponse(String toUserName,String fromUserName, String msgType){ 8 super(toUserName,fromUserName,msgType); 9 ArticleCount=0; 10 this.Articles = new ArrayList<Article>(); 11 } 12 public NewsResponse(BaseMessage baseMessage){ 13 super(baseMessage); 14 this.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_NEWS); 15 ArticleCount=0; 16 this.Articles = new ArrayList<Article>(); 17 } 18 19 //如果已经达到10篇文章,图文消息不可以再增加新的文章。返回FALSE; 20 public boolean addArticle(Article article) { 21 if (ArticleCount>=10){ 22 return false; 23 } 24 Articles.add(article); 25 ArticleCount++; 26 return true; 27 } 28 public Article getArticle(int articleIndex){ 29 return Articles.get(articleIndex); 30 } 31 32 public int getArticleCount() { 33 return ArticleCount; 34 } 35 36 // public void setArticleCount(int articleCount) { 37 // ArticleCount = articleCount; 38 // } 39 40 public List<Article> getArticles() { 41 return Articles; 42 } 43 44 // public void setArticles(List<Article> articles) { 45 // Articles = articles; 46 // } 47 }
这个NewsResponse类也要说一下,原来的设计也够奇葩的,居然直接把List结构暴露出来对其读写,List里的Article的数量,可以单独赋值,呵呵。于是果断注释掉计数变量的读写方法,新增了addArticle和getArticle这两个方法,用以从List中添加和获取Article,计算变量,应该自动累加,不应该对其手工赋值。
MusicResponse类的改造类似,不细说了。
三、归一化命名
原来的响应消息转成XML有三个函数,分别是textMessageToXml、musicMessageToXml、newsMessageToXml(详见http://blog.csdn.net/lyq8479/article/details/8949088),我把它们的命名归一为responseToXml,调用的时候,也不用区别传进去的是啥响应消息来选择不同名字的函数,省心。
1 /** 2 * 文本消息对象转换成xml 3 * 4 * @param textMessage 文本消息对象 5 * @return xml 6 */ 7 public static String responseToXml(TextResponse 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 responseToXml(MusicResponse 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 responseToXml(NewsResponse newsMessage) { 30 xstream.alias("xml", newsMessage.getClass()); 31 xstream.alias("item", newsMessage.getArticle(0).getClass()); 32 return xstream.toXML(newsMessage); 33 }
四、构造消息路由
原来的消息处理的主函数(详见http://blog.csdn.net/lyq8479/article/details/9393195),在一个超级巨大的processRequest函数中对消息进行处理,一堆if else if 下来,眼都花了,所以我简化了processRequest函数,只在这个函数里面构造消息路由,针对不同类型的消息,转去不同的处理逻辑中。
1 //定义一个logger,用于发送日志,可以在console或者服务器上看到。存储目录在log4j.properties中定义 2 private static final Logger logger = Logger.getLogger(CoreService.class); 3 4 //处理请求 5 public static String processRequest(HttpServletRequest request){ 6 String returnMsg=null; 7 try{ 8 Map<String, String> requestMap = MessageUtil.parseXml(request); //解析XML 9 String msgType = requestMap.get("MsgType"); //消息类型 10 logger.info("***step1 msgType=" + msgType); 11 switch (msgType){ 12 case MessageUtil.REQ_MESSAGE_TYPE_TEXT: 13 returnMsg=HandleTextMsg(new TextMessage(requestMap)); 14 break; 15 case MessageUtil.REQ_MESSAGE_TYPE_IMAGE: 16 returnMsg=HandleImageMsg(new ImageMessage(requestMap)); 17 break; 18 case MessageUtil.REQ_MESSAGE_TYPE_LINK: 19 returnMsg=HandleLinkMsg(new LinkMessage(requestMap)); 20 break; 21 case MessageUtil.REQ_MESSAGE_TYPE_LOCATION: 22 returnMsg=HandleLocationMsg(new LocationMessage(requestMap)); 23 break; 24 case MessageUtil.REQ_MESSAGE_TYPE_SHORT_VIDEO: 25 returnMsg=HandleShortVideoMsg(new ShortVideoMessage(requestMap)); 26 break; 27 case MessageUtil.REQ_MESSAGE_TYPE_VIDEO: 28 returnMsg=HandleVideoMsg(new VideoMessage(requestMap)); 29 break; 30 case MessageUtil.REQ_MESSAGE_TYPE_VOICE: 31 returnMsg=HandleVoiceMsg(new VoiceMessage(requestMap)); 32 break; 33 case MessageUtil.REQ_MESSAGE_TYPE_EVENT: 34 // 事件类型 35 String eventType = requestMap.get("Event"); 36 switch (eventType){ 37 case MessageUtil.EVENT_TYPE_SUBSCRIBE: 38 returnMsg=HandleSubscribeEvent(new BaseMessage(requestMap)); 39 break; 40 case MessageUtil.EVENT_TYPE_UNSUBSCRIBE: 41 returnMsg=HandleUnsubscribeEvent(new BaseMessage(requestMap)); 42 break; 43 case MessageUtil.EVENT_TYPE_CLICK: 44 returnMsg=HandleClickEvent(new BaseMessage(requestMap)); 45 break; 46 default: 47 returnMsg=HandleDefaultMsgOrEvent(new BaseMessage(requestMap)); 48 } 49 break; 50 default: 51 returnMsg=HandleDefaultMsgOrEvent(new BaseMessage(requestMap)); 52 } 53 }catch (Exception e) { 54 e.printStackTrace(); 55 } 56 return returnMsg; 57 }
下面是对各种消息的handle函数
1 public static String HandleTextMsg(TextMessage textMessage) 2 { 3 String respContent = "您发送的是文本消息!"; 4 if (textMessage.getContent().indexOf("/:") > -1) 5 respContent="你"+textMessage.getContent()+"什么?"; 6 TextResponse textResponse=new TextResponse(textMessage, respContent); 7 String respMsg=MessageUtil.responseToXml(textResponse); 8 logger.info("***step2 Response=" + respMsg); 9 return respMsg; 10 } 11 public static String HandleImageMsg(ImageMessage imageMessage) 12 { 13 String title="鉴定结果"; 14 String picUrl=imageMessage.getPicUrl(); 15 String forwardUrl= HttpTools.imagePlusPlusPath+picUrl; 16 logger.info("***step2 image++ Url=" + forwardUrl); 17 18 //从image++网站识别上传的图片的拍摄地点、拍摄物体。 19 String imagePlusPlusReturn=getHttpResponse(forwardUrl); 20 logger.info("***step3 image++ Response=" + imagePlusPlusReturn); 21 String description = ReadJSONString(imagePlusPlusReturn); 22 logger.info("***step4 image++ Response=" + description); 23 24 Article article=new Article(title,description,picUrl,forwardUrl); 25 NewsResponse newsResponse=new NewsResponse(imageMessage); 26 newsResponse.addArticle(article); 27 String respMsg=MessageUtil.responseToXml(newsResponse); 28 logger.info("***step5 Weixin Response=" + respMsg); 29 return respMsg; 30 } 31 public static String HandleLinkMsg(LinkMessage linkMessage) 32 { 33 String respContent = "您发送的是链接!"; 34 TextResponse textResponse=new TextResponse(linkMessage, respContent); 35 String respMsg=MessageUtil.responseToXml(textResponse); 36 logger.info("***step2 Response=" + respMsg); 37 return respMsg; 38 } 39 public static String HandleLocationMsg(LocationMessage locationMessage) 40 { 41 String respContent = "您发送的是地理位置!"; 42 TextResponse textResponse=new TextResponse(locationMessage, respContent); 43 String respMsg=MessageUtil.responseToXml(textResponse); 44 logger.info("***step2 Response=" + respMsg); 45 return respMsg; 46 } 47 public static String HandleShortVideoMsg(ShortVideoMessage shortVideoMessage) 48 { 49 String respContent = "您发送的是小视频!"; 50 TextResponse textResponse=new TextResponse(shortVideoMessage, respContent); 51 String respMsg=MessageUtil.responseToXml(textResponse); 52 logger.info("***step2 Response=" + respMsg); 53 return respMsg; 54 } 55 public static String HandleVideoMsg(VideoMessage videoMessage) 56 { 57 String respContent = "您发送的是视频!"; 58 TextResponse textResponse=new TextResponse(videoMessage, respContent); 59 String respMsg=MessageUtil.responseToXml(textResponse); 60 logger.info("***step2 Response=" + respMsg); 61 return respMsg; 62 } 63 public static String HandleVoiceMsg(VoiceMessage voiceMessage) 64 { 65 String respContent = "您发送的是语音!"; 66 TextResponse textResponse=new TextResponse(voiceMessage, respContent); 67 String respMsg=MessageUtil.responseToXml(textResponse); 68 logger.info("***step2 Response=" + respMsg); 69 return respMsg; 70 } 71 public static String HandleSubscribeEvent(BaseMessage baseMessage) 72 { 73 String respContent = "欢迎订阅!"; 74 TextResponse textResponse=new TextResponse(baseMessage, respContent); 75 String respMsg=MessageUtil.responseToXml(textResponse); 76 logger.info("***step2 Response=" + respMsg); 77 return respMsg; 78 } 79 public static String HandleUnsubscribeEvent(BaseMessage baseMessage) 80 { 81 String respContent = "再见,希望您再来!"; 82 TextResponse textResponse=new TextResponse(baseMessage, respContent); 83 String respMsg=MessageUtil.responseToXml(textResponse); 84 logger.info("***step2 Response=" + respMsg); 85 return respMsg; 86 } 87 public static String HandleClickEvent(BaseMessage baseMessage) 88 { 89 String respContent = "您点击了自定义菜单!"; 90 TextResponse textResponse=new TextResponse(baseMessage, respContent); 91 String respMsg=MessageUtil.responseToXml(textResponse); 92 logger.info("***step2 Response=" + respMsg); 93 return respMsg; 94 } 95 public static String HandleDefaultMsgOrEvent(BaseMessage baseMessage) 96 { 97 String respContent = "请求不可识别,请稍候尝试!"; 98 TextResponse textResponse=new TextResponse(baseMessage, respContent); 99 String respMsg=MessageUtil.responseToXml(textResponse); 100 logger.info("***step2 Response=" + respMsg); 101 return respMsg; 102 }
五、应用:试试图像识别
我的第一个微信公众号后台应用,是识别用户上传的图片,是什么东西,在哪里拍摄的。调用的是image++网站的api,核心代码如下:
1 /* 2 读取一个网址的所有返回内容 3 */ 4 public static String getHttpResponse(String connectUrl) { 5 BufferedReader in = null; 6 StringBuffer result = null; 7 try { 8 URI uri = new URI(connectUrl); 9 URL url = uri.toURL(); 10 URLConnection connection = url.openConnection(); 11 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 12 connection.setRequestProperty("Charset", "utf-8"); 13 connection.connect(); 14 15 result = new StringBuffer(); 16 //读取URL的响应 17 in = new BufferedReader(new InputStreamReader( 18 connection.getInputStream())); 19 String line; 20 while ((line = in.readLine()) != null) { 21 result.append(line); 22 } 23 24 return result.toString(); 25 26 } catch (Exception e) { 27 e.printStackTrace(); 28 }finally { 29 try { 30 if (in != null) { 31 in.close(); 32 } 33 } catch (Exception e2) { 34 e2.printStackTrace(); 35 } 36 } 37 return null; 38 } 39 40 /* 41 从JSON字符串中读取某些关键信息 42 JSON文件来自:http://www.imageplusplus.com/doc 43 */ 44 public static String ReadJSONString(String jSonString){ 45 try { 46 int imageConfidenceIndex=jSonString.indexOf("confidence",jSonString.indexOf("image")); 47 int imageConfidenceIndexEnd=jSonString.indexOf(",",imageConfidenceIndex); 48 String imageConfidence=jSonString.substring(imageConfidenceIndex+12,imageConfidenceIndexEnd); 49 int imageConfidencePercent=(int)(Float.parseFloat(imageConfidence)*100); 50 int imageValueIndex=jSonString.indexOf("value",jSonString.indexOf("image")); 51 int imageValueIndexEnd=jSonString.indexOf("}",imageValueIndex)-1; 52 String imageValue=jSonString.substring(imageValueIndex+8,imageValueIndexEnd).trim(); 53 54 int sceneConfidenceIndex=jSonString.indexOf("confidence",jSonString.indexOf("scene")); 55 int sceneConfidenceIndexEnd=jSonString.indexOf(",",sceneConfidenceIndex); 56 String sceneConfidence=jSonString.substring(sceneConfidenceIndex+12,sceneConfidenceIndexEnd); 57 int sceneConfidencePercent=(int)(Float.parseFloat(sceneConfidence)*100); 58 int sceneValueIndex=jSonString.indexOf("value",jSonString.indexOf("scene")); 59 int sceneValueIndexEnd=jSonString.indexOf("}",sceneValueIndex)-1; 60 String sceneValue=jSonString.substring(sceneValueIndex+8,sceneValueIndexEnd).trim(); 61 62 String result="这张照片有【"+imageConfidencePercent+"%】的可能是【"+imageValue+"】," + 63 "有【"+sceneConfidencePercent+"%】的可能是在【"+sceneValue+"】拍摄的。"; 64 return result; 65 } catch (Exception e) { 66 e.printStackTrace(); 67 } 68 return "服务器秀逗了,请重试!"; 69 } 70 }
全部的源代码,我放在了这里:20161013微信公众号后台应用示例src.zip
示例源码中用到几个外部jar lib,去官网下载,然后扔进项目里面就好。