[053] 微信公眾平臺開發教程第23篇-SAE不支援XStream框架的解決方案
問題描述
最近幾天(2014年8月20日之後),突然有不少網友反應,柳峰部落格中的微信公眾平臺開發程式碼在SAE上執行會報錯,或者是能正常部署,但向公眾號發訊息沒反應。以前也有一些初學者質疑過我部落格中的程式碼是否能正常執行,最後都被我一一證明是由於他們的不理解和粗心導致,但這一次短短几天就有很多人反應同樣的問題,這就引起了我的足夠重視。對於這種“同樣的程式碼以前可以正常執行,現在卻不能執行”的問題,我猜測可能是程式執行環境發生了某種變化,應該是SAE近期做了什麼更新導致的。
問題分析
如果Java Web專案中使用了日誌工具log4j或者slf4j,並且設定了將日誌輸出到控制檯(console),那麼在專案部署到SAE之後,可以在SAE網站的“日誌中心”看到應用的相關日誌。檢視HTTP服務error級別的日誌,能夠看到如下圖所示的錯誤日誌:
為了方便檢視和講解,我對上述錯誤日誌進行了格式化處理,結果如下:
從日誌中的第二行可以看出,在訪問/coreServlet時報了一個錯誤NoClassDefFoundError(類找不到),並且提示org.liufeng.weixin.util.MessageUtil類不能被例項化。在部署的WAR中,MessageUtil.class明明存在,為什麼會找不到類呢?我們來看看,MessageUtil.java中到底都寫了些什麼,原始碼如下:101.226.62.83 [27/Aug/2014:17:23:10 +0800] JAVA_SAE_Fatal_error: Error for /coreServletjava.lang.NoClassDefFoundError: Could not initialize class org.liufeng.weixin.util.MessageUtil at org.liufeng.gywodejia.service.CoreService.processRequest(CoreService.java:40) at org.liufeng.gywodejia.servlet.CoreServlet.doPost(CoreServlet.java:54) at javax.servlet.http.HttpServlet.service(HttpServlet.java:727) at javax.servlet.http.HttpServlet.service(HttpServlet.java:820) at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:538) at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:478) at com.sina.sae.servlet.SaeServletHandler.doHandle(SaeServletHandler.java:49) at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:119) at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:517) at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:225) at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:937) at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:406) at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:183) at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:871) at com.sina.sae.webapp.SaeWebAppContext.doScope(SaeWebAppContext.java:166) at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:117) at org.eclipse.jetty.server.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:259) at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:149) at com.sina.sae.handler.SaeUserInfoHandler.handle(SaeUserInfoHandler.java:105) at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:110) at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:305) at org.eclipse.jetty.server.handler.HandlerW yq36.javaruntime
MessageUtil是訊息處理工具類,該類的程式碼大致可以分為以下3部分:package org.liufeng.course.util; import java.io.InputStream; import java.io.Writer; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.liufeng.course.message.resp.Article; import org.liufeng.course.message.resp.MusicMessage; import org.liufeng.course.message.resp.NewsMessage; import org.liufeng.course.message.resp.TextMessage; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.core.util.QuickWriter; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.xml.PrettyPrintWriter; import com.thoughtworks.xstream.io.xml.XppDriver; /** * 訊息工具類 * * @author liufeng * @date 2013-05-19 */ public class MessageUtil { /** * 返回訊息型別:文字 */ public static final String RESP_MESSAGE_TYPE_TEXT = "text"; /** * 返回訊息型別:音樂 */ public static final String RESP_MESSAGE_TYPE_MUSIC = "music"; /** * 返回訊息型別:圖文 */ public static final String RESP_MESSAGE_TYPE_NEWS = "news"; /** * 請求訊息型別:文字 */ public static final String REQ_MESSAGE_TYPE_TEXT = "text"; /** * 請求訊息型別:圖片 */ public static final String REQ_MESSAGE_TYPE_IMAGE = "image"; /** * 請求訊息型別:連結 */ public static final String REQ_MESSAGE_TYPE_LINK = "link"; /** * 請求訊息型別:地理位置 */ public static final String REQ_MESSAGE_TYPE_LOCATION = "location"; /** * 請求訊息型別:音訊 */ public static final String REQ_MESSAGE_TYPE_VOICE = "voice"; /** * 請求訊息型別:推送 */ public static final String REQ_MESSAGE_TYPE_EVENT = "event"; /** * 事件型別:subscribe(訂閱) */ public static final String EVENT_TYPE_SUBSCRIBE = "subscribe"; /** * 事件型別:unsubscribe(取消訂閱) */ public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe"; /** * 事件型別:CLICK(自定義選單點選事件) */ public static final String EVENT_TYPE_CLICK = "CLICK"; /** * 解析微信發來的請求(XML) * * @param request * @return * @throws Exception */ @SuppressWarnings("unchecked") public static Map<String, String> parseXml(HttpServletRequest request) throws Exception { // 將解析結果儲存在HashMap中 Map<String, String> map = new HashMap<String, String>(); // 從request中取得輸入流 InputStream inputStream = request.getInputStream(); // 讀取輸入流 SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); // 得到xml根元素 Element root = document.getRootElement(); // 得到根元素的所有子節點 List<Element> elementList = root.elements(); // 遍歷所有子節點 for (Element e : elementList) map.put(e.getName(), e.getText()); // 釋放資源 inputStream.close(); inputStream = null; return map; } /** * 文字訊息物件轉換成xml * * @param textMessage 文字訊息物件 * @return xml */ public static String textMessageToXml(TextMessage textMessage) { xstream.alias("xml", textMessage.getClass()); return xstream.toXML(textMessage); } /** * 音樂訊息物件轉換成xml * * @param musicMessage 音樂訊息物件 * @return xml */ public static String musicMessageToXml(MusicMessage musicMessage) { xstream.alias("xml", musicMessage.getClass()); return xstream.toXML(musicMessage); } /** * 圖文訊息物件轉換成xml * * @param newsMessage 圖文訊息物件 * @return xml */ public static String newsMessageToXml(NewsMessage newsMessage) { xstream.alias("xml", newsMessage.getClass()); xstream.alias("item", new Article().getClass()); return xstream.toXML(newsMessage); } /** * 擴充套件xstream,使其支援CDATA塊 * * @date 2013-05-19 */ private static XStream xstream = new XStream(new XppDriver() { public HierarchicalStreamWriter createWriter(Writer out) { return new PrettyPrintWriter(out) { // 對所有xml節點的轉換都增加CDATA標記 boolean cdata = true; @SuppressWarnings("unchecked") public void startNode(String name, Class clazz) { super.startNode(name, clazz); } protected void writeText(QuickWriter writer, String text) { if (cdata) { writer.write("<![CDATA["); writer.write(text); writer.write("]]>"); } else { writer.write(text); } } }; } }); }
1)第33~91行:定義了若干常量,用於表示請求訊息型別、事件型別和響應訊息型別。
2)第93-124行:定義了一個parseXml()方法,通過dom4j工具解析微信伺服器發來的xml格式的訊息。
3)第126~187行:通過XStream工具將Java訊息物件轉換成xml。
很明顯,問題應該不會出現在第1部分程式碼中,因為這段程式碼太平常不過了。我猜想,問題可能與第2、3部分程式碼中引用的第三方工具dom4j或XStream有關,會不會是SAE做了什麼更新不支援dom4j或XStream了呢?要想證明也不難,寫一個最簡單的Java web工程,其中只用到dom4j或者只用到XStream工具,就能知道是哪裡出了問題。好在我認識一個SAE官方的運營人員,就偷了個懶,直接諮詢他,他幫忙問過SAE研發人員之後給出的答覆是:XStream原始碼中通過反射機制使用到了sun.misc.Unsafe類,而該類因為安全原因被SAE禁用掉了,這就是為什麼用到XStream的專案部署到SAE會報NoClassDefFoundError的原因。噢,原來是這麼回事,知道原因了就總能找到解決方案。
問題解決
XStream框架的作用是實現Java物件與XML的互相轉換,SAE研發人員建議用其他有類似功能的框架替代,如Xerces、jdom或者dom4j,當然,這是一個很不錯的建議,如果是在新的專案中,我肯定會這樣做。但現在的問題是,如果真的用其他框架來替換XStream,可能要修改的不僅僅是MessageUtil一個類,這樣的改動太大了,我也很難向這麼多讀者交待。正是出於這種考慮,讓我想到了有沒有可能通過修改XStream框架的原始碼來解決問題。
我在XStream官方網站http://xstream.codehaus.org/上找到了xstream-1.3.1.jar對應的原始碼,匯入到Eclipse,然後藉助Eclipse強大的搜尋功能,很快找到了使用sun.misc.Unsafe的類,我嘗試將這些類刪除或者修改它們的實現,避免使用sun.misc.Unsafe類,最終得到了一個新的jar包,我將其命名為xstream-1.3.1-sae-liufeng.jar,用它替換以前專案中使用的xstream-1.3.1.jar,最終專案再次順利地執行在SAE上。
可能很多看到標題進來的讀者,就是想知道這個問題是如何解決的,並不想聽我哆嗦半天。授人魚不如授人以漁,我之所以將問題的發現、分析和解決整個過程寫出來,也是希望能夠幫助更多初學者逐漸掌握自行解決問題的方法。
宣告
我提供的解決方案有些暴力,旨在幫助大家能夠在SAE上繼續測試學習微信公眾平臺開發,可能會影響到XStream的效能。如果是作為正式專案使用,在非SAE平臺上執行公眾平臺程式,還是建議用XStream官方原本的jar。
如果覺得部落格的文章對你有所幫助,請通過留言或關注下方的微信公眾賬號來支援柳峰!