diff --git a/.gitignore b/.gitignore index a2b51e4a..fbf348ae 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ test-config.xml /gradle/ *.bat /gradlew +**/build/ # OSX # Icon must end with two \r diff --git a/README.md b/README.md index 96b2bcd8..7774fa6a 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,33 @@ -# Weixin Java Tools 微信公众号/企业号开发Java SDK -## ![Maven Central](https://img.shields.io/maven-central/v/com.github.binarywang/weixin-java-parent.svg) [![Build Status](https://travis-ci.org/wechat-group/weixin-java-tools.svg?branch=develop)](https://travis-ci.org/wechat-group/weixin-java-tools) [![Join QQ Group](http://pub.idqqimg.com/wpa/images/group.png)](http://jq.qq.com/?_wv=1027&k=40lRskK) - +Weixin Java Tools 微信公众号/企业号开发Java SDK +===================================== +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.binarywang/weixin-java-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.binarywang/weixin-java-parent) +[![Build Status](https://travis-ci.org/wechat-group/weixin-java-tools.svg?branch=develop)](https://travis-ci.org/wechat-group/weixin-java-tools) ### 注意: 1. ***本项目Fork自chanjarster/weixin-java-tools,但由于原项目已停止维护,故单独维护和发布,且发布到maven上的groupId也会不同,详细信息见下文。*** 1. ***自2.0.0版本以来,公众号的接口调整比较大,主要是为了解决主接口类过于庞大不方便管理的问题,将接口实现代码按模块进行拆分。*** 1. 本SDK要求的最低JDK版本是7,为满足少量还在使用JDK6的用户的需求,特意抽出独立的代码分支项目,请参考 https://github.com/binarywang/weixin-java-tools-for-jdk6 ,其他更早的JDK版本则需要自己改造实现; -1. 最新更新:2016-09-30 发布2.2.0正式版! +1. 最新更新:2016-10-31 发布2.3.0正式版! =========== ## 开发交流方式及注意事项: -1. QQ群:343954419(因群人数限制即将爆满,故开启付费加群方式并不定期清理不活跃人群,感谢理解) [![Join QQ Group](http://pub.idqqimg.com/wpa/images/group.png)](http://jq.qq.com/?_wv=1027&k=40lRskK) +1. QQ群:343954419(推荐点击按钮入群: [![Join QQ Group](http://pub.idqqimg.com/wpa/images/group.png)](http://shang.qq.com/wpa/qunwpa?idkey=731dc3e7ea31ebe25376cc1a791445468612c63fd0e9e05399b088ec81fd9e15) 或 [![Join QQ Group](http://pub.idqqimg.com/wpa/images/group.png)](http://jq.qq.com/?_wv=1027&k=40lRskK),如果无反应,可以自行搜索群号进行添加 ) +1. 由于群容量有限即将爆满,现开启付费入群模式,并不定期清理长时间不活跃人士; 1. 微信群: 因微信群已达到100人限制,故如有想加入微信群的,请入QQ群后联系管理员,提供微信号以便邀请加入; 1. 新手提问前,请先阅读此文章:http://t.cn/RV93MRB 1. 寻求帮助时需贴代码或大长串异常信息的,请利用http://paste.ubuntu.com 1. 有功能需求或由于微信官方接口调整导致的代码问题,可以直接提出issue,便于讨论追踪问题; 1. 详细开发文档请看 [Wiki](https://github.com/wechat-group/weixin-java-tools/wiki),部分文档可能未能及时更新,如有发现,可以及时上报或者自行修改。 +1. 微信公众号官方文档入口地址:http://mp.weixin.qq.com/wiki (注意,从网上搜到的文档有的虽然地址前面跟这个一样,但明显左侧菜单不一致,是旧的文档,注意不要看错文档)。 +1. 各个模块的Javadoc可以在线查看:[weixin-java-mp](https://binarywang.github.io/weixin-java-mp-javadoc/)、[weixin-java-common](https://binarywang.github.io/weixin-java-common-javadoc/)、[weixin-java-cp](https://binarywang.github.io/weixin-java-cp-javadoc/) =========== ## 版本说明 1. 本项目定为每月发布一次正式版,版本号格式为X.X.0(如2.1.0,2.2.0等),月初或月底发布新版本,遇到重大问题需修复会及时提交新版本,欢迎大家随时提交Pull Request; 1. BUG修复和新特性一般会先发布成小版本作为临时版本(如2.0.1,2.0.2等,即尾号不为0,以区别于正式版); -1. 目前最新版本号为 ![Maven Central](https://img.shields.io/maven-central/v/com.github.binarywang/weixin-java-parent.svg) ,也可以通过访问链接 [【公众号】](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.github.binarywang%22%20AND%20a%3A%22weixin-java-mp%22) 、[【企业号】](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.github.binarywang%22%20AND%20a%3A%22weixin-java-cp%22) +1. 目前最新版本号为 [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.binarywang/weixin-java-parent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.binarywang/weixin-java-parent) ,也可以通过访问链接 [【公众号】](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.github.binarywang%22%20AND%20a%3A%22weixin-java-mp%22) 、[【企业号】](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.github.binarywang%22%20AND%20a%3A%22weixin-java-cp%22) 分别查看所有最新的版本。 ## Maven & Gradle @@ -35,12 +39,12 @@ maven: com.github.binarywang weixin-java-mp - 2.2.0 + 2.3.0 ``` gradle: ```groovy -compile 'com.github.binarywang:weixin-java-mp:2.2.0' +compile 'com.github.binarywang:weixin-java-mp:2.3.0' ``` * 企业号: @@ -50,12 +54,12 @@ maven: com.github.binarywang weixin-java-cp - 2.2.0 + 2.3.0 ``` gradle: ```groovy -compile 'com.github.binarywang:weixin-java-cp:2.2.0' +compile 'com.github.binarywang:weixin-java-cp:2.3.0' ``` =========== @@ -68,15 +72,42 @@ compile 'com.github.binarywang:weixin-java-cp:2.2.0' * https://git.coding.net/binarywang/weixin-java-tools.git =========== -## 目前可参考的Demo项目有两个(目前都是公众号的,风格不同,欢迎提供更多的demo供新手参考): -1. https://github.com/wechat-group/weixin-mp-demo +## 可参考的Demo项目 +#### 目前都是公众号的,风格不同,欢迎提供更多的demo供新手参考: +1. https://github.com/wechat-group/weixin-mp-demo +1. https://github.com/wechat-group/weixin-mp-multi-demo (支持多公众号) 1. https://github.com/wechat-group/weixin-java-tools-springmvc +1. https://github.com/wechat-group/weixin-mp-demo-springboot + =========== ## 关于代码贡献 1. 非常欢迎和感谢对本项目发起Pull Request的同学,本项目代码风格为使用2个空格代表一个Tab,因此在提交代码时请注意一下,否则很容易在IDE格式化代码后与原代码产生大量diff,这样会给其他人阅读代码带来极大的困扰。 1. 为了便于设置,本项目引入editorconfig插件,请使用eclipse的同学在贡献代码前安装相关插件,IntelliJ IDEA则自带支持,无需额外安装插件。 1. 本项目可以采用两种方式接受代码贡献: + * 第一种就是基于[Git Flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow)开发流程,因此在发起Pull Request的时候请选择develop分支,详细步骤参考后文。 + * 另外一种贡献代码的方式就是加入SDK Developers开发组,前提是对自己的代码足够自信就可以申请加入,加入之后可以随时直接提交代码,但要注意对所做的修改或新增的代码进行单元测试,保证提交代码没有明显问题,具体加入方式,请咨询QQ群管理员[![点击这里给我发消息](http://wpa.qq.com/pa?p=2:1211415707:51)](http://wpa.qq.com/msgrd?v=3&uin=1211415707&site=qq&menu=yes)。 - 1. 第一种就是基于[Git Flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow)开发流程,因此在发起Pull Request的时候请选择develop分支。 - 1. 另外一种贡献代码的方式就是加入SDK Developers开发组,前提是对自己的代码足够自信就可以申请加入,加入之后可以随时直接提交代码,但要注意对所做的修改或新增的代码进行单元测试,保证提交代码没有明显问题,具体加入方式,请咨询管理员。 +## PR方式贡献代码步骤 +* 在 GitHub 上 `fork` 到自己的仓库,如 `my_user/weixin-java-tools`,然后 `clone` 到本地,并设置用户信息。 +``` +$ git clone git@github.com:my_user/weixin-java-tools.git +$ cd weixin-java-tools +$ git config user.name "yourname" +$ git config user.email "your email" +``` +* 修改代码后提交,并推送到自己的仓库。 +``` +$ #do some change on the content +$ git commit -am "Fix issue #1: change something" +$ git push +``` +* 在 GitHub 网站上提交 Pull Request。 +* 定期使用项目仓库内容更新自己仓库内容。 +``` +$ git remote add upstream https://github.com/wechat-group/weixin-java-tools +$ git fetch upstream +$ git checkout develop +$ git rebase upstream/develop +$ git push -f origin develop +``` diff --git a/build.gradle b/build.gradle index defc5a25..a2858210 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ allprojects { apply plugin: 'maven' group = 'com.github.binarywang' - version = '2.2.0' + version = '2.4.0-SNAPSHOT' } subprojects { @@ -21,7 +21,7 @@ subprojects { dependencies { compile group: 'org.slf4j', name: 'slf4j-api', version:'1.7.10' compile group: 'org.apache.httpcomponents', name: 'httpmime', version:'4.5' - compile group: 'org.jodd', name: 'jodd-http', version:'3.6.7' + compile group: 'org.apache.httpcomponents', name: 'httpclient', version:'4.5' compile group: 'com.google.code.gson', name: 'gson', version:'2.7' compile group: 'com.google.guava', name: 'guava', version:'19.0' compile group: 'commons-codec', name: 'commons-codec', version:'1.10' diff --git a/pom.xml b/pom.xml index 7418974e..221d7756 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.github.binarywang weixin-java-parent - 2.3.0-SNAPSHOT + 2.4.0-SNAPSHOT pom WeiXin Java Tools - Parent 微信公众号、企业号上级POM @@ -34,6 +34,36 @@ gaigeshen@qq.com https://github.com/gaigeshen + + Liu Mingbo + liumingbo2008@gmail.com + https://github.com/FirenzesEagle + + + kakotor + kakotor@gmail.com + https://github.com/kakotor + + + xiong + zhaoxiong.tan@gmail.com + https://github.com/ZhaoxiongTan + + + LiuJunGuang + aimilin@yeah.net + https://github.com/aimilin6688 + + + Eric.Tsai + xiaodong.cai.ks@gmail.com + https://github.com/iwareserictsai + + + withinthefog + withinthefog@gmail.com + https://github.com/withinthefog + @@ -50,21 +80,22 @@ + 1.7 + 1.7 + UTF-8 true true 4.5 1.7.10 1.1.2 - 3.6.7 2.9.0 2.7 19.0 3.5 2.5 1.10 - 9.3.0.M0 - 9.3.10.v20160621 + 9.3.0.RC0 @@ -81,13 +112,13 @@ org.apache.httpcomponents - httpmime + httpclient ${httpclient.version} - org.jodd - jodd-http - ${jodd-http.version} + org.apache.httpcomponents + httpmime + ${httpclient.version} com.google.code.gson @@ -283,10 +314,8 @@ org.apache.maven.plugins maven-compiler-plugin - 2.3.2 + 3.6.0 - 1.7 - 1.7 UTF-8 diff --git a/weixin-java-common/build.gradle b/weixin-java-common/build.gradle index 07b5036a..71800493 100644 --- a/weixin-java-common/build.gradle +++ b/weixin-java-common/build.gradle @@ -6,7 +6,7 @@ dependencies { testCompile group: 'org.testng', name: 'testng', version:'6.8.7' testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5' testCompile group: 'com.google.inject', name: 'guice', version:'3.0' - testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version:'9.3.0.M0' - testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version:'9.3.0.M0' + testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version:'9.3.0.RC0' + testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version:'9.3.0.RC0' } test.useTestNG() diff --git a/weixin-java-common/pom.xml b/weixin-java-common/pom.xml index fec020e7..1f67e0da 100644 --- a/weixin-java-common/pom.xml +++ b/weixin-java-common/pom.xml @@ -6,7 +6,7 @@ com.github.binarywang weixin-java-parent - 2.3.0-SNAPSHOT + 2.4.0-SNAPSHOT weixin-java-common diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxCardApiSignature.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxCardApiSignature.java index f50b72f2..df272318 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxCardApiSignature.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxCardApiSignature.java @@ -1,9 +1,8 @@ package me.chanjar.weixin.common.bean; -import java.io.Serializable; +import me.chanjar.weixin.common.util.ToStringUtils; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; +import java.io.Serializable; /** * 卡券Api签名 @@ -35,7 +34,7 @@ public class WxCardApiSignature implements Serializable { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } public String getAppId() { diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuButton.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuButton.java index 1fe8bb90..02efac31 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuButton.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuButton.java @@ -1,11 +1,10 @@ package me.chanjar.weixin.common.bean.menu; +import me.chanjar.weixin.common.util.ToStringUtils; + import java.util.ArrayList; import java.util.List; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - public class WxMenuButton { private String type; @@ -18,10 +17,9 @@ public class WxMenuButton { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, - ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } - + public String getType() { return this.type; } @@ -69,4 +67,4 @@ public class WxMenuButton { public void setMediaId(String mediaId) { this.mediaId = mediaId; } -} \ No newline at end of file +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuRule.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuRule.java index ca6fdd24..714466f2 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuRule.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuRule.java @@ -1,7 +1,6 @@ package me.chanjar.weixin.common.bean.menu; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; +import me.chanjar.weixin.common.util.ToStringUtils; public class WxMenuRule { private String tagId; @@ -70,6 +69,6 @@ public class WxMenuRule { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } -} \ No newline at end of file +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java index c73f1600..8935bc32 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java @@ -8,6 +8,8 @@ import me.chanjar.weixin.common.bean.result.WxError; import me.chanjar.weixin.common.exception.WxErrorException; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -28,7 +30,9 @@ public class BeanUtils { public static void checkRequiredFields(Object bean) throws WxErrorException { List nullFields = Lists.newArrayList(); - for (Field field : bean.getClass().getDeclaredFields()) { + List fields = new ArrayList<>( Arrays.asList(bean.getClass().getDeclaredFields())); + fields.addAll(Arrays.asList(bean.getClass().getSuperclass().getDeclaredFields())); + for (Field field : fields) { try { boolean isAccessible = field.isAccessible(); field.setAccessible(true); @@ -55,7 +59,9 @@ public class BeanUtils { */ public static Map xmlBean2Map(Object bean) { Map result = Maps.newHashMap(); - for (Field field : bean.getClass().getDeclaredFields()) { + List fields = new ArrayList<>( Arrays.asList(bean.getClass().getDeclaredFields())); + fields.addAll(Arrays.asList(bean.getClass().getSuperclass().getDeclaredFields())); + for (Field field : fields) { try { boolean isAccessible = field.isAccessible(); field.setAccessible(true); diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/StringUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/StringUtils.java deleted file mode 100644 index 49acaeea..00000000 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/StringUtils.java +++ /dev/null @@ -1,98 +0,0 @@ -package me.chanjar.weixin.common.util; - -/** - * copy from apache-commons-lang3 - */ -public class StringUtils { - - /** - *

Checks if a CharSequence is whitespace, empty ("") or null.

- *

- *

-   * StringUtils.isBlank(null)      = true
-   * StringUtils.isBlank("")        = true
-   * StringUtils.isBlank(" ")       = true
-   * StringUtils.isBlank("bob")     = false
-   * StringUtils.isBlank("  bob  ") = false
-   * 
- * - * @param cs the CharSequence to check, may be null - * @return {@code true} if the CharSequence is null, empty or whitespace - * @since 3.0 Changed signature from isBlank(String) to isBlank(CharSequence) - */ - public static boolean isBlank(CharSequence cs) { - int strLen; - if (cs == null || (strLen = cs.length()) == 0) { - return true; - } - for (int i = 0; i < strLen; i++) { - if (Character.isWhitespace(cs.charAt(i)) == false) { - return false; - } - } - return true; - } - - /** - *

Checks if a CharSequence is not empty (""), not null and not whitespace only.

- *

- *

-   * StringUtils.isNotBlank(null)      = false
-   * StringUtils.isNotBlank("")        = false
-   * StringUtils.isNotBlank(" ")       = false
-   * StringUtils.isNotBlank("bob")     = true
-   * StringUtils.isNotBlank("  bob  ") = true
-   * 
- * - * @param cs the CharSequence to check, may be null - * @return {@code true} if the CharSequence is - * not empty and not null and not whitespace - * @since 3.0 Changed signature from isNotBlank(String) to isNotBlank(CharSequence) - */ - public static boolean isNotBlank(CharSequence cs) { - return !StringUtils.isBlank(cs); - } - - /** - *

Checks if a CharSequence is empty ("") or null.

- *

- *

-   * StringUtils.isEmpty(null)      = true
-   * StringUtils.isEmpty("")        = true
-   * StringUtils.isEmpty(" ")       = false
-   * StringUtils.isEmpty("bob")     = false
-   * StringUtils.isEmpty("  bob  ") = false
-   * 
- *

- *

NOTE: This method changed in Lang version 2.0. - * It no longer trims the CharSequence. - * That functionality is available in isBlank().

- * - * @param cs the CharSequence to check, may be null - * @return {@code true} if the CharSequence is empty or null - * @since 3.0 Changed signature from isEmpty(String) to isEmpty(CharSequence) - */ - public static boolean isEmpty(CharSequence cs) { - return cs == null || cs.length() == 0; - } - - /** - *

Checks if a CharSequence is not empty ("") and not null.

- *

- *

-   * StringUtils.isNotEmpty(null)      = false
-   * StringUtils.isNotEmpty("")        = false
-   * StringUtils.isNotEmpty(" ")       = true
-   * StringUtils.isNotEmpty("bob")     = true
-   * StringUtils.isNotEmpty("  bob  ") = true
-   * 
- * - * @param cs the CharSequence to check, may be null - * @return {@code true} if the CharSequence is not empty and not null - * @since 3.0 Changed signature from isNotEmpty(String) to isNotEmpty(CharSequence) - */ - public static boolean isNotEmpty(CharSequence cs) { - return !StringUtils.isEmpty(cs); - } - -} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/ToStringUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/ToStringUtils.java new file mode 100644 index 00000000..0fa38391 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/ToStringUtils.java @@ -0,0 +1,61 @@ +package me.chanjar.weixin.common.util; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +/** + *
+ * 自定义的ToString方法,用于产生去掉空值属性的字符串
+ * Created by Binary Wang on 2016-10-27.
+ * @author binarywang(Binary Wang)
+ * 
+ */ +public class ToStringUtils { + public static final ToStringStyle THE_STYLE = new SimpleMultiLineToStringStyle(); + private static class SimpleMultiLineToStringStyle extends ToStringStyle { + private static final long serialVersionUID = 4645306494220335355L; + private static final String LINE_SEPARATOR = "\n"; + private static final String NULL_TEXT = ""; + + public SimpleMultiLineToStringStyle() { + super(); + this.setContentStart("["); + this.setFieldSeparator(LINE_SEPARATOR + " "); + this.setFieldSeparatorAtStart(true); + this.setContentEnd(LINE_SEPARATOR + "]"); + this.setNullText(NULL_TEXT); + this.setUseShortClassName(true); + this.setUseIdentityHashCode(false); + } + } + + /** + * 用于产生去掉空值属性并以换行符分割各属性键值的toString字符串 + * @param obj + */ + public static String toSimpleString(Object obj) { + String toStringResult = ToStringBuilder.reflectionToString(obj, THE_STYLE); + String[] split = toStringResult.split(SimpleMultiLineToStringStyle.LINE_SEPARATOR); + StringBuilder result = new StringBuilder(); + for (String string : split) { + if (string.endsWith(SimpleMultiLineToStringStyle.NULL_TEXT)) { + continue; + } + + result.append(string + SimpleMultiLineToStringStyle.LINE_SEPARATOR); + } + + if (result.length() == 0) { + return ""; + } + + //如果没有非空的属性,就输出 + if (StringUtils.countMatches(result, SimpleMultiLineToStringStyle.LINE_SEPARATOR) == 2) { + return result.toString().split(SimpleMultiLineToStringStyle.LINE_SEPARATOR)[0] + + "]"; + } + + return result.deleteCharAt(result.length() - 1).toString(); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java index fead486e..a809fe3b 100755 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java @@ -1,300 +1,300 @@ -/** - * 对公众平台发送给公众账号的消息加解密示例代码. - * - * @copyright Copyright (c) 1998-2014 Tencent Inc. - *

- * 针对org.apache.commons.codec.binary.Base64, - * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本) - * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi - */ - -// ------------------------------------------------------------------------ - -/** - * 针对org.apache.commons.codec.binary.Base64, - * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本) - * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi - */ -package me.chanjar.weixin.common.util.crypto; - -import java.io.StringReader; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.Random; - -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.apache.commons.codec.binary.Base64; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.xml.sax.InputSource; - -public class WxCryptUtil { - - private static final Base64 base64 = new Base64(); - private static final Charset CHARSET = Charset.forName("utf-8"); - - private static final ThreadLocal builderLocal = new ThreadLocal() { - @Override - protected DocumentBuilder initialValue() { - try { - return DocumentBuilderFactory.newInstance().newDocumentBuilder(); - } catch (ParserConfigurationException exc) { - throw new IllegalArgumentException(exc); - } - } - }; - - protected byte[] aesKey; - protected String token; - protected String appidOrCorpid; - - public WxCryptUtil() { - super(); - } - - /** - * 构造函数 - * - * @param token 公众平台上,开发者设置的token - * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey - * @param appidOrCorpid 公众平台appid/corpid - */ - public WxCryptUtil(String token, String encodingAesKey, - String appidOrCorpid) { - this.token = token; - this.appidOrCorpid = appidOrCorpid; - this.aesKey = Base64.decodeBase64(encodingAesKey + "="); - } - - static String extractEncryptPart(String xml) { - try { - DocumentBuilder db = builderLocal.get(); - Document document = db.parse(new InputSource(new StringReader(xml))); - - Element root = document.getDocumentElement(); - return root.getElementsByTagName("Encrypt").item(0).getTextContent(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * 将公众平台回复用户的消息加密打包. - *

    - *
  1. 对要发送的消息进行AES-CBC加密
  2. - *
  3. 生成安全签名
  4. - *
  5. 将消息密文和安全签名打包成xml格式
  6. - *
- * - * @param plainText 公众平台待回复用户的消息,xml格式的字符串 - * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串 - */ - public String encrypt(String plainText) { - // 加密 - String encryptedXml = encrypt(genRandomStr(), plainText); - - // 生成安全签名 - String timeStamp = Long.toString(System.currentTimeMillis() / 1000l); - String nonce = genRandomStr(); - - String signature = SHA1.gen(this.token, timeStamp, nonce, encryptedXml); - String result = generateXml(encryptedXml, signature, timeStamp, nonce); - return result; - } - - /** - * 对明文进行加密. - * - * @param plainText 需要加密的明文 - * @return 加密后base64编码的字符串 - */ - protected String encrypt(String randomStr, String plainText) { - ByteGroup byteCollector = new ByteGroup(); - byte[] randomStringBytes = randomStr.getBytes(CHARSET); - byte[] plainTextBytes = plainText.getBytes(CHARSET); - byte[] bytesOfSizeInNetworkOrder = number2BytesInNetworkOrder( - plainTextBytes.length); - byte[] appIdBytes = this.appidOrCorpid.getBytes(CHARSET); - - // randomStr + networkBytesOrder + text + appid - byteCollector.addBytes(randomStringBytes); - byteCollector.addBytes(bytesOfSizeInNetworkOrder); - byteCollector.addBytes(plainTextBytes); - byteCollector.addBytes(appIdBytes); - - // ... + pad: 使用自定义的填充方式对明文进行补位填充 - byte[] padBytes = PKCS7Encoder.encode(byteCollector.size()); - byteCollector.addBytes(padBytes); - - // 获得最终的字节流, 未加密 - byte[] unencrypted = byteCollector.toBytes(); - - try { - // 设置加密模式为AES的CBC模式 - Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); - SecretKeySpec keySpec = new SecretKeySpec(this.aesKey, "AES"); - IvParameterSpec iv = new IvParameterSpec(this.aesKey, 0, 16); - cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); - - // 加密 - byte[] encrypted = cipher.doFinal(unencrypted); - - // 使用BASE64对加密后的字符串进行编码 - String base64Encrypted = base64.encodeToString(encrypted); - - return base64Encrypted; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * 检验消息的真实性,并且获取解密后的明文. - *
    - *
  1. 利用收到的密文生成安全签名,进行签名验证
  2. - *
  3. 若验证通过,则提取xml中的加密消息
  4. - *
  5. 对消息进行解密
  6. - *
- * - * @param msgSignature 签名串,对应URL参数的msg_signature - * @param timeStamp 时间戳,对应URL参数的timestamp - * @param nonce 随机串,对应URL参数的nonce - * @param encryptedXml 密文,对应POST请求的数据 - * @return 解密后的原文 - */ - public String decrypt(String msgSignature, String timeStamp, String nonce, - String encryptedXml) { - // 密钥,公众账号的app corpSecret - // 提取密文 - String cipherText = extractEncryptPart(encryptedXml); - - // 验证安全签名 - String signature = SHA1.gen(this.token, timeStamp, nonce, cipherText); - if (!signature.equals(msgSignature)) { - throw new RuntimeException("加密消息签名校验失败"); - } - - // 解密 - String result = decrypt(cipherText); - return result; - } - - /** - * 对密文进行解密. - * - * @param cipherText 需要解密的密文 - * @return 解密得到的明文 - */ - public String decrypt(String cipherText) { - byte[] original; - try { - // 设置解密模式为AES的CBC模式 - Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); - SecretKeySpec key_spec = new SecretKeySpec(this.aesKey, "AES"); - IvParameterSpec iv = new IvParameterSpec( - Arrays.copyOfRange(this.aesKey, 0, 16)); - cipher.init(Cipher.DECRYPT_MODE, key_spec, iv); - - // 使用BASE64对密文进行解码 - byte[] encrypted = Base64.decodeBase64(cipherText); - - // 解密 - original = cipher.doFinal(encrypted); - } catch (Exception e) { - throw new RuntimeException(e); - } - - String xmlContent, from_appid; - try { - // 去除补位字符 - byte[] bytes = PKCS7Encoder.decode(original); - - // 分离16位随机字符串,网络字节序和AppId - byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20); - - int xmlLength = bytesNetworkOrder2Number(networkOrder); - - xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), - CHARSET); - from_appid = new String( - Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET); - } catch (Exception e) { - throw new RuntimeException(e); - } - - // appid不相同的情况 - if (!from_appid.equals(this.appidOrCorpid)) { - throw new RuntimeException("AppID不正确"); - } - - return xmlContent; - - } - - /** - * 将一个数字转换成生成4个字节的网络字节序bytes数组 - * - * @param number - */ - private static byte[] number2BytesInNetworkOrder(int number) { - byte[] orderBytes = new byte[4]; - orderBytes[3] = (byte) (number & 0xFF); - orderBytes[2] = (byte) (number >> 8 & 0xFF); - orderBytes[1] = (byte) (number >> 16 & 0xFF); - orderBytes[0] = (byte) (number >> 24 & 0xFF); - return orderBytes; - } - - /** - * 4个字节的网络字节序bytes数组还原成一个数字 - * - * @param bytesInNetworkOrder - */ - private static int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) { - int sourceNumber = 0; - for (int i = 0; i < 4; i++) { - sourceNumber <<= 8; - sourceNumber |= bytesInNetworkOrder[i] & 0xff; - } - return sourceNumber; - } - - /** - * 随机生成16位字符串 - */ - private static String genRandomStr() { - String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - Random random = new Random(); - StringBuffer sb = new StringBuffer(); - for (int i = 0; i < 16; i++) { - int number = random.nextInt(base.length()); - sb.append(base.charAt(number)); - } - return sb.toString(); - } - - /** - * 生成xml消息 - * - * @param encrypt 加密后的消息密文 - * @param signature 安全签名 - * @param timestamp 时间戳 - * @param nonce 随机字符串 - * @return 生成的xml字符串 - */ - private static String generateXml(String encrypt, String signature, - String timestamp, String nonce) { - String format = "\n" + "\n" - + "\n" - + "%3$s\n" + "\n" - + ""; - return String.format(format, encrypt, signature, timestamp, nonce); - } - -} +/** + * 对公众平台发送给公众账号的消息加解密示例代码. + * + * @copyright Copyright (c) 1998-2014 Tencent Inc. + *

+ * 针对org.apache.commons.codec.binary.Base64, + * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本) + * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi + */ + +// ------------------------------------------------------------------------ + +/** + * 针对org.apache.commons.codec.binary.Base64, + * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本) + * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi + */ +package me.chanjar.weixin.common.util.crypto; + +import java.io.StringReader; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Random; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.commons.codec.binary.Base64; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; + +public class WxCryptUtil { + + private static final Base64 base64 = new Base64(); + private static final Charset CHARSET = Charset.forName("utf-8"); + + private static final ThreadLocal builderLocal = new ThreadLocal() { + @Override + protected DocumentBuilder initialValue() { + try { + return DocumentBuilderFactory.newInstance().newDocumentBuilder(); + } catch (ParserConfigurationException exc) { + throw new IllegalArgumentException(exc); + } + } + }; + + protected byte[] aesKey; + protected String token; + protected String appidOrCorpid; + + public WxCryptUtil() { + super(); + } + + /** + * 构造函数 + * + * @param token 公众平台上,开发者设置的token + * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey + * @param appidOrCorpid 公众平台appid/corpid + */ + public WxCryptUtil(String token, String encodingAesKey, + String appidOrCorpid) { + this.token = token; + this.appidOrCorpid = appidOrCorpid; + this.aesKey = Base64.decodeBase64(encodingAesKey + "="); + } + + static String extractEncryptPart(String xml) { + try { + DocumentBuilder db = builderLocal.get(); + Document document = db.parse(new InputSource(new StringReader(xml))); + + Element root = document.getDocumentElement(); + return root.getElementsByTagName("Encrypt").item(0).getTextContent(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 将公众平台回复用户的消息加密打包. + *

    + *
  1. 对要发送的消息进行AES-CBC加密
  2. + *
  3. 生成安全签名
  4. + *
  5. 将消息密文和安全签名打包成xml格式
  6. + *
+ * + * @param plainText 公众平台待回复用户的消息,xml格式的字符串 + * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串 + */ + public String encrypt(String plainText) { + // 加密 + String encryptedXml = encrypt(genRandomStr(), plainText); + + // 生成安全签名 + String timeStamp = Long.toString(System.currentTimeMillis() / 1000l); + String nonce = genRandomStr(); + + String signature = SHA1.gen(this.token, timeStamp, nonce, encryptedXml); + String result = generateXml(encryptedXml, signature, timeStamp, nonce); + return result; + } + + /** + * 对明文进行加密. + * + * @param plainText 需要加密的明文 + * @return 加密后base64编码的字符串 + */ + protected String encrypt(String randomStr, String plainText) { + ByteGroup byteCollector = new ByteGroup(); + byte[] randomStringBytes = randomStr.getBytes(CHARSET); + byte[] plainTextBytes = plainText.getBytes(CHARSET); + byte[] bytesOfSizeInNetworkOrder = number2BytesInNetworkOrder( + plainTextBytes.length); + byte[] appIdBytes = this.appidOrCorpid.getBytes(CHARSET); + + // randomStr + networkBytesOrder + text + appid + byteCollector.addBytes(randomStringBytes); + byteCollector.addBytes(bytesOfSizeInNetworkOrder); + byteCollector.addBytes(plainTextBytes); + byteCollector.addBytes(appIdBytes); + + // ... + pad: 使用自定义的填充方式对明文进行补位填充 + byte[] padBytes = PKCS7Encoder.encode(byteCollector.size()); + byteCollector.addBytes(padBytes); + + // 获得最终的字节流, 未加密 + byte[] unencrypted = byteCollector.toBytes(); + + try { + // 设置加密模式为AES的CBC模式 + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(this.aesKey, "AES"); + IvParameterSpec iv = new IvParameterSpec(this.aesKey, 0, 16); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); + + // 加密 + byte[] encrypted = cipher.doFinal(unencrypted); + + // 使用BASE64对加密后的字符串进行编码 + String base64Encrypted = base64.encodeToString(encrypted); + + return base64Encrypted; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 检验消息的真实性,并且获取解密后的明文. + *
    + *
  1. 利用收到的密文生成安全签名,进行签名验证
  2. + *
  3. 若验证通过,则提取xml中的加密消息
  4. + *
  5. 对消息进行解密
  6. + *
+ * + * @param msgSignature 签名串,对应URL参数的msg_signature + * @param timeStamp 时间戳,对应URL参数的timestamp + * @param nonce 随机串,对应URL参数的nonce + * @param encryptedXml 密文,对应POST请求的数据 + * @return 解密后的原文 + */ + public String decrypt(String msgSignature, String timeStamp, String nonce, + String encryptedXml) { + // 密钥,公众账号的app corpSecret + // 提取密文 + String cipherText = extractEncryptPart(encryptedXml); + + // 验证安全签名 + String signature = SHA1.gen(this.token, timeStamp, nonce, cipherText); + if (!signature.equals(msgSignature)) { + throw new RuntimeException("加密消息签名校验失败"); + } + + // 解密 + String result = decrypt(cipherText); + return result; + } + + /** + * 对密文进行解密. + * + * @param cipherText 需要解密的密文 + * @return 解密得到的明文 + */ + public String decrypt(String cipherText) { + byte[] original; + try { + // 设置解密模式为AES的CBC模式 + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec key_spec = new SecretKeySpec(this.aesKey, "AES"); + IvParameterSpec iv = new IvParameterSpec( + Arrays.copyOfRange(this.aesKey, 0, 16)); + cipher.init(Cipher.DECRYPT_MODE, key_spec, iv); + + // 使用BASE64对密文进行解码 + byte[] encrypted = Base64.decodeBase64(cipherText); + + // 解密 + original = cipher.doFinal(encrypted); + } catch (Exception e) { + throw new RuntimeException(e); + } + + String xmlContent, from_appid; + try { + // 去除补位字符 + byte[] bytes = PKCS7Encoder.decode(original); + + // 分离16位随机字符串,网络字节序和AppId + byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20); + + int xmlLength = bytesNetworkOrder2Number(networkOrder); + + xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), + CHARSET); + from_appid = new String( + Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET); + } catch (Exception e) { + throw new RuntimeException(e); + } + + // appid不相同的情况 + if (!from_appid.equals(this.appidOrCorpid)) { + throw new RuntimeException("AppID不正确"); + } + + return xmlContent; + + } + + /** + * 将一个数字转换成生成4个字节的网络字节序bytes数组 + * + * @param number + */ + private static byte[] number2BytesInNetworkOrder(int number) { + byte[] orderBytes = new byte[4]; + orderBytes[3] = (byte) (number & 0xFF); + orderBytes[2] = (byte) (number >> 8 & 0xFF); + orderBytes[1] = (byte) (number >> 16 & 0xFF); + orderBytes[0] = (byte) (number >> 24 & 0xFF); + return orderBytes; + } + + /** + * 4个字节的网络字节序bytes数组还原成一个数字 + * + * @param bytesInNetworkOrder + */ + private static int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) { + int sourceNumber = 0; + for (int i = 0; i < 4; i++) { + sourceNumber <<= 8; + sourceNumber |= bytesInNetworkOrder[i] & 0xff; + } + return sourceNumber; + } + + /** + * 随机生成16位字符串 + */ + private static String genRandomStr() { + String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 16; i++) { + int number = random.nextInt(base.length()); + sb.append(base.charAt(number)); + } + return sb.toString(); + } + + /** + * 生成xml消息 + * + * @param encrypt 加密后的消息密文 + * @param signature 安全签名 + * @param timestamp 时间戳 + * @param nonce 随机字符串 + * @return 生成的xml字符串 + */ + private static String generateXml(String encrypt, String signature, + String timestamp, String nonce) { + String format = "\n" + "\n" + + "\n" + + "%3$s\n" + "\n" + + ""; + return String.format(format, encrypt, signature, timestamp, nonce); + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/DefaultApacheHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/DefaultApacheHttpClientBuilder.java index cb4e571f..030fe98f 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/DefaultApacheHttpClientBuilder.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/DefaultApacheHttpClientBuilder.java @@ -1,8 +1,6 @@ package me.chanjar.weixin.common.util.http; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - +import org.apache.commons.lang3.StringUtils; import org.apache.http.annotation.NotThreadSafe; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -22,15 +20,22 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.protocol.HttpContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import me.chanjar.weixin.common.util.StringUtils; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; /** * httpclient 连接管理器 + * * @author kakotor */ @NotThreadSafe public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder { + protected final Logger log = LoggerFactory.getLogger(DefaultApacheHttpClientBuilder.class); + private final AtomicBoolean prepared = new AtomicBoolean(false); private int connectionRequestTimeout = 3000; private int connectionTimeout = 5000; private int soTimeout = 5000; @@ -47,21 +52,17 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder { }; private SSLConnectionSocketFactory sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory(); private PlainConnectionSocketFactory plainConnectionSocketFactory = PlainConnectionSocketFactory.getSocketFactory(); - private String httpProxyHost; private int httpProxyPort; private String httpProxyUsername; private String httpProxyPassword; - /** * 闲置连接监控线程 */ private IdleConnectionMonitorThread idleConnectionMonitorThread; - private HttpClientBuilder httpClientBuilder; private DefaultApacheHttpClientBuilder() { - prepare(); } public static DefaultApacheHttpClientBuilder get() { @@ -98,11 +99,98 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder { return this; } + /** + * 获取链接的超时时间设置,默认3000ms + *

+ * 设置为零时不超时,一直等待. + * 设置为负数是使用系统默认设置(非上述的3000ms的默认值,而是httpclient的默认设置). + *

+ * + * @param connectionRequestTimeout 获取链接的超时时间设置(单位毫秒),默认3000ms + */ + public void setConnectionRequestTimeout(int connectionRequestTimeout) { + this.connectionRequestTimeout = connectionRequestTimeout; + } + + /** + * 建立链接的超时时间,默认为5000ms.由于是在链接池获取链接,此设置应该并不起什么作用 + *

+ * 设置为零时不超时,一直等待. + * 设置为负数是使用系统默认设置(非上述的5000ms的默认值,而是httpclient的默认设置). + *

+ * + * @param connectionTimeout 建立链接的超时时间设置(单位毫秒),默认5000ms + */ + public void setConnectionTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + /** + * 默认NIO的socket超时设置,默认5000ms. + * + * @param soTimeout 默认NIO的socket超时设置,默认5000ms. + * @see java.net.SocketOptions#SO_TIMEOUT + */ + public void setSoTimeout(int soTimeout) { + this.soTimeout = soTimeout; + } + + /** + * 空闲链接的超时时间,默认60000ms. + *

+ * 超时的链接将在下一次空闲链接检查是被销毁 + *

+ * + * @param idleConnTimeout 空闲链接的超时时间,默认60000ms. + */ + public void setIdleConnTimeout(int idleConnTimeout) { + this.idleConnTimeout = idleConnTimeout; + } + + /** + * 检查空间链接的间隔周期,默认60000ms. + * + * @param checkWaitTime 检查空间链接的间隔周期,默认60000ms. + */ + public void setCheckWaitTime(int checkWaitTime) { + this.checkWaitTime = checkWaitTime; + } + + /** + * 每路的最大链接数,默认10 + * + * @param maxConnPerHost 每路的最大链接数,默认10 + */ + public void setMaxConnPerHost(int maxConnPerHost) { + this.maxConnPerHost = maxConnPerHost; + } + + /** + * 最大总连接数,默认50 + * + * @param maxTotalConn 最大总连接数,默认50 + */ + public void setMaxTotalConn(int maxTotalConn) { + this.maxTotalConn = maxTotalConn; + } + + /** + * 自定义httpclient的User Agent + * + * @param userAgent User Agent + */ + public void setUserAgent(String userAgent) { + this.userAgent = userAgent; + } + public IdleConnectionMonitorThread getIdleConnectionMonitorThread() { return this.idleConnectionMonitorThread; } - private void prepare() { + private synchronized void prepare() { + if(prepared.get()){ + return; + } Registry registry = RegistryBuilder.create() .register("http", this.plainConnectionSocketFactory) .register("https", this.sslConnectionSocketFactory) @@ -149,11 +237,14 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder { if (StringUtils.isNotBlank(this.userAgent)) { this.httpClientBuilder.setUserAgent(this.userAgent); } - + prepared.set(true); } @Override public CloseableHttpClient build() { + if(!prepared.get()){ + prepare(); + } return this.httpClientBuilder.build(); } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddGetRequestExecutor.java deleted file mode 100644 index 34f20779..00000000 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddGetRequestExecutor.java +++ /dev/null @@ -1,55 +0,0 @@ -package me.chanjar.weixin.common.util.http; - -import org.apache.http.HttpHost; -import org.apache.http.impl.client.CloseableHttpClient; - -import jodd.http.HttpRequest; -import jodd.http.HttpResponse; -import jodd.http.ProxyInfo; -import jodd.http.net.SocketHttpConnectionProvider; -import me.chanjar.weixin.common.bean.result.WxError; -import me.chanjar.weixin.common.exception.WxErrorException; - -/** - * 简单的GET请求执行器,请求的参数是String, 返回的结果也是String - * - * @author Daniel Qian - */ -public class JoddGetRequestExecutor implements RequestExecutor { - - @Override - public String execute(CloseableHttpClient httpclient, HttpHost httpProxy, String uri, - String queryParam) throws WxErrorException { - if (queryParam != null) { - if (uri.indexOf('?') == -1) { - uri += '?'; - } - uri += uri.endsWith("?") ? queryParam : '&' + queryParam; - } - - SocketHttpConnectionProvider provider = new SocketHttpConnectionProvider(); - - if (httpProxy != null) { - ProxyInfo proxyInfoObj = new ProxyInfo( - ProxyInfo.ProxyType.HTTP, - httpProxy.getHostName(), - httpProxy.getPort(), "", ""); - provider.useProxy(proxyInfoObj); - } - - HttpRequest request = HttpRequest.get(uri); - request.method("GET"); - request.charset("UTF-8"); - - HttpResponse response = request.open(provider).send(); - response.charset("UTF-8"); - String result = response.bodyText(); - - WxError error = WxError.fromJson(result); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } - return result; - } - -} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddPostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddPostRequestExecutor.java deleted file mode 100644 index 757db665..00000000 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/JoddPostRequestExecutor.java +++ /dev/null @@ -1,49 +0,0 @@ -package me.chanjar.weixin.common.util.http; - -import org.apache.http.HttpHost; -import org.apache.http.impl.client.CloseableHttpClient; - -import jodd.http.HttpRequest; -import jodd.http.HttpResponse; -import jodd.http.ProxyInfo; -import jodd.http.net.SocketHttpConnectionProvider; -import me.chanjar.weixin.common.bean.result.WxError; -import me.chanjar.weixin.common.exception.WxErrorException; - -/** - * 简单的POST请求执行器,请求的参数是String, 返回的结果也是String - * - * @author Edison Guo - */ -public class JoddPostRequestExecutor implements RequestExecutor { - - @Override - public String execute(CloseableHttpClient httpclient, HttpHost httpProxy, String uri, - String postEntity) throws WxErrorException { - SocketHttpConnectionProvider provider = new SocketHttpConnectionProvider(); - - if (httpProxy != null) { - ProxyInfo proxyInfoObj = new ProxyInfo( - ProxyInfo.ProxyType.HTTP, - httpProxy.getAddress().getHostAddress(), - httpProxy.getPort(), "", ""); - provider.useProxy(proxyInfoObj); - } - - HttpRequest request = HttpRequest.get(uri); - request.method("POST"); - request.charset("UTF-8"); - request.bodyText(postEntity); - - HttpResponse response = request.open(provider).send(); - response.charset("UTF-8"); - String result = response.bodyText(); - - WxError error = WxError.fromJson(result); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } - return result; - } - -} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java index c81658e2..57024f50 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaDownloadRequestExecutor.java @@ -2,8 +2,8 @@ package me.chanjar.weixin.common.util.http; import me.chanjar.weixin.common.bean.result.WxError; import me.chanjar.weixin.common.exception.WxErrorException; -import me.chanjar.weixin.common.util.StringUtils; import me.chanjar.weixin.common.util.fs.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/URIUtil.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/URIUtil.java index 9448f2d1..7e42aba7 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/URIUtil.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/URIUtil.java @@ -1,6 +1,7 @@ package me.chanjar.weixin.common.util.http; -import me.chanjar.weixin.common.util.StringUtils; + +import org.apache.commons.lang3.StringUtils; import java.io.UnsupportedEncodingException; diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java index e7692165..91095a20 100755 --- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java @@ -1,103 +1,103 @@ -package me.chanjar.weixin.common.util.crypto; - -import org.testng.annotations.Test; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; -import java.io.StringReader; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.fail; - -@Test -public class WxCryptUtilTest { - String encodingAesKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"; - String token = "pamtest"; - String timestamp = "1409304348"; - String nonce = "xxxxxx"; - String appId = "wxb11529c136998cb6"; - String randomStr = "aaaabbbbccccdddd"; - - String xmlFormat = ""; - String replyMsg = "我是中文abcd123"; - - String afterAesEncrypt = "jn1L23DB+6ELqJ+6bruv21Y6MD7KeIfP82D6gU39rmkgczbWwt5+3bnyg5K55bgVtVzd832WzZGMhkP72vVOfg=="; - - String replyMsg2 = "1407743423"; - String afterAesEncrypt2 = "jn1L23DB+6ELqJ+6bruv23M2GmYfkv0xBh2h+XTBOKVKcgDFHle6gqcZ1cZrk3e1qjPQ1F4RsLWzQRG9udbKWesxlkupqcEcW7ZQweImX9+wLMa0GaUzpkycA8+IamDBxn5loLgZpnS7fVAbExOkK5DYHBmv5tptA9tklE/fTIILHR8HLXa5nQvFb3tYPKAlHF3rtTeayNf0QuM+UW/wM9enGIDIJHF7CLHiDNAYxr+r+OrJCmPQyTy8cVWlu9iSvOHPT/77bZqJucQHQ04sq7KZI27OcqpQNSto2OdHCoTccjggX5Z9Mma0nMJBU+jLKJ38YB1fBIz+vBzsYjrTmFQ44YfeEuZ+xRTQwr92vhA9OxchWVINGC50qE/6lmkwWTwGX9wtQpsJKhP+oS7rvTY8+VdzETdfakjkwQ5/Xka042OlUb1/slTwo4RscuQ+RdxSGvDahxAJ6+EAjLt9d8igHngxIbf6YyqqROxuxqIeIch3CssH/LqRs+iAcILvApYZckqmA7FNERspKA5f8GoJ9sv8xmGvZ9Yrf57cExWtnX8aCMMaBropU/1k+hKP5LVdzbWCG0hGwx/dQudYR/eXp3P0XxjlFiy+9DMlaFExWUZQDajPkdPrEeOwofJb"; - - public void testNormal() throws ParserConfigurationException, SAXException, IOException { - WxCryptUtil pc = new WxCryptUtil(this.token, this.encodingAesKey, this.appId); - String encryptedXml = pc.encrypt(this.replyMsg); - - System.out.println(encryptedXml); - - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); - Document document = documentBuilder.parse(new InputSource(new StringReader(encryptedXml))); - - Element root = document.getDocumentElement(); - String cipherText = root.getElementsByTagName("Encrypt").item(0).getTextContent(); - System.out.println(cipherText); - - String msgSignature = root.getElementsByTagName("MsgSignature").item(0).getTextContent(); - System.out.println(msgSignature); - - String timestamp = root.getElementsByTagName("TimeStamp").item(0).getTextContent(); - System.out.println(timestamp); - - String nonce = root.getElementsByTagName("Nonce").item(0).getTextContent(); - System.out.println(nonce); - - String messageText = String.format(this.xmlFormat, cipherText); - System.out.println(messageText); - - // 第三方收到企业号平台发送的消息 - String plainMessage = pc.decrypt(cipherText); - System.out.println(plainMessage); - - assertEquals(plainMessage, this.replyMsg); - } - - public void testAesEncrypt() { - WxCryptUtil pc = new WxCryptUtil(this.token, this.encodingAesKey, this.appId); - assertEquals(pc.encrypt(this.randomStr, this.replyMsg), this.afterAesEncrypt); - } - - public void testAesEncrypt2() { - WxCryptUtil pc = new WxCryptUtil(this.token, this.encodingAesKey, this.appId); - assertEquals(pc.encrypt(this.randomStr, this.replyMsg2), this.afterAesEncrypt2); - } - - public void testValidateSignatureError() throws ParserConfigurationException, SAXException, - IOException { - try { - WxCryptUtil pc = new WxCryptUtil(this.token, this.encodingAesKey, this.appId); - String afterEncrpt = pc.encrypt(this.replyMsg); - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); - DocumentBuilder db = dbf.newDocumentBuilder(); - StringReader sr = new StringReader(afterEncrpt); - InputSource is = new InputSource(sr); - Document document = db.parse(is); - - Element root = document.getDocumentElement(); - NodeList nodelist1 = root.getElementsByTagName("Encrypt"); - - String encrypt = nodelist1.item(0).getTextContent(); - String fromXML = String.format(this.xmlFormat, encrypt); - pc.decrypt("12345", this.timestamp, this.nonce, fromXML); // 这里签名错误 - } catch (RuntimeException e) { - assertEquals(e.getMessage(), "加密消息签名校验失败"); - return; - } - fail("错误流程不抛出异常???"); - } - -} +package me.chanjar.weixin.common.util.crypto; + +import org.testng.annotations.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.StringReader; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +@Test +public class WxCryptUtilTest { + String encodingAesKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"; + String token = "pamtest"; + String timestamp = "1409304348"; + String nonce = "xxxxxx"; + String appId = "wxb11529c136998cb6"; + String randomStr = "aaaabbbbccccdddd"; + + String xmlFormat = ""; + String replyMsg = "我是中文abcd123"; + + String afterAesEncrypt = "jn1L23DB+6ELqJ+6bruv21Y6MD7KeIfP82D6gU39rmkgczbWwt5+3bnyg5K55bgVtVzd832WzZGMhkP72vVOfg=="; + + String replyMsg2 = "1407743423"; + String afterAesEncrypt2 = "jn1L23DB+6ELqJ+6bruv23M2GmYfkv0xBh2h+XTBOKVKcgDFHle6gqcZ1cZrk3e1qjPQ1F4RsLWzQRG9udbKWesxlkupqcEcW7ZQweImX9+wLMa0GaUzpkycA8+IamDBxn5loLgZpnS7fVAbExOkK5DYHBmv5tptA9tklE/fTIILHR8HLXa5nQvFb3tYPKAlHF3rtTeayNf0QuM+UW/wM9enGIDIJHF7CLHiDNAYxr+r+OrJCmPQyTy8cVWlu9iSvOHPT/77bZqJucQHQ04sq7KZI27OcqpQNSto2OdHCoTccjggX5Z9Mma0nMJBU+jLKJ38YB1fBIz+vBzsYjrTmFQ44YfeEuZ+xRTQwr92vhA9OxchWVINGC50qE/6lmkwWTwGX9wtQpsJKhP+oS7rvTY8+VdzETdfakjkwQ5/Xka042OlUb1/slTwo4RscuQ+RdxSGvDahxAJ6+EAjLt9d8igHngxIbf6YyqqROxuxqIeIch3CssH/LqRs+iAcILvApYZckqmA7FNERspKA5f8GoJ9sv8xmGvZ9Yrf57cExWtnX8aCMMaBropU/1k+hKP5LVdzbWCG0hGwx/dQudYR/eXp3P0XxjlFiy+9DMlaFExWUZQDajPkdPrEeOwofJb"; + + public void testNormal() throws ParserConfigurationException, SAXException, IOException { + WxCryptUtil pc = new WxCryptUtil(this.token, this.encodingAesKey, this.appId); + String encryptedXml = pc.encrypt(this.replyMsg); + + System.out.println(encryptedXml); + + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.parse(new InputSource(new StringReader(encryptedXml))); + + Element root = document.getDocumentElement(); + String cipherText = root.getElementsByTagName("Encrypt").item(0).getTextContent(); + System.out.println(cipherText); + + String msgSignature = root.getElementsByTagName("MsgSignature").item(0).getTextContent(); + System.out.println(msgSignature); + + String timestamp = root.getElementsByTagName("TimeStamp").item(0).getTextContent(); + System.out.println(timestamp); + + String nonce = root.getElementsByTagName("Nonce").item(0).getTextContent(); + System.out.println(nonce); + + String messageText = String.format(this.xmlFormat, cipherText); + System.out.println(messageText); + + // 第三方收到企业号平台发送的消息 + String plainMessage = pc.decrypt(cipherText); + System.out.println(plainMessage); + + assertEquals(plainMessage, this.replyMsg); + } + + public void testAesEncrypt() { + WxCryptUtil pc = new WxCryptUtil(this.token, this.encodingAesKey, this.appId); + assertEquals(pc.encrypt(this.randomStr, this.replyMsg), this.afterAesEncrypt); + } + + public void testAesEncrypt2() { + WxCryptUtil pc = new WxCryptUtil(this.token, this.encodingAesKey, this.appId); + assertEquals(pc.encrypt(this.randomStr, this.replyMsg2), this.afterAesEncrypt2); + } + + public void testValidateSignatureError() throws ParserConfigurationException, SAXException, + IOException { + try { + WxCryptUtil pc = new WxCryptUtil(this.token, this.encodingAesKey, this.appId); + String afterEncrpt = pc.encrypt(this.replyMsg); + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + StringReader sr = new StringReader(afterEncrpt); + InputSource is = new InputSource(sr); + Document document = db.parse(is); + + Element root = document.getDocumentElement(); + NodeList nodelist1 = root.getElementsByTagName("Encrypt"); + + String encrypt = nodelist1.item(0).getTextContent(); + String fromXML = String.format(this.xmlFormat, encrypt); + pc.decrypt("12345", this.timestamp, this.nonce, fromXML); // 这里签名错误 + } catch (RuntimeException e) { + assertEquals(e.getMessage(), "加密消息签名校验失败"); + return; + } + fail("错误流程不抛出异常???"); + } + +} diff --git a/weixin-java-cp/build.gradle b/weixin-java-cp/build.gradle index f58703ca..85d7c4bb 100644 --- a/weixin-java-cp/build.gradle +++ b/weixin-java-cp/build.gradle @@ -6,7 +6,7 @@ dependencies { testCompile group: 'org.testng', name: 'testng', version:'6.8.7' testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5' testCompile group: 'com.google.inject', name: 'guice', version:'3.0' - testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version:'9.3.0.M0' - testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version:'9.3.0.M0' + testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version:'9.3.0.RC0' + testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version:'9.3.0.RC0' } test.useTestNG() diff --git a/weixin-java-cp/pom.xml b/weixin-java-cp/pom.xml index f62f4d29..6fa3bde2 100644 --- a/weixin-java-cp/pom.xml +++ b/weixin-java-cp/pom.xml @@ -6,7 +6,7 @@ com.github.binarywang weixin-java-parent - 2.3.0-SNAPSHOT + 2.4.0-SNAPSHOT weixin-java-cp diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java index b334b85b..eb1e57ca 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpConfigStorage.java @@ -45,7 +45,7 @@ public interface WxCpConfigStorage { String getCorpSecret(); - String getAgentId(); + Integer getAgentId(); String getToken(); diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java index 8914da53..867dd6bb 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpInMemoryConfigStorage.java @@ -1,13 +1,11 @@ package me.chanjar.weixin.cp.api; -import java.io.File; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.util.ToStringUtils; import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder; +import java.io.File; + /** * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化 * @@ -21,7 +19,7 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { protected volatile String token; protected volatile String accessToken; protected volatile String aesKey; - protected volatile String agentId; + protected volatile Integer agentId; protected volatile long expiresTime; protected volatile String oauth2redirectUri; @@ -148,11 +146,11 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { } @Override - public String getAgentId() { + public Integer getAgentId() { return this.agentId; } - public void setAgentId(String agentId) { + public void setAgentId(Integer agentId) { this.agentId = agentId; } @@ -203,7 +201,7 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } @Override diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpJedisConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpJedisConfigStorage.java index 5209479f..92513965 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpJedisConfigStorage.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpJedisConfigStorage.java @@ -1,267 +1,259 @@ -package me.chanjar.weixin.cp.api; - -import java.io.File; - -import me.chanjar.weixin.common.bean.WxAccessToken; -import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; - -/** - * Jedis client implementor for wechat config storage - * - * @author gaigeshen - */ -public class WxCpJedisConfigStorage implements WxCpConfigStorage { - - /* Redis keys here */ - private static final String ACCESS_TOKEN_KEY = "WX_CP_ACCESS_TOKEN"; - private static final String ACCESS_TOKEN_EXPIRES_TIME_KEY = "WX_CP_ACCESS_TOKEN_EXPIRES_TIME"; - private static final String JS_API_TICKET_KEY = "WX_CP_JS_API_TICKET"; - private static final String JS_API_TICKET_EXPIRES_TIME_KEY = "WX_CP_JS_API_TICKET_EXPIRES_TIME"; - - private volatile String corpId; - private volatile String corpSecret; - - private volatile String token; - private volatile String aesKey; - private volatile String agentId; - - private volatile String oauth2redirectUri; - - private volatile String httpProxyHost; - private volatile int httpProxyPort; - private volatile String httpProxyUsername; - private volatile String httpProxyPassword; - - private volatile File tmpDirFile; - - private volatile ApacheHttpClientBuilder apacheHttpClientBuilder; - - /* Redis clients pool */ - private final JedisPool jedisPool; - - public WxCpJedisConfigStorage(String host, int port) { - this.jedisPool = new JedisPool(host, port); - } - - /** - * - * This method will be destroy jedis pool - */ - public void destroy() { - this.jedisPool.destroy(); - } - - @Override - public String getAccessToken() { - try (Jedis jedis = this.jedisPool.getResource()) { - return jedis.get(ACCESS_TOKEN_KEY); - } - } - - @Override - public boolean isAccessTokenExpired() { - try (Jedis jedis = this.jedisPool.getResource()) { - String expiresTimeStr = jedis.get(ACCESS_TOKEN_EXPIRES_TIME_KEY); - - if (expiresTimeStr != null) { - Long expiresTime = Long.parseLong(expiresTimeStr); - return System.currentTimeMillis() > expiresTime; - } - - return true; - - } - } - - @Override - public void expireAccessToken() { - try (Jedis jedis = this.jedisPool.getResource()) { - jedis.set(ACCESS_TOKEN_EXPIRES_TIME_KEY, "0"); - } - } - - @Override - public synchronized void updateAccessToken(WxAccessToken accessToken) { - this.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); - } - - @Override - public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) { - try (Jedis jedis = this.jedisPool.getResource()) { - jedis.set(ACCESS_TOKEN_KEY, accessToken); - - jedis.set(ACCESS_TOKEN_EXPIRES_TIME_KEY, - (System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L) + ""); - } - } - - @Override - public String getJsapiTicket() { - try (Jedis jedis = this.jedisPool.getResource()) { - return jedis.get(JS_API_TICKET_KEY); - } - } - - @Override - public boolean isJsapiTicketExpired() { - - try (Jedis jedis = this.jedisPool.getResource()) { - String expiresTimeStr = jedis.get(JS_API_TICKET_EXPIRES_TIME_KEY); - - if (expiresTimeStr != null) { - Long expiresTime = Long.parseLong(expiresTimeStr); - return System.currentTimeMillis() > expiresTime; - } - - return true; - - } - } - - @Override - public void expireJsapiTicket() { - try (Jedis jedis = this.jedisPool.getResource()) { - jedis.set(JS_API_TICKET_EXPIRES_TIME_KEY, "0"); - } - } - - @Override - public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) { - - try (Jedis jedis = this.jedisPool.getResource()) { - jedis.set(JS_API_TICKET_KEY, jsapiTicket); - - jedis.set(JS_API_TICKET_EXPIRES_TIME_KEY, - (System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L + "")); - } - - } - - @Override - public String getCorpId() { - return this.corpId; - } - - @Override - public String getCorpSecret() { - return this.corpSecret; - } - - @Override - public String getAgentId() { - return this.agentId; - } - - @Override - public String getToken() { - return this.token; - } - - @Override - public String getAesKey() { - return this.aesKey; - } - - @Override - public long getExpiresTime() { - try (Jedis jedis = this.jedisPool.getResource()) { - String expiresTimeStr = jedis.get(ACCESS_TOKEN_EXPIRES_TIME_KEY); - - if (expiresTimeStr != null) { - Long expiresTime = Long.parseLong(expiresTimeStr); - return expiresTime; - } - - return 0L; - - } - } - - @Override - public String getOauth2redirectUri() { - return this.oauth2redirectUri; - } - - @Override - public String getHttpProxyHost() { - return this.httpProxyHost; - } - - @Override - public int getHttpProxyPort() { - return this.httpProxyPort; - } - - @Override - public String getHttpProxyUsername() { - return this.httpProxyUsername; - } - - @Override - public String getHttpProxyPassword() { - return this.httpProxyPassword; - } - - @Override - public File getTmpDirFile() { - return this.tmpDirFile; - } - - @Override - public ApacheHttpClientBuilder getApacheHttpClientBuilder() { - return this.apacheHttpClientBuilder; - } - - public void setCorpId(String corpId) { - this.corpId = corpId; - } - - public void setCorpSecret(String corpSecret) { - this.corpSecret = corpSecret; - } - - public void setToken(String token) { - this.token = token; - } - - public void setAesKey(String aesKey) { - this.aesKey = aesKey; - } - - public void setAgentId(String agentId) { - this.agentId = agentId; - } - - // ============================ Setters below - - public void setOauth2redirectUri(String oauth2redirectUri) { - this.oauth2redirectUri = oauth2redirectUri; - } - - public void setHttpProxyHost(String httpProxyHost) { - this.httpProxyHost = httpProxyHost; - } - - public void setHttpProxyPort(int httpProxyPort) { - this.httpProxyPort = httpProxyPort; - } - - public void setHttpProxyUsername(String httpProxyUsername) { - this.httpProxyUsername = httpProxyUsername; - } - - public void setHttpProxyPassword(String httpProxyPassword) { - this.httpProxyPassword = httpProxyPassword; - } - - public void setTmpDirFile(File tmpDirFile) { - this.tmpDirFile = tmpDirFile; - } - - public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) { - this.apacheHttpClientBuilder = apacheHttpClientBuilder; - } - -} +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.io.File; + +/** + * Jedis client implementor for wechat config storage + * + * @author gaigeshen + */ +public class WxCpJedisConfigStorage implements WxCpConfigStorage { + + /* Redis keys here */ + private static final String ACCESS_TOKEN_KEY = "WX_CP_ACCESS_TOKEN"; + private static final String ACCESS_TOKEN_EXPIRES_TIME_KEY = "WX_CP_ACCESS_TOKEN_EXPIRES_TIME"; + private static final String JS_API_TICKET_KEY = "WX_CP_JS_API_TICKET"; + private static final String JS_API_TICKET_EXPIRES_TIME_KEY = "WX_CP_JS_API_TICKET_EXPIRES_TIME"; + /* Redis clients pool */ + private final JedisPool jedisPool; + private volatile String corpId; + private volatile String corpSecret; + private volatile String token; + private volatile String aesKey; + private volatile Integer agentId; + private volatile String oauth2redirectUri; + private volatile String httpProxyHost; + private volatile int httpProxyPort; + private volatile String httpProxyUsername; + private volatile String httpProxyPassword; + private volatile File tmpDirFile; + private volatile ApacheHttpClientBuilder apacheHttpClientBuilder; + + public WxCpJedisConfigStorage(String host, int port) { + this.jedisPool = new JedisPool(host, port); + } + + /** + * This method will be destroy jedis pool + */ + public void destroy() { + this.jedisPool.destroy(); + } + + @Override + public String getAccessToken() { + try (Jedis jedis = this.jedisPool.getResource()) { + return jedis.get(ACCESS_TOKEN_KEY); + } + } + + @Override + public boolean isAccessTokenExpired() { + try (Jedis jedis = this.jedisPool.getResource()) { + String expiresTimeStr = jedis.get(ACCESS_TOKEN_EXPIRES_TIME_KEY); + + if (expiresTimeStr != null) { + Long expiresTime = Long.parseLong(expiresTimeStr); + return System.currentTimeMillis() > expiresTime; + } + + return true; + + } + } + + @Override + public void expireAccessToken() { + try (Jedis jedis = this.jedisPool.getResource()) { + jedis.set(ACCESS_TOKEN_EXPIRES_TIME_KEY, "0"); + } + } + + @Override + public synchronized void updateAccessToken(WxAccessToken accessToken) { + this.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } + + @Override + public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) { + try (Jedis jedis = this.jedisPool.getResource()) { + jedis.set(ACCESS_TOKEN_KEY, accessToken); + + jedis.set(ACCESS_TOKEN_EXPIRES_TIME_KEY, + (System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L) + ""); + } + } + + @Override + public String getJsapiTicket() { + try (Jedis jedis = this.jedisPool.getResource()) { + return jedis.get(JS_API_TICKET_KEY); + } + } + + @Override + public boolean isJsapiTicketExpired() { + + try (Jedis jedis = this.jedisPool.getResource()) { + String expiresTimeStr = jedis.get(JS_API_TICKET_EXPIRES_TIME_KEY); + + if (expiresTimeStr != null) { + Long expiresTime = Long.parseLong(expiresTimeStr); + return System.currentTimeMillis() > expiresTime; + } + + return true; + + } + } + + @Override + public void expireJsapiTicket() { + try (Jedis jedis = this.jedisPool.getResource()) { + jedis.set(JS_API_TICKET_EXPIRES_TIME_KEY, "0"); + } + } + + @Override + public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) { + + try (Jedis jedis = this.jedisPool.getResource()) { + jedis.set(JS_API_TICKET_KEY, jsapiTicket); + + jedis.set(JS_API_TICKET_EXPIRES_TIME_KEY, + (System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L + "")); + } + + } + + @Override + public String getCorpId() { + return this.corpId; + } + + public void setCorpId(String corpId) { + this.corpId = corpId; + } + + @Override + public String getCorpSecret() { + return this.corpSecret; + } + + public void setCorpSecret(String corpSecret) { + this.corpSecret = corpSecret; + } + + @Override + public Integer getAgentId() { + return this.agentId; + } + + public void setAgentId(Integer agentId) { + this.agentId = agentId; + } + + @Override + public String getToken() { + return this.token; + } + + public void setToken(String token) { + this.token = token; + } + + @Override + public String getAesKey() { + return this.aesKey; + } + + public void setAesKey(String aesKey) { + this.aesKey = aesKey; + } + + @Override + public long getExpiresTime() { + try (Jedis jedis = this.jedisPool.getResource()) { + String expiresTimeStr = jedis.get(ACCESS_TOKEN_EXPIRES_TIME_KEY); + + if (expiresTimeStr != null) { + Long expiresTime = Long.parseLong(expiresTimeStr); + return expiresTime; + } + + return 0L; + + } + } + + @Override + public String getOauth2redirectUri() { + return this.oauth2redirectUri; + } + + public void setOauth2redirectUri(String oauth2redirectUri) { + this.oauth2redirectUri = oauth2redirectUri; + } + + @Override + public String getHttpProxyHost() { + return this.httpProxyHost; + } + + public void setHttpProxyHost(String httpProxyHost) { + this.httpProxyHost = httpProxyHost; + } + + @Override + public int getHttpProxyPort() { + return this.httpProxyPort; + } + + public void setHttpProxyPort(int httpProxyPort) { + this.httpProxyPort = httpProxyPort; + } + + @Override + public String getHttpProxyUsername() { + return this.httpProxyUsername; + } + + // ============================ Setters below + + public void setHttpProxyUsername(String httpProxyUsername) { + this.httpProxyUsername = httpProxyUsername; + } + + @Override + public String getHttpProxyPassword() { + return this.httpProxyPassword; + } + + public void setHttpProxyPassword(String httpProxyPassword) { + this.httpProxyPassword = httpProxyPassword; + } + + @Override + public File getTmpDirFile() { + return this.tmpDirFile; + } + + public void setTmpDirFile(File tmpDirFile) { + this.tmpDirFile = tmpDirFile; + } + + @Override + public ApacheHttpClientBuilder getApacheHttpClientBuilder() { + return this.apacheHttpClientBuilder; + } + + public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) { + this.apacheHttpClientBuilder = apacheHttpClientBuilder; + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageRouter.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageRouter.java index 6d4a8a97..da18e28e 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageRouter.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageRouter.java @@ -163,12 +163,12 @@ public class WxCpMessageRouter { // 返回最后一个非异步的rule的执行结果 if (rule.isAsync()) { futures.add( - this.executorService.submit(new Runnable() { - @Override - public void run() { - rule.service(wxMessage, WxCpMessageRouter.this.wxCpService, WxCpMessageRouter.this.sessionManager, WxCpMessageRouter.this.exceptionHandler); - } - }) + this.executorService.submit(new Runnable() { + @Override + public void run() { + rule.service(wxMessage, WxCpMessageRouter.this.wxCpService, WxCpMessageRouter.this.sessionManager, WxCpMessageRouter.this.exceptionHandler); + } + }) ); } else { res = rule.service(wxMessage, this.wxCpService, this.sessionManager, this.exceptionHandler); @@ -205,10 +205,10 @@ public class WxCpMessageRouter { String messageId = ""; if (wxMessage.getMsgId() == null) { messageId = String.valueOf(wxMessage.getCreateTime()) - + "-" + String.valueOf(wxMessage.getAgentId() == null ? "" : wxMessage.getAgentId()) - + "-" + wxMessage.getFromUserName() - + "-" + String.valueOf(wxMessage.getEventKey() == null ? "" : wxMessage.getEventKey()) - + "-" + String.valueOf(wxMessage.getEvent() == null ? "" : wxMessage.getEvent()) + + "-" + String.valueOf(wxMessage.getAgentId() == null ? "" : wxMessage.getAgentId()) + + "-" + wxMessage.getFromUserName() + + "-" + String.valueOf(wxMessage.getEventKey() == null ? "" : wxMessage.getEventKey()) + + "-" + String.valueOf(wxMessage.getEvent() == null ? "" : wxMessage.getEvent()) ; } else { messageId = String.valueOf(wxMessage.getMsgId()); diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageRouterRule.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageRouterRule.java index c81f045f..7bd332ad 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageRouterRule.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageRouterRule.java @@ -202,24 +202,24 @@ public class WxCpMessageRouterRule { protected boolean test(WxCpXmlMessage wxMessage) { return - (this.fromUser == null || this.fromUser.equals(wxMessage.getFromUserName())) - && - (this.agentId == null || this.agentId.equals(wxMessage.getAgentId())) - && - (this.msgType == null || this.msgType.equals(wxMessage.getMsgType())) - && - (this.event == null || this.event.equals(wxMessage.getEvent())) - && - (this.eventKey == null || this.eventKey.equals(wxMessage.getEventKey())) - && - (this.content == null || this.content - .equals(wxMessage.getContent() == null ? null : wxMessage.getContent().trim())) - && - (this.rContent == null || Pattern - .matches(this.rContent, wxMessage.getContent() == null ? "" : wxMessage.getContent().trim())) - && - (this.matcher == null || this.matcher.match(wxMessage)) - ; + (this.fromUser == null || this.fromUser.equals(wxMessage.getFromUserName())) + && + (this.agentId == null || this.agentId.equals(wxMessage.getAgentId())) + && + (this.msgType == null || this.msgType.equals(wxMessage.getMsgType())) + && + (this.event == null || this.event.equals(wxMessage.getEvent())) + && + (this.eventKey == null || this.eventKey.equals(wxMessage.getEventKey())) + && + (this.content == null || this.content + .equals(wxMessage.getContent() == null ? null : wxMessage.getContent().trim())) + && + (this.rContent == null || Pattern + .matches(this.rContent, wxMessage.getContent() == null ? "" : wxMessage.getContent().trim())) + && + (this.matcher == null || this.matcher.match(wxMessage)) + ; } /** diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java index e6f8f2ad..b5bf50c9 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java @@ -160,7 +160,7 @@ public interface WxCpService { * * @param menu * @throws WxErrorException - * @see #menuCreate(String, me.chanjar.weixin.common.bean.menu.WxMenu) + * @see #menuCreate(Integer, WxMenu) */ void menuCreate(WxMenu menu) throws WxErrorException; @@ -177,7 +177,7 @@ public interface WxCpService { * @throws WxErrorException * @see #menuCreate(me.chanjar.weixin.common.bean.menu.WxMenu) */ - void menuCreate(String agentId, WxMenu menu) throws WxErrorException; + void menuCreate(Integer agentId, WxMenu menu) throws WxErrorException; /** *
@@ -188,7 +188,7 @@ public interface WxCpService {
    * 
* * @throws WxErrorException - * @see #menuDelete(String) + * @see #menuDelete(Integer) */ void menuDelete() throws WxErrorException; @@ -204,7 +204,7 @@ public interface WxCpService { * @throws WxErrorException * @see #menuDelete() */ - void menuDelete(String agentId) throws WxErrorException; + void menuDelete(Integer agentId) throws WxErrorException; /** *
@@ -215,7 +215,7 @@ public interface WxCpService {
    * 
* * @throws WxErrorException - * @see #menuGet(String) + * @see #menuGet(Integer) */ WxMenu menuGet() throws WxErrorException; @@ -231,7 +231,7 @@ public interface WxCpService { * @throws WxErrorException * @see #menuGet() */ - WxMenu menuGet(String agentId) throws WxErrorException; + WxMenu menuGet(Integer agentId) throws WxErrorException; /** *
@@ -396,12 +396,12 @@ public interface WxCpService {
    *  
    * 构造oauth2授权的url连接
    * 
- * + * * @param state * @return url */ String oauth2buildAuthorizationUrl(String state); - + /** *
    * 构造oauth2授权的url连接
@@ -425,7 +425,7 @@ public interface WxCpService {
    *
    * @param code
    * @return [userid, deviceid]
-   * @see #oauth2getUserInfo(String, String)
+   * @see #oauth2getUserInfo(Integer, String)
    */
   String[] oauth2getUserInfo(String code) throws WxErrorException;
 
@@ -443,7 +443,7 @@ public interface WxCpService {
    * @return [userid, deviceid]
    * @see #oauth2getUserInfo(String)
    */
-  String[] oauth2getUserInfo(String agentId, String code) throws WxErrorException;
+  String[] oauth2getUserInfo(Integer agentId, String code) throws WxErrorException;
 
 
   /**
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java
index da95cab0..d44bbf5f 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpServiceImpl.java
@@ -12,7 +12,6 @@ import me.chanjar.weixin.common.session.StandardSessionManager;
 import me.chanjar.weixin.common.session.WxSession;
 import me.chanjar.weixin.common.session.WxSessionManager;
 import me.chanjar.weixin.common.util.RandomUtils;
-import me.chanjar.weixin.common.util.StringUtils;
 import me.chanjar.weixin.common.util.crypto.SHA1;
 import me.chanjar.weixin.common.util.fs.FileUtils;
 import me.chanjar.weixin.common.util.http.*;
@@ -22,8 +21,8 @@ import me.chanjar.weixin.cp.bean.WxCpMessage;
 import me.chanjar.weixin.cp.bean.WxCpTag;
 import me.chanjar.weixin.cp.bean.WxCpUser;
 import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.http.HttpHost;
-import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
@@ -69,7 +68,7 @@ public class WxCpServiceImpl implements WxCpService {
   public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) {
     try {
       return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data)
-          .equals(msgSignature);
+        .equals(msgSignature);
     } catch (Exception e) {
       return false;
     }
@@ -95,18 +94,18 @@ public class WxCpServiceImpl implements WxCpService {
       synchronized (this.globalAccessTokenRefreshLock) {
         if (this.configStorage.isAccessTokenExpired()) {
           String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?"
- + "&corpid=" + this.configStorage.getCorpId()
-              + "&corpsecret=" + this.configStorage.getCorpSecret();
+            + "&corpid=" + this.configStorage.getCorpId()
+            + "&corpsecret=" + this.configStorage.getCorpSecret();
           try {
             HttpGet httpGet = new HttpGet(url);
             if (this.httpProxy != null) {
               RequestConfig config = RequestConfig.custom()
-                  .setProxy(this.httpProxy).build();
+                .setProxy(this.httpProxy).build();
               httpGet.setConfig(config);
             }
             String resultContent = null;
             try (CloseableHttpClient httpclient = getHttpclient();
-                CloseableHttpResponse response = httpclient.execute(httpGet)) {
+                 CloseableHttpResponse response = httpclient.execute(httpGet)) {
               resultContent = new BasicResponseHandler().handleResponse(response);
             } finally {
               httpGet.releaseConnection();
@@ -117,9 +116,7 @@ public class WxCpServiceImpl implements WxCpService {
             }
             WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
             this.configStorage.updateAccessToken(
-                accessToken.getAccessToken(), accessToken.getExpiresIn());
-          } catch (ClientProtocolException e) {
-            throw new RuntimeException(e);
+              accessToken.getAccessToken(), accessToken.getExpiresIn());
           } catch (IOException e) {
             throw new RuntimeException(e);
           }
@@ -149,7 +146,7 @@ public class WxCpServiceImpl implements WxCpService {
           String jsapiTicket = tmpJsonObject.get("ticket").getAsString();
           int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt();
           this.configStorage.updateJsapiTicket(jsapiTicket,
-              expiresInSeconds);
+            expiresInSeconds);
         }
       }
     }
@@ -162,10 +159,10 @@ public class WxCpServiceImpl implements WxCpService {
     String noncestr = RandomUtils.getRandomStr();
     String jsapiTicket = getJsapiTicket(false);
     String signature = SHA1.genWithAmple(
-            "jsapi_ticket=" + jsapiTicket,
-            "noncestr=" + noncestr,
-            "timestamp=" + timestamp,
-            "url=" + url
+      "jsapi_ticket=" + jsapiTicket,
+      "noncestr=" + noncestr,
+      "timestamp=" + timestamp,
+      "url=" + url
     );
     WxJsapiSignature jsapiSignature = new WxJsapiSignature();
     jsapiSignature.setTimestamp(timestamp);
@@ -191,9 +188,9 @@ public class WxCpServiceImpl implements WxCpService {
   }
 
   @Override
-  public void menuCreate(String agentId, WxMenu menu) throws WxErrorException {
+  public void menuCreate(Integer agentId, WxMenu menu) throws WxErrorException {
     String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/create?agentid="
- + this.configStorage.getAgentId();
+      + this.configStorage.getAgentId();
     post(url, menu.toJson());
   }
 
@@ -203,7 +200,7 @@ public class WxCpServiceImpl implements WxCpService {
   }
 
   @Override
-  public void menuDelete(String agentId) throws WxErrorException {
+  public void menuDelete(Integer agentId) throws WxErrorException {
     String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/delete?agentid=" + agentId;
     get(url, null);
   }
@@ -214,7 +211,7 @@ public class WxCpServiceImpl implements WxCpService {
   }
 
   @Override
-  public WxMenu menuGet(String agentId) throws WxErrorException {
+  public WxMenu menuGet(Integer agentId) throws WxErrorException {
     String url = "https://qyapi.weixin.qq.com/cgi-bin/menu/get?agentid=" + agentId;
     try {
       String resultContent = get(url, null);
@@ -230,7 +227,7 @@ public class WxCpServiceImpl implements WxCpService {
 
   @Override
   public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream)
-          throws WxErrorException, IOException {
+    throws WxErrorException, IOException {
     return mediaUpload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
   }
 
@@ -244,9 +241,9 @@ public class WxCpServiceImpl implements WxCpService {
   public File mediaDownload(String media_id) throws WxErrorException {
     String url = "https://qyapi.weixin.qq.com/cgi-bin/media/get";
     return execute(
-        new MediaDownloadRequestExecutor(
-this.configStorage.getTmpDirFile()),
-        url, "media_id=" + media_id);
+      new MediaDownloadRequestExecutor(
+        this.configStorage.getTmpDirFile()),
+      url, "media_id=" + media_id);
   }
 
 
@@ -254,9 +251,9 @@ this.configStorage.getTmpDirFile()),
   public Integer departCreate(WxCpDepart depart) throws WxErrorException {
     String url = "https://qyapi.weixin.qq.com/cgi-bin/department/create";
     String responseContent = execute(
-            new SimplePostRequestExecutor(),
-            url,
-            depart.toJson());
+      new SimplePostRequestExecutor(),
+      url,
+      depart.toJson());
     JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
     return GsonHelper.getAsInteger(tmpJsonElement.getAsJsonObject().get("id"));
   }
@@ -283,11 +280,11 @@ this.configStorage.getTmpDirFile()),
      */
     JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
     return WxCpGsonBuilder.INSTANCE.create()
-            .fromJson(
-                    tmpJsonElement.getAsJsonObject().get("department"),
-                    new TypeToken>() {
-                    }.getType()
-            );
+      .fromJson(
+        tmpJsonElement.getAsJsonObject().get("department"),
+        new TypeToken>() {
+        }.getType()
+      );
   }
 
   @Override
@@ -313,8 +310,8 @@ this.configStorage.getTmpDirFile()),
     String url = "https://qyapi.weixin.qq.com/cgi-bin/user/batchdelete";
     JsonObject jsonObject = new JsonObject();
     JsonArray jsonArray = new JsonArray();
-    for (int i = 0; i < userids.length; i++) {
-      jsonArray.add(new JsonPrimitive(userids[i]));
+    for (String userid : userids) {
+      jsonArray.add(new JsonPrimitive(userid));
     }
     jsonObject.add("useridlist", jsonArray);
     post(url, jsonObject.toString());
@@ -343,11 +340,11 @@ this.configStorage.getTmpDirFile()),
     String responseContent = get(url, params);
     JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
     return WxCpGsonBuilder.INSTANCE.create()
-            .fromJson(
-                    tmpJsonElement.getAsJsonObject().get("userlist"),
-                    new TypeToken>() {
-                    }.getType()
-            );
+      .fromJson(
+        tmpJsonElement.getAsJsonObject().get("userlist"),
+        new TypeToken>() {
+        }.getType()
+      );
   }
 
   @Override
@@ -366,11 +363,11 @@ this.configStorage.getTmpDirFile()),
     String responseContent = get(url, params);
     JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
     return WxCpGsonBuilder.INSTANCE.create()
-            .fromJson(
-                    tmpJsonElement.getAsJsonObject().get("userlist"),
-                    new TypeToken>() {
-                    }.getType()
-            );
+      .fromJson(
+        tmpJsonElement.getAsJsonObject().get("userlist"),
+        new TypeToken>() {
+        }.getType()
+      );
   }
 
   @Override
@@ -404,11 +401,11 @@ this.configStorage.getTmpDirFile()),
     String responseContent = get(url, null);
     JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
     return WxCpGsonBuilder.INSTANCE.create()
-            .fromJson(
-                    tmpJsonElement.getAsJsonObject().get("taglist"),
-                    new TypeToken>() {
-                    }.getType()
-            );
+      .fromJson(
+        tmpJsonElement.getAsJsonObject().get("taglist"),
+        new TypeToken>() {
+        }.getType()
+      );
   }
 
   @Override
@@ -417,11 +414,11 @@ this.configStorage.getTmpDirFile()),
     String responseContent = get(url, null);
     JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
     return WxCpGsonBuilder.INSTANCE.create()
-            .fromJson(
-                    tmpJsonElement.getAsJsonObject().get("userlist"),
-                    new TypeToken>() {
-                    }.getType()
-            );
+      .fromJson(
+        tmpJsonElement.getAsJsonObject().get("userlist"),
+        new TypeToken>() {
+        }.getType()
+      );
   }
 
   @Override
@@ -460,14 +457,14 @@ this.configStorage.getTmpDirFile()),
   }
 
   @Override
-	public String oauth2buildAuthorizationUrl(String state) {
-  	return this.oauth2buildAuthorizationUrl(
-this.configStorage.getOauth2redirectUri(),
-  		state
-  	);
-	}
+  public String oauth2buildAuthorizationUrl(String state) {
+    return this.oauth2buildAuthorizationUrl(
+      this.configStorage.getOauth2redirectUri(),
+      state
+    );
+  }
 
-	@Override
+  @Override
   public String oauth2buildAuthorizationUrl(String redirectUri, String state) {
     String url = "https://open.weixin.qq.com/connect/oauth2/authorize?";
     url += "appid=" + this.configStorage.getCorpId();
@@ -487,10 +484,10 @@ this.configStorage.getOauth2redirectUri(),
   }
 
   @Override
-  public String[] oauth2getUserInfo(String agentId, String code) throws WxErrorException {
+  public String[] oauth2getUserInfo(Integer agentId, String code) throws WxErrorException {
     String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?"
-            + "code=" + code
-            + "&agentid=" + agentId;
+      + "code=" + code
+      + "&agentid=" + agentId;
     String responseText = get(url, null);
     JsonElement je = new JsonParser().parse(responseText);
     JsonObject jo = je.getAsJsonObject();
@@ -544,14 +541,14 @@ this.configStorage.getOauth2redirectUri(),
         return executeInternal(executor, uri, data);
       } catch (WxErrorException e) {
         WxError error = e.getError();
-        /**
+        /*
          * -1 系统繁忙, 1000ms后重试
          */
         if (error.getErrorCode() == -1) {
           int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
           try {
             this.log.debug("微信系统繁忙,{}ms 后重试(第{}次)", sleepMillis,
-                retryTimes + 1);
+              retryTimes + 1);
             Thread.sleep(sleepMillis);
           } catch (InterruptedException e1) {
             throw new RuntimeException(e1);
@@ -566,7 +563,7 @@ this.configStorage.getOauth2redirectUri(),
   }
 
   protected synchronized  T executeInternal(RequestExecutor executor, String uri, E data) throws WxErrorException {
-    if (uri.indexOf("access_token=") != -1) {
+    if (uri.contains("access_token=")) {
       throw new IllegalArgumentException("uri参数中不允许有access_token: " + uri);
     }
     String accessToken = getAccessToken(false);
@@ -576,7 +573,7 @@ this.configStorage.getOauth2redirectUri(),
 
     try {
       return executor.execute(getHttpclient(), this.httpProxy,
-          uriWithAccessToken, data);
+        uriWithAccessToken, data);
     } catch (WxErrorException e) {
       WxError error = e.getError();
       /*
@@ -593,8 +590,6 @@ this.configStorage.getOauth2redirectUri(),
         throw new WxErrorException(error);
       }
       return null;
-    } catch (ClientProtocolException e) {
-      throw new RuntimeException(e);
     } catch (IOException e) {
       throw new RuntimeException(e);
     }
@@ -608,15 +603,15 @@ this.configStorage.getOauth2redirectUri(),
   public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider) {
     this.configStorage = wxConfigProvider;
     ApacheHttpClientBuilder apacheHttpClientBuilder = this.configStorage
-        .getApacheHttpClientBuilder();
+      .getApacheHttpClientBuilder();
     if (null == apacheHttpClientBuilder) {
       apacheHttpClientBuilder = DefaultApacheHttpClientBuilder.get();
     }
 
     apacheHttpClientBuilder.httpProxyHost(this.configStorage.getHttpProxyHost())
-        .httpProxyPort(this.configStorage.getHttpProxyPort())
-        .httpProxyUsername(this.configStorage.getHttpProxyUsername())
-        .httpProxyPassword(this.configStorage.getHttpProxyPassword());
+      .httpProxyPort(this.configStorage.getHttpProxyPort())
+      .httpProxyUsername(this.configStorage.getHttpProxyUsername())
+      .httpProxyPassword(this.configStorage.getHttpProxyPassword());
 
     if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) {
       this.httpProxy = new HttpHost(this.configStorage.getHttpProxyHost(), this.configStorage.getHttpProxyPort());
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMessage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMessage.java
index e2422e3b..c25c170b 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMessage.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMessage.java
@@ -1,17 +1,12 @@
 package me.chanjar.weixin.cp.bean;
 
+import me.chanjar.weixin.cp.bean.messagebuilder.*;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 
-import me.chanjar.weixin.cp.bean.messagebuilder.FileBuilder;
-import me.chanjar.weixin.cp.bean.messagebuilder.ImageBuilder;
-import me.chanjar.weixin.cp.bean.messagebuilder.NewsBuilder;
-import me.chanjar.weixin.cp.bean.messagebuilder.TextBuilder;
-import me.chanjar.weixin.cp.bean.messagebuilder.VideoBuilder;
-import me.chanjar.weixin.cp.bean.messagebuilder.VoiceBuilder;
-import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
-
 /**
  * 消息
  *
@@ -23,7 +18,7 @@ public class WxCpMessage implements Serializable {
   private String toUser;
   private String toParty;
   private String toTag;
-  private String agentId;
+  private Integer agentId;
   private String msgType;
   private String content;
   private String mediaId;
@@ -101,11 +96,11 @@ public class WxCpMessage implements Serializable {
     this.toTag = toTag;
   }
 
-  public String getAgentId() {
+  public Integer getAgentId() {
     return this.agentId;
   }
 
-  public void setAgentId(String agentId) {
+  public void setAgentId(Integer agentId) {
     this.agentId = agentId;
   }
 
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/messagebuilder/BaseBuilder.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/messagebuilder/BaseBuilder.java
index 035777d8..7800f1c6 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/messagebuilder/BaseBuilder.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/messagebuilder/BaseBuilder.java
@@ -5,13 +5,13 @@ import me.chanjar.weixin.cp.bean.WxCpMessage;
 
 public class BaseBuilder {
   protected String msgType;
-  protected String agentId;
+  protected Integer agentId;
   protected String toUser;
   protected String toParty;
   protected String toTag;
   protected String safe;
 
-  public T agentId(String agentId) {
+  public T agentId(Integer agentId) {
     this.agentId = agentId;
     return (T) this;
   }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpMessageGsonAdapter.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpMessageGsonAdapter.java
index 322658c2..7bf09c71 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpMessageGsonAdapter.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/json/WxCpMessageGsonAdapter.java
@@ -10,8 +10,8 @@ package me.chanjar.weixin.cp.util.json;
 
 import com.google.gson.*;
 import me.chanjar.weixin.common.api.WxConsts;
-import me.chanjar.weixin.common.util.StringUtils;
 import me.chanjar.weixin.cp.bean.WxCpMessage;
+import org.apache.commons.lang3.StringUtils;
 
 import java.lang.reflect.Type;
 
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpBaseAPITest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpBaseAPITest.java
index d2eb2ec6..894bb3b6 100644
--- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpBaseAPITest.java
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpBaseAPITest.java
@@ -1,14 +1,12 @@
 package me.chanjar.weixin.cp.api;
 
+import com.google.inject.Inject;
+import me.chanjar.weixin.common.exception.WxErrorException;
+import org.apache.commons.lang3.StringUtils;
 import org.testng.Assert;
 import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
 
-import com.google.inject.Inject;
-
-import me.chanjar.weixin.common.exception.WxErrorException;
-import me.chanjar.weixin.common.util.StringUtils;
-
 /**
  * 基础API测试
  *
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpEndpointServlet.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpEndpointServlet.java
index d59eb3f3..43bf1799 100644
--- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpEndpointServlet.java
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/demo/WxCpEndpointServlet.java
@@ -1,18 +1,17 @@
 package me.chanjar.weixin.cp.demo;
 
-import java.io.IOException;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import me.chanjar.weixin.common.util.StringUtils;
 import me.chanjar.weixin.cp.api.WxCpConfigStorage;
 import me.chanjar.weixin.cp.api.WxCpMessageRouter;
 import me.chanjar.weixin.cp.api.WxCpService;
 import me.chanjar.weixin.cp.bean.WxCpXmlMessage;
 import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
 import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
 
 /**
  * @author Daniel Qian
diff --git a/weixin-java-mp/build.gradle b/weixin-java-mp/build.gradle
index dcc01548..de1534a3 100644
--- a/weixin-java-mp/build.gradle
+++ b/weixin-java-mp/build.gradle
@@ -6,8 +6,8 @@ dependencies {
     testCompile group: 'org.testng', name: 'testng', version:'6.8.7'
     testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5'
     testCompile group: 'com.google.inject', name: 'guice', version:'3.0'
-    testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version:'9.3.0.M0'
-    testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version:'9.3.0.M0'
+    testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version:'9.3.0.RC0'
+    testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version:'9.3.0.RC0'
     testCompile group: 'joda-time', name: 'joda-time', version:'2.9.4'
 }
 test.useTestNG()
diff --git a/weixin-java-mp/pom.xml b/weixin-java-mp/pom.xml
index a06184a8..58b634ec 100644
--- a/weixin-java-mp/pom.xml
+++ b/weixin-java-mp/pom.xml
@@ -6,7 +6,7 @@
     
         com.github.binarywang
         weixin-java-parent
-        2.3.0-SNAPSHOT
+        2.4.0-SNAPSHOT
     
     weixin-java-mp
     WeiXin Java Tools - MP
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java
index 3a2e7654..78b1a587 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpConfigStorage.java
@@ -5,6 +5,7 @@ import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder;
 
 import javax.net.ssl.SSLContext;
 import java.io.File;
+import java.util.concurrent.locks.Lock;
 
 /**
  * 微信客户端配置存储
@@ -15,6 +16,8 @@ public interface WxMpConfigStorage {
 
   String getAccessToken();
 
+  Lock getAccessTokenLock();
+
   boolean isAccessTokenExpired();
 
   /**
@@ -24,19 +27,21 @@ public interface WxMpConfigStorage {
 
   /**
    * 应该是线程安全的
-   * @param accessToken
+   * @param accessToken 要更新的WxAccessToken对象
    */
   void updateAccessToken(WxAccessToken accessToken);
 
   /**
    * 应该是线程安全的
-   * @param accessToken
-   * @param expiresIn
+   * @param accessToken 新的accessToken值
+   * @param expiresInSeconds 过期时间,以秒为单位
    */
-  void updateAccessToken(String accessToken, int expiresIn);
+  void updateAccessToken(String accessToken, int expiresInSeconds);
 
   String getJsapiTicket();
 
+  Lock getJsapiTicketLock();
+
   boolean isJsapiTicketExpired();
 
   /**
@@ -46,12 +51,15 @@ public interface WxMpConfigStorage {
 
   /**
    * 应该是线程安全的
-   * @param jsapiTicket
+   * @param jsapiTicket 新的jsapi ticket值
+   * @param expiresInSeconds 过期时间,以秒为单位
    */
   void updateJsapiTicket(String jsapiTicket, int expiresInSeconds);
 
   String getCardApiTicket();
 
+  Lock getCardApiTicketLock();
+
   boolean isCardApiTicketExpired();
 
   /**
@@ -61,7 +69,8 @@ public interface WxMpConfigStorage {
 
   /**
    * 应该是线程安全的
-   * @param cardApiTicket
+   * @param cardApiTicket 新的cardApi ticket值
+   * @param expiresInSeconds 过期时间,以秒为单位
    */
   void updateCardApiTicket(String cardApiTicket, int expiresInSeconds);
 
@@ -70,7 +79,7 @@ public interface WxMpConfigStorage {
   String getSecret();
 
   String getPartnerId();
-  
+
   String getPartnerKey();
 
   String getToken();
@@ -88,7 +97,7 @@ public interface WxMpConfigStorage {
   String getHttpProxyUsername();
 
   String getHttpProxyPassword();
-  
+
   File getTmpDirFile();
 
   SSLContext getSSLContext();
@@ -98,4 +107,9 @@ public interface WxMpConfigStorage {
    * @return ApacheHttpClientBuilder
    */
   ApacheHttpClientBuilder getApacheHttpClientBuilder();
+
+  /**
+   * 是否自动刷新token
+   */
+  boolean autoRefreshToken();
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java
index 784cfddc..e874e793 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpInMemoryConfigStorage.java
@@ -1,14 +1,13 @@
 package me.chanjar.weixin.mp.api;
 
 import me.chanjar.weixin.common.bean.WxAccessToken;
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.common.util.http.ApacheHttpClientBuilder;
 
 import javax.net.ssl.SSLContext;
-
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import java.io.File;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化
@@ -39,11 +38,15 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
   protected volatile String cardApiTicket;
   protected volatile long cardApiTicketExpiresTime;
 
+  protected Lock accessTokenLock = new ReentrantLock();
+  protected Lock jsapiTicketLock = new ReentrantLock();
+  protected Lock cardApiTicketLock = new ReentrantLock();
+
   /**
    * 临时文件目录
    */
   protected volatile File tmpDirFile;
-  
+
   protected volatile SSLContext sslContext;
 
   protected volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
@@ -53,6 +56,11 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
     return this.accessToken;
   }
 
+  @Override
+  public Lock getAccessTokenLock() {
+    return this.accessTokenLock;
+  }
+
   @Override
   public boolean isAccessTokenExpired() {
     return System.currentTimeMillis() > this.expiresTime;
@@ -62,11 +70,11 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
   public synchronized void updateAccessToken(WxAccessToken accessToken) {
     updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
   }
-  
+
   @Override
   public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
     this.accessToken = accessToken;
-    this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l;
+    this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
   }
 
   @Override
@@ -79,6 +87,11 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
     return this.jsapiTicket;
   }
 
+  @Override
+  public Lock getJsapiTicketLock() {
+    return this.jsapiTicketLock;
+  }
+
   public void setJsapiTicket(String jsapiTicket) {
     this.jsapiTicket = jsapiTicket;
   }
@@ -100,7 +113,7 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
   public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) {
     this.jsapiTicket = jsapiTicket;
     // 预留200秒的时间
-    this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l;
+    this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
   }
 
   @Override
@@ -116,6 +129,11 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
     return this.cardApiTicket;
   }
 
+  @Override
+  public Lock getCardApiTicketLock() {
+    return this.cardApiTicketLock;
+  }
+
   @Override
   public boolean isCardApiTicketExpired() {
     return System.currentTimeMillis() > this.cardApiTicketExpiresTime;
@@ -125,7 +143,7 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
   public synchronized void updateCardApiTicket(String cardApiTicket, int expiresInSeconds) {
     this.cardApiTicket = cardApiTicket;
     // 预留200秒的时间
-    this.cardApiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l;
+    this.cardApiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
   }
 
   @Override
@@ -229,7 +247,7 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this,ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
   @Override
@@ -263,7 +281,7 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
   public SSLContext getSSLContext() {
     return this.sslContext;
   }
-  
+
   public void setSSLContext(SSLContext context) {
     this.sslContext = context;
   }
@@ -273,6 +291,11 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage {
     return this.apacheHttpClientBuilder;
   }
 
+  @Override
+  public boolean autoRefreshToken() {
+    return true;
+  }
+
   public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) {
     this.apacheHttpClientBuilder = apacheHttpClientBuilder;
   }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpKefuService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpKefuService.java
index 5bad7da2..f57c9630 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpKefuService.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpKefuService.java
@@ -22,6 +22,7 @@ public interface WxMpKefuService {
    * 
    * 发送客服消息
    * 详情请见: 发送客服消息
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
    * 
*/ boolean sendKefuMessage(WxMpKefuMessage message) throws WxErrorException; diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMaterialService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMaterialService.java index 48a6f610..8c867c5f 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMaterialService.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMaterialService.java @@ -2,11 +2,12 @@ package me.chanjar.weixin.mp.api; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.common.exception.WxErrorException; -import me.chanjar.weixin.mp.bean.*; -import me.chanjar.weixin.mp.bean.result.*; +import me.chanjar.weixin.mp.bean.material.WxMpMaterial; +import me.chanjar.weixin.mp.bean.material.WxMpMaterialArticleUpdate; +import me.chanjar.weixin.mp.bean.material.WxMpMaterialNews; +import me.chanjar.weixin.mp.bean.material.*; import java.io.File; -import java.io.IOException; import java.io.InputStream; /** @@ -20,21 +21,58 @@ import java.io.InputStream; public interface WxMpMaterialService { /** + *
    * 新增临时素材
-   *
-   * @param mediaType
-   * @param file
+   * 公众号经常有需要用到一些临时性的多媒体素材的场景,例如在使用接口特别是发送消息时,对多媒体文件、多媒体消息的获取和调用等操作,是通过media_id来进行的。
+   * 素材管理接口对所有认证的订阅号和服务号开放。通过本接口,公众号可以新增临时素材(即上传临时多媒体文件)。
+   * 请注意:
+   *  1、对于临时素材,每个素材(media_id)会在开发者上传或粉丝发送到微信服务器3天后自动删除(所以用户发送给开发者的素材,若开发者需要,应尽快下载到本地),以节省服务器资源。
+   *  2、media_id是可复用的。
+   *  3、素材的格式大小等要求与公众平台官网一致。具体是,图片大小不超过2M,支持png/jpeg/jpg/gif格式,语音大小不超过5M,长度不超过60秒,支持mp3/amr格式
+   *  4、需使用https调用本接口。
+   *  本接口即为原“上传多媒体文件”接口。
+   *  注意事项:
+   *    上传的临时多媒体文件有格式和大小限制,如下:
+   *    图片(image): 2M,支持PNG\JPEG\JPG\GIF格式
+   *    语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
+   *    视频(video):10MB,支持MP4格式
+   *    缩略图(thumb):64KB,支持JPG格式
+   *媒体文件在后台保存时间为3天,即3天后media_id失效。
+   * 详情请见: 新增临时素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
+   * 
+ * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} + * @param file 文件对象 * @throws WxErrorException * @see #mediaUpload(String, String, InputStream) */ WxMediaUploadResult mediaUpload(String mediaType, File file) throws WxErrorException; + /** + *
+   * 新增临时素材
+   * 本接口即为原“上传多媒体文件”接口。
+   *
+   * 详情请见: 新增临时素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
+   * 
+ * + * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} + * @param fileType 文件类型,请看{@link me.chanjar.weixin.common.api.WxConsts} + * @param inputStream 输入流 + * @throws WxErrorException + * @see #mediaUpload(java.lang.String, java.io.File) + */ + WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException; + /** *
    * 获取临时素材
+   * 公众号可以使用本接口获取临时素材(即下载临时的多媒体文件)。请注意,视频文件不支持https下载,调用该接口需http协议。
    * 本接口即为原“下载多媒体文件”接口。
    * 根据微信文档,视频文件下载不了,会返回null
-   * 详情请见: 获取临时素材
+   * 详情请见: 获取临时素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
    * 
* * @param media_id @@ -46,10 +84,12 @@ public interface WxMpMaterialService { /** *
    * 上传图文消息内的图片获取URL
-   * 详情请见:http://mp.weixin.qq.com/wiki/15/40b6865b893947b764e2de8e4a1fb55f.html#.E4.B8.8A.E4.BC.A0.E5.9B.BE.E6.96.87.E6.B6.88.E6.81.AF.E5.86.85.E7.9A.84.E5.9B.BE.E7.89.87.E8.8E.B7.E5.8F.96URL.E3.80.90.E8.AE.A2.E9.98.85.E5.8F.B7.E4.B8.8E.E6.9C.8D.E5.8A.A1.E5.8F.B7.E8.AE.A4.E8.AF.81.E5.90.8E.E5.9D.87.E5.8F.AF.E7.94.A8.E3.80.91
+   * 请注意,本接口所上传的图片不占用公众号的素材库中图片数量的5000个的限制。图片仅支持jpg/png格式,大小必须在1MB以下。
+   * 详情请见: 新增永久素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=ACCESS_TOKEN
    * 
* - * @param file + * @param file 上传的文件对象 * @return WxMediaImgUploadResult 返回图片url * @throws WxErrorException */ @@ -57,114 +97,123 @@ public interface WxMpMaterialService { /** *
-   * 新增临时素材
-   * 本接口即为原“上传多媒体文件”接口。
-   *
-   * 上传的多媒体文件有格式和大小限制,如下:
-   *   图片(image): 1M,支持JPG格式
-   *   语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
-   *   视频(video):10MB,支持MP4格式
-   *   缩略图(thumb):64KB,支持JPG格式
-   *
-   * 详情请见: 新增临时素材
-   * 
- * - * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} - * @param fileType 文件类型,请看{@link me.chanjar.weixin.common.api.WxConsts} - * @param inputStream 输入流 - * @throws WxErrorException - */ - WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException; - - /** - *
-   * 上传非图文永久素材
-   *
-   * 上传的多媒体文件有格式和大小限制,如下:
-   *   图片(image): 图片大小不超过2M,支持bmp/png/jpeg/jpg/gif格式
-   *   语音(voice):语音大小不超过5M,长度不超过60秒,支持mp3/wma/wav/amr格式
-   *   视频(video):在上传视频素材时需要POST另一个表单,id为description,包含素材的描述信息,内容格式为JSON
-   *   缩略图(thumb):文档未说明
-   *
-   * 详情请见: http://mp.weixin.qq.com/wiki/14/7e6c03263063f4813141c3e17dd4350a.html
+   * 新增非图文永久素材
+   * 通过POST表单来调用接口,表单id为media,包含需要上传的素材内容,有filename、filelength、content-type等信息。请注意:图片素材将进入公众平台官网素材管理模块中的默认分组。
+   * 新增永久视频素材需特别注意:
+   * 在上传视频素材时需要POST另一个表单,id为description,包含素材的描述信息,内容格式为JSON,格式如下:
+   * {   "title":VIDEO_TITLE,   "introduction":INTRODUCTION   }
+   * 详情请见: 新增永久素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN&type=TYPE
+   *
+   * 除了3天就会失效的临时素材外,开发者有时需要永久保存一些素材,届时就可以通过本接口新增永久素材。
+   永久图片素材新增后,将带有URL返回给开发者,开发者可以在腾讯系域名内使用(腾讯系域名外使用,图片将被屏蔽)。
+   请注意:
+   1、新增的永久素材也可以在公众平台官网素材管理模块中看到
+   2、永久素材的数量是有上限的,请谨慎新增。图文消息素材和图片素材的上限为5000,其他类型为1000
+   3、素材的格式大小等要求与公众平台官网一致。具体是,图片大小不超过2M,支持bmp/png/jpeg/jpg/gif格式,语音大小不超过5M,长度不超过60秒,支持mp3/wma/wav/amr格式
+   4、调用该接口需https协议
    * 
* * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} - * @param material 上传的素材, 请看{@link me.chanjar.weixin.mp.bean.WxMpMaterial} + * @param material 上传的素材, 请看{@link WxMpMaterial} */ WxMpMaterialUploadResult materialFileUpload(String mediaType, WxMpMaterial material) throws WxErrorException; /** *
-   * 上传永久图文素材
-   *
-   * 详情请见: http://mp.weixin.qq.com/wiki/14/7e6c03263063f4813141c3e17dd4350a.html
+   * 新增永久图文素材
+   *
+   * 详情请见: 新增永久素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=ACCESS_TOKEN
+   *
+   * 除了3天就会失效的临时素材外,开发者有时需要永久保存一些素材,届时就可以通过本接口新增永久素材。
+   永久图片素材新增后,将带有URL返回给开发者,开发者可以在腾讯系域名内使用(腾讯系域名外使用,图片将被屏蔽)。
+   请注意:
+   1、新增的永久素材也可以在公众平台官网素材管理模块中看到
+   2、永久素材的数量是有上限的,请谨慎新增。图文消息素材和图片素材的上限为5000,其他类型为1000
+   3、素材的格式大小等要求与公众平台官网一致。具体是,图片大小不超过2M,支持bmp/png/jpeg/jpg/gif格式,语音大小不超过5M,长度不超过60秒,支持mp3/wma/wav/amr格式
+   4、调用该接口需https协议
    * 
* - * @param news 上传的图文消息, 请看{@link me.chanjar.weixin.mp.bean.WxMpMaterialNews} + * @param news 上传的图文消息, 请看{@link WxMpMaterialNews} */ WxMpMaterialUploadResult materialNewsUpload(WxMpMaterialNews news) throws WxErrorException; /** *
-   * 下载声音或者图片永久素材
+   * 获取声音或者图片永久素材
    *
-   * 详情请见: http://mp.weixin.qq.com/wiki/4/b3546879f07623cb30df9ca0e420a5d0.html
+   * 详情请见: 获取永久素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=ACCESS_TOKEN
    * 
* - * @param media_id 永久素材的id + * @param mediaId 永久素材的id */ - InputStream materialImageOrVoiceDownload(String media_id) throws WxErrorException; + InputStream materialImageOrVoiceDownload(String mediaId) throws WxErrorException; /** *
    * 获取视频永久素材的信息和下载地址
    *
-   * 详情请见: http://mp.weixin.qq.com/wiki/4/b3546879f07623cb30df9ca0e420a5d0.html
+   * 详情请见: 获取永久素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=ACCESS_TOKEN
    * 
* - * @param media_id 永久素材的id + * @param mediaId 永久素材的id */ - WxMpMaterialVideoInfoResult materialVideoInfo(String media_id) throws WxErrorException; + WxMpMaterialVideoInfoResult materialVideoInfo(String mediaId) throws WxErrorException; /** *
    * 获取图文永久素材的信息
    *
-   * 详情请见: http://mp.weixin.qq.com/wiki/4/b3546879f07623cb30df9ca0e420a5d0.html
+   * 详情请见: 获取永久素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/material/get_material?access_token=ACCESS_TOKEN
    * 
* - * @param media_id 永久素材的id + * @param mediaId 永久素材的id */ - WxMpMaterialNews materialNewsInfo(String media_id) throws WxErrorException; + WxMpMaterialNews materialNewsInfo(String mediaId) throws WxErrorException; /** *
-   * 更新图文永久素材
+   * 修改永久图文素材
    *
-   * 详情请见: http://mp.weixin.qq.com/wiki/4/19a59cba020d506e767360ca1be29450.html
+   * 详情请见: 修改永久图文素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/material/update_news?access_token=ACCESS_TOKEN
    * 
* - * @param wxMpMaterialArticleUpdate 用来更新图文素材的bean, 请看{@link me.chanjar.weixin.mp.bean.WxMpMaterialArticleUpdate} + * @param wxMpMaterialArticleUpdate 用来更新图文素材的bean, 请看{@link WxMpMaterialArticleUpdate} */ boolean materialNewsUpdate(WxMpMaterialArticleUpdate wxMpMaterialArticleUpdate) throws WxErrorException; /** *
    * 删除永久素材
-   *
-   * 详情请见: http://mp.weixin.qq.com/wiki/5/e66f61c303db51a6c0f90f46b15af5f5.html
+   * 在新增了永久素材后,开发者可以根据本接口来删除不再需要的永久素材,节省空间。
+   * 请注意:
+   *  1、请谨慎操作本接口,因为它可以删除公众号在公众平台官网素材管理模块中新建的图文消息、语音、视频等素材(但需要先通过获取素材列表来获知素材的media_id)
+   *  2、临时素材无法通过本接口删除
+   *  3、调用该接口需https协议
+   * 详情请见: 删除永久素材
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/material/del_material?access_token=ACCESS_TOKEN
    * 
* - * @param media_id 永久素材的id + * @param mediaId 永久素材的id */ - boolean materialDelete(String media_id) throws WxErrorException; + boolean materialDelete(String mediaId) throws WxErrorException; /** *
    * 获取各类素材总数
-   *
-   * 详情请见: http://mp.weixin.qq.com/wiki/16/8cc64f8c189674b421bee3ed403993b8.html
+   * 开发者可以根据本接口来获取永久素材的列表,需要时也可保存到本地。
+   * 请注意:
+   *  1.永久素材的总数,也会计算公众平台官网素材管理中的素材
+   *  2.图片和图文消息素材(包括单图文和多图文)的总数上限为5000,其他素材的总数上限为1000
+   *  3.调用该接口需https协议
+   *
+   * 详情请见: 获取素材总数
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/material/get_materialcount?access_token=ACCESS_TOKEN
    * 
*/ WxMpMaterialCountResult materialCount() throws WxErrorException; @@ -173,7 +222,8 @@ public interface WxMpMaterialService { *
    * 分页获取图文素材列表
    *
-   * 详情请见: http://mp.weixin.qq.com/wiki/12/2108cd7aafff7f388f41f37efa710204.html
+   * 详情请见: 获取素材列表
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=ACCESS_TOKEN
    * 
* * @param offset 从全部素材的该偏移位置开始返回,0表示从第一个素材 返回 @@ -185,7 +235,8 @@ public interface WxMpMaterialService { *
    * 分页获取其他媒体素材列表
    *
-   * 详情请见: http://mp.weixin.qq.com/wiki/12/2108cd7aafff7f388f41f37efa710204.html
+   * 详情请见: 获取素材列表
+   * 接口url格式:https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=ACCESS_TOKEN
    * 
* * @param type 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageHandler.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageHandler.java index dae2a238..5a336bc8 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageHandler.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageHandler.java @@ -2,8 +2,8 @@ package me.chanjar.weixin.mp.api; import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.common.session.WxSessionManager; -import me.chanjar.weixin.mp.bean.WxMpXmlMessage; -import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import java.util.Map; diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageInterceptor.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageInterceptor.java index 3fcd6d95..15223895 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageInterceptor.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageInterceptor.java @@ -2,7 +2,7 @@ package me.chanjar.weixin.mp.api; import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.common.session.WxSessionManager; -import me.chanjar.weixin.mp.bean.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; import java.util.Map; diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageMatcher.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageMatcher.java index 68798ae2..812992c9 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageMatcher.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageMatcher.java @@ -1,6 +1,6 @@ package me.chanjar.weixin.mp.api; -import me.chanjar.weixin.mp.bean.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; /** * 消息匹配器,用在消息路由的时候 diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageRouter.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageRouter.java index 7db50e79..24dc4451 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageRouter.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageRouter.java @@ -1,230 +1,230 @@ -package me.chanjar.weixin.mp.api; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import me.chanjar.weixin.common.api.WxErrorExceptionHandler; -import me.chanjar.weixin.common.api.WxMessageDuplicateChecker; -import me.chanjar.weixin.common.api.WxMessageInMemoryDuplicateChecker; -import me.chanjar.weixin.common.session.InternalSession; -import me.chanjar.weixin.common.session.InternalSessionManager; -import me.chanjar.weixin.common.session.StandardSessionManager; -import me.chanjar.weixin.common.session.WxSessionManager; -import me.chanjar.weixin.common.util.LogExceptionHandler; -import me.chanjar.weixin.mp.bean.WxMpXmlMessage; -import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; - -/** - *
- * 微信消息路由器,通过代码化的配置,把来自微信的消息交给handler处理
- * 
- * 说明:
- * 1. 配置路由规则时要按照从细到粗的原则,否则可能消息可能会被提前处理
- * 2. 默认情况下消息只会被处理一次,除非使用 {@link WxMpMessageRouterRule#next()}
- * 3. 规则的结束必须用{@link WxMpMessageRouterRule#end()}或者{@link WxMpMessageRouterRule#next()},否则不会生效
- * 
- * 使用方法:
- * WxMpMessageRouter router = new WxMpMessageRouter();
- * router
- *   .rule()
- *       .msgType("MSG_TYPE").event("EVENT").eventKey("EVENT_KEY").content("CONTENT")
- *       .interceptor(interceptor, ...).handler(handler, ...)
- *   .end()
- *   .rule()
- *       // 另外一个匹配规则
- *   .end()
- * ;
- * 
- * // 将WxXmlMessage交给消息路由器
- * router.route(message);
- * 
- * 
- * @author Daniel Qian - * - */ -public class WxMpMessageRouter { - - protected final Logger log = LoggerFactory.getLogger(WxMpMessageRouter.class); - - private static final int DEFAULT_THREAD_POOL_SIZE = 100; - - private final List rules = new ArrayList<>(); - - private final WxMpService wxMpService; - - private ExecutorService executorService; - - private WxMessageDuplicateChecker messageDuplicateChecker; - - private WxSessionManager sessionManager; - - private WxErrorExceptionHandler exceptionHandler; - - public WxMpMessageRouter(WxMpService wxMpService) { - this.wxMpService = wxMpService; - this.executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE); - this.messageDuplicateChecker = new WxMessageInMemoryDuplicateChecker(); - this.sessionManager = new StandardSessionManager(); - this.exceptionHandler = new LogExceptionHandler(); - } - - /** - *
-   * 设置自定义的 {@link ExecutorService}
-   * 如果不调用该方法,默认使用 Executors.newFixedThreadPool(100)
-   * 
- * @param executorService - */ - public void setExecutorService(ExecutorService executorService) { - this.executorService = executorService; - } - - /** - *
-   * 设置自定义的 {@link me.chanjar.weixin.common.api.WxMessageDuplicateChecker}
-   * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.api.WxMessageInMemoryDuplicateChecker}
-   * 
- * @param messageDuplicateChecker - */ - public void setMessageDuplicateChecker(WxMessageDuplicateChecker messageDuplicateChecker) { - this.messageDuplicateChecker = messageDuplicateChecker; - } - - /** - *
-   * 设置自定义的{@link me.chanjar.weixin.common.session.WxSessionManager}
-   * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.session.StandardSessionManager}
-   * 
- * @param sessionManager - */ - public void setSessionManager(WxSessionManager sessionManager) { - this.sessionManager = sessionManager; - } - - /** - *
-   * 设置自定义的{@link me.chanjar.weixin.common.api.WxErrorExceptionHandler}
-   * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.util.LogExceptionHandler}
-   * 
- * @param exceptionHandler - */ - public void setExceptionHandler(WxErrorExceptionHandler exceptionHandler) { - this.exceptionHandler = exceptionHandler; - } - - List getRules() { - return this.rules; - } - - /** - * 开始一个新的Route规则 - */ - public WxMpMessageRouterRule rule() { - return new WxMpMessageRouterRule(this); - } - - /** - * 处理微信消息 - * @param wxMessage - */ - public WxMpXmlOutMessage route(final WxMpXmlMessage wxMessage) { - if (isDuplicateMessage(wxMessage)) { - // 如果是重复消息,那么就不做处理 - return null; - } - - final List matchRules = new ArrayList<>(); - // 收集匹配的规则 - for (final WxMpMessageRouterRule rule : this.rules) { - if (rule.test(wxMessage)) { - matchRules.add(rule); - if(!rule.isReEnter()) { - break; - } - } - } - - if (matchRules.size() == 0) { - return null; - } - - WxMpXmlOutMessage res = null; - final List> futures = new ArrayList<>(); - for (final WxMpMessageRouterRule rule : matchRules) { - // 返回最后一个非异步的rule的执行结果 - if(rule.isAsync()) { - futures.add( - this.executorService.submit(new Runnable() { - @Override - public void run() { - rule.service(wxMessage, WxMpMessageRouter.this.wxMpService, WxMpMessageRouter.this.sessionManager, WxMpMessageRouter.this.exceptionHandler); - } - }) - ); - } else { - res = rule.service(wxMessage, this.wxMpService, this.sessionManager, this.exceptionHandler); - // 在同步操作结束,session访问结束 - this.log.debug("End session access: async=false, sessionId={}", wxMessage.getFromUser()); - sessionEndAccess(wxMessage); - } - } - - if (futures.size() > 0) { - this.executorService.submit(new Runnable() { - @Override - public void run() { - for (Future future : futures) { - try { - future.get(); - WxMpMessageRouter.this.log.debug("End session access: async=true, sessionId={}", wxMessage.getFromUser()); - // 异步操作结束,session访问结束 - sessionEndAccess(wxMessage); - } catch (InterruptedException e) { - WxMpMessageRouter.this.log.error("Error happened when wait task finish", e); - } catch (ExecutionException e) { - WxMpMessageRouter.this.log.error("Error happened when wait task finish", e); - } - } - } - }); - } - return res; - } - - protected boolean isDuplicateMessage(WxMpXmlMessage wxMessage) { - - StringBuffer messageId = new StringBuffer(); - if (wxMessage.getMsgId() == null) { - messageId.append(wxMessage.getCreateTime()) - .append("-").append(wxMessage.getFromUser()) - .append("-").append(wxMessage.getEventKey() == null ? "" : wxMessage.getEventKey()) - .append("-").append(wxMessage.getEvent() == null ? "" : wxMessage.getEvent()) - ; - } else { - messageId.append(wxMessage.getMsgId()); - } - - return this.messageDuplicateChecker.isDuplicate(messageId.toString()); - - } - - /** - * 对session的访问结束 - * @param wxMessage - */ - protected void sessionEndAccess(WxMpXmlMessage wxMessage) { - - InternalSession session = ((InternalSessionManager)this.sessionManager).findSession(wxMessage.getFromUser()); - if (session != null) { - session.endAccess(); - } - - } -} +package me.chanjar.weixin.mp.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import me.chanjar.weixin.common.api.WxErrorExceptionHandler; +import me.chanjar.weixin.common.api.WxMessageDuplicateChecker; +import me.chanjar.weixin.common.api.WxMessageInMemoryDuplicateChecker; +import me.chanjar.weixin.common.session.InternalSession; +import me.chanjar.weixin.common.session.InternalSessionManager; +import me.chanjar.weixin.common.session.StandardSessionManager; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.common.util.LogExceptionHandler; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; + +/** + *
+ * 微信消息路由器,通过代码化的配置,把来自微信的消息交给handler处理
+ *
+ * 说明:
+ * 1. 配置路由规则时要按照从细到粗的原则,否则可能消息可能会被提前处理
+ * 2. 默认情况下消息只会被处理一次,除非使用 {@link WxMpMessageRouterRule#next()}
+ * 3. 规则的结束必须用{@link WxMpMessageRouterRule#end()}或者{@link WxMpMessageRouterRule#next()},否则不会生效
+ *
+ * 使用方法:
+ * WxMpMessageRouter router = new WxMpMessageRouter();
+ * router
+ *   .rule()
+ *       .msgType("MSG_TYPE").event("EVENT").eventKey("EVENT_KEY").content("CONTENT")
+ *       .interceptor(interceptor, ...).handler(handler, ...)
+ *   .end()
+ *   .rule()
+ *       // 另外一个匹配规则
+ *   .end()
+ * ;
+ *
+ * // 将WxXmlMessage交给消息路由器
+ * router.route(message);
+ *
+ * 
+ * @author Daniel Qian + * + */ +public class WxMpMessageRouter { + + protected final Logger log = LoggerFactory.getLogger(WxMpMessageRouter.class); + + private static final int DEFAULT_THREAD_POOL_SIZE = 100; + + private final List rules = new ArrayList<>(); + + private final WxMpService wxMpService; + + private ExecutorService executorService; + + private WxMessageDuplicateChecker messageDuplicateChecker; + + private WxSessionManager sessionManager; + + private WxErrorExceptionHandler exceptionHandler; + + public WxMpMessageRouter(WxMpService wxMpService) { + this.wxMpService = wxMpService; + this.executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE); + this.messageDuplicateChecker = new WxMessageInMemoryDuplicateChecker(); + this.sessionManager = new StandardSessionManager(); + this.exceptionHandler = new LogExceptionHandler(); + } + + /** + *
+   * 设置自定义的 {@link ExecutorService}
+   * 如果不调用该方法,默认使用 Executors.newFixedThreadPool(100)
+   * 
+ * @param executorService + */ + public void setExecutorService(ExecutorService executorService) { + this.executorService = executorService; + } + + /** + *
+   * 设置自定义的 {@link me.chanjar.weixin.common.api.WxMessageDuplicateChecker}
+   * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.api.WxMessageInMemoryDuplicateChecker}
+   * 
+ * @param messageDuplicateChecker + */ + public void setMessageDuplicateChecker(WxMessageDuplicateChecker messageDuplicateChecker) { + this.messageDuplicateChecker = messageDuplicateChecker; + } + + /** + *
+   * 设置自定义的{@link me.chanjar.weixin.common.session.WxSessionManager}
+   * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.session.StandardSessionManager}
+   * 
+ * @param sessionManager + */ + public void setSessionManager(WxSessionManager sessionManager) { + this.sessionManager = sessionManager; + } + + /** + *
+   * 设置自定义的{@link me.chanjar.weixin.common.api.WxErrorExceptionHandler}
+   * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.util.LogExceptionHandler}
+   * 
+ * @param exceptionHandler + */ + public void setExceptionHandler(WxErrorExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + List getRules() { + return this.rules; + } + + /** + * 开始一个新的Route规则 + */ + public WxMpMessageRouterRule rule() { + return new WxMpMessageRouterRule(this); + } + + /** + * 处理微信消息 + * @param wxMessage + */ + public WxMpXmlOutMessage route(final WxMpXmlMessage wxMessage) { + if (isDuplicateMessage(wxMessage)) { + // 如果是重复消息,那么就不做处理 + return null; + } + + final List matchRules = new ArrayList<>(); + // 收集匹配的规则 + for (final WxMpMessageRouterRule rule : this.rules) { + if (rule.test(wxMessage)) { + matchRules.add(rule); + if(!rule.isReEnter()) { + break; + } + } + } + + if (matchRules.size() == 0) { + return null; + } + + WxMpXmlOutMessage res = null; + final List> futures = new ArrayList<>(); + for (final WxMpMessageRouterRule rule : matchRules) { + // 返回最后一个非异步的rule的执行结果 + if(rule.isAsync()) { + futures.add( + this.executorService.submit(new Runnable() { + @Override + public void run() { + rule.service(wxMessage, WxMpMessageRouter.this.wxMpService, WxMpMessageRouter.this.sessionManager, WxMpMessageRouter.this.exceptionHandler); + } + }) + ); + } else { + res = rule.service(wxMessage, this.wxMpService, this.sessionManager, this.exceptionHandler); + // 在同步操作结束,session访问结束 + this.log.debug("End session access: async=false, sessionId={}", wxMessage.getFromUser()); + sessionEndAccess(wxMessage); + } + } + + if (futures.size() > 0) { + this.executorService.submit(new Runnable() { + @Override + public void run() { + for (Future future : futures) { + try { + future.get(); + WxMpMessageRouter.this.log.debug("End session access: async=true, sessionId={}", wxMessage.getFromUser()); + // 异步操作结束,session访问结束 + sessionEndAccess(wxMessage); + } catch (InterruptedException e) { + WxMpMessageRouter.this.log.error("Error happened when wait task finish", e); + } catch (ExecutionException e) { + WxMpMessageRouter.this.log.error("Error happened when wait task finish", e); + } + } + } + }); + } + return res; + } + + protected boolean isDuplicateMessage(WxMpXmlMessage wxMessage) { + + StringBuffer messageId = new StringBuffer(); + if (wxMessage.getMsgId() == null) { + messageId.append(wxMessage.getCreateTime()) + .append("-").append(wxMessage.getFromUser()) + .append("-").append(wxMessage.getEventKey() == null ? "" : wxMessage.getEventKey()) + .append("-").append(wxMessage.getEvent() == null ? "" : wxMessage.getEvent()) + ; + } else { + messageId.append(wxMessage.getMsgId()); + } + + return this.messageDuplicateChecker.isDuplicate(messageId.toString()); + + } + + /** + * 对session的访问结束 + * @param wxMessage + */ + protected void sessionEndAccess(WxMpXmlMessage wxMessage) { + + InternalSession session = ((InternalSessionManager)this.sessionManager).findSession(wxMessage.getFromUser()); + if (session != null) { + session.endAccess(); + } + + } +} diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageRouterRule.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageRouterRule.java index 317303b8..6859b7d7 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageRouterRule.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpMessageRouterRule.java @@ -3,8 +3,8 @@ package me.chanjar.weixin.mp.api; import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.api.WxErrorExceptionHandler; -import me.chanjar.weixin.mp.bean.WxMpXmlMessage; -import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import java.util.ArrayList; import java.util.HashMap; diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpPayService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpPayService.java index 411c2abf..2fcd4cb9 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpPayService.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpPayService.java @@ -1,6 +1,8 @@ package me.chanjar.weixin.mp.api; import me.chanjar.weixin.common.exception.WxErrorException; +import me.chanjar.weixin.mp.bean.pay.WxPayJsSDKCallback; +import me.chanjar.weixin.mp.bean.pay.result.WxPayOrderCloseResult; import me.chanjar.weixin.mp.bean.pay.request.WxEntPayRequest; import me.chanjar.weixin.mp.bean.pay.request.WxPayRefundRequest; import me.chanjar.weixin.mp.bean.pay.request.WxPaySendRedpackRequest; @@ -34,6 +36,22 @@ public interface WxMpPayService { */ WxPayOrderQueryResult queryOrder(String transactionId, String outTradeNo) throws WxErrorException; + /** + *
+   * 关闭订单
+   * 应用场景
+   * 以下情况需要调用关单接口:
+   * 1. 商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
+   * 2. 系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
+   * 注意:订单生成后不能马上调用关单接口,最短调用时间间隔为5分钟。
+   * 接口地址:https://api.mch.weixin.qq.com/pay/closeorder
+   * 是否需要证书:   不需要。
+   * 
+ * @param outTradeNo 商户系统内部的订单号,当没提供transaction_id时需要传这个。 + * @throws WxErrorException + */ + WxPayOrderCloseResult closeOrder(String outTradeNo) throws WxErrorException; + /** * 统一下单(详见http://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1) * 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识" @@ -63,6 +81,13 @@ public interface WxMpPayService { */ WxPayRefundResult refund(WxPayRefundRequest request, File keyFile) throws WxErrorException; + /** + * 读取支付结果通知 + * 详见http://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7 + * + */ + WxPayJsSDKCallback getJSSDKCallbackData(String xmlData) throws WxErrorException; + /** *
    * 计算Map键值对是否和签名相符,
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
index e77b60d9..a74e718d 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpService.java
@@ -5,6 +5,7 @@ import me.chanjar.weixin.common.exception.WxErrorException;
 import me.chanjar.weixin.common.util.http.RequestExecutor;
 import me.chanjar.weixin.mp.bean.*;
 import me.chanjar.weixin.mp.bean.result.*;
+import org.apache.http.HttpHost;
 
 /**
  * 微信API的Service
@@ -113,6 +114,20 @@ public interface WxMpService {
    */
   WxMpMassSendResult massOpenIdsMessageSend(WxMpMassOpenIdsMessage message) throws WxErrorException;
 
+  /**
+   * 
+   * 群发消息预览接口
+   * 开发者可通过该接口发送消息给指定用户,在手机端查看消息的样式和排版。为了满足第三方平台开发者的需求,在保留对openID预览能力的同时,增加了对指定微信号发送预览的能力,但该能力每日调用次数有限制(100次),请勿滥用。
+   * 接口调用请求说明
+   *  http请求方式: POST
+   *  https://api.weixin.qq.com/cgi-bin/message/mass/preview?access_token=ACCESS_TOKEN
+   * 详情请见:http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140549&token=&lang=zh_CN
+   * 
+ * + * @return wxMpMassSendResult + */ + WxMpMassSendResult massMessagePreview(WxMpMassPreviewMessage wxMpMassPreviewMessage) throws Exception; + /** *
    * 长链接转短链接接口
@@ -214,6 +229,11 @@ public interface WxMpService {
    */
    T execute(RequestExecutor executor, String uri, E data) throws WxErrorException;
 
+  /**
+   * 获取代理对象
+   */
+  HttpHost getHttpProxy();
+
   /**
    * 注入 {@link WxMpConfigStorage} 的实现
    */
@@ -235,16 +255,6 @@ public interface WxMpService {
    */
   void setMaxRetryTimes(int maxRetryTimes);
 
-  /**
-   * 
-   * 预览接口
-   * 详情请见:http://mp.weixin.qq.com/wiki/15/40b6865b893947b764e2de8e4a1fb55f.html#.E9.A2.84.E8.A7.88.E6.8E.A5.E5.8F.A3.E3.80.90.E8.AE.A2.E9.98.85.E5.8F.B7.E4.B8.8E.E6.9C.8D.E5.8A.A1.E5.8F.B7.E8.AE.A4.E8.AF.81.E5.90.8E.E5.9D.87.E5.8F.AF.E7.94.A8.E3.80.91
-   * 
- * - * @return wxMpMassSendResult - */ - WxMpMassSendResult massMessagePreview(WxMpMassPreviewMessage wxMpMassPreviewMessage) throws Exception; - /** * 获取WxMpConfigStorage 对象 * diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpUserService.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpUserService.java index f46ca634..96499128 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpUserService.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/WxMpUserService.java @@ -1,12 +1,12 @@ package me.chanjar.weixin.mp.api; -import java.util.List; - import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.mp.bean.WxMpUserQuery; import me.chanjar.weixin.mp.bean.result.WxMpUser; import me.chanjar.weixin.mp.bean.result.WxMpUserList; +import java.util.List; + /** * 用户管理相关操作接口 * @@ -16,8 +16,10 @@ public interface WxMpUserService { /** *
-   * 设置用户备注名接口
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=设置用户备注名接口
+   * 设置用户备注名
+   * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140838&token=&lang=zh_CN
+   * http请求方式: POST(请使用https协议)
+   * 接口地址:https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=ACCESS_TOKEN
    * 
* * @param openid 用户openid @@ -25,10 +27,24 @@ public interface WxMpUserService { */ void userUpdateRemark(String openid, String remark) throws WxErrorException; + /** + *
+   * 获取用户基本信息(语言为默认的zh_CN 简体)
+   * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140839&token=&lang=zh_CN
+   * http请求方式: GET
+   * 接口地址:https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
+   * 
+ * + * @param openid 用户openid + */ + WxMpUser userInfo(String openid) throws WxErrorException; + /** *
    * 获取用户基本信息
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取用户基本信息
+   * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140839&token=&lang=zh_CN
+   * http请求方式: GET
+   * 接口地址:https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
    * 
* * @param openid 用户openid @@ -39,7 +55,10 @@ public interface WxMpUserService { /** *
    * 获取用户基本信息列表
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=批量获取用户基本信息
+   * 开发者可通过该接口来批量获取用户基本信息。最多支持一次拉取100条。
+   * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140839&token=&lang=zh_CN
+   * http请求方式: POST
+   * 接口地址:https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token=ACCESS_TOKEN
    * 
* * @param openids 用户openid列表 @@ -49,7 +68,10 @@ public interface WxMpUserService { /** *
    * 获取用户基本信息列表
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=批量获取用户基本信息
+   * 开发者可通过该接口来批量获取用户基本信息。最多支持一次拉取100条。
+   * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140839&token=&lang=zh_CN
+   * http请求方式: POST
+   * 接口地址:https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token=ACCESS_TOKEN
    * 
* * @param userQuery 详细查询参数 @@ -58,8 +80,11 @@ public interface WxMpUserService { /** *
-   * 获取关注者列表
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取关注者列表
+   * 获取用户列表
+   * 公众号可通过本接口来获取帐号的关注者列表,关注者列表由一串OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的)组成。一次拉取调用最多拉取10000个关注者的OpenID,可以通过多次拉取的方式来满足需求。
+   * 详情请见: http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140840&token=&lang=zh_CN
+   * http请求方式: GET(请使用https协议)
+   * 接口地址:https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID
    * 
* * @param nextOpenid 可选,第一个拉取的OPENID,null为从头开始拉取 diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpCardServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpCardServiceImpl.java index 63e13809..2e6bf88e 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpCardServiceImpl.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpCardServiceImpl.java @@ -1,6 +1,7 @@ package me.chanjar.weixin.mp.api.impl; import java.util.Arrays; +import java.util.concurrent.locks.Lock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,11 +30,6 @@ public class WxMpCardServiceImpl implements WxMpCardService { private final Logger log = LoggerFactory.getLogger(WxMpCardServiceImpl.class); - /** - * 全局的是否正在刷新卡券api_ticket的锁 - */ - private final Object globalCardApiTicketRefreshLock = new Object(); - private WxMpService wxMpService; public WxMpCardServiceImpl(WxMpService wxMpService) { @@ -66,21 +62,25 @@ public class WxMpCardServiceImpl implements WxMpCardService { */ @Override public String getCardApiTicket(boolean forceRefresh) throws WxErrorException { - if (forceRefresh) { - this.wxMpService.getWxMpConfigStorage().expireCardApiTicket(); - } - if (this.wxMpService.getWxMpConfigStorage().isCardApiTicketExpired()) { - synchronized (this.globalCardApiTicketRefreshLock) { - if (this.wxMpService.getWxMpConfigStorage().isCardApiTicketExpired()) { - String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=wx_card"; - String responseContent = this.wxMpService.execute(new SimpleGetRequestExecutor(), url, null); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); - JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); - String cardApiTicket = tmpJsonObject.get("ticket").getAsString(); - int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt(); - this.wxMpService.getWxMpConfigStorage().updateCardApiTicket(cardApiTicket, expiresInSeconds); - } + Lock lock = wxMpService.getWxMpConfigStorage().getCardApiTicketLock(); + try { + lock.lock(); + + if (forceRefresh) { + this.wxMpService.getWxMpConfigStorage().expireCardApiTicket(); + } + + if (this.wxMpService.getWxMpConfigStorage().isCardApiTicketExpired()) { + String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=wx_card"; + String responseContent = this.wxMpService.execute(new SimpleGetRequestExecutor(), url, null); + JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); + String cardApiTicket = tmpJsonObject.get("ticket").getAsString(); + int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt(); + this.wxMpService.getWxMpConfigStorage().updateCardApiTicket(cardApiTicket, expiresInSeconds); } + } finally { + lock.unlock(); } return this.wxMpService.getWxMpConfigStorage().getCardApiTicket(); } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpMaterialServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpMaterialServiceImpl.java index 310bbe78..af9fa54c 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpMaterialServiceImpl.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpMaterialServiceImpl.java @@ -10,10 +10,10 @@ import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; import me.chanjar.weixin.common.util.json.WxGsonBuilder; import me.chanjar.weixin.mp.api.WxMpMaterialService; import me.chanjar.weixin.mp.api.WxMpService; -import me.chanjar.weixin.mp.bean.WxMpMaterial; -import me.chanjar.weixin.mp.bean.WxMpMaterialArticleUpdate; -import me.chanjar.weixin.mp.bean.WxMpMaterialNews; -import me.chanjar.weixin.mp.bean.result.*; +import me.chanjar.weixin.mp.bean.material.WxMpMaterial; +import me.chanjar.weixin.mp.bean.material.WxMpMaterialArticleUpdate; +import me.chanjar.weixin.mp.bean.material.WxMpMaterialNews; +import me.chanjar.weixin.mp.bean.material.*; import me.chanjar.weixin.mp.util.http.*; import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; @@ -37,8 +37,13 @@ public class WxMpMaterialServiceImpl implements WxMpMaterialService { } @Override - public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException { - return this.mediaUpload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType)); + public WxMediaUploadResult mediaUpload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException { + try { + return this.mediaUpload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType)); + } catch (IOException e) { + e.printStackTrace(); + throw new WxErrorException(WxError.newBuilder().setErrorMsg(e.getMessage()).build()); + } } @Override diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImpl.java index 3b3b7510..3273cd2b 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImpl.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImpl.java @@ -7,11 +7,15 @@ import me.chanjar.weixin.common.util.BeanUtils; import me.chanjar.weixin.common.util.xml.XStreamInitializer; import me.chanjar.weixin.mp.api.WxMpPayService; import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.pay.WxPayJsSDKCallback; +import me.chanjar.weixin.mp.bean.pay.result.WxPayOrderCloseResult; import me.chanjar.weixin.mp.bean.pay.request.*; import me.chanjar.weixin.mp.bean.pay.result.*; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.http.Consts; +import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.DefaultHostnameVerifier; @@ -21,10 +25,13 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.security.KeyStore; import java.util.*; @@ -35,6 +42,8 @@ import java.util.*; */ public class WxMpPayServiceImpl implements WxMpPayService { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + private static final String PAY_BASE_URL = "https://api.mch.weixin.qq.com"; private static final String[] TRADE_TYPES = new String[]{"JSAPI","NATIVE", "APP"}; private static final String[] REFUND_ACCOUNT = new String[]{"REFUND_SOURCE_RECHARGE_FUNDS", @@ -52,8 +61,8 @@ public class WxMpPayServiceImpl implements WxMpPayService { checkParameters(request); XStream xstream = XStreamInitializer.getInstance(); - xstream.processAnnotations(WxPayRefundResult.class); xstream.processAnnotations(WxPayRefundRequest.class); + xstream.processAnnotations(WxPayRefundResult.class); request.setAppid(this.wxMpService.getWxMpConfigStorage().getAppId()); String partnerId = this.wxMpService.getWxMpConfigStorage().getPartnerId(); @@ -65,21 +74,20 @@ public class WxMpPayServiceImpl implements WxMpPayService { String url = PAY_BASE_URL + "/secapi/pay/refund"; String responseContent = this.executeRequestWithKeyFile(url, keyFile, xstream.toXML(request), partnerId); - WxPayRefundResult wxMpPayRefundResult = (WxPayRefundResult) xstream.fromXML(responseContent); - - if (!"SUCCESS".equalsIgnoreCase(wxMpPayRefundResult.getResultCode()) - || !"SUCCESS".equalsIgnoreCase(wxMpPayRefundResult.getReturnCode())) { - WxError error = new WxError(); - error.setErrorCode(-1); - error.setErrorMsg("return_code:" + wxMpPayRefundResult.getReturnCode() - + ";return_msg:" + wxMpPayRefundResult.getReturnMsg() - + ";result_code:" + wxMpPayRefundResult.getResultCode() + ";err_code" - + wxMpPayRefundResult.getErrCode() + ";err_code_des" - + wxMpPayRefundResult.getErrCodeDes()); - throw new WxErrorException(error); - } + WxPayRefundResult result = (WxPayRefundResult) xstream.fromXML(responseContent); + this.checkResult(result); + return result; + } - return wxMpPayRefundResult; + private void checkResult(WxPayBaseResult result) throws WxErrorException { + if (!"SUCCESS".equalsIgnoreCase(result.getReturnCode()) + || !"SUCCESS".equalsIgnoreCase(result.getResultCode())) { + throw new WxErrorException(WxError.newBuilder().setErrorCode(-1) + .setErrorMsg("返回代码:" + result.getReturnCode() + ", 返回信息: " + + result.getReturnMsg() + ", 结果代码: " + result.getResultCode() + ", 错误代码: " + + result.getErrCode() + ", 错误详情: " + result.getErrCodeDes()) + .build()); + } } private void checkParameters(WxPayRefundRequest request) throws WxErrorException { @@ -96,6 +104,18 @@ public class WxMpPayServiceImpl implements WxMpPayService { } } + @Override + public WxPayJsSDKCallback getJSSDKCallbackData(String xmlData) throws WxErrorException { + try { + XStream xstream = XStreamInitializer.getInstance(); + xstream.alias("xml", WxPayJsSDKCallback.class); + return (WxPayJsSDKCallback) xstream.fromXML(xmlData); + } catch (Exception e) { + e.printStackTrace(); + throw new WxErrorException(WxError.newBuilder().setErrorMsg("发生异常" + e.getMessage()).build()); + } + } + @Override public boolean checkJSSDKCallbackDataSignature(Map kvm, String signature) { @@ -126,16 +146,10 @@ public class WxMpPayServiceImpl implements WxMpPayService { } String responseContent = this.executeRequestWithKeyFile(url, keyFile, xstream.toXML(request), mchId); - WxPaySendRedpackResult redpackResult = (WxPaySendRedpackResult) xstream + WxPaySendRedpackResult result = (WxPaySendRedpackResult) xstream .fromXML(responseContent); - if ("FAIL".equals(redpackResult.getResultCode())) { - throw new WxErrorException(WxError.newBuilder() - .setErrorMsg( - redpackResult.getErrCode() + ":" + redpackResult.getErrCodeDes()) - .build()); - } - - return redpackResult; + this.checkResult(result); + return result; } /** @@ -185,15 +199,39 @@ public class WxMpPayServiceImpl implements WxMpPayService { String url = PAY_BASE_URL + "/pay/orderquery"; - String responseContent = this.wxMpService.post(url, xstream.toXML(request)); + String responseContent = this.executeRequest(url, xstream.toXML(request)); WxPayOrderQueryResult result = (WxPayOrderQueryResult) xstream.fromXML(responseContent); result.composeCoupons(responseContent); - if ("FAIL".equals(result.getResultCode())) { - throw new WxErrorException(WxError.newBuilder() - .setErrorMsg(result.getErrCode() + ":" + result.getErrCodeDes()) - .build()); + this.checkResult(result); + return result; + } + + @Override + public WxPayOrderCloseResult closeOrder(String outTradeNo) throws WxErrorException { + if (StringUtils.isBlank(outTradeNo)) { + throw new IllegalArgumentException("out_trade_no 不能为空"); } + XStream xstream = XStreamInitializer.getInstance(); + xstream.processAnnotations(WxPayOrderCloseRequest.class); + xstream.processAnnotations(WxPayOrderCloseResult.class); + + WxPayOrderCloseRequest request = new WxPayOrderCloseRequest(); + request.setOutTradeNo(StringUtils.trimToNull(outTradeNo)); + request.setAppid(this.wxMpService.getWxMpConfigStorage().getAppId()); + request.setMchId(this.wxMpService.getWxMpConfigStorage().getPartnerId()); + request.setNonceStr(System.currentTimeMillis() + ""); + + String sign = this.createSign(BeanUtils.xmlBean2Map(request), + this.wxMpService.getWxMpConfigStorage().getPartnerKey()); + request.setSign(sign); + + String url = PAY_BASE_URL + "/pay/closeorder"; + + String responseContent = this.executeRequest(url, xstream.toXML(request)); + WxPayOrderCloseResult result = (WxPayOrderCloseResult) xstream.fromXML(responseContent); + this.checkResult(result); + return result; } @@ -216,15 +254,10 @@ public class WxMpPayServiceImpl implements WxMpPayService { String url = PAY_BASE_URL + "/pay/unifiedorder"; - String responseContent = this.wxMpService.post(url, xstream.toXML(request)); + String responseContent = this.executeRequest(url, xstream.toXML(request)); WxPayUnifiedOrderResult result = (WxPayUnifiedOrderResult) xstream .fromXML(responseContent); - if ("FAIL".equals(result.getResultCode())) { - throw new WxErrorException(WxError.newBuilder() - .setErrorMsg(result.getErrCode() + ":" + result.getErrCodeDes()) - .build()); - } - + this.checkResult(result); return result; } @@ -247,16 +280,6 @@ public class WxMpPayServiceImpl implements WxMpPayService { @Override public Map getPayInfo(WxPayUnifiedOrderRequest request) throws WxErrorException { WxPayUnifiedOrderResult unifiedOrderResult = this.unifiedOrder(request); - - if (!"SUCCESS".equalsIgnoreCase(unifiedOrderResult.getReturnCode()) - || !"SUCCESS".equalsIgnoreCase(unifiedOrderResult.getResultCode())) { - throw new WxErrorException(WxError.newBuilder().setErrorCode(-1) - .setErrorMsg("return_code:" + unifiedOrderResult.getReturnCode() + ";return_msg:" - + unifiedOrderResult.getReturnMsg() + ";result_code:" + unifiedOrderResult.getResultCode() + ";err_code" - + unifiedOrderResult.getErrCode() + ";err_code_des" + unifiedOrderResult.getErrCodeDes()) - .build()); - } - String prepayId = unifiedOrderResult.getPrepayId(); if (StringUtils.isBlank(prepayId)) { throw new RuntimeException(String.format("Failed to get prepay id due to error code '%s'(%s).", @@ -298,10 +321,7 @@ public class WxMpPayServiceImpl implements WxMpPayService { String responseContent = this.executeRequestWithKeyFile(url, keyFile, xstream.toXML(request), request.getMchId()); WxEntPayResult result = (WxEntPayResult) xstream.fromXML(responseContent); - if ("FAIL".equals(result.getResultCode())) { - throw new WxErrorException( - WxError.newBuilder().setErrorMsg(result.getErrCode() + ":" + result.getErrCodeDes()).build()); - } + this.checkResult(result); return result; } @@ -323,13 +343,32 @@ public class WxMpPayServiceImpl implements WxMpPayService { String responseContent = this.executeRequestWithKeyFile(url, keyFile, xstream.toXML(request), request.getMchId()); WxEntPayQueryResult result = (WxEntPayQueryResult) xstream.fromXML(responseContent); - if ("FAIL".equals(result.getResultCode())) { - throw new WxErrorException( - WxError.newBuilder().setErrorMsg(result.getErrCode() + ":" + result.getErrCodeDes()).build()); - } + this.checkResult(result); return result; } + private String executeRequest( String url, String requestStr) throws WxErrorException { + HttpPost httpPost = new HttpPost(url); + if (this.wxMpService.getHttpProxy() != null) { + httpPost.setConfig(RequestConfig.custom().setProxy(this.wxMpService.getHttpProxy()).build()); + } + + try (CloseableHttpClient httpclient = HttpClients.custom().build()) { + httpPost.setEntity(new StringEntity(new String(requestStr.getBytes("UTF-8"), "ISO-8859-1"))); + + try (CloseableHttpResponse response = httpclient.execute(httpPost)) { + String result = EntityUtils.toString(response.getEntity(), Consts.UTF_8); + this.log.debug("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}",url, requestStr, result); + return result; + } + } catch (IOException e) { + this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[EXCEPTION]: {}", url, requestStr, e.getMessage()); + throw new WxErrorException(WxError.newBuilder().setErrorCode(-1).setErrorMsg(e.getMessage()).build(), e); + }finally { + httpPost.releaseConnection(); + } + } + private String executeRequestWithKeyFile( String url, File keyFile, String requestStr, String mchId) throws WxErrorException { try (FileInputStream inputStream = new FileInputStream(keyFile)) { KeyStore keyStore = KeyStore.getInstance("PKCS12"); @@ -339,16 +378,24 @@ public class WxMpPayServiceImpl implements WxMpPayService { SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[] { "TLSv1" }, null, new DefaultHostnameVerifier()); + HttpPost httpPost = new HttpPost(url); + if (this.wxMpService.getHttpProxy() != null) { + httpPost.setConfig(RequestConfig.custom().setProxy(this.wxMpService.getHttpProxy()).build()); + } + try (CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build()) { - HttpPost httpPost = new HttpPost(url); httpPost.setEntity(new StringEntity(new String(requestStr.getBytes("UTF-8"), "ISO-8859-1"))); - try (CloseableHttpResponse response = httpclient.execute(httpPost)) { - return EntityUtils.toString(response.getEntity()); + String result = EntityUtils.toString(response.getEntity(), Consts.UTF_8); + this.log.debug("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}",url, requestStr, result); + return result; } + }finally { + httpPost.releaseConnection(); } } catch (Exception e) { - throw new WxErrorException(WxError.newBuilder().setErrorMsg(e.getMessage()).build(), e); + this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[EXCEPTION]: {}", url, requestStr, e.getMessage()); + throw new WxErrorException(WxError.newBuilder().setErrorCode(-1).setErrorMsg(e.getMessage()).build(), e); } } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImpl.java index 7b5171f5..fa7dc795 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImpl.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImpl.java @@ -28,22 +28,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.concurrent.locks.Lock; public class WxMpServiceImpl implements WxMpService { private static final JsonParser JSON_PARSER = new JsonParser(); - protected final Logger log = LoggerFactory.getLogger(WxMpServiceImpl.class); - - /** - * 全局的是否正在刷新access token的锁 - */ - private final Object globalAccessTokenRefreshLock = new Object(); - - /** - * 全局的是否正在刷新jsapi_ticket的锁 - */ - private final Object globalJsapiTicketRefreshLock = new Object(); + protected final Logger log = LoggerFactory.getLogger(this.getClass()); private WxMpConfigStorage configStorage; @@ -98,38 +89,42 @@ public class WxMpServiceImpl implements WxMpService { @Override public String getAccessToken(boolean forceRefresh) throws WxErrorException { - if (forceRefresh) { - this.configStorage.expireAccessToken(); - } - if (this.configStorage.isAccessTokenExpired()) { - synchronized (this.globalAccessTokenRefreshLock) { - if (this.configStorage.isAccessTokenExpired()) { - String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" + - "&appid=" + this.configStorage.getAppId() + "&secret=" - + this.configStorage.getSecret(); - try { - HttpGet httpGet = new HttpGet(url); - if (this.httpProxy != null) { - RequestConfig config = RequestConfig.custom().setProxy(this.httpProxy).build(); - httpGet.setConfig(config); - } - try (CloseableHttpResponse response = getHttpclient().execute(httpGet)) { - String resultContent = new BasicResponseHandler().handleResponse(response); - WxError error = WxError.fromJson(resultContent); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } - WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); - this.configStorage.updateAccessToken(accessToken.getAccessToken(), - accessToken.getExpiresIn()); - }finally { - httpGet.releaseConnection(); + Lock lock = configStorage.getAccessTokenLock(); + try { + lock.lock(); + + if (forceRefresh) { + this.configStorage.expireAccessToken(); + } + + if (this.configStorage.isAccessTokenExpired()) { + String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" + + "&appid=" + this.configStorage.getAppId() + "&secret=" + + this.configStorage.getSecret(); + try { + HttpGet httpGet = new HttpGet(url); + if (this.httpProxy != null) { + RequestConfig config = RequestConfig.custom().setProxy(this.httpProxy).build(); + httpGet.setConfig(config); + } + try (CloseableHttpResponse response = getHttpclient().execute(httpGet)) { + String resultContent = new BasicResponseHandler().handleResponse(response); + WxError error = WxError.fromJson(resultContent); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); } - } catch (IOException e) { - throw new RuntimeException(e); + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + this.configStorage.updateAccessToken(accessToken.getAccessToken(), + accessToken.getExpiresIn()); + }finally { + httpGet.releaseConnection(); } + } catch (IOException e) { + throw new RuntimeException(e); } } + } finally { + lock.unlock(); } return this.configStorage.getAccessToken(); } @@ -141,22 +136,25 @@ public class WxMpServiceImpl implements WxMpService { @Override public String getJsapiTicket(boolean forceRefresh) throws WxErrorException { - if (forceRefresh) { - this.configStorage.expireJsapiTicket(); - } + Lock lock = configStorage.getJsapiTicketLock(); + try { + lock.lock(); - if (this.configStorage.isJsapiTicketExpired()) { - synchronized (this.globalJsapiTicketRefreshLock) { - if (this.configStorage.isJsapiTicketExpired()) { - String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi"; - String responseContent = execute(new SimpleGetRequestExecutor(), url, null); - JsonElement tmpJsonElement = JSON_PARSER.parse(responseContent); - JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); - String jsapiTicket = tmpJsonObject.get("ticket").getAsString(); - int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt(); - this.configStorage.updateJsapiTicket(jsapiTicket, expiresInSeconds); - } + if (forceRefresh) { + this.configStorage.expireJsapiTicket(); } + + if (this.configStorage.isJsapiTicketExpired()) { + String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi"; + String responseContent = execute(new SimpleGetRequestExecutor(), url, null); + JsonElement tmpJsonElement = JSON_PARSER.parse(responseContent); + JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); + String jsapiTicket = tmpJsonObject.get("ticket").getAsString(); + int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt(); + this.configStorage.updateJsapiTicket(jsapiTicket, expiresInSeconds); + } + } finally { + lock.unlock(); } return this.configStorage.getJsapiTicket(); } @@ -370,9 +368,7 @@ public class WxMpServiceImpl implements WxMpService { return result; } catch (WxErrorException e) { WxError error = e.getError(); - /** - * -1 系统繁忙, 1000ms后重试 - */ + // -1 系统繁忙, 1000ms后重试 if (error.getErrorCode() == -1) { int sleepMillis = this.retrySleepMillis * (1 << retryTimes); try { @@ -411,8 +407,11 @@ public class WxMpServiceImpl implements WxMpService { if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { // 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token this.configStorage.expireAccessToken(); - return this.execute(executor, uri, data); + if(this.configStorage.autoRefreshToken()){ + return this.execute(executor, uri, data); + } } + if (error.getErrorCode() != 0) { this.log.error("\n[URL]: {}\n[PARAMS]: {}\n[RESPONSE]: {}", uri, data, error); @@ -425,6 +424,7 @@ public class WxMpServiceImpl implements WxMpService { } } + @Override public HttpHost getHttpProxy() { return this.httpProxy; } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpUserServiceImpl.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpUserServiceImpl.java index 9267407c..c796cbd3 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpUserServiceImpl.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpUserServiceImpl.java @@ -1,9 +1,6 @@ package me.chanjar.weixin.mp.api.impl; -import java.util.List; - import com.google.gson.JsonObject; - import me.chanjar.weixin.common.exception.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.api.WxMpUserService; @@ -11,6 +8,8 @@ import me.chanjar.weixin.mp.bean.WxMpUserQuery; import me.chanjar.weixin.mp.bean.result.WxMpUser; import me.chanjar.weixin.mp.bean.result.WxMpUserList; +import java.util.List; + /** * Created by Binary Wang on 2016/7/21. */ @@ -31,6 +30,11 @@ public class WxMpUserServiceImpl implements WxMpUserService { this.wxMpService.post(url, json.toString()); } + @Override + public WxMpUser userInfo(String openid) throws WxErrorException { + return this.userInfo(openid, null); + } + @Override public WxMpUser userInfo(String openid, String lang) throws WxErrorException { String url = API_URL_PREFIX + "/info"; diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpCard.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpCard.java index 7bb4330f..bc50ce05 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpCard.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpCard.java @@ -1,6 +1,8 @@ package me.chanjar.weixin.mp.bean; +import me.chanjar.weixin.common.util.ToStringUtils; + /** * 微信卡券 * @@ -61,12 +63,6 @@ public class WxMpCard { @Override public String toString() { - return "WxMpCard{" + - "cardId='" + this.cardId + '\'' + - ", beginTime=" + this.beginTime + - ", endTime=" + this.endTime + - ", userCardStatus='" + this.userCardStatus + '\'' + - ", canConsume=" + this.canConsume + - '}'; + return ToStringUtils.toSimpleString(this); } } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassNews.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassNews.java index 4b059ea8..3e65fcde 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassNews.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassNews.java @@ -1,5 +1,6 @@ package me.chanjar.weixin.mp.bean; +import me.chanjar.weixin.common.util.ToStringUtils; import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; import java.io.Serializable; @@ -14,7 +15,7 @@ import java.util.List; public class WxMpMassNews implements Serializable { /** - * + * */ private static final long serialVersionUID = 565937155013581016L; private List articles = new ArrayList<>(); @@ -137,14 +138,12 @@ public class WxMpMassNews implements Serializable { @Override public String toString() { - return "WxMpMassNewsArticle [" + "thumbMediaId=" + this.thumbMediaId + ", author=" + this.author + ", title=" + this.title + - ", contentSourceUrl=" + this.contentSourceUrl + ", content=" + this.content + ", digest=" + this.digest + - ", showCoverPic=" + this.showCoverPic + "]"; + return ToStringUtils.toSimpleString(this); } } @Override public String toString() { - return "WxMpMassNews [" + "articles=" + this.articles + "]"; + return ToStringUtils.toSimpleString(this); } } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassOpenIdsMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassOpenIdsMessage.java index 38bd8ea7..982cb78f 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassOpenIdsMessage.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassOpenIdsMessage.java @@ -1,13 +1,13 @@ package me.chanjar.weixin.mp.bean; -import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; - import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; + /** - * OpenId列表群发的消息 + * openid列表群发的消息 * * @author chanjarster */ @@ -64,17 +64,26 @@ public class WxMpMassOpenIdsMessage implements Serializable { } /** - * OpenId列表,最多支持10,000个 + * openid列表,最多支持10,000个 */ public List getToUsers() { return this.toUsers; } /** - * 添加OpenId,最多支持10,000个 - * @param openId + * 添加openid,最多支持10,000个 + * @param openid */ - public void addUser(String openId) { - this.toUsers.add(openId); + public void addUser(String openid) { + this.toUsers.add(openid); } + + /** + * 提供set方法,方便客户端直接设置所有群发对象的openid列表 + * @param toUsers + */ + public void setToUsers(List toUsers) { + this.toUsers = toUsers; + } + } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassPreviewMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassPreviewMessage.java index 78e699e9..b49ffc38 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassPreviewMessage.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMassPreviewMessage.java @@ -8,9 +8,6 @@ import java.io.Serializable; * @author miller */ public class WxMpMassPreviewMessage implements Serializable { - /** - * - */ private static final long serialVersionUID = 9095211638358424020L; private String toWxUsername; private String msgType; diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeArticleResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeArticleResult.java index d763543e..35cae74d 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeArticleResult.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeArticleResult.java @@ -1,13 +1,12 @@ package me.chanjar.weixin.mp.bean.datacube; -import java.util.List; - import com.google.gson.JsonParser; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; - import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; +import java.util.List; + /** * 图文分析数据接口返回结果对象 * @author binarywang(Binary Wang) @@ -26,7 +25,9 @@ public class WxDataCubeArticleResult extends WxDataCubeBaseResult { /** * msgid - * 请注意:这里的msgid实际上是由msgid(图文消息id,这也就是群发接口调用后返回的msg_data_id)和index(消息次序索引)组成, 例如12003_3, 其中12003是msgid,即一次群发的消息的id; 3为index,假设该次群发的图文消息共5个文章(因为可能为多图文),3表示5个中的第3个 + * 请注意:这里的msgid实际上是由msgid(图文消息id,这也就是群发接口调用后返回的msg_data_id) + * 和index(消息次序索引)组成, 例如12003_3, 其中12003是msgid,即一次群发的消息的id; 3为index, + * 假设该次群发的图文消息共5个文章(因为可能为多图文),3表示5个中的第3个 */ @SerializedName("msgid") private String msgId; diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeArticleTotalDetail.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeArticleTotalDetail.java index e228a3cc..251c5e76 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeArticleTotalDetail.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeArticleTotalDetail.java @@ -14,7 +14,7 @@ public class WxDataCubeArticleTotalDetail { * 统计的日期,在getarticletotal接口中,ref_date指的是文章群发出日期, 而stat_date是数据统计日期 */ @SerializedName("stat_date") - private Integer statDate; + private String statDate; /** * target_user @@ -191,11 +191,11 @@ public class WxDataCubeArticleTotalDetail { @SerializedName("feed_share_from_other_cnt") private Integer feedShareFromOtherCnt; - public Integer getStatDate() { + public String getStatDate() { return this.statDate; } - public void setStatDate(Integer statDate) { + public void setStatDate(String statDate) { this.statDate = statDate; } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeBaseResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeBaseResult.java index f3999fa2..a9415d9f 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeBaseResult.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeBaseResult.java @@ -1,8 +1,7 @@ package me.chanjar.weixin.mp.bean.datacube; import com.google.gson.annotations.SerializedName; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; +import me.chanjar.weixin.common.util.ToStringUtils; /** * 统计接口的共用属性类 @@ -12,7 +11,7 @@ import org.apache.commons.lang3.builder.ToStringStyle; public class WxDataCubeBaseResult { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } /** diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeUserCumulate.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeUserCumulate.java index e764f244..40e35ded 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeUserCumulate.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeUserCumulate.java @@ -1,17 +1,14 @@ package me.chanjar.weixin.mp.bean.datacube; -import java.io.Serializable; -import java.util.Date; -import java.util.List; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - import com.google.gson.JsonParser; import com.google.gson.reflect.TypeToken; - +import me.chanjar.weixin.common.util.ToStringUtils; import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + /** *
  * 累计用户数据接口的返回JSON数据包
@@ -46,7 +43,7 @@ public class WxDataCubeUserCumulate implements Serializable {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
   public static List fromJson(String json) {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeUserSummary.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeUserSummary.java
index 54eb0d4a..8f478885 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeUserSummary.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/datacube/WxDataCubeUserSummary.java
@@ -1,17 +1,14 @@
 package me.chanjar.weixin.mp.bean.datacube;
 
-import java.io.Serializable;
-import java.util.Date;
-import java.util.List;
-
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.google.gson.JsonParser;
 import com.google.gson.reflect.TypeToken;
-
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
 
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
 /**
  * 
  * 用户增减数据接口的返回JSON数据包
@@ -65,7 +62,7 @@ public class WxDataCubeUserSummary implements Serializable {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
   public static List fromJson(String json) {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/WxMpKefuMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/WxMpKefuMessage.java
index 36c2525c..228351fd 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/WxMpKefuMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/WxMpKefuMessage.java
@@ -4,14 +4,14 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 
-import me.chanjar.weixin.mp.bean.custombuilder.ImageBuilder;
-import me.chanjar.weixin.mp.bean.custombuilder.MpNewsBuilder;
-import me.chanjar.weixin.mp.bean.custombuilder.MusicBuilder;
-import me.chanjar.weixin.mp.bean.custombuilder.NewsBuilder;
-import me.chanjar.weixin.mp.bean.custombuilder.TextBuilder;
-import me.chanjar.weixin.mp.bean.custombuilder.VideoBuilder;
-import me.chanjar.weixin.mp.bean.custombuilder.VoiceBuilder;
-import me.chanjar.weixin.mp.bean.custombuilder.WxCardBuilder;
+import me.chanjar.weixin.mp.builder.kefu.ImageBuilder;
+import me.chanjar.weixin.mp.builder.kefu.MpNewsBuilder;
+import me.chanjar.weixin.mp.builder.kefu.MusicBuilder;
+import me.chanjar.weixin.mp.builder.kefu.NewsBuilder;
+import me.chanjar.weixin.mp.builder.kefu.TextBuilder;
+import me.chanjar.weixin.mp.builder.kefu.VideoBuilder;
+import me.chanjar.weixin.mp.builder.kefu.VoiceBuilder;
+import me.chanjar.weixin.mp.builder.kefu.WxCardBuilder;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
 
 /**
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/request/WxMpKfSessionRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/request/WxMpKfSessionRequest.java
index 420c0d62..ac59e323 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/request/WxMpKfSessionRequest.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/request/WxMpKfSessionRequest.java
@@ -1,14 +1,11 @@
 package me.chanjar.weixin.mp.bean.kefu.request;
 
-import java.io.Serializable;
-
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.google.gson.annotations.SerializedName;
-
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
 
+import java.io.Serializable;
+
 public class WxMpKfSessionRequest implements Serializable {
   private static final long serialVersionUID = -5451863610674856927L;
 
@@ -17,7 +14,7 @@ public class WxMpKfSessionRequest implements Serializable {
    */
   @SerializedName("kf_account")
   private String kfAccount;
-  
+
   /**
    * openid 客户openid
    */
@@ -31,9 +28,9 @@ public class WxMpKfSessionRequest implements Serializable {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
-  
+
   public String toJson() {
     return WxMpGsonBuilder.INSTANCE.create().toJson(this);
   }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfInfo.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfInfo.java
index bcb811bc..868bf519 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfInfo.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfInfo.java
@@ -1,12 +1,10 @@
 package me.chanjar.weixin.mp.bean.kefu.result;
 
-import java.io.Serializable;
-
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
+import me.chanjar.weixin.common.util.ToStringUtils;
+
+import java.io.Serializable;
 
 /**
  * 客服基本信息以及客服在线状态信息
@@ -127,7 +125,7 @@ public class WxMpKfInfo implements Serializable {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
   public String getWxAccount() {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfList.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfList.java
index 3465073a..9ec9eeff 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfList.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfList.java
@@ -1,15 +1,12 @@
 package me.chanjar.weixin.mp.bean.kefu.result;
 
-import java.util.List;
-
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.google.gson.annotations.SerializedName;
-
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
+
+import java.util.List;
 /**
- * 
+ *
  * @author Binary Wang
  *
  */
@@ -19,7 +16,7 @@ public class WxMpKfList {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
   public List getKfList() {
@@ -28,8 +25,8 @@ public class WxMpKfList {
 
   public void setKfList(List kfList) {
     this.kfList = kfList;
-  }  
-  
+  }
+
   public static WxMpKfList fromJson(String json) {
     return WxMpGsonBuilder.INSTANCE.create().fromJson(json, WxMpKfList.class);
   }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfMsgList.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfMsgList.java
index cfcbba92..7c04acb3 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfMsgList.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfMsgList.java
@@ -1,9 +1,8 @@
 package me.chanjar.weixin.mp.bean.kefu.result;
 
 import com.google.gson.annotations.SerializedName;
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
 
 import java.util.List;
 
@@ -46,7 +45,7 @@ public class WxMpKfMsgList {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
   public static WxMpKfMsgList fromJson(String responseContent) {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfMsgRecord.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfMsgRecord.java
index 14b26567..0ed9c0b2 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfMsgRecord.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfMsgRecord.java
@@ -1,9 +1,7 @@
 package me.chanjar.weixin.mp.bean.kefu.result;
 
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.google.gson.annotations.SerializedName;
+import me.chanjar.weixin.common.util.ToStringUtils;
 
 /**
  * Created by Binary Wang on 2016/7/18.
@@ -41,8 +39,9 @@ public class WxMpKfMsgRecord {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
+
   public String getWorker() {
     return this.worker;
   }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfOnlineList.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfOnlineList.java
index 1cc674c9..97500095 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfOnlineList.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfOnlineList.java
@@ -1,15 +1,12 @@
 package me.chanjar.weixin.mp.bean.kefu.result;
 
-import java.util.List;
-
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.google.gson.annotations.SerializedName;
-
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
+
+import java.util.List;
 /**
- * 
+ *
  * @author Binary Wang
  *
  */
@@ -19,9 +16,9 @@ public class WxMpKfOnlineList {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
-  
+
   public List getKfOnlineList() {
     return this.kfOnlineList;
   }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSession.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSession.java
index 8a9ef842..da29b4ae 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSession.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSession.java
@@ -1,12 +1,10 @@
 package me.chanjar.weixin.mp.bean.kefu.result;
 
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.google.gson.annotations.SerializedName;
+import me.chanjar.weixin.common.util.ToStringUtils;
 
 /**
- * 
+ *
  * @author Binary Wang
  *
  */
@@ -39,9 +37,9 @@ public class WxMpKfSession {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
-  
+
   public String getKfAccount() {
     return this.kfAccount;
   }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionGetResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionGetResult.java
index eb78f3b6..2eb73143 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionGetResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionGetResult.java
@@ -1,14 +1,11 @@
 package me.chanjar.weixin.mp.bean.kefu.result;
 
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.google.gson.annotations.SerializedName;
-
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
 
 /**
- * 
+ *
  * @author Binary Wang
  *
  */
@@ -27,13 +24,13 @@ public class WxMpKfSessionGetResult {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
-  
+
   public static WxMpKfSessionGetResult fromJson(String json) {
     return WxMpGsonBuilder.INSTANCE.create().fromJson(json, WxMpKfSessionGetResult.class);
   }
-  
+
   public String getKfAccount() {
     return this.kfAccount;
   }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionList.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionList.java
index 8bcac0e4..a9a0640e 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionList.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionList.java
@@ -1,16 +1,13 @@
 package me.chanjar.weixin.mp.bean.kefu.result;
 
-import java.util.List;
-
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.google.gson.annotations.SerializedName;
-
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
 
+import java.util.List;
+
 /**
- * 
+ *
  * @author Binary Wang
  *
  */
@@ -23,7 +20,7 @@ public class WxMpKfSessionList {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
   public static WxMpKfSessionList fromJson(String json) {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionWaitCaseList.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionWaitCaseList.java
index 7e876901..22bcc4da 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionWaitCaseList.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/kefu/result/WxMpKfSessionWaitCaseList.java
@@ -1,16 +1,13 @@
 package me.chanjar.weixin.mp.bean.kefu.result;
 
-import java.util.List;
-
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.google.gson.annotations.SerializedName;
-
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
 
+import java.util.List;
+
 /**
- * 
+ *
  * @author Binary Wang
  *
  */
@@ -29,7 +26,7 @@ public class WxMpKfSessionWaitCaseList {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
   public static WxMpKfSessionWaitCaseList fromJson(String json) {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMediaImgUploadResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMediaImgUploadResult.java
similarity index 91%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMediaImgUploadResult.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMediaImgUploadResult.java
index 58ff57a3..387a791a 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMediaImgUploadResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMediaImgUploadResult.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.result;
+package me.chanjar.weixin.mp.bean.material;
 
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
 
@@ -9,7 +9,7 @@ import java.io.Serializable;
  */
 public class WxMediaImgUploadResult implements Serializable {
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = 1996392453428768829L;
   private String url;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterial.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterial.java
similarity index 97%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterial.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterial.java
index 058d9034..8725510a 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterial.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterial.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.material;
 
 import java.io.File;
 import java.util.HashMap;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterialArticleUpdate.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialArticleUpdate.java
similarity index 95%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterialArticleUpdate.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialArticleUpdate.java
index f8ee265e..371b7e0d 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterialArticleUpdate.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialArticleUpdate.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.material;
 
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
 
@@ -7,7 +7,7 @@ import java.io.Serializable;
 public class WxMpMaterialArticleUpdate implements Serializable {
 
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = -7611963949517780270L;
   private String mediaId;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialCountResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialCountResult.java
similarity index 78%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialCountResult.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialCountResult.java
index 94e20d50..6dd74889 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialCountResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialCountResult.java
@@ -1,12 +1,10 @@
-package me.chanjar.weixin.mp.bean.result;
+package me.chanjar.weixin.mp.bean.material;
+
+import me.chanjar.weixin.common.util.ToStringUtils;
 
 import java.io.Serializable;
 
 public class WxMpMaterialCountResult implements Serializable {
-
-  /**
-   * 
-   */
   private static final long serialVersionUID = -5568772662085874138L;
   private int voiceCount;
   private int videoCount;
@@ -47,8 +45,7 @@ public class WxMpMaterialCountResult implements Serializable {
 
   @Override
   public String toString() {
-    return "WxMpMaterialCountResult [" + "voiceCount=" + this.voiceCount + ", videoCount=" + this.videoCount
-        + ", imageCount=" + this.imageCount + ", newsCount=" + this.newsCount + "]";
+    return ToStringUtils.toSimpleString(this);
   }
 }
 
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialFileBatchGetResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialFileBatchGetResult.java
similarity index 81%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialFileBatchGetResult.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialFileBatchGetResult.java
index 40eaecc1..5df9f454 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialFileBatchGetResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialFileBatchGetResult.java
@@ -1,14 +1,12 @@
-package me.chanjar.weixin.mp.bean.result;
+package me.chanjar.weixin.mp.bean.material;
+
+import me.chanjar.weixin.common.util.ToStringUtils;
 
 import java.io.Serializable;
 import java.util.Date;
 import java.util.List;
 
 public class WxMpMaterialFileBatchGetResult implements Serializable {
-
-  /**
-   * 
-   */
   private static final long serialVersionUID = -560388368297267884L;
   private int totalCount;
   private int itemCount;
@@ -40,7 +38,7 @@ public class WxMpMaterialFileBatchGetResult implements Serializable {
 
   @Override
   public String toString() {
-    return "WxMpMaterialFileBatchGetResult [" + "totalCount=" + this.totalCount + ", itemCount=" + this.itemCount + ", items=" + this.items + "]";
+    return ToStringUtils.toSimpleString(this);
   }
 
   public static class WxMaterialFileBatchGetNewsItem {
@@ -83,7 +81,7 @@ public class WxMpMaterialFileBatchGetResult implements Serializable {
 
     @Override
     public String toString() {
-      return "WxMaterialFileBatchGetNewsItem [" + "mediaId=" + this.mediaId + ", updateTime=" + this.updateTime + ", name=" + this.name + ", url=" + this.url + "]";
+      return ToStringUtils.toSimpleString(this);
     }
   }
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterialNews.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialNews.java
similarity index 89%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterialNews.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialNews.java
index fc7c983c..f67a8cb4 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpMaterialNews.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialNews.java
@@ -1,8 +1,7 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.material;
 
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -32,7 +31,7 @@ public class WxMpMaterialNews implements Serializable {
   /**
    * 
    * 群发图文消息article
-   * 1. thumbMediaId  (必填) 图文消息缩略图的media_id,可以在基础支持-上传多媒体文件接口中获得
+   * 1. thumbMediaId  (必填) 图文消息的封面图片素材id(必须是永久mediaID)
    * 2. author          图文消息的作者
    * 3. title           (必填) 图文消息的标题
    * 4. contentSourceUrl 在图文消息页面点击“阅读原文”后的页面链接
@@ -157,12 +156,12 @@ public class WxMpMaterialNews implements Serializable {
 
     @Override
     public String toString() {
-      return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+      return ToStringUtils.toSimpleString(this);
     }
   }
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialNewsBatchGetResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialNewsBatchGetResult.java
similarity index 79%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialNewsBatchGetResult.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialNewsBatchGetResult.java
index 48563244..03f0a6d8 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialNewsBatchGetResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialNewsBatchGetResult.java
@@ -1,17 +1,14 @@
-package me.chanjar.weixin.mp.bean.result;
+package me.chanjar.weixin.mp.bean.material;
 
-import me.chanjar.weixin.mp.bean.WxMpMaterialNews;
+import me.chanjar.weixin.common.util.ToStringUtils;
 
 import java.io.Serializable;
 import java.util.Date;
 import java.util.List;
 
 public class WxMpMaterialNewsBatchGetResult implements Serializable {
-
-  /**
-   * 
-   */
   private static final long serialVersionUID = -1617952797921001666L;
+
   private int totalCount;
   private int itemCount;
   private List items;
@@ -42,7 +39,7 @@ public class WxMpMaterialNewsBatchGetResult implements Serializable {
 
   @Override
   public String toString() {
-    return "WxMpMaterialNewsBatchGetResult [" + "totalCount=" + this.totalCount + ", itemCount=" + this.itemCount + ", items=" + this.items + "]";
+    return ToStringUtils.toSimpleString(this);
   }
 
   public static class WxMaterialNewsBatchGetNewsItem {
@@ -76,7 +73,7 @@ public class WxMpMaterialNewsBatchGetResult implements Serializable {
 
     @Override
     public String toString() {
-      return "WxMaterialNewsBatchGetNewsItem [" + "mediaId=" + this.mediaId + ", updateTime=" + this.updateTime + ", content=" + this.content + "]";
+      return ToStringUtils.toSimpleString(this);
     }
   }
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialUploadResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialUploadResult.java
similarity index 94%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialUploadResult.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialUploadResult.java
index cb9cecd1..2b3a1e57 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialUploadResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialUploadResult.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.result;
+package me.chanjar.weixin.mp.bean.material;
 
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
 
@@ -7,7 +7,7 @@ import java.io.Serializable;
 public class WxMpMaterialUploadResult implements Serializable {
 
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = -128818731449449537L;
   private String mediaId;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialVideoInfoResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialVideoInfoResult.java
similarity index 95%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialVideoInfoResult.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialVideoInfoResult.java
index 456544d1..06b85b7e 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpMaterialVideoInfoResult.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/material/WxMpMaterialVideoInfoResult.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.result;
+package me.chanjar.weixin.mp.bean.material;
 
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
 
@@ -7,7 +7,7 @@ import java.io.Serializable;
 public class WxMpMaterialVideoInfoResult implements Serializable {
 
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = 1269131745333792202L;
   private String title;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessage.java
similarity index 97%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlMessage.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessage.java
index 864b0d98..732c0d54 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessage.java
@@ -1,22 +1,19 @@
-package me.chanjar.weixin.mp.bean;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
+package me.chanjar.weixin.mp.bean.message;
 
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamConverter;
-
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.common.util.xml.XStreamCDataConverter;
 import me.chanjar.weixin.mp.api.WxMpConfigStorage;
 import me.chanjar.weixin.mp.util.crypto.WxMpCryptUtil;
 import me.chanjar.weixin.mp.util.xml.XStreamTransformer;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * 
@@ -56,6 +53,9 @@ public class WxMpXmlMessage implements Serializable {
   @XStreamConverter(value = XStreamCDataConverter.class)
   private String content;
 
+  @XStreamAlias("MenuId")
+  private Long menuId;
+
   @XStreamAlias("MsgId")
   private Long msgId;
 
@@ -687,6 +687,14 @@ public class WxMpXmlMessage implements Serializable {
     this.sendLocationInfo = sendLocationInfo;
   }
 
+  public Long getMenuId() {
+    return this.menuId;
+  }
+
+  public void setMenuId(Long menuId) {
+    this.menuId = menuId;
+  }
+
   public String getKfAccount() {
     return this.kfAccount;
   }
@@ -715,7 +723,7 @@ public class WxMpXmlMessage implements Serializable {
   public static class ScanCodeInfo {
     @Override
     public String toString() {
-      return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+      return ToStringUtils.toSimpleString(this);
     }
 
     @XStreamAlias("ScanType")
@@ -755,7 +763,7 @@ public class WxMpXmlMessage implements Serializable {
   public static class SendPicsInfo {
     @Override
     public String toString() {
-      return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+      return ToStringUtils.toSimpleString(this);
     }
 
     @XStreamAlias("Count")
@@ -780,8 +788,7 @@ public class WxMpXmlMessage implements Serializable {
     public static class Item {
       @Override
       public String toString() {
-        return ToStringBuilder.reflectionToString(this,
-            ToStringStyle.MULTI_LINE_STYLE);
+        return ToStringUtils.toSimpleString(this);
       }
 
       @XStreamAlias("PicMd5Sum")
@@ -823,7 +830,7 @@ public class WxMpXmlMessage implements Serializable {
 
     @Override
     public String toString() {
-      return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+      return ToStringUtils.toSimpleString(this);
     }
 
     public String getLocationX() {
@@ -869,6 +876,6 @@ public class WxMpXmlMessage implements Serializable {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutImageMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutImageMessage.java
similarity index 93%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutImageMessage.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutImageMessage.java
index 18204a51..62852ca8 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutImageMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutImageMessage.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamConverter;
@@ -9,7 +9,7 @@ import me.chanjar.weixin.common.util.xml.XStreamMediaIdConverter;
 public class WxMpXmlOutImageMessage extends WxMpXmlOutMessage {
 
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = -2684778597067990723L;
   @XStreamAlias("Image")
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMessage.java
similarity index 96%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutMessage.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMessage.java
index 2eb6818b..5b8b81e5 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMessage.java
@@ -1,10 +1,10 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamConverter;
 import me.chanjar.weixin.common.util.xml.XStreamCDataConverter;
 import me.chanjar.weixin.mp.api.WxMpConfigStorage;
-import me.chanjar.weixin.mp.bean.outxmlbuilder.*;
+import me.chanjar.weixin.mp.builder.outxml.*;
 import me.chanjar.weixin.mp.util.crypto.WxMpCryptUtil;
 import me.chanjar.weixin.mp.util.xml.XStreamTransformer;
 
@@ -18,14 +18,14 @@ public abstract class WxMpXmlOutMessage implements Serializable {
   @XStreamAlias("ToUserName")
   @XStreamConverter(value=XStreamCDataConverter.class)
   protected String toUserName;
-  
+
   @XStreamAlias("FromUserName")
   @XStreamConverter(value=XStreamCDataConverter.class)
   protected String fromUserName;
-  
+
   @XStreamAlias("CreateTime")
   protected Long createTime;
-  
+
   @XStreamAlias("MsgType")
   @XStreamConverter(value=XStreamCDataConverter.class)
   protected String msgType;
@@ -61,7 +61,7 @@ public abstract class WxMpXmlOutMessage implements Serializable {
   public void setMsgType(String msgType) {
     this.msgType = msgType;
   }
-  
+
   public String toXml() {
     return XStreamTransformer.toXml((Class) this.getClass(), this);
   }
@@ -95,28 +95,28 @@ public abstract class WxMpXmlOutMessage implements Serializable {
   public static VoiceBuilder VOICE() {
     return new VoiceBuilder();
   }
-  
+
   /**
    * 获得视频消息builder
    */
   public static VideoBuilder VIDEO() {
     return new VideoBuilder();
   }
-  
+
   /**
    * 获得音乐消息builder
    */
   public static MusicBuilder MUSIC() {
     return new MusicBuilder();
   }
-  
+
   /**
    * 获得图文消息builder
    */
   public static NewsBuilder NEWS() {
     return new NewsBuilder();
   }
-  
+
   /**
    * 获得客服消息builder
    */
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutMusicMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMusicMessage.java
similarity index 97%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutMusicMessage.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMusicMessage.java
index ec71167c..a5b48619 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutMusicMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMusicMessage.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamConverter;
@@ -9,7 +9,7 @@ import me.chanjar.weixin.common.util.xml.XStreamCDataConverter;
 public class WxMpXmlOutMusicMessage extends WxMpXmlOutMessage {
 
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = -4159937804975448945L;
   @XStreamAlias("Music")
@@ -34,7 +34,7 @@ public class WxMpXmlOutMusicMessage extends WxMpXmlOutMessage {
   public void setDescription(String description) {
     this.music.setDescription(description);
   }
-  
+
   public String getThumbMediaId() {
     return this.music.getThumbMediaId();
   }
@@ -58,10 +58,10 @@ public class WxMpXmlOutMusicMessage extends WxMpXmlOutMessage {
   public void setHqMusicUrl(String hqMusicUrl) {
     this.music.setHqMusicUrl(hqMusicUrl);
   }
-  
+
   @XStreamAlias("Music")
   public static class Music {
-    
+
     @XStreamAlias("Title")
     @XStreamConverter(value=XStreamCDataConverter.class)
     private String title;
@@ -73,15 +73,15 @@ public class WxMpXmlOutMusicMessage extends WxMpXmlOutMessage {
     @XStreamAlias("ThumbMediaId")
     @XStreamConverter(value=XStreamCDataConverter.class)
     private String thumbMediaId;
-    
+
     @XStreamAlias("MusicUrl")
     @XStreamConverter(value=XStreamCDataConverter.class)
     private String musicUrl;
-    
+
     @XStreamAlias("HQMusicUrl")
     @XStreamConverter(value=XStreamCDataConverter.class)
     private String hqMusicUrl;
-    
+
     public String getTitle() {
       return this.title;
     }
@@ -121,7 +121,7 @@ public class WxMpXmlOutMusicMessage extends WxMpXmlOutMessage {
     public void setHqMusicUrl(String hqMusicUrl) {
       this.hqMusicUrl = hqMusicUrl;
     }
-    
+
   }
 
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutNewsMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutNewsMessage.java
similarity index 96%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutNewsMessage.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutNewsMessage.java
index 170023a3..413a6d7c 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutNewsMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutNewsMessage.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamConverter;
@@ -12,16 +12,16 @@ import java.util.List;
 public class WxMpXmlOutNewsMessage extends WxMpXmlOutMessage {
 
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = -4604402850905714772L;
 
   @XStreamAlias("ArticleCount")
   protected int articleCount;
-  
+
   @XStreamAlias("Articles")
   protected final List articles = new ArrayList<>();
-  
+
   public WxMpXmlOutNewsMessage() {
     this.msgType = WxConsts.XML_MSG_NEWS;
   }
@@ -34,15 +34,15 @@ public class WxMpXmlOutNewsMessage extends WxMpXmlOutMessage {
     this.articles.add(item);
     this.articleCount = this.articles.size();
   }
-  
+
   public List getArticles() {
     return this.articles;
   }
-  
-  
+
+
   @XStreamAlias("item")
   public static class Item {
-    
+
     @XStreamAlias("Title")
     @XStreamConverter(value=XStreamCDataConverter.class)
     private String Title;
@@ -54,11 +54,11 @@ public class WxMpXmlOutNewsMessage extends WxMpXmlOutMessage {
     @XStreamAlias("PicUrl")
     @XStreamConverter(value=XStreamCDataConverter.class)
     private String PicUrl;
-    
+
     @XStreamAlias("Url")
     @XStreamConverter(value=XStreamCDataConverter.class)
     private String Url;
-    
+
     public String getTitle() {
       return this.Title;
     }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTextMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTextMessage.java
similarity index 93%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTextMessage.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTextMessage.java
index 4c439a7c..0719b1ed 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTextMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTextMessage.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamConverter;
@@ -7,9 +7,9 @@ import me.chanjar.weixin.common.util.xml.XStreamCDataConverter;
 
 @XStreamAlias("xml")
 public class WxMpXmlOutTextMessage extends WxMpXmlOutMessage {
-  
+
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = -3972786455288763361L;
   @XStreamAlias("Content")
@@ -19,7 +19,7 @@ public class WxMpXmlOutTextMessage extends WxMpXmlOutMessage {
   public WxMpXmlOutTextMessage() {
     this.msgType = WxConsts.XML_MSG_TEXT;
   }
-  
+
   public String getContent() {
     return this.content;
   }
@@ -28,5 +28,5 @@ public class WxMpXmlOutTextMessage extends WxMpXmlOutMessage {
     this.content = content;
   }
 
-  
+
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferKefuMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferKefuMessage.java
similarity index 96%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferKefuMessage.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferKefuMessage.java
index 3022e70d..b0eece70 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferKefuMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferKefuMessage.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamConverter;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutVideoMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVideoMessage.java
similarity index 97%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutVideoMessage.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVideoMessage.java
index a8f751cf..3c042f6d 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutVideoMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVideoMessage.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamConverter;
@@ -9,7 +9,7 @@ import me.chanjar.weixin.common.util.xml.XStreamCDataConverter;
 public class WxMpXmlOutVideoMessage extends WxMpXmlOutMessage {
 
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = 1745902309380113978L;
   @XStreamAlias("Video")
@@ -42,11 +42,11 @@ public class WxMpXmlOutVideoMessage extends WxMpXmlOutMessage {
   public void setDescription(String description) {
     this.video.setDescription(description);
   }
-  
+
 
   @XStreamAlias("Video")
     public static class Video {
-    
+
     @XStreamAlias("MediaId")
     @XStreamConverter(value=XStreamCDataConverter.class)
     private String mediaId;
@@ -82,7 +82,7 @@ public class WxMpXmlOutVideoMessage extends WxMpXmlOutMessage {
     public void setDescription(String description) {
       this.description = description;
     }
-    
+
   }
 
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutVoiceMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVoiceMessage.java
similarity index 93%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutVoiceMessage.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVoiceMessage.java
index a47766dc..ec354ddf 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/WxMpXmlOutVoiceMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVoiceMessage.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import com.thoughtworks.xstream.annotations.XStreamAlias;
 import com.thoughtworks.xstream.annotations.XStreamConverter;
@@ -9,7 +9,7 @@ import me.chanjar.weixin.common.util.xml.XStreamMediaIdConverter;
 public class WxMpXmlOutVoiceMessage extends WxMpXmlOutMessage {
 
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = 240367390249860551L;
   @XStreamAlias("Voice")
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/WxPayJsSDKCallback.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/WxPayJsSDKCallback.java
index 742f5e18..07a3fe16 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/WxPayJsSDKCallback.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/WxPayJsSDKCallback.java
@@ -1,5 +1,7 @@
 package me.chanjar.weixin.mp.bean.pay;
 
+import me.chanjar.weixin.common.util.ToStringUtils;
+
 import java.io.Serializable;
 
 /**
@@ -259,21 +261,7 @@ public class WxPayJsSDKCallback implements Serializable {
 
   @Override
   public String toString() {
-    return "WxPayJsSDKCallback [return_code=" + this.return_code + ", return_msg="
-      + this.return_msg + ", appid=" + this.appid + ", mch_id=" + this.mch_id
-      + ", device_info=" + this.device_info + ", nonce_str=" + this.nonce_str
-      + ", sign=" + this.sign + ", result_code=" + this.result_code
-      + ", err_code=" + this.err_code + ", err_code_des=" + this.err_code_des
-      + ", openid=" + this.openid + ", is_subscribe=" + this.is_subscribe
-      + ", trade_type=" + this.trade_type + ", bank_type=" + this.bank_type
-      + ", total_fee=" + this.total_fee + ", fee_type=" + this.fee_type
-      + ", cash_fee=" + this.cash_fee + ", cash_fee_type=" + this.cash_fee_type
-      + ", coupon_fee=" + this.coupon_fee + ", coupon_count="
-      + this.coupon_count + ", coupon_batch_id_$n=" + this.coupon_batch_id_$n
-      + ", coupon_id_$n=" + this.coupon_id_$n + ", coupon_fee_$n="
-      + this.coupon_fee_$n + ", transaction_id=" + this.transaction_id
-      + ", out_trade_no=" + this.out_trade_no + ", attach=" + this.attach
-      + ", time_end=" + this.time_end + "]";
+    return ToStringUtils.toSimpleString(this);
   }
 
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxEntPayQueryRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxEntPayQueryRequest.java
index 499e111d..5982d3ee 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxEntPayQueryRequest.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxEntPayQueryRequest.java
@@ -1,11 +1,8 @@
 package me.chanjar.weixin.mp.bean.pay.request;
 
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.thoughtworks.xstream.annotations.XStreamAlias;
-
 import me.chanjar.weixin.common.annotation.Required;
+import me.chanjar.weixin.common.util.ToStringUtils;
 
 /**
  * 
@@ -71,7 +68,7 @@ public class WxEntPayQueryRequest extends WxPayBaseRequest {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxEntPayRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxEntPayRequest.java
index 196ef95f..ef6e8d0a 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxEntPayRequest.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxEntPayRequest.java
@@ -1,9 +1,7 @@
 package me.chanjar.weixin.mp.bean.pay.request;
 
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.thoughtworks.xstream.annotations.XStreamAlias;
+import me.chanjar.weixin.common.util.ToStringUtils;
 
 /**
  * 
@@ -278,7 +276,7 @@ public class WxEntPayRequest {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxPayBaseRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxPayBaseRequest.java
index aabbdfd1..341c3b2e 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxPayBaseRequest.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxPayBaseRequest.java
@@ -1,9 +1,7 @@
 package me.chanjar.weixin.mp.bean.pay.request;
 
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-
 import com.thoughtworks.xstream.annotations.XStreamAlias;
+import me.chanjar.weixin.common.util.ToStringUtils;
 
 /**
  * 
@@ -103,6 +101,6 @@ public abstract class WxPayBaseRequest {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxPayOrderCloseRequest.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxPayOrderCloseRequest.java
new file mode 100644
index 00000000..8f26750f
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/request/WxPayOrderCloseRequest.java
@@ -0,0 +1,36 @@
+package me.chanjar.weixin.mp.bean.pay.request;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * 
+ *  关闭订单请求对象类
+ * Created by Binary Wang on 2016-10-27.
+ * @author binarywang(Binary Wang)
+ * 
+ */ +@XStreamAlias("xml") +public class WxPayOrderCloseRequest extends WxPayBaseRequest{ + + /** + *
+   * 商户订单号
+   * out_trade_no
+   * 二选一
+   * String(32)
+   * 20150806125346
+   * 商户系统内部的订单号,当没提供transaction_id时需要传这个。
+   * 
+ */ + @XStreamAlias("out_trade_no") + private String outTradeNo; + + public String getOutTradeNo() { + return this.outTradeNo; + } + + public void setOutTradeNo(String outTradeNo) { + this.outTradeNo = outTradeNo; + } + +} diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/result/WxPayBaseResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/result/WxPayBaseResult.java index 79f8dbb5..64310138 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/result/WxPayBaseResult.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/result/WxPayBaseResult.java @@ -1,9 +1,7 @@ package me.chanjar.weixin.mp.bean.pay.result; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - import com.thoughtworks.xstream.annotations.XStreamAlias; +import me.chanjar.weixin.common.util.ToStringUtils; /** *
@@ -15,7 +13,7 @@ import com.thoughtworks.xstream.annotations.XStreamAlias;
 public abstract class WxPayBaseResult {
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
   /**
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/result/WxPayOrderCloseResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/result/WxPayOrderCloseResult.java
new file mode 100644
index 00000000..ccfa07bf
--- /dev/null
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/pay/result/WxPayOrderCloseResult.java
@@ -0,0 +1,28 @@
+package me.chanjar.weixin.mp.bean.pay.result;
+
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+
+/**
+ * 
+ * 关闭订单结果对象类
+ * Created by Binary Wang on 2016-10-27.
+ * @author binarywang(Binary Wang)
+ * 
+ */ +@XStreamAlias("xml") +public class WxPayOrderCloseResult extends WxPayBaseResult { + + /** + * 业务结果描述 + */ + @XStreamAlias("result_msg") + private String resultMsg; + + public String getResultMsg() { + return this.resultMsg; + } + + public void setResultMsg(String resultMsg) { + this.resultMsg = resultMsg; + } +} diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpCardResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpCardResult.java index 7a711ba1..b39d2fed 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpCardResult.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpCardResult.java @@ -1,8 +1,7 @@ package me.chanjar.weixin.mp.bean.result; +import me.chanjar.weixin.common.util.ToStringUtils; import me.chanjar.weixin.mp.bean.WxMpCard; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; import java.io.Serializable; @@ -15,7 +14,7 @@ import java.io.Serializable; public class WxMpCardResult implements Serializable { /** - * + * */ private static final long serialVersionUID = -7950878428289035637L; @@ -65,7 +64,7 @@ public class WxMpCardResult implements Serializable { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } public String getUserCardStatus() { diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpUser.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpUser.java index e3e894cb..8d7090fd 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpUser.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/result/WxMpUser.java @@ -1,18 +1,15 @@ package me.chanjar.weixin.mp.bean.result; -import java.io.Serializable; -import java.lang.reflect.Type; -import java.util.List; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; - +import me.chanjar.weixin.common.util.ToStringUtils; import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; +import java.io.Serializable; +import java.lang.reflect.Type; +import java.util.List; + /** * 微信用户信息 * @author chanjarster @@ -175,7 +172,7 @@ public class WxMpUser implements Serializable { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreBaseInfo.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreBaseInfo.java index 409e41c3..91d52d2e 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreBaseInfo.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreBaseInfo.java @@ -4,9 +4,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; import me.chanjar.weixin.common.annotation.Required; +import me.chanjar.weixin.common.util.ToStringUtils; import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; import java.math.BigDecimal; import java.util.List; @@ -19,7 +18,7 @@ import java.util.List; public class WxMpStoreBaseInfo { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } public static WxMpStoreBaseInfo fromJson(String json) { diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreInfo.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreInfo.java index 9d07eb03..1450bc3c 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreInfo.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreInfo.java @@ -1,14 +1,12 @@ package me.chanjar.weixin.mp.bean.store; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - import com.google.gson.annotations.SerializedName; +import me.chanjar.weixin.common.util.ToStringUtils; public class WxMpStoreInfo { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } @SerializedName("base_info") diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreListResult.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreListResult.java index f16c1534..f9cc3fbe 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreListResult.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/store/WxMpStoreListResult.java @@ -1,14 +1,11 @@ package me.chanjar.weixin.mp.bean.store; -import java.util.List; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - import com.google.gson.annotations.SerializedName; - +import me.chanjar.weixin.common.util.ToStringUtils; import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; +import java.util.List; + /** * 门店列表结果类 * @author binarywang(Binary Wang) @@ -18,7 +15,7 @@ import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; public class WxMpStoreListResult { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } public static WxMpStoreListResult fromJson(String json) { diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/tag/WxTagListUser.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/tag/WxTagListUser.java index 47ed067a..e1307be7 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/tag/WxTagListUser.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/tag/WxTagListUser.java @@ -1,14 +1,11 @@ package me.chanjar.weixin.mp.bean.tag; -import java.util.List; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - import com.google.gson.annotations.SerializedName; - +import me.chanjar.weixin.common.util.ToStringUtils; import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; +import java.util.List; + /** * 获取标签下粉丝列表的结果对象 * @author binarywang(Binary Wang) @@ -26,7 +23,7 @@ public class WxTagListUser { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } /** @@ -74,7 +71,7 @@ public class WxTagListUser { public static class WxTagListUserData { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } /** diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/tag/WxUserTag.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/tag/WxUserTag.java index 67b443a4..6554c261 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/tag/WxUserTag.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/tag/WxUserTag.java @@ -1,15 +1,12 @@ package me.chanjar.weixin.mp.bean.tag; -import java.util.List; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - import com.google.gson.JsonParser; import com.google.gson.reflect.TypeToken; - +import me.chanjar.weixin.common.util.ToStringUtils; import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; +import java.util.List; + /** * 用户标签对象 * @author binarywang(Binary Wang) @@ -73,6 +70,6 @@ public class WxUserTag { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + return ToStringUtils.toSimpleString(this); } } diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplate.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplate.java index 0b3a1e61..368284f2 100644 --- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplate.java +++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplate.java @@ -1,16 +1,13 @@ package me.chanjar.weixin.mp.bean.template; -import java.util.List; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - import com.google.gson.JsonParser; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; - +import me.chanjar.weixin.common.util.ToStringUtils; import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; +import java.util.List; + /** *
  * 模板列表信息
@@ -30,9 +27,9 @@ public class WxMpTemplate {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
-  
+
   /**
    * template_id
    * 模板ID
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplateIndustry.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplateIndustry.java
index b0f689a4..6d0db1ab 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplateIndustry.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplateIndustry.java
@@ -1,9 +1,8 @@
 package me.chanjar.weixin.mp.bean.template;
 
 
+import me.chanjar.weixin.common.util.ToStringUtils;
 import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
 
 import java.io.Serializable;
 
@@ -48,7 +47,7 @@ public class WxMpTemplateIndustry implements Serializable {
 
     @Override
     public String toString() {
-      return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+      return ToStringUtils.toSimpleString(this);
     }
 
     public String getId() {
@@ -78,7 +77,7 @@ public class WxMpTemplateIndustry implements Serializable {
 
   @Override
   public String toString() {
-    return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
+    return ToStringUtils.toSimpleString(this);
   }
 
   public static WxMpTemplateIndustry fromJson(String json) {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplateMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplateMessage.java
index 106e5fb6..df8e708d 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplateMessage.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/template/WxMpTemplateMessage.java
@@ -1,18 +1,20 @@
 package me.chanjar.weixin.mp.bean.template;
 
+import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
+
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 
-import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder;
-
+/**
+ * 参考 http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433751277&token=&lang=zh_CN 发送模板消息接口部分
+ */
 public class WxMpTemplateMessage implements Serializable {
   private static final long serialVersionUID = 5063374783759519418L;
 
   private String toUser;
   private String templateId;
   private String url;
-  private String topColor;
   private List data = new ArrayList<>();
 
   public String getToUser() {
@@ -39,14 +41,6 @@ public class WxMpTemplateMessage implements Serializable {
     this.url = url;
   }
 
-  public String getTopColor() {
-    return this.topColor;
-  }
-
-  public void setTopColor(String topColor) {
-    this.topColor = topColor;
-  }
-
   public List getData() {
     return this.data;
   }
@@ -71,7 +65,6 @@ public class WxMpTemplateMessage implements Serializable {
     private String toUser;
     private String templateId;
     private String url;
-    private String topColor;
     private List data = new ArrayList<>();
 
     public WxMpTemplateMessageBuilder toUser(String toUser) {
@@ -89,11 +82,6 @@ public class WxMpTemplateMessage implements Serializable {
       return this;
     }
 
-    public WxMpTemplateMessageBuilder topColor(String topColor) {
-      this.topColor = topColor;
-      return this;
-    }
-
     public WxMpTemplateMessageBuilder data(List data) {
       this.data = data;
       return this;
@@ -103,7 +91,6 @@ public class WxMpTemplateMessage implements Serializable {
       this.toUser(origin.toUser);
       this.templateId(origin.templateId);
       this.url(origin.url);
-      this.topColor(origin.topColor);
       this.data(origin.data);
       return this;
     }
@@ -113,7 +100,6 @@ public class WxMpTemplateMessage implements Serializable {
       m.toUser = this.toUser;
       m.templateId = this.templateId;
       m.url = this.url;
-      m.topColor = this.topColor;
       m.data = this.data;
       return m;
     }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/BaseBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/BaseBuilder.java
similarity index 88%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/BaseBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/BaseBuilder.java
index 911e3f62..c5285d6f 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/BaseBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/BaseBuilder.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.custombuilder;
+package me.chanjar.weixin.mp.builder.kefu;
 
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
 
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/ImageBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/ImageBuilder.java
similarity index 93%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/ImageBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/ImageBuilder.java
index 85855d25..9533f5b3 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/ImageBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/ImageBuilder.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.custombuilder;
+package me.chanjar.weixin.mp.builder.kefu;
 
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/MpNewsBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/MpNewsBuilder.java
similarity index 93%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/MpNewsBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/MpNewsBuilder.java
index 4ab90dce..615802b3 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/MpNewsBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/MpNewsBuilder.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.custombuilder;
+package me.chanjar.weixin.mp.builder.kefu;
 
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/MusicBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/MusicBuilder.java
similarity index 97%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/MusicBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/MusicBuilder.java
index 3555f7a9..8d23bacd 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/MusicBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/MusicBuilder.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.custombuilder;
+package me.chanjar.weixin.mp.builder.kefu;
 
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/NewsBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/NewsBuilder.java
similarity index 94%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/NewsBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/NewsBuilder.java
index d909eb7d..900babb5 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/NewsBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/NewsBuilder.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.custombuilder;
+package me.chanjar.weixin.mp.builder.kefu;
 
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/TextBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/TextBuilder.java
similarity index 93%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/TextBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/TextBuilder.java
index 770675f5..9e5f9384 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/TextBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/TextBuilder.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.custombuilder;
+package me.chanjar.weixin.mp.builder.kefu;
 
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/VideoBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/VideoBuilder.java
similarity index 96%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/VideoBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/VideoBuilder.java
index f9740e2f..13ade68c 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/VideoBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/VideoBuilder.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.custombuilder;
+package me.chanjar.weixin.mp.builder.kefu;
 
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/VoiceBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/VoiceBuilder.java
similarity index 93%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/VoiceBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/VoiceBuilder.java
index 7686a56d..c9cb32b2 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/VoiceBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/VoiceBuilder.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.custombuilder;
+package me.chanjar.weixin.mp.builder.kefu;
 
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/WxCardBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/WxCardBuilder.java
similarity index 93%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/WxCardBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/WxCardBuilder.java
index fb5e81c2..e600df6c 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/custombuilder/WxCardBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/kefu/WxCardBuilder.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean.custombuilder;
+package me.chanjar.weixin.mp.builder.kefu;
 
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/BaseBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/BaseBuilder.java
similarity index 83%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/BaseBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/BaseBuilder.java
index 77db2617..e594565c 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/BaseBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/BaseBuilder.java
@@ -1,29 +1,29 @@
-package me.chanjar.weixin.mp.bean.outxmlbuilder;
+package me.chanjar.weixin.mp.builder.outxml;
 
-import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 
 public abstract class BaseBuilder {
-  
+
   protected String toUserName;
-  
+
   protected String fromUserName;
-  
+
   public BuilderType toUser(String touser) {
     this.toUserName = touser;
     return (BuilderType) this;
   }
-  
+
   public BuilderType fromUser(String fromusername) {
     this.fromUserName = fromusername;
     return (BuilderType) this;
   }
 
   public abstract ValueType build();
-  
+
   public void setCommon(WxMpXmlOutMessage m) {
     m.setToUserName(this.toUserName);
     m.setFromUserName(this.fromUserName);
     m.setCreateTime(System.currentTimeMillis() / 1000l);
   }
-  
+
 }
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/ImageBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/ImageBuilder.java
similarity index 80%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/ImageBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/ImageBuilder.java
index ee265bf3..0bf441bf 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/ImageBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/ImageBuilder.java
@@ -1,6 +1,6 @@
-package me.chanjar.weixin.mp.bean.outxmlbuilder;
+package me.chanjar.weixin.mp.builder.outxml;
 
-import me.chanjar.weixin.mp.bean.WxMpXmlOutImageMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutImageMessage;
 
 /**
  * 图片消息builder
@@ -22,5 +22,5 @@ public final class ImageBuilder extends BaseBuilder {
diff --git a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/NewsBuilder.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/NewsBuilder.java
similarity index 84%
rename from weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/NewsBuilder.java
rename to weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/NewsBuilder.java
index 1014c452..1936c5f5 100644
--- a/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/outxmlbuilder/NewsBuilder.java
+++ b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/builder/outxml/NewsBuilder.java
@@ -1,6 +1,6 @@
-package me.chanjar.weixin.mp.bean.outxmlbuilder;
+package me.chanjar.weixin.mp.builder.outxml;
 
-import me.chanjar.weixin.mp.bean.WxMpXmlOutNewsMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -12,12 +12,12 @@ import java.util.List;
 public final class NewsBuilder extends BaseBuilder {
 
   protected final List articles = new ArrayList<>();
-  
+
   public NewsBuilder addArticle(WxMpXmlOutNewsMessage.Item item) {
     this.articles.add(item);
     return this;
   }
-  
+
   @Override
   public WxMpXmlOutNewsMessage build() {
     WxMpXmlOutNewsMessage m = new WxMpXmlOutNewsMessage();
@@ -27,5 +27,5 @@ public final class NewsBuilder extends BaseBuilder, XStream> CLASS_2_XSTREAM_INSTANCE = configXStreamInstance();
+public class XStreamTransformer {
+  private static final Map, XStream> CLASS_2_XSTREAM_INSTANCE = new HashMap<>();
+
+  static {
+    registerClass(WxMpXmlMessage.class);
+    registerClass(WxMpXmlOutMusicMessage.class);
+    registerClass(WxMpXmlOutNewsMessage.class);
+    registerClass(WxMpXmlOutTextMessage.class);
+    registerClass(WxMpXmlOutImageMessage.class);
+    registerClass(WxMpXmlOutVideoMessage.class);
+    registerClass(WxMpXmlOutVoiceMessage.class);
+    registerClass(WxMpXmlOutTransferKefuMessage.class);
+  }
 
   /**
    * xml -> pojo
@@ -36,16 +36,6 @@ public class XStreamTransformer {
     return object;
   }
 
-  /**
-   * 注册扩展消息的解析器
-   * @param clz 类型
-   * @param xStream xml解析器
-   */
-  public static void register(Class clz, XStream xStream) {
-    CLASS_2_XSTREAM_INSTANCE.put(clz,xStream);
-  }
-
-
   /**
    * pojo -> xml
    */
@@ -53,83 +43,44 @@ public class XStreamTransformer {
     return CLASS_2_XSTREAM_INSTANCE.get(clazz).toXML(object);
   }
 
-  private static Map, XStream> configXStreamInstance() {
-    Map, XStream> map = new HashMap<>();
-    map.put(WxMpXmlMessage.class, config_WxMpXmlMessage());
-    map.put(WxMpXmlOutMusicMessage.class, config_WxMpXmlOutMusicMessage());
-    map.put(WxMpXmlOutNewsMessage.class, config_WxMpXmlOutNewsMessage());
-    map.put(WxMpXmlOutTextMessage.class, config_WxMpXmlOutTextMessage());
-    map.put(WxMpXmlOutImageMessage.class, config_WxMpXmlOutImageMessage());
-    map.put(WxMpXmlOutVideoMessage.class, config_WxMpXmlOutVideoMessage());
-    map.put(WxMpXmlOutVoiceMessage.class, config_WxMpXmlOutVoiceMessage());
-    map.put(WxMpXmlOutTransferKefuMessage.class, config_WxMpXmlOutTransferCustomerServiceMessage());
-
-    return map;
-  }
-
-  private static XStream config_WxMpXmlMessage() {
-    XStream xstream = XStreamInitializer.getInstance();
-    xstream.processAnnotations(WxMpXmlMessage.class);
-    xstream.processAnnotations(WxMpXmlMessage.ScanCodeInfo.class);
-    xstream.processAnnotations(WxMpXmlMessage.SendPicsInfo.class);
-    xstream.processAnnotations(WxMpXmlMessage.SendPicsInfo.Item.class);
-    xstream.processAnnotations(WxMpXmlMessage.SendLocationInfo.class);
-
-    xstream.aliasField("MsgID", WxMpXmlMessage.class, "msgId");
-    return xstream;
-  }
-
-  private static XStream config_WxMpXmlOutImageMessage() {
-    XStream xstream = XStreamInitializer.getInstance();
-    xstream.processAnnotations(WxMpXmlOutMessage.class);
-    xstream.processAnnotations(WxMpXmlOutImageMessage.class);
-    return xstream;
-  }
-
-  private static XStream config_WxMpXmlOutNewsMessage() {
-    XStream xstream = XStreamInitializer.getInstance();
-    xstream.processAnnotations(WxMpXmlOutMessage.class);
-    xstream.processAnnotations(WxMpXmlOutNewsMessage.class);
-    xstream.processAnnotations(WxMpXmlOutNewsMessage.Item.class);
-    return xstream;
+  /**
+   * 注册扩展消息的解析器
+   *
+   * @param clz     类型
+   * @param xStream xml解析器
+   */
+  public static void register(Class clz, XStream xStream) {
+    CLASS_2_XSTREAM_INSTANCE.put(clz, xStream);
   }
 
-  private static XStream config_WxMpXmlOutMusicMessage() {
+  /**
+   * 会自动注册该类及其子类
+   * @param clz 要注册的类
+   */
+  public static void registerClass(Class clz) {
     XStream xstream = XStreamInitializer.getInstance();
-    xstream.processAnnotations(WxMpXmlOutMessage.class);
-    xstream.processAnnotations(WxMpXmlOutMusicMessage.class);
-    xstream.processAnnotations(WxMpXmlOutMusicMessage.Music.class);
-    return xstream;
+    xstream.processAnnotations(clz);
+    xstream.processAnnotations(getInnerClasses(clz));
+    register(clz, xstream);
   }
 
-  private static XStream config_WxMpXmlOutTextMessage() {
-    XStream xstream = XStreamInitializer.getInstance();
-    xstream.processAnnotations(WxMpXmlOutMessage.class);
-    xstream.processAnnotations(WxMpXmlOutTextMessage.class);
-    return xstream;
-  }
+  private static Class[] getInnerClasses(Class clz) {
+    Class[] innerClasses = clz.getClasses();
+    if (innerClasses == null) {
+      return null;
+    }
 
-  private static XStream config_WxMpXmlOutVideoMessage() {
-    XStream xstream = XStreamInitializer.getInstance();
-    xstream.processAnnotations(WxMpXmlOutMessage.class);
-    xstream.processAnnotations(WxMpXmlOutVideoMessage.class);
-    xstream.processAnnotations(WxMpXmlOutVideoMessage.Video.class);
-    return xstream;
-  }
+    List> result = new ArrayList<>();
+    result.addAll(Arrays.asList(innerClasses));
+    for (Class inner : innerClasses) {
+      Class[] innerClz = getInnerClasses(inner);
+      if (innerClz == null) {
+        continue;
+      }
 
-  private static XStream config_WxMpXmlOutVoiceMessage() {
-    XStream xstream = XStreamInitializer.getInstance();
-    xstream.processAnnotations(WxMpXmlOutMessage.class);
-    xstream.processAnnotations(WxMpXmlOutVoiceMessage.class);
-    return xstream;
-  }
+      result.addAll(Arrays.asList(innerClz));
+    }
 
-  private static XStream config_WxMpXmlOutTransferCustomerServiceMessage() {
-    XStream xstream = XStreamInitializer.getInstance();
-    xstream.processAnnotations(WxMpXmlOutMessage.class);
-    xstream.processAnnotations(WxMpXmlOutTransferKefuMessage.class);
-    xstream.processAnnotations(WxMpXmlOutTransferKefuMessage.TransInfo.class);
-    return xstream;
+    return result.toArray(new Class[0]);
   }
-
 }
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/ApiTestModule.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/ApiTestModule.java
index 4ec12cbd..2b749b9f 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/ApiTestModule.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/ApiTestModule.java
@@ -1,15 +1,15 @@
 package me.chanjar.weixin.mp.api;
 
-import java.io.IOException;
-import java.io.InputStream;
-
 import com.google.inject.Binder;
 import com.google.inject.Module;
 import com.thoughtworks.xstream.XStream;
-
 import me.chanjar.weixin.common.util.xml.XStreamInitializer;
 import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.locks.ReentrantLock;
+
 public class ApiTestModule implements Module {
 
   @Override
@@ -18,10 +18,11 @@ public class ApiTestModule implements Module {
         .getSystemResourceAsStream("test-config.xml")) {
       WxXmlMpInMemoryConfigStorage config = this
           .fromXml(WxXmlMpInMemoryConfigStorage.class, is1);
-      WxMpServiceImpl wxService = new WxMpServiceImpl();
+      config.setAccessTokenLock(new ReentrantLock());
+      WxMpService wxService = new WxMpServiceImpl();
       wxService.setWxMpConfigStorage(config);
 
-      binder.bind(WxMpServiceImpl.class).toInstance(wxService);
+      binder.bind(WxMpService.class).toInstance(wxService);
       binder.bind(WxMpConfigStorage.class).toInstance(config);
     } catch (IOException e) {
       e.printStackTrace();
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpBaseAPITest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpBaseAPITest.java
index 7caaee50..b6904eb1 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpBaseAPITest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpBaseAPITest.java
@@ -2,8 +2,7 @@ package me.chanjar.weixin.mp.api;
 
 import com.google.inject.Inject;
 import me.chanjar.weixin.common.exception.WxErrorException;
-import me.chanjar.weixin.common.util.StringUtils;
-import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+import org.apache.commons.lang3.StringUtils;
 import org.testng.Assert;
 import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
@@ -18,7 +17,7 @@ import org.testng.annotations.Test;
 public class WxMpBaseAPITest {
 
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   public void testRefreshAccessToken() throws WxErrorException {
     WxMpConfigStorage configStorage = this.wxService.getWxMpConfigStorage();
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpJsAPITest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpJsAPITest.java
index d4c49a0b..54dcdc04 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpJsAPITest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpJsAPITest.java
@@ -8,11 +8,10 @@ import com.google.inject.Inject;
 
 import me.chanjar.weixin.common.exception.WxErrorException;
 import me.chanjar.weixin.common.util.crypto.SHA1;
-import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
 
 /**
  * 测试jsapi ticket接口
- * 
+ *
  * @author chanjarster
  */
 @Test(groups = "jsAPI", dependsOnGroups = "baseAPI")
@@ -20,7 +19,7 @@ import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
 public class WxMpJsAPITest {
 
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
 
   public void testJsapiTicket() throws WxErrorException {
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMassMessageAPITest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMassMessageAPITest.java
index e093d575..bab9b7ab 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMassMessageAPITest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMassMessageAPITest.java
@@ -13,7 +13,6 @@ import com.google.inject.Inject;
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
 import me.chanjar.weixin.common.exception.WxErrorException;
-import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
 import me.chanjar.weixin.mp.bean.WxMpMassNews;
 import me.chanjar.weixin.mp.bean.WxMpMassOpenIdsMessage;
 import me.chanjar.weixin.mp.bean.WxMpMassTagMessage;
@@ -31,7 +30,7 @@ import me.chanjar.weixin.mp.bean.result.WxMpMassUploadResult;
 public class WxMpMassMessageAPITest {
 
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   @Test
   public void testTextMassOpenIdsMessageSend() throws WxErrorException {
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMessageRouterTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMessageRouterTest.java
index fdc90744..dc922aab 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMessageRouterTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMessageRouterTest.java
@@ -3,8 +3,8 @@ package me.chanjar.weixin.mp.api;
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.common.session.StandardSessionManager;
 import me.chanjar.weixin.common.session.WxSessionManager;
-import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
-import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 import org.testng.Assert;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
@@ -18,7 +18,7 @@ import java.util.Map;
  */
 @Test
 public class WxMpMessageRouterTest {
-  
+
   @Test(enabled = false)
   public void prepare(boolean async, StringBuffer sb, WxMpMessageRouter router) {
     router
@@ -50,7 +50,7 @@ public class WxMpMessageRouterTest {
       }).handler(new WxEchoMpMessageHandler(sb, "matcher")).end()
       .rule().async(async).handler(new WxEchoMpMessageHandler(sb, "ALL")).end();
   }
-  
+
   @Test(dataProvider="messages-1")
   public void testSync(WxMpXmlMessage message, String expected) {
     StringBuffer sb = new StringBuffer();
@@ -59,7 +59,7 @@ public class WxMpMessageRouterTest {
     router.route(message);
     Assert.assertEquals(sb.toString(), expected);
   }
-  
+
   @Test(dataProvider="messages-1")
   public void testAsync(WxMpXmlMessage message, String expected) throws InterruptedException {
     StringBuffer sb = new StringBuffer();
@@ -69,7 +69,7 @@ public class WxMpMessageRouterTest {
     Thread.sleep(500l);
     Assert.assertEquals(sb.toString(), expected);
   }
-  
+
   public void testConcurrency() throws InterruptedException {
     final WxMpMessageRouter router = new WxMpMessageRouter(null);
     router.rule().handler(new WxMpMessageHandler() {
@@ -79,7 +79,7 @@ public class WxMpMessageRouterTest {
         return null;
       }
     }).end();
-    
+
     final WxMpXmlMessage m = new WxMpXmlMessage();
     Runnable r = new Runnable() {
       @Override
@@ -94,26 +94,26 @@ public class WxMpMessageRouterTest {
     for (int i = 0; i < 10; i++) {
       new Thread(r).start();
     }
-    
+
     Thread.sleep(1000l * 2);
   }
   @DataProvider(name="messages-1")
   public Object[][] messages2() {
     WxMpXmlMessage message1 = new WxMpXmlMessage();
     message1.setMsgType(WxConsts.XML_MSG_TEXT);
-  
+
     WxMpXmlMessage message2 = new WxMpXmlMessage();
     message2.setEvent(WxConsts.EVT_CLICK);
-    
+
     WxMpXmlMessage message3 = new WxMpXmlMessage();
     message3.setEventKey("KEY_1");
-    
+
     WxMpXmlMessage message4 = new WxMpXmlMessage();
     message4.setContent("CONTENT_1");
-    
+
     WxMpXmlMessage message5 = new WxMpXmlMessage();
     message5.setContent("BLA");
-    
+
     WxMpXmlMessage message6 = new WxMpXmlMessage();
     message6.setContent("abcd");
 
@@ -123,18 +123,18 @@ public class WxMpMessageRouterTest {
     WxMpXmlMessage c2 = new WxMpXmlMessage();
     c2.setMsgType(WxConsts.XML_MSG_TEXT);
     c2.setEvent(WxConsts.EVT_CLICK);
-    
+
     WxMpXmlMessage c3 = new WxMpXmlMessage();
     c3.setMsgType(WxConsts.XML_MSG_TEXT);
     c3.setEvent(WxConsts.EVT_CLICK);
     c3.setEventKey("KEY_1");
-    
+
     WxMpXmlMessage c4 = new WxMpXmlMessage();
     c4.setMsgType(WxConsts.XML_MSG_TEXT);
     c4.setEvent(WxConsts.EVT_CLICK);
     c4.setEventKey("KEY_1");
     c4.setContent("CONTENT_1");
-    
+
     return new Object[][] {
         new Object[] { message1, WxConsts.XML_MSG_TEXT + "," },
         new Object[] { message2, WxConsts.EVT_CLICK + "," },
@@ -147,7 +147,7 @@ public class WxMpMessageRouterTest {
         new Object[] { c3, "COMBINE_3," },
         new Object[] { c4, "COMBINE_4," }
     };
-    
+
   }
 
   public static class WxEchoMpMessageHandler implements WxMpMessageHandler {
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMiscAPITest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMiscAPITest.java
index de0725ea..656c7162 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMiscAPITest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpMiscAPITest.java
@@ -2,7 +2,6 @@ package me.chanjar.weixin.mp.api;
 
 import com.google.inject.Inject;
 import me.chanjar.weixin.common.exception.WxErrorException;
-import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
 import org.testng.Assert;
 import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
@@ -19,7 +18,7 @@ import java.util.Arrays;
 public class WxMpMiscAPITest {
 
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   @Test
   public void testGetCallbackIP() throws WxErrorException {
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpShortUrlAPITest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpShortUrlAPITest.java
index d392d1f5..87cf7406 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpShortUrlAPITest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxMpShortUrlAPITest.java
@@ -2,14 +2,13 @@ package me.chanjar.weixin.mp.api;
 
 import com.google.inject.Inject;
 import me.chanjar.weixin.common.exception.WxErrorException;
-import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
 import org.testng.Assert;
 import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
 
 /**
  * 测试短连接
- * 
+ *
  * @author chanjarster
  */
 @Test(groups = "shortURLAPI")
@@ -17,7 +16,7 @@ import org.testng.annotations.Test;
 public class WxMpShortUrlAPITest {
 
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   public void testShortUrl() throws WxErrorException {
     String shortUrl = this.wxService.shortUrl("www.baidu.com");
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxXmlMpInMemoryConfigStorage.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxXmlMpInMemoryConfigStorage.java
index c9835a0a..75494efe 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxXmlMpInMemoryConfigStorage.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/WxXmlMpInMemoryConfigStorage.java
@@ -1,8 +1,9 @@
 package me.chanjar.weixin.mp.api;
 
+import com.thoughtworks.xstream.annotations.XStreamAlias;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 
-import com.thoughtworks.xstream.annotations.XStreamAlias;
+import java.util.concurrent.locks.Lock;
 
 @XStreamAlias("xml")
 public class WxXmlMpInMemoryConfigStorage
@@ -50,4 +51,7 @@ public class WxXmlMpInMemoryConfigStorage
     this.templateId = templateId;
   }
 
-}
\ No newline at end of file
+  public void setAccessTokenLock(Lock lock){
+    super.accessTokenLock = lock;
+  }
+}
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpCardServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpCardServiceImplTest.java
index b7981dbd..97619223 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpCardServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpCardServiceImplTest.java
@@ -3,6 +3,7 @@ package me.chanjar.weixin.mp.api.impl;
 import com.google.inject.Inject;
 import me.chanjar.weixin.common.bean.WxCardApiSignature;
 import me.chanjar.weixin.mp.api.ApiTestModule;
+import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.bean.result.WxMpCardResult;
 import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
@@ -19,7 +20,7 @@ import static org.testng.AssertJUnit.assertNotNull;
 public class WxMpCardServiceImplTest {
 
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
   private String cardId = "123";
   private String code = "good";
   private String openid = "abc";
@@ -90,4 +91,4 @@ public class WxMpCardServiceImplTest {
     System.out.println(result);
   }
 
-}
\ No newline at end of file
+}
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpDataCubeServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpDataCubeServiceImplTest.java
index 1111f96a..3e2257a3 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpDataCubeServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpDataCubeServiceImplTest.java
@@ -4,6 +4,7 @@ import java.text.ParseException;
 import java.util.Date;
 import java.util.List;
 
+import me.chanjar.weixin.mp.api.WxMpService;
 import org.apache.commons.lang3.time.FastDateFormat;
 import org.testng.Assert;
 import org.testng.annotations.DataProvider;
@@ -32,7 +33,7 @@ public class WxMpDataCubeServiceImplTest {
       .getInstance("yyyy-MM-dd");
 
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   @DataProvider
   public Object[][] oneDay() throws ParseException {
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpKefuServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpKefuServiceImplTest.java
index 224fc994..3150dec3 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpKefuServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpKefuServiceImplTest.java
@@ -4,6 +4,7 @@ import com.google.inject.Inject;
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.common.exception.WxErrorException;
 import me.chanjar.weixin.mp.api.ApiTestModule;
+import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.api.WxXmlMpInMemoryConfigStorage;
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
 import me.chanjar.weixin.mp.bean.kefu.request.WxMpKfAccountRequest;
@@ -27,7 +28,7 @@ import java.util.Date;
 public class WxMpKefuServiceImplTest {
 
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   public void testSendKefuMpNewsMessage() throws WxErrorException {
     WxXmlMpInMemoryConfigStorage configStorage = (WxXmlMpInMemoryConfigStorage) this.wxService
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpMaterialServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpMaterialServiceImplTest.java
index 7cde30f4..043a49ec 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpMaterialServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpMaterialServiceImplTest.java
@@ -6,10 +6,11 @@ import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
 import me.chanjar.weixin.common.exception.WxErrorException;
 import me.chanjar.weixin.common.util.fs.FileUtils;
 import me.chanjar.weixin.mp.api.ApiTestModule;
-import me.chanjar.weixin.mp.bean.WxMpMaterial;
-import me.chanjar.weixin.mp.bean.WxMpMaterialArticleUpdate;
-import me.chanjar.weixin.mp.bean.WxMpMaterialNews;
-import me.chanjar.weixin.mp.bean.result.*;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.material.WxMpMaterial;
+import me.chanjar.weixin.mp.bean.material.WxMpMaterialArticleUpdate;
+import me.chanjar.weixin.mp.bean.material.WxMpMaterialNews;
+import me.chanjar.weixin.mp.bean.material.*;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
@@ -32,7 +33,7 @@ import static org.junit.Assert.*;
 @Guice(modules = ApiTestModule.class)
 public class WxMpMaterialServiceImplTest {
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   private Map> mediaIds = new LinkedHashMap<>();
   // 缩略图的id,测试上传图文使用
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpMenuServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpMenuServiceImplTest.java
index 1e0b60dd..f31a0e4b 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpMenuServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpMenuServiceImplTest.java
@@ -1,5 +1,6 @@
 package me.chanjar.weixin.mp.api.impl;
 
+import me.chanjar.weixin.mp.api.WxMpService;
 import org.testng.Assert;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Guice;
@@ -24,8 +25,8 @@ import me.chanjar.weixin.mp.api.ApiTestModule;
 public class WxMpMenuServiceImplTest {
 
   @Inject
-  protected WxMpServiceImpl wxService;
-  
+  protected WxMpService wxService;
+
   @Test(dataProvider = "menu")
   public void testCreateMenu(WxMenu wxMenu) throws WxErrorException {
     System.out.println(wxMenu.toJson());
@@ -82,12 +83,12 @@ public class WxMpMenuServiceImplTest {
     Assert.assertNotNull(wxMenu);
     System.out.println(wxMenu.toJson());
   }
-  
+
   @Test(dependsOnMethods = { "testGetMenu"})
   public void testDeleteMenu() throws WxErrorException {
     this.wxService.getMenuService().menuDelete();
   }
-  
+
   @DataProvider(name="menu")
   public Object[][] getMenu() {
     WxMenu menu = new WxMenu();
@@ -95,45 +96,45 @@ public class WxMpMenuServiceImplTest {
     button1.setType(WxConsts.BUTTON_CLICK);
     button1.setName("今日歌曲");
     button1.setKey("V1001_TODAY_MUSIC");
-    
+
     WxMenuButton button2 = new WxMenuButton();
     button2.setType(WxConsts.BUTTON_CLICK);
     button2.setName("歌手简介");
     button2.setKey("V1001_TODAY_SINGER");
-    
+
     WxMenuButton button3 = new WxMenuButton();
     button3.setName("菜单");
-    
+
     menu.getButtons().add(button1);
     menu.getButtons().add(button2);
     menu.getButtons().add(button3);
-    
+
     WxMenuButton button31 = new WxMenuButton();
     button31.setType(WxConsts.BUTTON_VIEW);
     button31.setName("搜索");
     button31.setUrl("http://www.soso.com/");
-    
+
     WxMenuButton button32 = new WxMenuButton();
     button32.setType(WxConsts.BUTTON_VIEW);
     button32.setName("视频");
     button32.setUrl("http://v.qq.com/");
-    
+
     WxMenuButton button33 = new WxMenuButton();
     button33.setType(WxConsts.BUTTON_CLICK);
     button33.setName("赞一下我们");
     button33.setKey("V1001_GOOD");
-    
+
     button3.getSubButtons().add(button31);
     button3.getSubButtons().add(button32);
     button3.getSubButtons().add(button33);
-    
+
     return new Object[][] {
         new Object[] {
             menu
         }
     };
-  
+
   }
-  
-  
+
+
 }
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImplTest.java
index 0f946dc3..b4e22f11 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpPayServiceImplTest.java
@@ -3,6 +3,7 @@ package me.chanjar.weixin.mp.api.impl;
 import com.google.inject.Inject;
 import me.chanjar.weixin.common.exception.WxErrorException;
 import me.chanjar.weixin.mp.api.ApiTestModule;
+import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.api.WxXmlMpInMemoryConfigStorage;
 import me.chanjar.weixin.mp.bean.pay.request.WxEntPayRequest;
 import me.chanjar.weixin.mp.bean.pay.request.WxPayRefundRequest;
@@ -26,7 +27,7 @@ import java.io.File;
 public class WxMpPayServiceImplTest {
 
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   @Test
   public void testGetPayInfo() throws Exception {
@@ -65,7 +66,6 @@ public class WxMpPayServiceImplTest {
 
   /**
    * Test method for {@link me.chanjar.weixin.mp.api.impl.WxMpPayServiceImpl#unifiedOrder(WxPayUnifiedOrderRequest)}.
-   * @throws WxErrorException
    */
   @Test
   public void testUnifiedOrder() throws WxErrorException {
@@ -78,7 +78,6 @@ public class WxMpPayServiceImplTest {
 
   /**
    * Test method for {@link me.chanjar.weixin.mp.api.impl.WxMpPayServiceImpl#queryOrder(String, String)} .
-   * @throws WxErrorException
    */
   @Test
   public final void testQueryOrder() throws WxErrorException {
@@ -87,9 +86,16 @@ public class WxMpPayServiceImplTest {
     System.err.println(this.wxService.getPayService().queryOrder(null, "11111"));
   }
 
+  /**
+   * Test method for {@link me.chanjar.weixin.mp.api.impl.WxMpPayServiceImpl#closeOrder(String)} .
+   */
+  @Test
+  public final void testCloseOrder() throws WxErrorException {
+    System.err.println(this.wxService.getPayService().closeOrder("11212121"));
+  }
+
   /**
    * Test method for {@link me.chanjar.weixin.mp.api.impl.WxMpPayServiceImpl#entPay(WxEntPayRequest, File)}.
-   * @throws WxErrorException
    */
   @Test
   public final void testEntPay() throws WxErrorException {
@@ -100,7 +106,6 @@ public class WxMpPayServiceImplTest {
 
   /**
    * Test method for {@link me.chanjar.weixin.mp.api.impl.WxMpPayServiceImpl#queryEntPay(String, File)}.
-   * @throws WxErrorException
    */
   @Test
   public final void testQueryEntPay() throws WxErrorException {
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpQrCodeServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpQrCodeServiceImplTest.java
index d2ed01da..4c53ac17 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpQrCodeServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpQrCodeServiceImplTest.java
@@ -3,6 +3,7 @@ package me.chanjar.weixin.mp.api.impl;
 import com.google.inject.Inject;
 import me.chanjar.weixin.common.exception.WxErrorException;
 import me.chanjar.weixin.mp.api.ApiTestModule;
+import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
 import org.testng.Assert;
 import org.testng.annotations.Guice;
@@ -12,7 +13,7 @@ import java.io.File;
 
 /**
  * 测试用户相关的接口
- * 
+ *
  * @author chanjarster
  */
 @Test(groups = "qrCodeAPI")
@@ -20,7 +21,7 @@ import java.io.File;
 public class WxMpQrCodeServiceImplTest {
 
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   public void testQrCodeCreateTmpTicket() throws WxErrorException {
     WxMpQrCodeTicket ticket = this.wxService.getQrcodeService().qrCodeCreateTmpTicket(1, null);
@@ -44,7 +45,7 @@ public class WxMpQrCodeServiceImplTest {
     Assert.assertNotNull(file);
     System.out.println(file.getAbsolutePath());
   }
-  
+
   public void testQrCodePictureUrl() throws WxErrorException {
     WxMpQrCodeTicket ticket = this.wxService.getQrcodeService().qrCodeCreateLastTicket(1);
     String url = this.wxService.getQrcodeService().qrCodePictureUrl(ticket.getTicket());
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImplTest.java
index 49316948..c6b8689f 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpServiceImplTest.java
@@ -3,6 +3,7 @@ package me.chanjar.weixin.mp.api.impl;
 import com.google.inject.Inject;
 import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.mp.api.ApiTestModule;
+import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.api.WxXmlMpInMemoryConfigStorage;
 import org.testng.Assert;
 import org.testng.annotations.Guice;
@@ -13,7 +14,7 @@ import org.testng.annotations.Test;
 public class WxMpServiceImplTest {
 
   @Inject
-  private WxMpServiceImpl wxService;
+  private WxMpService wxService;
 
   @Test
   public void testCheckSignature() {
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpStoreServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpStoreServiceImplTest.java
index e610204b..63609293 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpStoreServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpStoreServiceImplTest.java
@@ -3,6 +3,7 @@ package me.chanjar.weixin.mp.api.impl;
 import com.google.inject.Inject;
 import me.chanjar.weixin.common.exception.WxErrorException;
 import me.chanjar.weixin.mp.api.ApiTestModule;
+import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.bean.store.WxMpStoreBaseInfo;
 import me.chanjar.weixin.mp.bean.store.WxMpStoreInfo;
 import me.chanjar.weixin.mp.bean.store.WxMpStoreListResult;
@@ -23,7 +24,7 @@ import static org.junit.Assert.assertNotNull;
 @Guice(modules = ApiTestModule.class)
 public class WxMpStoreServiceImplTest {
   @Inject
-  private WxMpServiceImpl wxMpService;
+  private WxMpService wxMpService;
 
   /**
    * Test method for {@link me.chanjar.weixin.mp.api.impl.WxMpStoreServiceImpl#add(me.chanjar.weixin.mp.bean.store.WxMpStoreBaseInfo)}.
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpTemplateMsgServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpTemplateMsgServiceImplTest.java
index d6f479b5..ee975181 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpTemplateMsgServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpTemplateMsgServiceImplTest.java
@@ -3,11 +3,13 @@ package me.chanjar.weixin.mp.api.impl;
 import com.google.inject.Inject;
 import me.chanjar.weixin.common.exception.WxErrorException;
 import me.chanjar.weixin.mp.api.ApiTestModule;
+import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.api.WxXmlMpInMemoryConfigStorage;
 import me.chanjar.weixin.mp.bean.template.WxMpTemplate;
 import me.chanjar.weixin.mp.bean.template.WxMpTemplateData;
 import me.chanjar.weixin.mp.bean.template.WxMpTemplateIndustry;
 import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
+import org.apache.commons.lang3.RandomStringUtils;
 import org.testng.Assert;
 import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
@@ -25,9 +27,9 @@ import java.util.List;
 @Guice(modules = ApiTestModule.class)
 public class WxMpTemplateMsgServiceImplTest {
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
-  @Test(invocationCount = 10, threadPoolSize = 10)
+  @Test(invocationCount = 5, threadPoolSize = 3)
   public void testSendTemplateMsg() throws WxErrorException {
     SimpleDateFormat dateFormat = new SimpleDateFormat(
       "yyyy-MM-dd HH:mm:ss.SSS");
@@ -38,6 +40,8 @@ public class WxMpTemplateMsgServiceImplTest {
       .templateId(configStorage.getTemplateId()).build();
     templateMessage.addWxMpTemplateData(
       new WxMpTemplateData("first", dateFormat.format(new Date())));
+    templateMessage.addWxMpTemplateData(
+      new WxMpTemplateData("remark", RandomStringUtils.randomAlphanumeric(100), "#FF00FF"));
     String msgId = this.wxService.getTemplateMsgService().sendTemplateMsg(templateMessage);
     Assert.assertNotNull(msgId);
     System.out.println(msgId);
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserBlacklistServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserBlacklistServiceImplTest.java
index aebf6926..2bcc3aea 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserBlacklistServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserBlacklistServiceImplTest.java
@@ -1,6 +1,7 @@
 package me.chanjar.weixin.mp.api.impl;
 
 import me.chanjar.weixin.mp.api.ApiTestModule;
+import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.api.WxXmlMpInMemoryConfigStorage;
 import me.chanjar.weixin.mp.bean.result.WxMpUserBlacklistGetResult;
 import org.testng.Assert;
@@ -18,7 +19,7 @@ import java.util.List;
 @Guice(modules = ApiTestModule.class)
 public class WxMpUserBlacklistServiceImplTest {
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   @Test
   public void testGetBlacklist() throws Exception {
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserServiceImplTest.java
index 4242df10..02252800 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserServiceImplTest.java
@@ -4,6 +4,7 @@ package me.chanjar.weixin.mp.api.impl;
 import java.util.ArrayList;
 import java.util.List;
 
+import me.chanjar.weixin.mp.api.WxMpService;
 import org.testng.Assert;
 import org.testng.annotations.BeforeTest;
 import org.testng.annotations.Guice;
@@ -29,7 +30,7 @@ import me.chanjar.weixin.mp.bean.result.WxMpUserList;
 public class WxMpUserServiceImplTest {
 
   @Inject
-  private WxMpServiceImpl wxService;
+  private WxMpService wxService;
 
   private WxXmlMpInMemoryConfigStorage configProvider;
 
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserTagServiceImplTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserTagServiceImplTest.java
index 2c67ea3f..bca04a06 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserTagServiceImplTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/api/impl/WxMpUserTagServiceImplTest.java
@@ -2,6 +2,7 @@ package me.chanjar.weixin.mp.api.impl;
 
 import java.util.List;
 
+import me.chanjar.weixin.mp.api.WxMpService;
 import org.testng.Assert;
 import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
@@ -22,7 +23,7 @@ import me.chanjar.weixin.mp.bean.tag.WxUserTag;
 @Guice(modules = ApiTestModule.class)
 public class WxMpUserTagServiceImplTest {
   @Inject
-  protected WxMpServiceImpl wxService;
+  protected WxMpService wxService;
 
   private Long tagId = 2L;
 
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessageTest.java
similarity index 99%
rename from weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlMessageTest.java
rename to weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessageTest.java
index 87d8428b..8f49ce99 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlMessageTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessageTest.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import me.chanjar.weixin.common.api.WxConsts;
 import org.testng.Assert;
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlOutImageMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutImageMessageTest.java
similarity index 96%
rename from weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlOutImageMessageTest.java
rename to weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutImageMessageTest.java
index 9c92cab8..3addedb5 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlOutImageMessageTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutImageMessageTest.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
@@ -12,7 +12,7 @@ public class WxMpXmlOutImageMessageTest {
     m.setCreateTime(1122l);
     m.setFromUserName("from");
     m.setToUserName("to");
-    
+
     String expected = ""
         + ""
         + ""
@@ -23,7 +23,7 @@ public class WxMpXmlOutImageMessageTest {
     System.out.println(m.toXml());
     Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
   }
-  
+
   public void testBuild() {
     WxMpXmlOutImageMessage m = WxMpXmlOutMessage.IMAGE().mediaId("ddfefesfsdfef").fromUser("from").toUser("to").build();
     String expected = ""
@@ -38,11 +38,11 @@ public class WxMpXmlOutImageMessageTest {
               m
                 .toXml()
                 .replaceAll("\\s", "")
-                .replaceAll(".*?", ""), 
+                .replaceAll(".*?", ""),
               expected
                 .replaceAll("\\s", "")
                 .replaceAll(".*?", "")
               );
-    
+
   }
 }
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutMusicMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMusicMessageTest.java
similarity index 94%
rename from weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutMusicMessageTest.java
rename to weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMusicMessageTest.java
index 4397a677..3e7a96f0 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutMusicMessageTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutMusicMessageTest.java
@@ -1,10 +1,10 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 @Test
-public class WxXmlOutMusicMessageTest {
+public class WxMpXmlOutMusicMessageTest {
 
   public void test() {
     WxMpXmlOutMusicMessage m = new WxMpXmlOutMusicMessage();
@@ -16,7 +16,7 @@ public class WxXmlOutMusicMessageTest {
     m.setCreateTime(1122l);
     m.setFromUserName("fromUser");
     m.setToUserName("toUser");
-    
+
     String expected = ""
         + ""
         + ""
@@ -33,7 +33,7 @@ public class WxXmlOutMusicMessageTest {
     System.out.println(m.toXml());
     Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
   }
-  
+
   public void testBuild() {
     WxMpXmlOutMusicMessage m = WxMpXmlOutMessage.MUSIC()
           .fromUser("fromUser")
@@ -62,11 +62,11 @@ public class WxXmlOutMusicMessageTest {
         m
         .toXml()
         .replaceAll("\\s", "")
-        .replaceAll(".*?", ""), 
+        .replaceAll(".*?", ""),
         expected
         .replaceAll("\\s", "")
         .replaceAll(".*?", "")
         );
   }
-  
+
 }
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutNewsMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutNewsMessageTest.java
similarity index 95%
rename from weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutNewsMessageTest.java
rename to weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutNewsMessageTest.java
index 5e2da0b0..60d8571a 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutNewsMessageTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutNewsMessageTest.java
@@ -1,17 +1,17 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 @Test
-public class WxXmlOutNewsMessageTest {
+public class WxMpXmlOutNewsMessageTest {
 
   public void test() {
     WxMpXmlOutNewsMessage m = new WxMpXmlOutNewsMessage();
     m.setCreateTime(1122l);
     m.setFromUserName("fromUser");
     m.setToUserName("toUser");
-    
+
     WxMpXmlOutNewsMessage.Item item = new WxMpXmlOutNewsMessage.Item();
     item.setDescription("description");
     item.setPicUrl("picUrl");
@@ -43,14 +43,14 @@ public class WxXmlOutNewsMessageTest {
     System.out.println(m.toXml());
     Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
   }
-  
+
   public void testBuild() {
     WxMpXmlOutNewsMessage.Item item = new WxMpXmlOutNewsMessage.Item();
     item.setDescription("description");
     item.setPicUrl("picUrl");
     item.setTitle("title");
     item.setUrl("url");
-    
+
     WxMpXmlOutNewsMessage m = WxMpXmlOutMessage.NEWS()
           .fromUser("fromUser")
           .toUser("toUser")
@@ -83,11 +83,11 @@ public class WxXmlOutNewsMessageTest {
         m
         .toXml()
         .replaceAll("\\s", "")
-        .replaceAll(".*?", ""), 
+        .replaceAll(".*?", ""),
         expected
         .replaceAll("\\s", "")
         .replaceAll(".*?", "")
         );
   }
-  
+
 }
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutTextMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTextMessageTest.java
similarity index 90%
rename from weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutTextMessageTest.java
rename to weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTextMessageTest.java
index 2a752051..56658967 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutTextMessageTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTextMessageTest.java
@@ -1,10 +1,10 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 @Test
-public class WxXmlOutTextMessageTest {
+public class WxMpXmlOutTextMessageTest {
 
   public void test() {
     WxMpXmlOutTextMessage m = new WxMpXmlOutTextMessage();
@@ -12,7 +12,7 @@ public class WxXmlOutTextMessageTest {
     m.setCreateTime(1122l);
     m.setFromUserName("from");
     m.setToUserName("to");
-    
+
     String expected = ""
         + ""
         + ""
@@ -23,7 +23,7 @@ public class WxXmlOutTextMessageTest {
     System.out.println(m.toXml());
     Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
   }
-  
+
   public void testBuild() {
     WxMpXmlOutTextMessage m = WxMpXmlOutMessage.TEXT().content("content").fromUser("from").toUser("to").build();
     String expected = ""
@@ -38,13 +38,13 @@ public class WxXmlOutTextMessageTest {
         m
         .toXml()
         .replaceAll("\\s", "")
-        .replaceAll(".*?", ""), 
+        .replaceAll(".*?", ""),
         expected
         .replaceAll("\\s", "")
         .replaceAll(".*?", "")
         );
-    
+
   }
-  
+
 
 }
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferKefuMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferKefuMessageTest.java
similarity index 98%
rename from weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferKefuMessageTest.java
rename to weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferKefuMessageTest.java
index ad167738..222f563f 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxMpXmlOutTransferKefuMessageTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutTransferKefuMessageTest.java
@@ -1,4 +1,4 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutVideoMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVideoMessageTest.java
similarity index 92%
rename from weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutVideoMessageTest.java
rename to weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVideoMessageTest.java
index 19fbe62a..0c53b28a 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutVideoMessageTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVideoMessageTest.java
@@ -1,10 +1,10 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 @Test
-public class WxXmlOutVideoMessageTest {
+public class WxMpXmlOutVideoMessageTest {
 
   public void test() {
     WxMpXmlOutVideoMessage m = new WxMpXmlOutVideoMessage();
@@ -14,7 +14,7 @@ public class WxXmlOutVideoMessageTest {
     m.setCreateTime(1122l);
     m.setFromUserName("fromUser");
     m.setToUserName("toUser");
-    
+
     String expected = ""
         + ""
         + ""
@@ -29,7 +29,7 @@ public class WxXmlOutVideoMessageTest {
     System.out.println(m.toXml());
     Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
   }
-  
+
   public void testBuild() {
     WxMpXmlOutVideoMessage m = WxMpXmlOutMessage.VIDEO()
           .mediaId("media_id")
@@ -54,11 +54,11 @@ public class WxXmlOutVideoMessageTest {
         m
         .toXml()
         .replaceAll("\\s", "")
-        .replaceAll(".*?", ""), 
+        .replaceAll(".*?", ""),
         expected
         .replaceAll("\\s", "")
         .replaceAll(".*?", "")
         );
   }
-  
+
 }
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutVoiceMessageTest.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVoiceMessageTest.java
similarity index 90%
rename from weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutVoiceMessageTest.java
rename to weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVoiceMessageTest.java
index 0eb848cd..34314390 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/WxXmlOutVoiceMessageTest.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/message/WxMpXmlOutVoiceMessageTest.java
@@ -1,10 +1,10 @@
-package me.chanjar.weixin.mp.bean;
+package me.chanjar.weixin.mp.bean.message;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
 @Test
-public class WxXmlOutVoiceMessageTest {
+public class WxMpXmlOutVoiceMessageTest {
 
   public void test() {
     WxMpXmlOutVoiceMessage m = new WxMpXmlOutVoiceMessage();
@@ -12,7 +12,7 @@ public class WxXmlOutVoiceMessageTest {
     m.setCreateTime(1122l);
     m.setFromUserName("from");
     m.setToUserName("to");
-    
+
     String expected = ""
         + ""
         + ""
@@ -23,7 +23,7 @@ public class WxXmlOutVoiceMessageTest {
     System.out.println(m.toXml());
     Assert.assertEquals(m.toXml().replaceAll("\\s", ""), expected.replaceAll("\\s", ""));
   }
-  
+
   public void testBuild() {
     WxMpXmlOutVoiceMessage m = WxMpXmlOutMessage.VOICE().mediaId("ddfefesfsdfef").fromUser("from").toUser("to").build();
     String expected = ""
@@ -38,11 +38,11 @@ public class WxXmlOutVoiceMessageTest {
         m
         .toXml()
         .replaceAll("\\s", "")
-        .replaceAll(".*?", ""), 
+        .replaceAll(".*?", ""),
         expected
         .replaceAll("\\s", "")
         .replaceAll(".*?", "")
         );
   }
-  
+
 }
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoGuessNumberHandler.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoGuessNumberHandler.java
index 604531b1..69636d0b 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoGuessNumberHandler.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoGuessNumberHandler.java
@@ -11,8 +11,8 @@ import me.chanjar.weixin.mp.api.WxMpMessageHandler;
 import me.chanjar.weixin.mp.api.WxMpMessageMatcher;
 import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
-import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
-import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 
 public class DemoGuessNumberHandler implements WxMpMessageHandler, WxMpMessageMatcher {
 
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoImageHandler.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoImageHandler.java
index cd733210..145a0130 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoImageHandler.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoImageHandler.java
@@ -6,11 +6,10 @@ import me.chanjar.weixin.common.exception.WxErrorException;
 import me.chanjar.weixin.common.session.WxSessionManager;
 import me.chanjar.weixin.mp.api.WxMpMessageHandler;
 import me.chanjar.weixin.mp.api.WxMpService;
-import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
-import me.chanjar.weixin.mp.bean.WxMpXmlOutImageMessage;
-import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutImageMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 
-import java.io.IOException;
 import java.util.Map;
 
 public class DemoImageHandler implements WxMpMessageHandler {
@@ -30,9 +29,8 @@ public class DemoImageHandler implements WxMpMessageHandler {
       return m;
     } catch (WxErrorException e) {
       e.printStackTrace();
-    } catch (IOException e) {
-      e.printStackTrace();
     }
+
     return null;
   }
 }
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoLogHandler.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoLogHandler.java
index 5245bc03..8e38b3f7 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoLogHandler.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoLogHandler.java
@@ -3,8 +3,8 @@ package me.chanjar.weixin.mp.demo;
 import me.chanjar.weixin.common.session.WxSessionManager;
 import me.chanjar.weixin.mp.api.WxMpMessageHandler;
 import me.chanjar.weixin.mp.api.WxMpService;
-import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
-import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 
 import java.util.Map;
 
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoOAuth2Handler.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoOAuth2Handler.java
index 5dc5437d..82046452 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoOAuth2Handler.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoOAuth2Handler.java
@@ -4,8 +4,8 @@ import me.chanjar.weixin.common.api.WxConsts;
 import me.chanjar.weixin.common.session.WxSessionManager;
 import me.chanjar.weixin.mp.api.WxMpMessageHandler;
 import me.chanjar.weixin.mp.api.WxMpService;
-import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
-import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 
 import java.util.Map;
 
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoTextHandler.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoTextHandler.java
index 9ee15a28..5934eff2 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoTextHandler.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/DemoTextHandler.java
@@ -3,9 +3,9 @@ package me.chanjar.weixin.mp.demo;
 import me.chanjar.weixin.common.session.WxSessionManager;
 import me.chanjar.weixin.mp.api.WxMpMessageHandler;
 import me.chanjar.weixin.mp.api.WxMpService;
-import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
-import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
-import me.chanjar.weixin.mp.bean.WxMpXmlOutTextMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutTextMessage;
 
 import java.util.Map;
 
diff --git a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/WxMpEndpointServlet.java b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/WxMpEndpointServlet.java
index 63052f8b..4a6fd85b 100644
--- a/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/WxMpEndpointServlet.java
+++ b/weixin-java-mp/src/test/java/me/chanjar/weixin/mp/demo/WxMpEndpointServlet.java
@@ -1,17 +1,16 @@
 package me.chanjar.weixin.mp.demo;
 
-import java.io.IOException;
+import me.chanjar.weixin.mp.api.WxMpConfigStorage;
+import me.chanjar.weixin.mp.api.WxMpMessageRouter;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import org.apache.commons.lang3.StringUtils;
 
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-
-import me.chanjar.weixin.common.util.StringUtils;
-import me.chanjar.weixin.mp.api.WxMpConfigStorage;
-import me.chanjar.weixin.mp.api.WxMpMessageRouter;
-import me.chanjar.weixin.mp.api.WxMpService;
-import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
-import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+import java.io.IOException;
 
 /**
  * @author Daniel Qian
diff --git a/weixin-java-mp/src/test/resources/testng.xml b/weixin-java-mp/src/test/resources/testng.xml
index addf7be4..d1499983 100644
--- a/weixin-java-mp/src/test/resources/testng.xml
+++ b/weixin-java-mp/src/test/resources/testng.xml
@@ -18,13 +18,13 @@
 	
 		
 			
-			
-			
-			
-			
-			
-			
-			
+			
+			
+			
+			
+			
+			
+			
 		
 	
 
diff --git a/weixin-java-osgi/pom.xml b/weixin-java-osgi/pom.xml
index 381ec5f9..3833499c 100644
--- a/weixin-java-osgi/pom.xml
+++ b/weixin-java-osgi/pom.xml
@@ -6,7 +6,7 @@
   
     com.github.binarywang
     weixin-java-parent
-    2.3.0-SNAPSHOT
+    2.4.0-SNAPSHOT
   
 
   weixin-java-osgi
@@ -15,11 +15,6 @@
   微信公众号Java SDK OSGI
 
   
-    
-      org.jodd
-      jodd-bean
-      ${jodd-http.version}
-    
     
       org.apache.servicemix.bundles
       org.apache.servicemix.bundles.xmlpull