From 44a1dfaf6371c4d0539e1a386f2da9391ce42af6 Mon Sep 17 00:00:00 2001 From: Binary Wang Date: Wed, 26 Oct 2016 15:29:57 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84bean=E5=92=8Cbuilder=E7=9A=84?= =?UTF-8?q?=E5=8C=85=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/util/crypto/WxCryptUtil.java | 600 +++++++++--------- .../common/util/crypto/WxCryptUtilTest.java | 206 +++--- .../weixin/mp/api/WxMpMessageHandler.java | 4 +- .../weixin/mp/api/WxMpMessageInterceptor.java | 2 +- .../weixin/mp/api/WxMpMessageMatcher.java | 2 +- .../weixin/mp/api/WxMpMessageRouter.java | 460 +++++++------- .../weixin/mp/api/WxMpMessageRouterRule.java | 4 +- .../weixin/mp/bean/kefu/WxMpKefuMessage.java | 16 +- .../mp/bean/{ => message}/WxMpXmlMessage.java | 22 +- .../{ => message}/WxMpXmlOutImageMessage.java | 4 +- .../bean/{ => message}/WxMpXmlOutMessage.java | 20 +- .../{ => message}/WxMpXmlOutMusicMessage.java | 18 +- .../{ => message}/WxMpXmlOutNewsMessage.java | 20 +- .../{ => message}/WxMpXmlOutTextMessage.java | 10 +- .../WxMpXmlOutTransferKefuMessage.java | 2 +- .../{ => message}/WxMpXmlOutVideoMessage.java | 10 +- .../{ => message}/WxMpXmlOutVoiceMessage.java | 4 +- .../kefu}/BaseBuilder.java | 2 +- .../kefu}/ImageBuilder.java | 2 +- .../kefu}/MpNewsBuilder.java | 2 +- .../kefu}/MusicBuilder.java | 2 +- .../kefu}/NewsBuilder.java | 2 +- .../kefu}/TextBuilder.java | 2 +- .../kefu}/VideoBuilder.java | 2 +- .../kefu}/VoiceBuilder.java | 2 +- .../kefu}/WxCardBuilder.java | 2 +- .../outxml}/BaseBuilder.java | 16 +- .../outxml}/ImageBuilder.java | 6 +- .../outxml}/MusicBuilder.java | 6 +- .../outxml}/NewsBuilder.java | 10 +- .../outxml}/TextBuilder.java | 4 +- .../TransferCustomerServiceBuilder.java | 4 +- .../outxml}/VideoBuilder.java | 8 +- .../outxml}/VoiceBuilder.java | 8 +- .../mp/util/xml/XStreamTransformer.java | 18 +- .../weixin/mp/api/WxMpMessageRouterTest.java | 34 +- .../{ => message}/WxMpXmlMessageTest.java | 2 +- .../WxMpXmlOutImageMessageTest.java | 10 +- .../WxMpXmlOutMusicMessageTest.java} | 12 +- .../WxMpXmlOutNewsMessageTest.java} | 14 +- .../WxMpXmlOutTextMessageTest.java} | 14 +- .../WxMpXmlOutTransferKefuMessageTest.java | 2 +- .../WxMpXmlOutVideoMessageTest.java} | 12 +- .../WxMpXmlOutVoiceMessageTest.java} | 12 +- .../mp/demo/DemoGuessNumberHandler.java | 4 +- .../weixin/mp/demo/DemoImageHandler.java | 6 +- .../weixin/mp/demo/DemoLogHandler.java | 4 +- .../weixin/mp/demo/DemoOAuth2Handler.java | 4 +- .../weixin/mp/demo/DemoTextHandler.java | 6 +- .../weixin/mp/demo/WxMpEndpointServlet.java | 4 +- weixin-java-mp/src/test/resources/testng.xml | 14 +- 51 files changed, 827 insertions(+), 829 deletions(-) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlMessage.java (99%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlOutImageMessage.java (93%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlOutMessage.java (96%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlOutMusicMessage.java (97%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlOutNewsMessage.java (96%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlOutTextMessage.java (93%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlOutTransferKefuMessage.java (96%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlOutVideoMessage.java (97%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlOutVoiceMessage.java (93%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/custombuilder => builder/kefu}/BaseBuilder.java (88%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/custombuilder => builder/kefu}/ImageBuilder.java (93%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/custombuilder => builder/kefu}/MpNewsBuilder.java (93%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/custombuilder => builder/kefu}/MusicBuilder.java (97%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/custombuilder => builder/kefu}/NewsBuilder.java (94%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/custombuilder => builder/kefu}/TextBuilder.java (93%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/custombuilder => builder/kefu}/VideoBuilder.java (96%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/custombuilder => builder/kefu}/VoiceBuilder.java (93%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/custombuilder => builder/kefu}/WxCardBuilder.java (93%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/outxmlbuilder => builder/outxml}/BaseBuilder.java (83%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/outxmlbuilder => builder/outxml}/ImageBuilder.java (80%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/outxmlbuilder => builder/outxml}/MusicBuilder.java (91%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/outxmlbuilder => builder/outxml}/NewsBuilder.java (84%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/outxmlbuilder => builder/outxml}/TextBuilder.java (80%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/outxmlbuilder => builder/outxml}/TransferCustomerServiceBuilder.java (88%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/outxmlbuilder => builder/outxml}/VideoBuilder.java (87%) rename weixin-java-mp/src/main/java/me/chanjar/weixin/mp/{bean/outxmlbuilder => builder/outxml}/VoiceBuilder.java (80%) rename weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlMessageTest.java (99%) rename weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlOutImageMessageTest.java (96%) rename weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/{WxXmlOutMusicMessageTest.java => message/WxMpXmlOutMusicMessageTest.java} (94%) rename weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/{WxXmlOutNewsMessageTest.java => message/WxMpXmlOutNewsMessageTest.java} (95%) rename weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/{WxXmlOutTextMessageTest.java => message/WxMpXmlOutTextMessageTest.java} (90%) rename weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/{ => message}/WxMpXmlOutTransferKefuMessageTest.java (98%) rename weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/{WxXmlOutVideoMessageTest.java => message/WxMpXmlOutVideoMessageTest.java} (92%) rename weixin-java-mp/src/test/java/me/chanjar/weixin/mp/bean/{WxXmlOutVoiceMessageTest.java => message/WxMpXmlOutVoiceMessageTest.java} (90%) 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/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-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/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/WxMpXmlMessage.java b/weixin-java-mp/src/main/java/me/chanjar/weixin/mp/bean/message/WxMpXmlMessage.java similarity index 99% 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..baca6698 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,20 @@ -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.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 org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; /** *
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/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"
         + ""
@@ -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..3d07437b 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,9 +6,9 @@ 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;
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..c560f397 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
@@ -10,8 +10,8 @@ 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 me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
 
 /**
  * @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 @@
 	
 		
 			
-			
-			
-			
-			
-			
-			
-			
+			
+			
+			
+			
+			
+			
+