上回书说道pushlet的集群解决方案,但是节点是全部起的,没有什么问题。
这礼拜线上节点先起了两个,之后起得节点全pushlet全失效了,觉得很不可思议,又再次探究了一下。
HTTP ERROR 503
Problem accessing /pushlet.srv. Reason:
javax.servlet.ServletException: Failed to initialize Pushlet framework java.lang.NullPointerException
这是当时爆的一个异常信息,是前端请求/pushlet.srv时的异常。
/pushlet.srv就像配置servlet一样是配置在web.xml里面的,于是就去找pushlet的处理类
/** <a href="http://www.cpupk.com/decompiler">Eclipse Class Decompiler</a> plugin, Copyright (c) 2017 Chen Chao. **/
/* */ package nl.justobjects.pushlet.servlet;
/* */
/* */ import java.io.IOException;
/* */ import java.io.InputStreamReader;
/* */ import java.util.Enumeration;
/* */ import javax.servlet.ServletContext;
/* */ import javax.servlet.ServletException;
/* */ import javax.servlet.http.HttpServlet;
/* */ import javax.servlet.http.HttpServletRequest;
/* */ import javax.servlet.http.HttpServletResponse;
/* */ import nl.justobjects.pushlet.Version;
/* */ import nl.justobjects.pushlet.core.Command;
/* */ import nl.justobjects.pushlet.core.Config;
/* */ import nl.justobjects.pushlet.core.Controller;
/* */ import nl.justobjects.pushlet.core.Dispatcher;
/* */ import nl.justobjects.pushlet.core.Event;
/* */ import nl.justobjects.pushlet.core.EventParser;
/* */ import nl.justobjects.pushlet.core.EventSourceManager;
/* */ import nl.justobjects.pushlet.core.Protocol;
/* */ import nl.justobjects.pushlet.core.Session;
/* */ import nl.justobjects.pushlet.core.SessionManager;
/* */ import nl.justobjects.pushlet.util.Log;
/* */ import nl.justobjects.pushlet.util.Servlets;
/* */
/* */ public class Pushlet extends HttpServlet/* */ implements Protocol
/* */ {
/* */ public void init()/* */ throws ServletException
/* */ {
/* */ try
/* */ {
/* 31 */ String webInfPath = getServletContext().getRealPath("/") + "/WEB-INF";
/* 32 */ Config.load(webInfPath);
/* */
/* 34 */ Log.init();
/* */
/* 37 */ Log.info(
"init() Pushlet Webapp - version=" + Version.SOFTWARE_VERSION + " built=" + Version.BUILD_DATE);
/* */
/* 40 */ SessionManager.getInstance().start();
/* */
/* 43 */ Dispatcher.getInstance().start();
/* */
/* 46 */ if (Config.getBoolProperty("sources.activate"))/* 47 */ EventSourceManager.start(webInfPath);
/* */ else/* 49 */ Log.info("Not starting local event sources");
/* */ }
/* */ catch (Throwable t) {
/* 52 */ throw new ServletException("Failed to initialize Pushlet framework " + t, t);
/* */ }
/* */ }
/* */
/* */ public void destroy() {
/* 57 */ Log.info("destroy(): Exit Pushlet webapp");
/* */
/* 59 */ if (Config.getBoolProperty("sources.activate"))
/* */ {
/* 61 */ EventSourceManager.stop();
/* */ }
/* */ else
Log.info("No local event sources to stop");
/* */
/* 67 */ Dispatcher.getInstance().stop();
/* */
/* 70 */ SessionManager.getInstance().stop();
/* */ }
/* */
/* */ public void doGet(HttpServletRequest request, HttpServletResponse response)
/* */ throws ServletException, IOException
/* */ {
/* 77 */ Event event = null;
/* */ Enumeration e;
/* */ try
/* */ {
/* 81 */ String eventType = Servlets.getParameter(request, "p_event");
/* */
/* 84 */ if (eventType == null) {
/* 85 */ Log.warn("Pushlet.doGet(): bad request, no event specified");
/* 86 */ response.sendError(400, "No eventType specified");
/* 87 */ return;
/* */ }
/* */
/* 91 */ event = new Event(eventType);
/* 92 */ for (e = request.getParameterNames(); e.hasMoreElements();) {
/* 93 */ String nextAttribute = (String) e.nextElement();
/* 94 */ event.setField(nextAttribute, request.getParameter(nextAttribute));
/* */ }
/* */
/* */ }
/* */ catch (Throwable t)
/* */ {
/* 100 */ Log.warn("Pushlet: Error creating event in doGet(): ", t);
/* 101 */ response.setStatus(400);
/* 102 */ return;
/* */ }
/* */
/* 106 */ doRequest(event, request, response);
/* */ }
/* */
/* */ public void doPost(HttpServletRequest request, HttpServletResponse response)
/* */ throws ServletException, IOException
/* */ {
/* 114 */ Event event = null;
/* */ try
/* */ {
/* 117 */ event = EventParser.parse(new InputStreamReader(request.getInputStream()));
/* */
/* 120 */ if (event.getEventType() == null) {
/* 121 */ Log.warn("Pushlet.doPost(): bad request, no event specified");
/* 122 */ response.sendError(400, "No eventType specified");
/* 123 */ return;
/* */ }
/* */
/* */ }
/* */ catch (Throwable t)
/* */ {
/* 129 */ Log.warn("Pushlet: Error creating event in doPost(): ", t);
/* 130 */ response.setStatus(400);
/* 131 */ return;
/* */ }
/* */
/* 135 */ doRequest(event, request, response);
/* */ }
/* */
/* */ protected void doRequest(Event anEvent, HttpServletRequest request, HttpServletResponse response)
/* */ {
/* 144 */ String eventType = anEvent.getEventType();
/* */ try
/* */ {
/* 149 */ Session session = null;
/* 150 */ if (eventType.startsWith("join"))
/* */ {
/* 152 */ session = SessionManager.getInstance().createSession(anEvent);
/* */
/* 154 */ String userAgent = request.getHeader("User-Agent");
/* 155 */ if (userAgent != null)/* 156 */ userAgent = userAgent.toLowerCase();
/* */ else {
/* 158 */ userAgent = "unknown";
/* */ }
/* 160 */ session.setUserAgent(userAgent);
/* */ }
/* */ else
/* */ {
/* 166 */ String id = anEvent.getField("p_id");
/* */
/* 169 */ if (id == null) {
/* 170 */ response.sendError(400, "No id specified");
/* 171 */ Log.warn("Pushlet: bad request, no id specified event=" + eventType);
/* 172 */ return;
/* */ }
/* */
/* 176 */ session = SessionManager.getInstance().getSession(id);
/* */
/* 179 */ if (session == null) {
/* 180 */ response.sendError(400, "Invalid or expired id: " + id);
/* 181 */ Log.warn("Pushlet: bad request, no session found id=" + id + " event=" + eventType);
/* 182 */ return;
/* */ }
/* */
/* */ }
/* */
/* 190 */ Command command = Command.create(session, anEvent, request, response);
/* 191 */ session.getController().doCommand(command);
/* */ }
/* */ catch (Throwable t) {
/* 194 */ Log.warn("Pushlet: Exception in doRequest() event=" + eventType, t);
/* 195 */ t.printStackTrace();
/* 196 */ response.setStatus(500);
/* */ }
/* */ }
/* */ }
反编译的代码,凑活着看吧。
那个异常是在pushlet的init方法里面爆的,很显然上面一个地方爆空指针了。
一个最直接的疑点就是为啥会报异常。
再仔细想想,我web.xml中的配置
<servlet>
<servlet-name>pushlet</servlet-name>
<servlet-class>nl.justobjects.pushlet.servlet.Pushlet</servlet-class>
<load-on-startup>3</load-on-startup>
</servlet>
答案显然只有一个,初始化失败了。
也就是说第一次调用init方法的时候就报错了
去看线上日志也没有我想要的答案,但至少确定,是在初始化的时候发生的异常。
通过断点调试(class文件调试的话需要插件)我们发现,后面请求中爆的空指针异常来自
SessionManager.getInstance().start();
getInstance返回了一个null
让我们来看看这个getInstance方法究竟是个啥
public static SessionManager getInstance()
{
return instance;
}
一开始我看到这里的时候我是惊呆了的。。。我还以为是一个复杂的静态工厂方法
既然getInstance只是返回一个实例,那真正的初始化是在哪儿的呢
我们来到SessionManager的静态块
try
/* */ {
/* 33 */ instance = (SessionManager) Config
.getClass("sessionmanager.class", "nl.justobjects.pushlet.core.SessionManager").newInstance();
/* 34 */ Log.info("SessionManager created className=" + instance.getClass());
/* */ } catch (Throwable t) {
/* 36 */ Log.fatal("Cannot instantiate SessionManager from config", t);
/* */ }
/* */ }
原来实例化发生在静态块里面。
很明确,应该是执行静态块的时候发什么了什么异常,导致session无法正常实例化,因为静态块只在类加载的时候执行一次,我们的instance就永远只能是null了
那究竟又是发生么什么导致静态块执行失败了呢?
我们到Config类里继续看
public static Class getClass(String aClassNameProp, String aDefault)/* */ throws PushletException
/* */ {
/* 50 */ String clazz = (aDefault == null)
? getProperty(aClassNameProp)
: getProperty(aClassNameProp, aDefault);
/* */ try
/* */ {
/* 53 */ return Class.forName(clazz);
/* */ }
/* */ catch (ClassNotFoundException t) {
/* 56 */ throw new PushletException("Cannot find class for " + aClassNameProp + "=" + clazz, t);
/* */ }
/* */ }
public static String getProperty(String aName, String aDefault) {
/* 88 */ return properties.getProperty(aName, aDefault);
/* */ }
/* */
public static String getProperty(String aName) {
/* 92 */ String value = properties.getProperty(aName);
/* 93 */ if (value == null) {
/* 94 */ throw new IllegalArgumentException("Unknown property: " + aName);
/* */ }
/* 96 */ return value;
/* */ }
public static void load(String aDirPath)
/* */ {
/* */ try
/* */ {
/* 67 */ Log.info("Config: loading pushlet.properties from classpath");
/* 68 */ properties = Sys.loadPropertiesResource("pushlet.properties");
/* */ }
/* */ catch (Throwable t) {
/* 71 */ String filePath = aDirPath + File.separator + "pushlet.properties";
/* 72 */ Log.info("Config: cannot load pushlet.properties from classpath, will try from " + filePath);
/* */ try
/* */ {
/* 75 */ properties = Sys.loadPropertiesFile(filePath);
/* */ } catch (Throwable t2) {
/* 77 */ Log.fatal("Config: cannot load properties file from " + filePath, t);
/* */
/* 80 */ return;
/* */ }
/* */ }
/* */
/* 84 */ Log.info("Config: loaded values=" + properties);
/* */ }
我们发现getClass方法进去会调用一个getProperty方法,我们遇到了一个可能会爆空指针的点
properties.getProperty(aName, aDefault)
如果properties为空的话就会报错,
而这个properties的赋值是在load方法里面的,只要保证load方法在getProperty方法之前被调用就不会爆空指针了。
那load方法又是什么时候被调用的呢,让我们回到pushlet类的init方法。
先调load在初始化session,没毛病啊,这流程下来没有任何问题啊!
怎么可能执行session静态块的时候load方法还没执行啊!
一开始我也想不通,后来看了点基础知识。。web容器的初始化过程
图片网上copy的。。。
spring容器的初始化在servlet初始化之前。也就是说这个加载顺序完完全全具有发生调用session静态块在load之前,即在load之前调用到了session的条件
那么问题来了,到底是啥调用了session呢。。。
然后一想就想到了。。。没错,就是那个mq的消费者。
spring容器初始化完成,mq监听容器也完成了初始化,mq消费者开始消费消息。
有人可能会问了,应用刚起来,哪儿来的消息啊。。关键是为了做到集群,我确实用到了mq订阅模式。
因为第二次发布的时候,是先起了两个节点,中间可能有人发了mq,到后面节点启动的时候,每个节点在spring容器一初始化好就开始了消费。。。。
后面节点启动时会较pushlet初始化前开始消费,这一点都认同的吧。。
那么问题来了,我消费者里写了啥呢。。。说来惭愧,就是利用pushlet发送消息的方法,也就是说,确确实实在pushlet初始化前调用了session。。。。
问题知道出在哪儿解决就简单了。我的做法是在调用pushlet发消息的工具类里写个静态块,初始化手动pushlet。。。。