本文出处:http://blog.csdn.net/chaijunkun/article/details/53396765,转载请注明。由于本人不定期会整理相关博文,会对相应内容作出完善。因此强烈建议在原始出处查看此文

在上一篇博文中,简单对微信接口分成了两大类:被动回调接口和主动调用接口。并阐述了两类接口的实现机理。在序列图中我们可以得知任何数据都是有具体格式要求的,那么如何管理这些结构化的数据呢?这里就跟大家分享一下我在管理实体对象时走过的一些弯路和技术探索。

本专栏代码可在本人的CSDN代码库中下载,项目地址为:https://github.com/chaijunkun/wechat-common

抽象数据层次关系

Java是一门面向对象的语言,要方便地使用结构化的数据,就必须对其进行对象化映射。抽象数据层次是一个很主观的事情,好在微信API已经规定好了格式,我们要做的就是对其进行更好地适配。一定要时刻谨记面向对象三要素:继承、封装和多态。

接下来就是发现规律的时刻。挑重点看了一下关于回调接口文档,下面是一些消息的格式说明:

普通消息

普通消息是用户显式发送给公众号的消息,现有的消息类型分别覆盖了在客户端能直接发送的所有消息种类。

文本消息

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[fromUser]]></FromUserName><CreateTime>1348831860</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[this is a test]]></Content><MsgId>1234567890123456</MsgId>
</xml>

图片消息

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[fromUser]]></FromUserName><CreateTime>1348831860</CreateTime><MsgType><![CDATA[image]]></MsgType><PicUrl><![CDATA[this is a url]]></PicUrl><MediaId><![CDATA[media_id]]></MediaId><MsgId>1234567890123456</MsgId>
</xml>

语音消息

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[fromUser]]></FromUserName><CreateTime>1357290913</CreateTime><MsgType><![CDATA[voice]]></MsgType><MediaId><![CDATA[media_id]]></MediaId><Format><![CDATA[Format]]></Format><Recognition><![CDATA[腾讯微信团队]]></Recognition><MsgId>1234567890123456</MsgId>
</xml>

视频消息

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[fromUser]]></FromUserName><CreateTime>1357290913</CreateTime><MsgType><![CDATA[video]]></MsgType><MediaId><![CDATA[media_id]]></MediaId><ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId><MsgId>1234567890123456</MsgId>
</xml>

小视频消息

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[fromUser]]></FromUserName><CreateTime>1357290913</CreateTime><MsgType><![CDATA[shortvideo]]></MsgType><MediaId><![CDATA[media_id]]></MediaId><ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId><MsgId>1234567890123456</MsgId>
</xml>

地理位置消息

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[fromUser]]></FromUserName><CreateTime>1351776360</CreateTime><MsgType><![CDATA[location]]></MsgType><Location_X>23.134521</Location_X><Location_Y>113.358803</Location_Y><Scale>20</Scale><Label><![CDATA[位置信息]]></Label><MsgId>1234567890123456</MsgId>
</xml>

链接消息

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[fromUser]]></FromUserName><CreateTime>1351776360</CreateTime><MsgType><![CDATA[link]]></MsgType><Title><![CDATA[公众平台官网链接]]></Title><Description><![CDATA[公众平台官网链接]]></Description><Url><![CDATA[url]]></Url><MsgId>1234567890123456</MsgId>
</xml>

普通消息格式的总结

从以上数据中我们可以发现如下规律:

  1. 数据均为XML格式,且根节点名称均为“xml”;
  2. 目前都是一层深度,没有标签的嵌套,标签内也没有属性,结构比较简单
  3. 有一些节点为了防止内容注入攻击,使用了CDATA
  4. 有共同的节点。这里我们可以抽出出一个公共的XML结构
<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[fromUser]]></FromUserName><CreateTime>1351776360</CreateTime><MsgType><![CDATA[link]]></MsgType><MsgId>1234567890123456</MsgId>
</xml>

有人一定觉得,这太容易了,就用这个接口映射的实体当父类,然后依具体消息类型格式对其进行继承。先别着急,当时作者也是基于先进行了映射,导致在实现后续的功能时发现了问题。

事件推送

在本节中,我们接触一种特殊的消息。该消息并不是由用户显式发送,而是由用户一系列操作引发微信通知公众号平台而收到的消息。

关注/取消关注事件

关注事件

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[FromUser]]></FromUserName><CreateTime>123456789</CreateTime><MsgType><![CDATA[event]]></MsgType><Event><![CDATA[subscribe]]></Event>
</xml>

用户未关注时,扫描二维码进入,进行关注后的事件推送

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[FromUser]]></FromUserName><CreateTime>123456789</CreateTime><MsgType><![CDATA[event]]></MsgType><Event><![CDATA[subscribe]]></Event><EventKey><![CDATA[qrscene_123123]]></EventKey><Ticket><![CDATA[TICKET]]></Ticket>
</xml>

值得一提的是,由于微信产品设计的原因,当用户扫描公众号生成的二维码后,若用户没有关注过该公众号,则用户在点击提示信息中的“确定”后,微信回调公众号后台的消息并非是扫描带参数二维码事件,而是“关注事件”。与普通的关注事件不同的是,由二维码扫描进入的回调事件消息会多出EventKey和Ticket两个节点

取消关注事件

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[FromUser]]></FromUserName><CreateTime>123456789</CreateTime><MsgType><![CDATA[event]]></MsgType><Event><![CDATA[unsubscribe]]></Event>
</xml>

扫描带参数二维码事件

用户已关注时的事件推送

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[FromUser]]></FromUserName><CreateTime>123456789</CreateTime><MsgType><![CDATA[event]]></MsgType><Event><![CDATA[SCAN]]></Event><EventKey><![CDATA[SCENE_VALUE]]></EventKey><Ticket><![CDATA[TICKET]]></Ticket>
</xml>

上报地理位置事件

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[fromUser]]></FromUserName><CreateTime>123456789</CreateTime><MsgType><![CDATA[event]]></MsgType><Event><![CDATA[LOCATION]]></Event><Latitude>23.137466</Latitude><Longitude>113.352425</Longitude><Precision>119.385040</Precision>
</xml>

自定义菜单事件

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[FromUser]]></FromUserName><CreateTime>123456789</CreateTime><MsgType><![CDATA[event]]></MsgType><Event><![CDATA[CLICK]]></Event><EventKey><![CDATA[EVENTKEY]]></EventKey>
</xml>

事件推送格式的总结

从以上数据中我们可以发现如下规律:

  1. 数据均为XML格式,且根节点名称均为“xml”;
  2. 目前都是一层深度,没有标签的嵌套,标签内也没有属性,结构比较简单
  3. 有一些节点为了防止内容注入攻击,使用了CDATA
  4. 有共同的节点。这里我们可以抽出出一个公共的XML结构
<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[FromUser]]></FromUserName><CreateTime>123456789</CreateTime><MsgType><![CDATA[event]]></MsgType><Event><![CDATA[subscribe]]></Event>
</xml>

搭建数据结构继承关系

通过上文我们可以总结出,无论是普通消息还是事件推送,都拥有很多类似的结构,这也就保证了对数据结构的封装构想是行得通的。首先我们来分析一下不同类型的消息是怎样区分开的,因为只有深入了解这一区别,才能让我们搭建的数据结构很好地服务于最终的程序。消息的具体类型是由MsgType和Event两个因子共同决定的

Created with Raphaël 2.2.0接收到消息获取MsgType字段获取Event字段是事件推送吗?分析事件类型转换为对应事件结束是普通消息吗?分析消息类型转换为对应消息忽略消息yesnoyesno

消息类型的判断

MsgType Event 消息分类 消息类型
event subscribe 事件推送 关注事件
event unsubscribe 事件推送 取消关注事件
event SCAN 事件推送 扫描带参数二维码事件
event LOCATION 事件推送 上报地理位置事件
event CLICK 事件推送 自定义菜单事件
text N/A 普通消息 文本消息
image N/A 普通消息 图片消息
voice N/A 普通消息 语音消息
video N/A 普通消息 视频消息
shortvideo N/A 普通消息 小视频消息
location N/A 普通消息 地理位置消息
link N/A 普通消息 链接消息

按照上述流程和对应关系进行匹配后,即可得到具体消息类型。于是我们有了充分的理由,先构建一个专门用于判断消息类型的实体:TypeAnalyzingBean

public class TypeAnalyzingBean {/** 消息类型 */private String msgType;/** 事件类型 */private String event;//省略getters 和 setters...
}

得到了具体类型后就可以拆分为普通消息事件推送的父类定义,但是过程中我们发现两者还有很多共同之处,所以新建了一个公共的字段的实体定义:CommonXML

public class CommonXML {/** 接收方微信号 */private String toUserName;/** 发送方微信号,若为普通用户,则是一个OpenID */private String fromUserName;/** 消息创建时间 */private Long createTime;/** 消息类型 */private String msgType;//省略getters 和 setters...
}

普通消息的基础实体定义:BaseMsg,实际上只是在公共字段的基础上增加了msgId属性。

public class BaseMsg extends CommonXML {/** 消息id */private Long msgId;//省略getters 和 setters...
}

事件推送的基础实体定义:BaseEvent,实际上只是在公共字段的基础上增加了event属性。

public class BaseEvent extends CommonXML {/** 事件类型 */private String event;//省略getters 和 setters...
}

有了这两个基础实体,其他的消息和事件只需要分别继承两个基类,进行相应的属性拓展即可,这里就不赘述了。

选一个好的OXM框架很重要

微信回调给我们的数据是XML格式的,而我们要面向对象开发,于是必须将这两者映射起来,这就是OXM(Object/XML Mapping)。细心的你也许已经发现,回调给我们的代码风格和Java属性命名风格是不一致的,因此我们要把原有的节点名转义后映射到对应的实体属性上。虽然Java包中提供的w3c API可以实现XML的DOM解析,但是需要一个节点一个节点地爬,整个过程将异常繁琐,后期极难维护,这里就不再展示了。接下来我将介绍第一版使用的JAXB和最终采用的基于Jackson StAX的方案,并进行对比。

在这里我们以最常用的文本消息作为示例,它的继承关系为:

Created with Raphaël 2.2.0TextMsgBaseMsgCommonXML
public class TextMsg extends BaseMsg {/** 文本消息内容 */private String content;//省略getters 和 setters...
}

JAXB方式的OXM

JAXB支持annotation方式的命名转义,这也正是我在设计之初选择其作为基础OXM框架的原因。由于所有的XML根节名称都是“xml”,因此想将根节点重命名annotation放到CommonXML上,这样,其他类直接或间接继承自他,注解也能够通过继承链来爬取到相应的配置。

这里可以很负责地告诉你,我的想法还是Too young too simple了,最早尝试的是JDK1.6+自带的JAXB实现,这个实现有个恶心的问题:父类注解无法继承到子类。只有在每一个相关类上设置@XmlRootElement(name=“xml”)才可以,否则将报这样的错误:com.sun.istack.internal.SAXException2: unable to marshal type “com.github.chaijunkun.wechat.common.callback.xml.msg.TextMsg” as an element because it is missing an @XmlRootElement annotation。

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;import org.eclipse.persistence.oxm.annotations.XmlCDATA;@XmlRootElement(name="xml")
@XmlType(name="", namespace="")
@XmlAccessorType(XmlAccessType.FIELD)
public class CommonXML {@XmlElement(name="ToUserName")@XmlCDATAprivate String toUserName;//后面的代码就不贴了,只是一些重命名操作和一些getters/setters
}

由于JAXB对CDATA支持得不好,需要自己写**@XmlJavaTypeAdapter(CDataAdapter.class)**中的CDataAdapter,而且网上找到的例子经过评估,有注入漏洞,于是又额外引入了EclipseLink的moxy(一套JAXB的实现):

<dependency><groupId>org.eclipse.persistence</groupId><artifactId>org.eclipse.persistence.moxy</artifactId><version>2.6.3</version>
</dependency>

为此我写了一个单元测试看下序列化与反序列化的测试:

public class XMLTest extends BaseTest {@Testpublic void doTest() throws JAXBException{TextMsg msg = new TextMsg();msg.setToUserName("jackson");msg.setFromUserName("hawaii");msg.setContent("jack<xml val='Json'>]]>");ByteArrayOutputStream xmlOut = null;ByteArrayInputStream xmlIn = null;try{xmlOut = new ByteArrayOutputStream();XMLFactory.toXML(msg, xmlOut);String xml = new String(xmlOut.toByteArray());logger.info("生成xml:{}", xml);xmlIn = new ByteArrayInputStream(xml.getBytes());TextMsg msgFromXml = XMLFactory.fromXML(xmlIn, TextMsg.class);logger.info("反序列化结果:发送方:{}, 接收方:{}, 内容:{}", msgFromXml.getFromUserName(), msgFromXml.getToUserName(), msgFromXml.getContent());}finally{IOUtils.closeQuietly(xmlIn);IOUtils.closeQuietly(xmlOut);}}
}

由于CDATA的结束符是“]]>”,在设置消息内容时特别使用了带有歧义性的文字来测试注入危险。先来看下序列化的结果:

<?xml version="1.0" encoding="utf-8"?>
<xml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="textMsg"><ToUserName><![CDATA[jackson]]></ToUserName><FromUserName><![CDATA[hawaii]]></FromUserName><Content><![CDATA[jack<xml val='Json'>]]]]><![CDATA[>]]></Content>
</xml>

moxy实现的JAXB有效地解决了父级注解无法继承到子类上的问题,CDATA内容也顺利通过了我们的测试(它的解决方案是将能够引起歧义的字符组合"]]>“拆分成了”]]"和“>”,然后再分别使用CDATA标签进行包装),但是也带来了新的问题。我们再来回顾一下官方文档提供给我们的回调xml数据格式:

<xml><ToUserName><![CDATA[toUser]]></ToUserName><FromUserName><![CDATA[fromUser]]></FromUserName><CreateTime>1348831860</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[this is a test]]></Content><MsgId>1234567890123456</MsgId>
</xml>

通过对比我们发现使用moxy生成的数据首行多出了xml指令。它标记了文档版本和编码格式,经过相关验证,这个变动和微信对接没有影响,可以正常运行。而根节点“xml”中增加了**“xmlns:xsi”“xsi:type”属性,在将这样的数据返回给微信回调时报错了,这两个多余的属性必须去掉**才能正常工作。

尽管实体->XML没问题了,但接入微信联调时发现XML->实体又出现了状况:在确定了消息类型后,我们需要把XML字符串立即转换成明确的具体消息类型,例如TextMsg.class。然而由于TextMsg实体上没有**@XmlRootElement(name=“xml”)**根节点注解,转换失败,类似于使用原生JAXB的状况。设想一下,本来希望“根节点命名”这件事放在一处(CommonXML)来做,现在不得不每个子类上都标注上这头疼的家伙,将来维护起来是多么困难。

经过很多尝试都无果的情况下,JAXB方案我准备放弃了。为了方便大家研究,这部分代码我也上传到了我的代码仓库中,有兴趣的读者可以运行单元测试一试下,仓库地址:https://github.com/chaijunkun/wechat-common-draft

另辟蹊径的Jackson

说到Jackson很多人都体验过它的JSON处理能力,无论是从灵活性、性能还是文档健全度上都是值得在生产环境中使用的,本人也是其忠实拥趸。就在和JAXB纠结寻找其它途径时,得知Jackson的XML处理能力也很了得,于是立刻将代码进行改造,适配Jackson类似功能的注解:

import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlCData;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;@JacksonXmlRootElement(localName="xml")
public class CommonXML {/** 接收方微信号 */@JacksonXmlProperty(localName = "ToUserName")@JacksonXmlCDataprivate String toUserName;//后面的代码就不贴了,只是一些重命名操作和一些getters/setters
}

根据一些StackOverflow上的资料,整理出了一些必要的依赖:

<!-- json和xml的序列化与反序列化工具 -->
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.5.2</version>
</dependency>
<dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId><version>2.5.2</version>
</dependency>
<!-- 使用woodstox不仅比jdk提供的stax实现更快,且可以避免一些已知问题例如在根节点自动添加namespace -->
<dependency><groupId>org.codehaus.woodstox</groupId><artifactId>woodstox-core-asl</artifactId><version>4.4.1</version>
</dependency>

使用Jackson框架处理实体与XML相互转换所担心的注入问题还可以通过转义单个特殊字符的形式来处理,特殊字符及其可用代替字符的对照表:

特殊字符 转义字符
& &amp;
< &lt;
> &gt;
" &quot;
&apos;

因此只需要写一个特殊字符互转功能即可。代码逻辑很简单,感兴趣的读者可参阅项目中的XMLStringSerializerXMLStringDeserializer,之后在XMLUtil初始化时将它们注册到JacksonXmlModule实例中即可:

JacksonXmlModule module = new JacksonXmlModule();
module.setDefaultUseWrapper(false);
//序列化与反序列化时针对特殊字符自动转义的工具
module.addSerializer(String.class, new XMLStringSerializer());
module.addDeserializer(String.class, new XMLStringDeserializer());

同样的测试数据,我们看下使用Jackson后会生成什么样的结果:

<?xml version='1.0' encoding='UTF-8'?>
<xml><ToUserName><![CDATA[jackson]]></ToUserName><FromUserName><![CDATA[hawaii]]></FromUserName><Content><![CDATA[jack&lt;xml val=&apos;Json&apos;&gt;]]&gt;]]></Content>
</xml>

可以很明显地看到,根节点不再有讨厌的多余属性了,CDATA注入测试也通过了,没有产生歧义。后续和微信联调时再也没遇到格式不正确的问题了。

对比性能

我们已经通过Jackson完成了实体与XML的相互转换功能,然而性能怎么样呢?不妨做个测试。分别做10,000次转换。
实体->XML转换的测试代码:

/** 计数器 */
private int counter = 10000;@Test
public void doTest() throws JAXBException{TextMsg msg = new TextMsg();msg.setToUserName("jackson");msg.setFromUserName("hawaii");msg.setContent("jack<xml val='Json'>]]>");long start = System.currentTimeMillis();for(int i=0; i< counter; i++){ByteArrayOutputStream xmlOut = null;ByteArrayInputStream xmlIn = null;try{xmlOut = new ByteArrayOutputStream();//使用Jackson时,替换成XMLUtil及其配套注解的TextMsg对象XMLFactory.toXML(msg, xmlOut);String xml = new String(xmlOut.toByteArray());}finally{IOUtils.closeQuietly(xmlIn);IOUtils.closeQuietly(xmlOut);}}long end = System.currentTimeMillis();logger.info("耗时:{}", end - start);
}

XML->实体转换的测试代码:

/** 计数器 */
private int counter = 10000;@Test
public void doTest() throws IOException, JAXBException{String xml = "<?xml version='1.0' encoding='UTF-8'?><xml><ToUserName><![CDATA[jackson]]></ToUserName><FromUserName><![CDATA[hawaii]]></FromUserName><Content><![CDATA[jack&lt;xml val=&apos;Json&apos;&gt;]]&gt;]]></Content></xml>";long start = System.currentTimeMillis();for(int i=0; i< counter; i++){ByteArrayOutputStream xmlOut = null;ByteArrayInputStream xmlIn = null;try{xmlOut = new ByteArrayOutputStream();//使用Jackson时,替换成XMLUtil及其配套注解的TextMsg对象TextMsg textMsg = XMLFactory.fromXML(xml, TextMsg.class);}finally{IOUtils.closeQuietly(xmlIn);IOUtils.closeQuietly(xmlOut);}}long end = System.currentTimeMillis();logger.info("耗时:{}", end - start);
}

截取的实验结果(单位:毫秒,3次实验取平均值)如下表所示:

转换方式 \ 转换方案 JAXB Jackson 时间占比
实体->XML 24716 1123 22:1
XML->实体 31622 1049 30:1

结果很明显:执行相同的任务,序列化方向Jackson用时只需JAXB的1/22,而反序列化方向优势更明显,只是JAXB方案的1/30的时间。

微信接入探秘(二)——懒人的OXM之路相关推荐

  1. 微信接入探秘(五)——万事俱备,只欠架构(API篇)

    本文出处:http://blog.csdn.net/chaijunkun/article/details/53504856,转载请注明.由于本人不定期会整理相关博文,会对相应内容作出完善.因此强烈建议 ...

  2. 基于微信小程序的懒人美食帮小程序

    文末联系获取源码 开发语言:Java 框架:springboot JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7/8.0 数据库工具:Navicat11 开发软件:ecl ...

  3. 微信接入探秘(三)——加密消息的处理

    本文出处:http://blog.csdn.net/chaijunkun/article/details/53435972,转载请注明.由于本人不定期会整理相关博文,会对相应内容作出完善.因此强烈建议 ...

  4. 微信接入探秘(一)——从零认识微信接口

    本文出处:http://blog.csdn.net/chaijunkun/article/details/53385088,转载请注明.由于本人不定期会整理相关博文,会对相应内容作出完善.因此强烈建议 ...

  5. 基于微信小程序的懒人美食帮设计与实现

    人民生活水平的提高就会造成生活节奏越来越快,很多人吃饭都采用点外卖的方式.现在点外卖的平台已有很多,大多都需要安装它们的APP才可以使用.如果一味的使用外卖平台不仅会造成商家成本的增加,还不利于商家订 ...

  6. 用python画微信捂脸_懒人小技巧 (1):python 实现 IPA 上传到蒲公英

    一.为什么要做这件事 在测试过程中,ios app的安装包(.IPA文件)不像安卓的apk文件那样直接放到手机里点击安装即可,一般来说可以通过一下几种来方式来安装: xcode安装到收集中(点击win ...

  7. 懒人开关 ESP32控制舵机旋转(基于ESP32+SG90舵机+微信小程序)

    提示:站在巨人肩膀上的小白,大家可以提出自己的看法.如有侵删: 原文参考链接:esp32单片机控制舵机 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/566 ...

  8. 网络编程懒人入门(二):快速理解网络通信协议(下篇)

    1.前言 本文上篇<网络编程懒人入门(一):快速理解网络通信协议(上篇)>分析了互联网的总体构思,从下至上,每一层协议的设计思想.基于知识连贯性的考虑,建议您先看完上篇后再来阅读本文. 本 ...

  9. 适合新手学习的laravel接入微信接口,实现微信公众号二次开发

    2019独角兽企业重金招聘Python工程师标准>>> 最近使用laravel做微信公众号二次开发,发现网上能够参考的资料基本上很少,很多地方都讲的不够详细,致使许多新手采坑无数,所 ...

最新文章

  1. 【Linux】linux使用mplayer播放摄像头
  2. “编程不规范,同事两行泪!”
  3. java opengl_java基于OpenGL ES实现渲染实例
  4. react学习(3)----不能在该位置用setstate
  5. Part5 数据的共享与保护 5.4类的友元5.5共享数据的保护
  6. 大幅广告显示隐藏效果
  7. C++ + Irrlicht整一个东东?
  8. 个人宏工作簿PERSONAL.XLSB 保存位置 启动加载项
  9. linux之解决lib***.so.*: cannot open shared object file
  10. Android:是时候掌握WebView与Js的交互技术了
  11. Xmind 8 下载以及破解
  12. 习惯三:要事第一--自我管理的原则
  13. c语言课程设计(图书馆管理系统)
  14. 一门课程学习转录组调控分析和R可视化第十四期 (线上线下开课)
  15. 侵权和违约的区别是哪些
  16. 用前端代码编写一个动态的罗盘时钟
  17. 数据库学习记录806
  18. 微信小程序一定高度文字的展开与收起
  19. 数据分析师该这样霸气回应“0.00008的转化也很好”的谬论
  20. 干货|红外热成像摄像头拆解分析

热门文章

  1. 备份曾经开放数据源码
  2. 考研高数之无穷级数题型三:将函数展开成幂级数和傅里叶级数(题目讲解)
  3. PDF 压缩常用方法比较
  4. libaio.so.1 is needed by mysql_libaio.so.1()(64bit) is needed by MySQL-server
  5. 边做边学单片机c语言课后答案,HIFIDIY论坛-单片机边学边玩之-现学现做+小儿科+其它。仅供新手参考,不喜勿入! - Powered by Discuz!...
  6. 云服务器怎样帮助简化游戏开发
  7. photoshop 魔术棒以及反选功能
  8. Python自动化 —— 大麦网自动抢购原价演唱会门票
  9. 《炬丰科技-半导体工艺》 超临界二氧化碳清洗晶圆工艺
  10. POI导入导出工具类