diff --git a/src/main/java/chanjarster/weixin/api/WxConfigProvider.java b/src/main/java/chanjarster/weixin/api/WxConfigProvider.java new file mode 100644 index 00000000..059a2755 --- /dev/null +++ b/src/main/java/chanjarster/weixin/api/WxConfigProvider.java @@ -0,0 +1,26 @@ +package chanjarster.weixin.api; + +import chanjarster.weixin.bean.WxAccessToken; + +/** + * 微信配置的工具类 + * @author chanjarster + * + */ +public interface WxConfigProvider { + + public void updateAccessToken(WxAccessToken accessToken); + + public void updateAccessToken(String accessToken, int expiresIn); + + public String getAccessToken(); + + public String getAppId(); + + public String getSecret(); + + public String getToken(); + + public int getExpiresIn(); + +} diff --git a/src/main/java/chanjarster/weixin/service/WxMsgType.java b/src/main/java/chanjarster/weixin/api/WxConsts.java similarity index 85% rename from src/main/java/chanjarster/weixin/service/WxMsgType.java rename to src/main/java/chanjarster/weixin/api/WxConsts.java index de305f5b..d0176177 100644 --- a/src/main/java/chanjarster/weixin/service/WxMsgType.java +++ b/src/main/java/chanjarster/weixin/api/WxConsts.java @@ -1,6 +1,6 @@ -package chanjarster.weixin.service; +package chanjarster.weixin.api; -public class WxMsgType { +public class WxConsts { public static final String TEXT = "text"; public static final String IMAGE = "image"; diff --git a/src/main/java/chanjarster/weixin/api/WxMemoryConfigProvider.java b/src/main/java/chanjarster/weixin/api/WxMemoryConfigProvider.java new file mode 100644 index 00000000..c93a35ac --- /dev/null +++ b/src/main/java/chanjarster/weixin/api/WxMemoryConfigProvider.java @@ -0,0 +1,47 @@ +package chanjarster.weixin.api; + +import chanjarster.weixin.bean.WxAccessToken; + +/** + * 基于内存的微信配置provider + * @author chanjarster + * + */ +public class WxMemoryConfigProvider implements WxConfigProvider { + + protected String appId; + protected String secret; + protected String token; + protected String accessToken; + protected int expiresIn; + + public void updateAccessToken(WxAccessToken accessToken) { + updateAccessToken(accessToken.getAccess_token(), accessToken.getExpires_in()); + } + + public void updateAccessToken(String accessToken, int expiresIn) { + this.accessToken = accessToken; + this.expiresIn = expiresIn; + } + + public String getAccessToken() { + return this.accessToken; + } + + public String getAppId() { + return this.appId; + } + + public String getSecret() { + return this.secret; + } + + public String getToken() { + return this.token; + } + + public int getExpiresIn() { + return this.expiresIn; + } + +} diff --git a/src/main/java/chanjarster/weixin/api/WxMessageHandler.java b/src/main/java/chanjarster/weixin/api/WxMessageHandler.java new file mode 100644 index 00000000..0a2ac74e --- /dev/null +++ b/src/main/java/chanjarster/weixin/api/WxMessageHandler.java @@ -0,0 +1,16 @@ +package chanjarster.weixin.api; + +import java.util.Map; + +import chanjarster.weixin.bean.WxXmlMessage; + +/** + * 处理微信推送消息的处理器 + * @author chanjarster + * + */ +public interface WxMessageHandler { + + public void handle(WxXmlMessage wxMessage, Map context); + +} diff --git a/src/main/java/chanjarster/weixin/service/WxMessageInterceptor.java b/src/main/java/chanjarster/weixin/api/WxMessageInterceptor.java similarity index 61% rename from src/main/java/chanjarster/weixin/service/WxMessageInterceptor.java rename to src/main/java/chanjarster/weixin/api/WxMessageInterceptor.java index 0dd5564f..10967268 100644 --- a/src/main/java/chanjarster/weixin/service/WxMessageInterceptor.java +++ b/src/main/java/chanjarster/weixin/api/WxMessageInterceptor.java @@ -1,8 +1,8 @@ -package chanjarster.weixin.service; +package chanjarster.weixin.api; import java.util.Map; -import chanjarster.weixin.out.WxUserMessage; +import chanjarster.weixin.bean.WxXmlMessage; /** @@ -17,6 +17,6 @@ public interface WxMessageInterceptor { * @param wxMessage * @return true代表OK,false代表不OK */ - public boolean intercept(WxUserMessage wxMessage, Map context); + public boolean intercept(WxXmlMessage wxMessage, Map context); } diff --git a/src/main/java/chanjarster/weixin/service/WxMessageRouter.java b/src/main/java/chanjarster/weixin/api/WxMessageRouter.java similarity index 94% rename from src/main/java/chanjarster/weixin/service/WxMessageRouter.java rename to src/main/java/chanjarster/weixin/api/WxMessageRouter.java index c7fabb5d..e66aba8d 100644 --- a/src/main/java/chanjarster/weixin/service/WxMessageRouter.java +++ b/src/main/java/chanjarster/weixin/api/WxMessageRouter.java @@ -1,11 +1,11 @@ -package chanjarster.weixin.service; +package chanjarster.weixin.api; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import chanjarster.weixin.out.WxUserMessage; +import chanjarster.weixin.bean.WxXmlMessage; /** * 微信消息路由器,通过代码化的配置,把来自微信的消息交给某个的handler处理 @@ -28,7 +28,7 @@ public class WxMessageRouter { * 处理微信消息 * @param wxMessage */ - public void route(WxUserMessage wxMessage) { + public void route(WxXmlMessage wxMessage) { for (Rule rule : rules) { boolean doNext = rule.service(wxMessage); if (!doNext) { @@ -148,7 +148,7 @@ public class WxMessageRouter { return this.routerBuilder; } - protected boolean test(WxUserMessage wxMessage) { + protected boolean test(WxXmlMessage wxMessage) { return (this.msgType == null || this.msgType.equals(wxMessage.getMsgType())) && @@ -165,7 +165,7 @@ public class WxMessageRouter { * @param wxMessage * @return true 代表继续执行别的router,false 代表停止执行别的router */ - protected boolean service(WxUserMessage wxMessage) { + protected boolean service(WxXmlMessage wxMessage) { // 如果不匹配本规则,那么接着执行后面的Rule if (!test(wxMessage)) { return true; diff --git a/src/main/java/chanjarster/weixin/api/WxService.java b/src/main/java/chanjarster/weixin/api/WxService.java new file mode 100644 index 00000000..9d331590 --- /dev/null +++ b/src/main/java/chanjarster/weixin/api/WxService.java @@ -0,0 +1,54 @@ +package chanjarster.weixin.api; + +import chanjarster.weixin.bean.WxCustomMessage; +import chanjarster.weixin.bean.WxMenu; +import chanjarster.weixin.exception.WxErrorException; + +/** + * 微信API的Service + */ +public interface WxService { + + /** + * 刷新access_token,这个方法是线程安全的 + * 且在高并发情况下只会刷新一次,而不是刷新多次 + * 在非必要情况下不要主动调用此方法 + * 本service的所有方法都会在access_token过期的情况下调用此方法 + * + * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token + * @throws WxErrorException + */ + public void refreshAccessToken() throws WxErrorException; + + /** + * 发送客服消息 + * @param message + * @return + * @throws WxErrorException + */ + public String sendCustomMessage(WxCustomMessage message) throws WxErrorException; + + /** + * 创建菜单 + * @param menu + * @return + * @throws WxErrorException + */ + public String createMenu(WxMenu menu) throws WxErrorException; + + /** + * 删除菜单 + * @return + * @throws WxErrorException + */ + public String deleteMenu() throws WxErrorException; + + /** + * 获得菜单 + * @return + * @throws WxErrorException + */ + public WxMenu getMenu() throws WxErrorException; + + public void setWxConfigProvider(WxConfigProvider wxConfigProvider); +} diff --git a/src/main/java/chanjarster/weixin/service/WxServiceImpl.java b/src/main/java/chanjarster/weixin/api/WxServiceImpl.java similarity index 93% rename from src/main/java/chanjarster/weixin/service/WxServiceImpl.java rename to src/main/java/chanjarster/weixin/api/WxServiceImpl.java index c11af531..12fda07e 100644 --- a/src/main/java/chanjarster/weixin/service/WxServiceImpl.java +++ b/src/main/java/chanjarster/weixin/api/WxServiceImpl.java @@ -1,4 +1,4 @@ -package chanjarster.weixin.service; +package chanjarster.weixin.api; import java.io.IOException; import java.nio.charset.Charset; @@ -14,11 +14,11 @@ import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; +import chanjarster.weixin.bean.WxAccessToken; +import chanjarster.weixin.bean.WxCustomMessage; +import chanjarster.weixin.bean.WxError; +import chanjarster.weixin.bean.WxMenu; import chanjarster.weixin.exception.WxErrorException; -import chanjarster.weixin.in.WxAccessToken; -import chanjarster.weixin.in.WxError; -import chanjarster.weixin.out.WxCustomMessage; -import chanjarster.weixin.out.WxMenu; public class WxServiceImpl implements WxService { @@ -35,12 +35,6 @@ public class WxServiceImpl implements WxService { protected WxConfigProvider wxConfigProvider; - /** - * 获得access_token - * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token - * @return - * @throws WxErrorException - */ public void refreshAccessToken() throws WxErrorException { if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) { try { diff --git a/src/main/java/chanjarster/weixin/in/WxAccessToken.java b/src/main/java/chanjarster/weixin/bean/WxAccessToken.java similarity index 94% rename from src/main/java/chanjarster/weixin/in/WxAccessToken.java rename to src/main/java/chanjarster/weixin/bean/WxAccessToken.java index eb6c901e..90b357d0 100644 --- a/src/main/java/chanjarster/weixin/in/WxAccessToken.java +++ b/src/main/java/chanjarster/weixin/bean/WxAccessToken.java @@ -1,4 +1,4 @@ -package chanjarster.weixin.in; +package chanjarster.weixin.bean; import chanjarster.weixin.util.WxGsonBuilder; diff --git a/src/main/java/chanjarster/weixin/out/WxCustomMessage.java b/src/main/java/chanjarster/weixin/bean/WxCustomMessage.java similarity index 97% rename from src/main/java/chanjarster/weixin/out/WxCustomMessage.java rename to src/main/java/chanjarster/weixin/bean/WxCustomMessage.java index e2a22ec5..9b41f8bf 100644 --- a/src/main/java/chanjarster/weixin/out/WxCustomMessage.java +++ b/src/main/java/chanjarster/weixin/bean/WxCustomMessage.java @@ -1,4 +1,4 @@ -package chanjarster.weixin.out; +package chanjarster.weixin.bean; import java.util.ArrayList; import java.util.List; @@ -6,7 +6,7 @@ import java.util.List; import chanjarster.weixin.util.WxGsonBuilder; /** - * 回复给用户的客服消息 + * 客服消息 * @author chanjarster * */ diff --git a/src/main/java/chanjarster/weixin/in/WxError.java b/src/main/java/chanjarster/weixin/bean/WxError.java similarity index 94% rename from src/main/java/chanjarster/weixin/in/WxError.java rename to src/main/java/chanjarster/weixin/bean/WxError.java index 6298c9dd..6fd63f92 100644 --- a/src/main/java/chanjarster/weixin/in/WxError.java +++ b/src/main/java/chanjarster/weixin/bean/WxError.java @@ -1,4 +1,4 @@ -package chanjarster.weixin.in; +package chanjarster.weixin.bean; import chanjarster.weixin.util.WxGsonBuilder; diff --git a/src/main/java/chanjarster/weixin/out/WxMenu.java b/src/main/java/chanjarster/weixin/bean/WxMenu.java similarity index 72% rename from src/main/java/chanjarster/weixin/out/WxMenu.java rename to src/main/java/chanjarster/weixin/bean/WxMenu.java index 4696ff83..a1662c7d 100644 --- a/src/main/java/chanjarster/weixin/out/WxMenu.java +++ b/src/main/java/chanjarster/weixin/bean/WxMenu.java @@ -1,8 +1,12 @@ -package chanjarster.weixin.out; +package chanjarster.weixin.bean; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; +import chanjarster.weixin.util.WxGsonBuilder; + /** * 公众号菜单 * @author chanjarster @@ -20,6 +24,18 @@ public class WxMenu { this.button = button; } + public String toJson() { + return WxGsonBuilder.create().toJson(this); + } + + public static WxMenu fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxMenu.class); + } + + public static WxMenu fromJson(InputStream is) { + return WxGsonBuilder.create().fromJson(new InputStreamReader(is), WxMenu.class); + } + public static class WxMenuButton { protected String type; diff --git a/src/main/java/chanjarster/weixin/out/WxUserMessage.java b/src/main/java/chanjarster/weixin/bean/WxXmlMessage.java similarity index 95% rename from src/main/java/chanjarster/weixin/out/WxUserMessage.java rename to src/main/java/chanjarster/weixin/bean/WxXmlMessage.java index f8eff04b..0e9dae71 100644 --- a/src/main/java/chanjarster/weixin/out/WxUserMessage.java +++ b/src/main/java/chanjarster/weixin/bean/WxXmlMessage.java @@ -1,10 +1,11 @@ -package chanjarster.weixin.out; +package chanjarster.weixin.bean; + +import java.io.InputStream; import javax.xml.bind.JAXBException; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRegistry; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; @@ -13,7 +14,7 @@ import chanjarster.weixin.util.XmlTransformer; /** *
- * 微信推送过来的消息,同时也是同步回复给用户的消息
+ * 微信推送过来的消息,也是同步回复给用户的消息,xml格式
  * 相关字段的解释看微信开发者文档:
  * http://mp.weixin.qq.com/wiki/index.php?title=接收普通消息
  * http://mp.weixin.qq.com/wiki/index.php?title=接收事件推送
@@ -24,7 +25,7 @@ import chanjarster.weixin.util.XmlTransformer;
  */
 @XmlRootElement(name = "xml")
 @XmlAccessorType(XmlAccessType.FIELD)
-public class WxUserMessage {
+public class WxXmlMessage {
 
   ///////////////////////
   // 以下都是微信推送过来的消息的xml的element所对应的属性
@@ -288,21 +289,31 @@ public class WxUserMessage {
   
   public String toXml() {
     try {
-      return XmlTransformer.toXml(WxUserMessage.class, this);
+      return XmlTransformer.toXml(WxXmlMessage.class, this);
     } catch (JAXBException e) {
       e.printStackTrace();
     }
     return "";
   }
   
-  public static WxUserMessage fromXml(String xml) {
+  public static WxXmlMessage fromXml(String xml) {
     try {
-      return XmlTransformer.fromXml(WxUserMessage.class, xml);
+      return XmlTransformer.fromXml(WxXmlMessage.class, xml);
     } catch (JAXBException e) {
       e.printStackTrace();
     }
     return null;
   }
+  
+  public static WxXmlMessage fromXml(InputStream is) {
+    try {
+      return XmlTransformer.fromXml(WxXmlMessage.class, is);
+    } catch (JAXBException e) {
+      e.printStackTrace();
+    }
+    return null;
+  }
+  
   @Override
   public int hashCode() {
     final int prime = 31;
@@ -338,7 +349,7 @@ public class WxUserMessage {
     if (this == obj) return true;
     if (obj == null) return false;
     if (getClass() != obj.getClass()) return false;
-    WxUserMessage other = (WxUserMessage) obj;
+    WxXmlMessage other = (WxXmlMessage) obj;
     if (Content == null) {
       if (other.Content != null) return false;
     } else if (!Content.equals(other.Content)) return false;
diff --git a/src/main/java/chanjarster/weixin/exception/WxErrorException.java b/src/main/java/chanjarster/weixin/exception/WxErrorException.java
index 352a75f9..9b95d5eb 100644
--- a/src/main/java/chanjarster/weixin/exception/WxErrorException.java
+++ b/src/main/java/chanjarster/weixin/exception/WxErrorException.java
@@ -1,6 +1,6 @@
 package chanjarster.weixin.exception;
 
-import chanjarster.weixin.in.WxError;
+import chanjarster.weixin.bean.WxError;
 
 public class WxErrorException extends Exception {
 
diff --git a/src/main/java/chanjarster/weixin/service/WxConfigProvider.java b/src/main/java/chanjarster/weixin/service/WxConfigProvider.java
deleted file mode 100644
index fd41a6bf..00000000
--- a/src/main/java/chanjarster/weixin/service/WxConfigProvider.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package chanjarster.weixin.service;
-
-public interface WxConfigProvider {
-
-  public void updateAccessToken(String accessToken, Integer expiresIn);
-  
-  public String getAccessToken();
-  
-  public String getAppId();
-  
-  public String getSecret();
-  
-  public String getToken();
-  
-}
diff --git a/src/main/java/chanjarster/weixin/service/WxMessageHandler.java b/src/main/java/chanjarster/weixin/service/WxMessageHandler.java
deleted file mode 100644
index b0a9bfd1..00000000
--- a/src/main/java/chanjarster/weixin/service/WxMessageHandler.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package chanjarster.weixin.service;
-
-import java.util.Map;
-
-import chanjarster.weixin.out.WxUserMessage;
-
-/**
- * 处理微信推送消息的处理器
- * @author chanjarster
- *
- */
-public interface WxMessageHandler {
-
-  public void handle(WxUserMessage wxMessage, Map context);
-  
-}
diff --git a/src/main/java/chanjarster/weixin/service/WxService.java b/src/main/java/chanjarster/weixin/service/WxService.java
deleted file mode 100644
index 4023a492..00000000
--- a/src/main/java/chanjarster/weixin/service/WxService.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package chanjarster.weixin.service;
-
-import chanjarster.weixin.exception.WxErrorException;
-import chanjarster.weixin.out.WxCustomMessage;
-import chanjarster.weixin.out.WxMenu;
-
-/**
- * 微信相关的常量
- */
-public interface WxService {
-  
-  public void refreshAccessToken() throws WxErrorException;
-  
-  public String sendCustomMessage(WxCustomMessage message) throws WxErrorException;
-  
-  public String createMenu(WxMenu menu) throws WxErrorException;
-  
-  public String deleteMenu() throws WxErrorException;
-  
-  public WxMenu getMenu() throws WxErrorException;
-
-  public void setWxConfigProvider(WxConfigProvider wxConfigProvider);
-}
diff --git a/src/main/java/chanjarster/weixin/util/GsonHelper.java b/src/main/java/chanjarster/weixin/util/GsonHelper.java
new file mode 100644
index 00000000..0b138a5e
--- /dev/null
+++ b/src/main/java/chanjarster/weixin/util/GsonHelper.java
@@ -0,0 +1,115 @@
+/*
+ * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved.
+ *
+ * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended
+ * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction
+ * arose from modification of the original source, or other redistribution of this source
+ * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD.
+ */
+package chanjarster.weixin.util;
+
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+
+public class GsonHelper {
+
+	public static boolean isNull(JsonElement element) {
+		return element == null || element.isJsonNull();
+	}
+	
+	public static boolean isNotNull(JsonElement element) {
+		return !isNull(element);
+	}
+	
+	public static Long getLong(JsonObject json, String property) {
+		return getAsLong(json.get(property));
+	}
+	
+	public static long getPrimitiveLong(JsonObject json, String property) {
+		return getAsPrimitiveLong(json.get(property));
+	}
+
+	public static Integer getInteger(JsonObject json, String property) {
+		return getAsInteger(json.get(property));
+	}
+
+	public static int getPrimitiveInteger(JsonObject json, String property) {
+		return getAsPrimitiveInt(json.get(property));
+	}
+	
+	public static Double getDouble(JsonObject json, String property) {
+		return getAsDouble(json.get(property));
+	}
+	
+	public static double getPrimitiveDouble(JsonObject json, String property) {
+		return getAsPrimitiveDouble(json.get(property));
+	}
+	
+	public static Float getFloat(JsonObject json, String property) {
+		return getAsFloat(json.get(property));
+	}
+	
+	public static float getPrimitiveFloat(JsonObject json, String property) {
+		return getAsPrimitiveFloat(json.get(property));
+	}
+	
+	public static Boolean getBoolean(JsonObject json, String property) {
+		return getAsBoolean(json.get(property));
+	}
+
+	public static String getString(JsonObject json, String property) {
+		return getAsString(json.get(property));
+	}
+	
+	public static String getAsString(JsonElement element) {
+		return isNull(element) ? null : element.getAsString();
+	}
+	
+	public static Long getAsLong(JsonElement element) {
+		return isNull(element) ? null : element.getAsLong();
+	}
+	
+	public static long getAsPrimitiveLong(JsonElement element) {
+		Long r = getAsLong(element);
+		return r == null ? 0l : r;
+	}
+	
+	public static Integer getAsInteger(JsonElement element) {
+		return isNull(element) ? null : element.getAsInt();
+	}
+	
+	public static int getAsPrimitiveInt(JsonElement element) {
+		Integer r = getAsInteger(element);
+		return r == null ? 0 : r;
+	}
+	
+	public static Boolean getAsBoolean(JsonElement element) {
+		return isNull(element) ? null : element.getAsBoolean();
+	}
+	
+	public static boolean getAsPrimitiveBool(JsonElement element) {
+		Boolean r = getAsBoolean(element);
+		return r == null ? false : r.booleanValue();
+	}
+	
+	public static Double getAsDouble(JsonElement element) {
+		return isNull(element) ? null : element.getAsDouble();
+	}
+	
+	public static double getAsPrimitiveDouble(JsonElement element) {
+		Double r = getAsDouble(element);
+		return r == null ? 0d : r;
+	}
+	
+	public static Float getAsFloat(JsonElement element) {
+		return isNull(element) ? null : element.getAsFloat();
+	}
+	
+	public static float getAsPrimitiveFloat(JsonElement element) {
+		Float r = getAsFloat(element);
+		return r == null ? 0f : r;
+	}
+	
+}
\ No newline at end of file
diff --git a/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java b/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java
index 777db95b..abf6eab1 100644
--- a/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java
+++ b/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java
@@ -10,9 +10,9 @@ package chanjarster.weixin.util;
 
 import java.lang.reflect.Type;
 
-import chanjarster.weixin.out.WxCustomMessage;
-import chanjarster.weixin.out.WxCustomMessage.WxArticle;
-import chanjarster.weixin.service.WxMsgType;
+import chanjarster.weixin.api.WxConsts;
+import chanjarster.weixin.bean.WxCustomMessage;
+import chanjarster.weixin.bean.WxCustomMessage.WxArticle;
 
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
@@ -32,25 +32,25 @@ public class WxCustomMessageGsonAdapter implements JsonSerializer {
+public class WxMenuGsonAdapter implements JsonSerializer, JsonDeserializer {
 
   public JsonElement serialize(WxMenu menu, Type typeOfSrc, JsonSerializationContext context) {
     JsonObject json = new JsonObject();
 
     JsonArray buttonArray = new JsonArray();
     for (WxMenuButton button : menu.getButton()) {
-      JsonObject buttonJson = serialize(button);
+      JsonObject buttonJson = convertToJson(button);
       buttonArray.add(buttonJson);
     }
     json.add("button", buttonArray);
@@ -39,17 +42,48 @@ public class WxMenuGsonAdapter implements JsonSerializer {
     return json;
   }
 
-  protected JsonObject serialize(WxMenuButton button) {
+  protected JsonObject convertToJson(WxMenuButton button) {
     JsonObject buttonJson = new JsonObject();
+    buttonJson.addProperty("type", button.getType());
     buttonJson.addProperty("name", button.getName());
-    // TODO 其他字段
-    if (button.getSub_button() == null || button.getSub_button().size() == 0) {
+    buttonJson.addProperty("key", button.getKey());
+    buttonJson.addProperty("url", button.getUrl());
+    if (button.getSub_button() != null && button.getSub_button().size() > 0) {
       JsonArray buttonArray = new JsonArray();
       for (WxMenuButton sub_button : button.getSub_button()) {
-        buttonArray.add(serialize(sub_button));
+        buttonArray.add(convertToJson(sub_button));
       }
       buttonJson.add("sub_button", buttonArray);
     }
     return buttonJson;
   }
-}
+
+  public WxMenu deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+    WxMenu menu = new WxMenu();
+    JsonArray buttonsJson = json.getAsJsonObject().get("button").getAsJsonArray();
+    for (int i = 0; i < buttonsJson.size(); i++) {
+      JsonObject buttonJson = buttonsJson.get(i).getAsJsonObject();
+      WxMenuButton button = convertFromJson(buttonJson);
+      menu.getButton().add(button);
+      if (buttonJson.get("sub_button") == null || buttonJson.get("sub_button").isJsonNull()) {
+        continue;
+      }
+      JsonArray sub_buttonsJson = buttonJson.get("sub_button").getAsJsonArray();
+      for (int j = 0; j < sub_buttonsJson.size(); j++) {
+        JsonObject sub_buttonJson = sub_buttonsJson.get(j).getAsJsonObject();
+        button.getSub_button().add(convertFromJson(sub_buttonJson));
+      }
+    }
+    return menu;
+  }
+  
+  protected WxMenuButton convertFromJson(JsonObject json) {
+    WxMenuButton button = new WxMenuButton();
+    button.setName(GsonHelper.getString(json, "name"));
+    button.setKey(GsonHelper.getString(json, "key"));
+    button.setUrl(GsonHelper.getString(json, "url"));
+    button.setType(GsonHelper.getString(json, "type"));
+    return button;
+  }
+
+}
\ No newline at end of file
diff --git a/src/test/java/chanjarster/weixin/service/WxMessageRouterTest.java b/src/test/java/chanjarster/weixin/api/WxMessageRouterTest.java
similarity index 56%
rename from src/test/java/chanjarster/weixin/service/WxMessageRouterTest.java
rename to src/test/java/chanjarster/weixin/api/WxMessageRouterTest.java
index e963f646..4c20f66b 100644
--- a/src/test/java/chanjarster/weixin/service/WxMessageRouterTest.java
+++ b/src/test/java/chanjarster/weixin/api/WxMessageRouterTest.java
@@ -1,4 +1,4 @@
-package chanjarster.weixin.service;
+package chanjarster.weixin.api;
 
 // TODO
 public class WxMessageRouterTest {
diff --git a/src/test/java/chanjarster/weixin/service/WxServiceTest.java b/src/test/java/chanjarster/weixin/api/WxServiceTest.java
similarity index 60%
rename from src/test/java/chanjarster/weixin/service/WxServiceTest.java
rename to src/test/java/chanjarster/weixin/api/WxServiceTest.java
index e45c5d31..6d5a6a84 100644
--- a/src/test/java/chanjarster/weixin/service/WxServiceTest.java
+++ b/src/test/java/chanjarster/weixin/api/WxServiceTest.java
@@ -1,4 +1,4 @@
-package chanjarster.weixin.service;
+package chanjarster.weixin.api;
 
 import java.io.InputStream;
 
@@ -12,14 +12,18 @@ import org.testng.Assert;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
+import chanjarster.weixin.api.WxConsts;
+import chanjarster.weixin.api.WxMemoryConfigProvider;
+import chanjarster.weixin.api.WxService;
+import chanjarster.weixin.api.WxServiceImpl;
+import chanjarster.weixin.bean.WxCustomMessage;
 import chanjarster.weixin.exception.WxErrorException;
-import chanjarster.weixin.out.WxCustomMessage;
 import chanjarster.weixin.util.XmlTransformer;
 
 public class WxServiceTest {
 
   @Test(dataProvider = "configs")
-  public void testRefreshAccessToken(WxConfigProvider config) throws WxErrorException {
+  public void testRefreshAccessToken(WxTestConfigProvider config) throws WxErrorException {
     String before = config.getAccessToken();
     
     WxService wxService = new WxServiceImpl();
@@ -33,12 +37,12 @@ public class WxServiceTest {
   }
   
   @Test(dataProvider = "configs")
-  public void sendCustomMessage(SimpleWxConfigProvider config) throws WxErrorException {
+  public void sendCustomMessage(WxTestConfigProvider config) throws WxErrorException {
     WxService wxService = new WxServiceImpl();
     wxService.setWxConfigProvider(config);
     
     WxCustomMessage message = new WxCustomMessage();
-    message.setMsgtype(WxMsgType.TEXT);
+    message.setMsgtype(WxConsts.TEXT);
     message.setTouser(config.getOpenId());
     message.setContent("欢迎使用教务系统微信公众号\n下面\nHello World");
 
@@ -56,7 +60,7 @@ public class WxServiceTest {
      */
     // 没有access_token
     InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml");
-    SimpleWxConfigProvider config1 = XmlTransformer.fromXml(SimpleWxConfigProvider.class, is1);
+    WxTestConfigProvider config1 = XmlTransformer.fromXml(WxTestConfigProvider.class, is1);
     return new Object[][] {
         new Object[] {
             config1
@@ -66,47 +70,10 @@ public class WxServiceTest {
   
   @XmlRootElement(name = "xml")
   @XmlAccessorType(XmlAccessType.FIELD)
-  public static class SimpleWxConfigProvider implements WxConfigProvider {
-    private String appId;
-    private String secret;
-    private String accessToken = "";
-    private Integer expiresIn;
-    private String token;
-    private String openId;
-    public void updateAccessToken(String accessToken, Integer expiresIn) {
-      this.accessToken = accessToken;
-      this.expiresIn = expiresIn;
-    }
-    public String getAccessToken() {
-      return accessToken;
-    }
-    public String getAppId() {
-      return appId;
-    }
-    public String getSecret() {
-      return secret;
-    }
-    public String getToken() {
-      return token;
-    }
-    public void setAppId(String appId) {
-      this.appId = appId;
-    }
-    public void setSecret(String secret) {
-      this.secret = secret;
-    }
-    public void setToken(String token) {
-      this.token = token;
-    }
-    public Integer getExpiresIn() {
-      return expiresIn;
-    }
-    public void setExpiresIn(Integer expiresIn) {
-      this.expiresIn = expiresIn;
-    }
-    public void setAccessToken(String accessToken) {
-      this.accessToken = accessToken;
-    }
+  public static class WxTestConfigProvider extends WxMemoryConfigProvider {
+    
+    protected String openId;
+    
     public String getOpenId() {
       return openId;
     }
diff --git a/src/test/java/chanjarster/weixin/bean/WxAccessTokenTest.java b/src/test/java/chanjarster/weixin/bean/WxAccessTokenTest.java
new file mode 100644
index 00000000..2115eb12
--- /dev/null
+++ b/src/test/java/chanjarster/weixin/bean/WxAccessTokenTest.java
@@ -0,0 +1,18 @@
+package chanjarster.weixin.bean;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+@Test
+public class WxAccessTokenTest {
+
+  public void testFromJson() {
+
+    String json = "{\"access_token\":\"ACCESS_TOKEN\",\"expires_in\":7200}";
+    WxAccessToken wxError = WxAccessToken.fromJson(json);
+    Assert.assertEquals(wxError.getAccess_token(), "ACCESS_TOKEN");
+    Assert.assertTrue(wxError.getExpires_in() == 7200);
+
+  }
+  
+}
diff --git a/src/test/java/chanjarster/weixin/out/WxCustomMessageTest.java b/src/test/java/chanjarster/weixin/bean/WxCustomMessageTest.java
similarity index 87%
rename from src/test/java/chanjarster/weixin/out/WxCustomMessageTest.java
rename to src/test/java/chanjarster/weixin/bean/WxCustomMessageTest.java
index 8828be4f..b6922fae 100644
--- a/src/test/java/chanjarster/weixin/out/WxCustomMessageTest.java
+++ b/src/test/java/chanjarster/weixin/bean/WxCustomMessageTest.java
@@ -1,18 +1,19 @@
-package chanjarster.weixin.out;
+package chanjarster.weixin.bean;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import chanjarster.weixin.out.WxCustomMessage;
-import chanjarster.weixin.out.WxCustomMessage.WxArticle;
-import chanjarster.weixin.service.WxMsgType;
+import chanjarster.weixin.api.WxConsts;
+import chanjarster.weixin.bean.WxCustomMessage;
+import chanjarster.weixin.bean.WxCustomMessage.WxArticle;
+
 @Test
 public class WxCustomMessageTest {
 
   public void testTextReply() {
     WxCustomMessage reply = new WxCustomMessage();
     reply.setTouser("OPENID");
-    reply.setMsgtype(WxMsgType.TEXT);
+    reply.setMsgtype(WxConsts.TEXT);
     reply.setContent("sfsfdsdf");
     Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"text\",\"text\":{\"content\":\"sfsfdsdf\"}}");
   }
@@ -20,7 +21,7 @@ public class WxCustomMessageTest {
   public void testImageReply() {
     WxCustomMessage reply = new WxCustomMessage();
     reply.setTouser("OPENID");
-    reply.setMsgtype(WxMsgType.IMAGE);
+    reply.setMsgtype(WxConsts.IMAGE);
     reply.setMedia_id("MEDIA_ID");
     Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"image\",\"image\":{\"media_id\":\"MEDIA_ID\"}}");
   }
@@ -28,7 +29,7 @@ public class WxCustomMessageTest {
   public void testVoiceReply() {
     WxCustomMessage reply = new WxCustomMessage();
     reply.setTouser("OPENID");
-    reply.setMsgtype(WxMsgType.VOICE);
+    reply.setMsgtype(WxConsts.VOICE);
     reply.setMedia_id("MEDIA_ID");
     Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"voice\",\"voice\":{\"media_id\":\"MEDIA_ID\"}}");
   }
@@ -36,7 +37,7 @@ public class WxCustomMessageTest {
   public void testVideoReply() {
     WxCustomMessage reply = new WxCustomMessage();
     reply.setTouser("OPENID");
-    reply.setMsgtype(WxMsgType.VIDEO);
+    reply.setMsgtype(WxConsts.VIDEO);
     reply.setMedia_id("MEDIA_ID");
     reply.setThumb_media_id("MEDIA_ID");
     reply.setTitle("TITLE");
@@ -47,7 +48,7 @@ public class WxCustomMessageTest {
   public void testMusicReply() {
     WxCustomMessage reply = new WxCustomMessage();
     reply.setTouser("OPENID");
-    reply.setMsgtype(WxMsgType.MUSIC);
+    reply.setMsgtype(WxConsts.MUSIC);
     reply.setThumb_media_id("MEDIA_ID");
     reply.setDescription("DESCRIPTION");
     reply.setTitle("TITLE");
@@ -59,7 +60,7 @@ public class WxCustomMessageTest {
   public void testNewsReply() {
     WxCustomMessage reply = new WxCustomMessage();
     reply.setTouser("OPENID");
-    reply.setMsgtype(WxMsgType.NEWS);
+    reply.setMsgtype(WxConsts.NEWS);
     
     WxArticle article1 = new WxArticle();
     article1.setUrl("URL");
diff --git a/src/test/java/chanjarster/weixin/in/WxErrorTest.java b/src/test/java/chanjarster/weixin/bean/WxErrorTest.java
similarity index 93%
rename from src/test/java/chanjarster/weixin/in/WxErrorTest.java
rename to src/test/java/chanjarster/weixin/bean/WxErrorTest.java
index c052de70..9c938160 100644
--- a/src/test/java/chanjarster/weixin/in/WxErrorTest.java
+++ b/src/test/java/chanjarster/weixin/bean/WxErrorTest.java
@@ -1,9 +1,9 @@
-package chanjarster.weixin.in;
+package chanjarster.weixin.bean;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import chanjarster.weixin.in.WxError;
+import chanjarster.weixin.bean.WxError;
 
 @Test
 public class WxErrorTest {
diff --git a/src/test/java/chanjarster/weixin/bean/WxMenuTest.java b/src/test/java/chanjarster/weixin/bean/WxMenuTest.java
new file mode 100644
index 00000000..4962c9ae
--- /dev/null
+++ b/src/test/java/chanjarster/weixin/bean/WxMenuTest.java
@@ -0,0 +1,104 @@
+package chanjarster.weixin.bean;
+
+import org.apache.http.util.Asserts;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import chanjarster.weixin.bean.WxMenu.WxMenuButton;
+
+@Test
+public class WxMenuTest {
+
+  @Test(dataProvider="json")
+  public void testFromJson(String json) {
+    WxMenu menu = WxMenu.fromJson(json);
+    Assert.assertEquals(menu.getButton().size(), 3);
+  }
+  
+  @Test(dataProvider="json")
+  public void testToJson(String json) {
+    WxMenu menu = new WxMenu();
+    WxMenuButton button1 = new WxMenuButton();
+    button1.setType("click");
+    button1.setName("今日歌曲");
+    button1.setKey("V1001_TODAY_MUSIC");
+    
+    WxMenuButton button2 = new WxMenuButton();
+    button2.setType("click");
+    button2.setName("歌手简介");
+    button2.setKey("V1001_TODAY_SINGER");
+    
+    WxMenuButton button3 = new WxMenuButton();
+    button3.setName("菜单");
+    
+    menu.getButton().add(button1);
+    menu.getButton().add(button2);
+    menu.getButton().add(button3);
+    
+    WxMenuButton button31 = new WxMenuButton();
+    button31.setType("view");
+    button31.setName("搜索");
+    button31.setUrl("http://www.soso.com/");
+    
+    WxMenuButton button32 = new WxMenuButton();
+    button32.setType("view");
+    button32.setName("视频");
+    button32.setUrl("http://v.qq.com/");
+    
+    WxMenuButton button33 = new WxMenuButton();
+    button33.setType("click");
+    button33.setName("赞一下我们");
+    button33.setKey("V1001_GOOD");
+    
+    button3.getSub_button().add(button31);
+    button3.getSub_button().add(button32);
+    button3.getSub_button().add(button33);
+    
+    System.out.println(menu.toJson());
+    Assert.assertEquals(menu.toJson(), json);
+  }
+  
+  @DataProvider(name="json")
+  public Object[][] getMenuJson() {
+    String json = 
+        "{"
+            +"\"button\":["
+              +"{"
+                +"\"type\":\"click\","
+                +"\"name\":\"今日歌曲\","
+                +"\"key\":\"V1001_TODAY_MUSIC\""
+              +"},"
+              +"{"
+                +"\"type\":\"click\","
+                +"\"name\":\"歌手简介\","
+                +"\"key\":\"V1001_TODAY_SINGER\""
+              +"},"
+              +"{"
+                +"\"name\":\"菜单\","
+                +"\"sub_button\":["
+                  +"{"
+                    +"\"type\":\"view\","
+                    +"\"name\":\"搜索\","
+                    +"\"url\":\"http://www.soso.com/\""
+                  +"},"
+                  +"{"
+                    +"\"type\":\"view\","
+                    +"\"name\":\"视频\","
+                    +"\"url\":\"http://v.qq.com/\""
+                  +"},"
+                  +"{"
+                  +"\"type\":\"click\","
+                  +"\"name\":\"赞一下我们\","
+                  +"\"key\":\"V1001_GOOD\""
+                  +"}"
+                +"]"
+              +"}"
+            +"]"
+        +"}";
+    return new Object[][] {
+        new Object[] { json }
+    };
+  }
+  
+}
diff --git a/src/test/java/chanjarster/weixin/out/WxUserMessageTest.java b/src/test/java/chanjarster/weixin/bean/WxXmlMessageTest.java
similarity index 93%
rename from src/test/java/chanjarster/weixin/out/WxUserMessageTest.java
rename to src/test/java/chanjarster/weixin/bean/WxXmlMessageTest.java
index 2db5397d..f6db5f89 100644
--- a/src/test/java/chanjarster/weixin/out/WxUserMessageTest.java
+++ b/src/test/java/chanjarster/weixin/bean/WxXmlMessageTest.java
@@ -1,13 +1,13 @@
-package chanjarster.weixin.out;
+package chanjarster.weixin.bean;
 
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
-import chanjarster.weixin.out.WxUserMessage;
-import chanjarster.weixin.service.WxMsgType;
+import chanjarster.weixin.api.WxConsts;
+import chanjarster.weixin.bean.WxXmlMessage;
 
 @Test
-public class WxUserMessageTest {
+public class WxXmlMessageTest {
 
   public void testFromXml() {
 
@@ -36,11 +36,11 @@ public class WxUserMessageTest {
                 + "113.352425"
                 + "119.385040"
                 + "";
-    WxUserMessage wxMessage = WxUserMessage.fromXml(xml);
+    WxXmlMessage wxMessage = WxXmlMessage.fromXml(xml);
     Assert.assertEquals(wxMessage.getToUserName(), "toUser");
     Assert.assertEquals(wxMessage.getFromUserName(), "fromUser");
     Assert.assertEquals(wxMessage.getCreateTime(), new Long(1348831860l));
-    Assert.assertEquals(wxMessage.getMsgType(), WxMsgType.TEXT);
+    Assert.assertEquals(wxMessage.getMsgType(), WxConsts.TEXT);
     Assert.assertEquals(wxMessage.getContent(), "this is a test");
     Assert.assertEquals(wxMessage.getMsgId(), new Long(1234567890123456l));
     Assert.assertEquals(wxMessage.getPicUrl(), "this is a url");
@@ -63,11 +63,11 @@ public class WxUserMessageTest {
   }
   
   public void testToXml() {
-    WxUserMessage wxMessage = new WxUserMessage();
+    WxXmlMessage wxMessage = new WxXmlMessage();
     wxMessage.setToUserName("toUser");
     wxMessage.setFromUserName("fromUser");
     wxMessage.setCreateTime(new Long(1348831860l));
-    wxMessage.setMsgType(WxMsgType.TEXT);
+    wxMessage.setMsgType(WxConsts.TEXT);
     wxMessage.setContent("this is a test");
     wxMessage.setMsgId(new Long(1234567890123456l));
     wxMessage.setPicUrl("this is a url");