1. 程式人生 > >[053] 微信公眾平臺開發教程第23篇-SAE不支援XStream框架的解決方案

[053] 微信公眾平臺開發教程第23篇-SAE不支援XStream框架的解決方案

問題描述

最近幾天(2014年8月20日之後),突然有不少網友反應,柳峰部落格中的微信公眾平臺開發程式碼在SAE上執行會報錯,或者是能正常部署,但向公眾號發訊息沒反應。以前也有一些初學者質疑過我部落格中的程式碼是否能正常執行,最後都被我一一證明是由於他們的不理解和粗心導致,但這一次短短几天就有很多人反應同樣的問題,這就引起了我的足夠重視。對於這種“同樣的程式碼以前可以正常執行,現在卻不能執行”的問題,我猜測可能是程式執行環境發生了某種變化,應該是SAE近期做了什麼更新導致的。

問題分析

如果Java Web專案中使用了日誌工具log4j或者slf4j,並且設定了將日誌輸出到控制檯(console),那麼在專案部署到SAE之後,可以在SAE網站的“日誌中心”看到應用的相關日誌。檢視HTTP服務error級別的日誌,能夠看到如下圖所示的錯誤日誌:


為了方便檢視和講解,我對上述錯誤日誌進行了格式化處理,結果如下:

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 
從日誌中的第二行可以看出,在訪問/coreServlet時報了一個錯誤NoClassDefFoundError(類找不到),並且提示org.liufeng.weixin.util.MessageUtil類不能被例項化。在部署的WAR中,MessageUtil.class明明存在,為什麼會找不到類呢?我們來看看,MessageUtil.java中到底都寫了些什麼,原始碼如下:
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);
					}
				}
			};
		}
	});
}
MessageUtil是訊息處理工具類,該類的程式碼大致可以分為以下3部分:

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。

如果覺得部落格的文章對你有所幫助,請通過留言或關注下方的微信公眾賬號來支援柳峰!