tomcat集群时,原来通过HttpSessionListener实现类监听session的创建和销毁来统计在线人数的方法不再有效,因为不是每个人登陆都会在同一个tomcat服务器上,而在另一台tomcat上登陆的人的session是通过session复制创建的,而复制过程不会调用HttpSessionListener接口的方法,也一直没找着如何监听session复制的方法,所以就没法统计在线人了。
今天突然回想起tomcat下的manager应用上面就能看到session数和session的内容,于是本文的实现原理就是,做一个类似这样的servlet,此servlet把tomcat上负责管理应用的对象保存下来,供我任意使用。在tomcat上看应用的信息时,使用的是http://localhost:8080/manager/html/list这个路径,页面信息见下图:
于是把源码下来看看 (apache-tomcat-6.0.39-src),细看tomcat下webapps/manager/WEB-INF/web.xml文件配置,发现原来tomcat是通过org.apache.catalina.manager.ManagerServlet这个类来提供以上服务的,跟踪此类doGet方法代码
1 // --------------------------------------------------------- Public Methods 2 3 /** 4 * Process a GET request for the specified resource. 5 * 6 * @param request 7 * The servlet request we are processing 8 * @param response 9 * The servlet response we are creating 10 * 11 * @exception IOException 12 * if an input/output error occurs 13 * @exception ServletException 14 * if a servlet-specified error occurs 15 */ 16 public void doGet(HttpServletRequest request, HttpServletResponse response) 17 throws IOException, ServletException { 18 19 // Identify the request parameters that we need 20 // 取得访问路径, 21 String command = request.getPathInfo(); 22 23 String path = request.getParameter("path"); 24 String deployPath = request.getParameter("deployPath"); 25 String deployConfig = request.getParameter("deployConfig"); 26 String deployWar = request.getParameter("deployWar"); 27 28 // Prepare our output writer to generate the response message 29 response.setContentType("text/html; charset=" + Constants.CHARSET); 30 31 String message = ""; 32 // Process the requested command 33 if (command == null || command.equals("/")) { 34 } else if (command.equals("/deploy")) { 35 message = deployInternal(deployConfig, deployPath, deployWar); 36 // 找到了,就是这个路径,往下看list方法 37 } else if (command.equals("/list")) { 38 } else if (command.equals("/reload")) { 39 message = reload(path); 40 } else if (command.equals("/undeploy")) { 41 message = undeploy(path); 42 } else if (command.equals("/expire")) { 43 message = expireSessions(path, request); 44 } else if (command.equals("/sessions")) { 45 try { 46 doSessions(path, request, response); 47 return; 48 } catch (Exception e) { 49 log("HTMLManagerServlet.sessions[" + path + "]", e); 50 message = sm 51 .getString("managerServlet.exception", e.toString()); 52 } 53 } else if (command.equals("/start")) { 54 message = start(path); 55 } else if (command.equals("/stop")) { 56 message = stop(path); 57 } else if (command.equals("/findleaks")) { 58 message = findleaks(); 59 } else { 60 message = sm.getString("managerServlet.unknownCommand", command); 61 } 62 // 就是这个方法生成上面的那个页面 63 list(request, response, message); 64 }
list
1 /** 2 * Render a HTML list of the currently active Contexts in our virtual host, 3 * and memory and server status information. 4 * 5 * @param request 6 * The request 7 * @param response 8 * The response 9 * @param message 10 * a message to display 11 */ 12 public void list(HttpServletRequest request, HttpServletResponse response, 13 String message) throws IOException { 14 15 if (debug >= 1) 16 log("list: Listing contexts for virtual host '" + host.getName() 17 + "'"); 18 19 PrintWriter writer = response.getWriter(); 20 21 // HTML Header Section 22 writer.print(Constants.HTML_HEADER_SECTION); 23 24 // Body Header Section 25 Object[] args = new Object[2]; 26 args[0] = request.getContextPath(); 27 args[1] = sm.getString("htmlManagerServlet.title"); 28 writer.print(MessageFormat.format(Constants.BODY_HEADER_SECTION, args)); 29 30 // Message Section 31 args = new Object[3]; 32 args[0] = sm.getString("htmlManagerServlet.messageLabel"); 33 if (message == null || message.length() == 0) { 34 args[1] = "OK"; 35 } else { 36 args[1] = RequestUtil.filter(message); 37 } 38 writer.print(MessageFormat.format(Constants.MESSAGE_SECTION, args)); 39 40 // Manager Section 41 args = new Object[9]; 42 args[0] = sm.getString("htmlManagerServlet.manager"); 43 args[1] = response.encodeURL(request.getContextPath() + "/html/list"); 44 args[2] = sm.getString("htmlManagerServlet.list"); 45 args[3] = response.encodeURL(request.getContextPath() + "/" 46 + sm.getString("htmlManagerServlet.helpHtmlManagerFile")); 47 args[4] = sm.getString("htmlManagerServlet.helpHtmlManager"); 48 args[5] = response.encodeURL(request.getContextPath() + "/" 49 + sm.getString("htmlManagerServlet.helpManagerFile")); 50 args[6] = sm.getString("htmlManagerServlet.helpManager"); 51 args[7] = response.encodeURL(request.getContextPath() + "/status"); 52 args[8] = sm.getString("statusServlet.title"); 53 writer.print(MessageFormat.format(Constants.MANAGER_SECTION, args)); 54 55 // Apps Header Section 56 args = new Object[6]; 57 args[0] = sm.getString("htmlManagerServlet.appsTitle"); 58 args[1] = sm.getString("htmlManagerServlet.appsPath"); 59 args[2] = sm.getString("htmlManagerServlet.appsName"); 60 args[3] = sm.getString("htmlManagerServlet.appsAvailable"); 61 args[4] = sm.getString("htmlManagerServlet.appsSessions"); 62 args[5] = sm.getString("htmlManagerServlet.appsTasks"); 63 writer.print(MessageFormat.format(APPS_HEADER_SECTION, args)); 64 65 // Apps Row Section 66 // Create sorted map of deployed applications context paths. 67 68 // host就当是当前的tomcat吧,那么contexts就此tomcat下的所有应用 69 Container children[] = host.findChildren(); 70 String contextPaths[] = new String[children.length]; 71 // 循环每个应用 72 for (int i = 0; i < children.length; i++) 73 // 应用名称 74 contextPaths[i] = children[i].getName(); 75 76 TreeMap sortedContextPathsMap = new TreeMap(); 77 78 for (int i = 0; i < contextPaths.length; i++) { 79 // 应用部署路径 80 String displayPath = contextPaths[i]; 81 sortedContextPathsMap.put(displayPath, contextPaths[i]); 82 } 83 84 String appsStart = sm.getString("htmlManagerServlet.appsStart"); 85 String appsStop = sm.getString("htmlManagerServlet.appsStop"); 86 String appsReload = sm.getString("htmlManagerServlet.appsReload"); 87 String appsUndeploy = sm.getString("htmlManagerServlet.appsUndeploy"); 88 String appsExpire = sm.getString("htmlManagerServlet.appsExpire"); 89 90 Iterator iterator = sortedContextPathsMap.entrySet().iterator(); 91 boolean isHighlighted = true; 92 boolean isDeployed = true; 93 String highlightColor = null; 94 95 while (iterator.hasNext()) { 96 // Bugzilla 34818, alternating row colors 97 isHighlighted = !isHighlighted; 98 if (isHighlighted) { 99 highlightColor = "#C3F3C3"; 100 } else { 101 highlightColor = "#FFFFFF"; 102 } 103 104 Map.Entry entry = (Map.Entry) iterator.next(); 105 String displayPath = (String) entry.getKey(); 106 String contextPath = (String) entry.getValue(); 107 Context context = (Context) host.findChild(contextPath); 108 if (displayPath.equals("")) { 109 displayPath = "/"; 110 } 111 112 if (context != null) { 113 try { 114 isDeployed = isDeployed(contextPath); 115 } catch (Exception e) { 116 // Assume false on failure for safety 117 isDeployed = false; 118 } 119 120 args = new Object[7]; 121 args[0] = URL_ENCODER.encode(contextPath + "/"); 122 args[1] = RequestUtil.filter(displayPath); 123 if (context.getDisplayName() == null) { 124 args[2] = " "; 125 } else { 126 args[2] = RequestUtil.filter(context.getDisplayName()); 127 } 128 managerServlet 129 // 应用是否已启动 130 args[3] = new Boolean(context.getAvailable()); 131 args[4] = response.encodeURL(request.getContextPath() 132 + "/html/sessions?path=" 133 + URL_ENCODER.encode(displayPath)); 134 if (context.getManager() != null) { 135 args[5] = new Integer(context.getManager() 136 .getActiveSessions()); 137 } else { 138 args[5] = new Integer(0); 139 } 140 141 args[6] = highlightColor; 142 //打印出一行关于此应用的信息,应用的URL,当前状态,session数等,具体见上图 143 writer.print(MessageFormat.format(APPS_ROW_DETAILS_SECTION, 144 args)); 145 146 args = new Object[14]; 147 args[0] = response 148 .encodeURL(request.getContextPath() 149 + "/html/start?path=" 150 + URL_ENCODER.encode(displayPath)); 151 args[1] = appsStart; 152 args[2] = response.encodeURL(request.getContextPath() 153 + "/html/stop?path=" + URL_ENCODER.encode(displayPath)); 154 args[3] = appsStop; 155 args[4] = response.encodeURL(request.getContextPath() 156 + "/html/reload?path=" 157 + URL_ENCODER.encode(displayPath)); 158 args[5] = appsReload; 159 args[6] = response.encodeURL(request.getContextPath() 160 + "/html/undeploy?path=" 161 + URL_ENCODER.encode(displayPath)); 162 args[7] = appsUndeploy; 163 164 args[8] = response.encodeURL(request.getContextPath() 165 + "/html/expire?path=" 166 + URL_ENCODER.encode(displayPath)); 167 args[9] = appsExpire; 168 args[10] = sm.getString("htmlManagerServlet.expire.explain"); 169 Manager manager = context.getManager(); 170 if (manager == null) { 171 args[11] = sm.getString("htmlManagerServlet.noManager"); 172 } else { 173 args[11] = new Integer(context.getManager() 174 .getMaxInactiveInterval() / 60); 175 } 176 args[12] = sm.getString("htmlManagerServlet.expire.unit"); 177 178 args[13] = highlightColor; 179 180 if (context.getPath().equals(this.context.getPath())) { 181 writer.print(MessageFormat.format( 182 MANAGER_APP_ROW_BUTTON_SECTION, args)); 183 } else if (context.getAvailable() && isDeployed) { 184 writer.print(MessageFormat.format( 185 STARTED_DEPLOYED_APPS_ROW_BUTTON_SECTION, args)); 186 } else if (context.getAvailable() && !isDeployed) { 187 writer.print(MessageFormat.format( 188 STARTED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION, args)); 189 } else if (!context.getAvailable() && isDeployed) { 190 writer.print(MessageFormat.format( 191 STOPPED_DEPLOYED_APPS_ROW_BUTTON_SECTION, args)); 192 } else { 193 writer.print(MessageFormat.format( 194 STOPPED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION, args)); 195 } 196 197 } 198 } 199 200 // Deploy Section 201 args = new Object[7]; 202 args[0] = sm.getString("htmlManagerServlet.deployTitle"); 203 args[1] = sm.getString("htmlManagerServlet.deployServer"); 204 args[2] = response.encodeURL(request.getContextPath() + "/html/deploy"); 205 args[3] = sm.getString("htmlManagerServlet.deployPath"); 206 args[4] = sm.getString("htmlManagerServlet.deployConfig"); 207 args[5] = sm.getString("htmlManagerServlet.deployWar"); 208 args[6] = sm.getString("htmlManagerServlet.deployButton"); 209 writer.print(MessageFormat.format(DEPLOY_SECTION, args)); 210 211 args = new Object[4]; 212 args[0] = sm.getString("htmlManagerServlet.deployUpload"); 213 args[1] = response.encodeURL(request.getContextPath() + "/html/upload"); 214 args[2] = sm.getString("htmlManagerServlet.deployUploadFile"); 215 args[3] = sm.getString("htmlManagerServlet.deployButton"); 216 writer.print(MessageFormat.format(UPLOAD_SECTION, args)); 217 218 // Diagnostics section 219 args = new Object[5]; 220 args[0] = sm.getString("htmlManagerServlet.diagnosticsTitle"); 221 args[1] = sm.getString("htmlManagerServlet.diagnosticsLeak"); 222 args[2] = response.encodeURL(request.getContextPath() 223 + "/html/findleaks"); 224 args[3] = sm.getString("htmlManagerServlet.diagnosticsLeakWarning"); 225 args[4] = sm.getString("htmlManagerServlet.diagnosticsLeakButton"); 226 writer.print(MessageFormat.format(DIAGNOSTICS_SECTION, args)); 227 228 // Server Header Section 229 args = new Object[7]; 230 args[0] = sm.getString("htmlManagerServlet.serverTitle"); 231 args[1] = sm.getString("htmlManagerServlet.serverVersion"); 232 args[2] = sm.getString("htmlManagerServlet.serverJVMVersion"); 233 args[3] = sm.getString("htmlManagerServlet.serverJVMVendor"); 234 args[4] = sm.getString("htmlManagerServlet.serverOSName"); 235 args[5] = sm.getString("htmlManagerServlet.serverOSVersion"); 236 args[6] = sm.getString("htmlManagerServlet.serverOSArch"); 237 writer.print(MessageFormat 238 .format(Constants.SERVER_HEADER_SECTION, args)); 239 240 // Server Row Section 241 args = new Object[6]; 242 args[0] = ServerInfo.getServerInfo(); 243 args[1] = System.getProperty("java.runtime.version"); 244 args[2] = System.getProperty("java.vm.vendor"); 245 args[3] = System.getProperty("os.name"); 246 args[4] = System.getProperty("os.version"); 247 args[5] = System.getProperty("os.arch"); 248 writer.print(MessageFormat.format(Constants.SERVER_ROW_SECTION, args)); 249 250 // HTML Tail Section 251 writer.print(Constants.HTML_TAIL_SECTION); 252 253 // Finish up the response 254 writer.flush(); 255 writer.close(); 256 }
注意:context.getManager().findSessions()可以取得所有session,但这是个org.apache.catalina.Session[]数组,不是HttpSession[]数组,但这个Session接口里面有个getSession方法,返回结果正是HttpSession类型,没错,就是循环这个数组并调用其getSession方法就可以取得所有在线用户了
上面的Session[]数组是从context对象里面来的,而context是从host对象来的,host是个初始值为NULL的成员变量,是什么时候赋上值的?是在init方法执行前,setWrapper方法执行时赋的值,请看setWrapper方法代码
public class HostManagerServlet
extends HttpServlet implements ContainerServlet
1 /** 2 * Set the Wrapper with which we are associated. 3 * 4 * @param wrapper The new wrapper 5 */ 6 public void setWrapper(Wrapper wrapper) { 7 8 //这里所有需要的对象都有了,其实下面我们需要拿到wrapper就够了 9 this.wrapper = wrapper; 10 if (wrapper == null) { 11 context = null; 12 host = null; 13 engine = null; 14 } else { 15 context = (Context) wrapper.getParent(); 16 host = (Host) context.getParent(); 17 engine = (Engine) host.getParent(); 18 } 19 20 // Retrieve the MBean server 21 mBeanServer = Registry.getRegistry(null, null).getMBeanServer(); 22 23 }
setWrapper会在初始化时被调用,怎么实现的,首先看web.xml中对此servlet的配置,没什么特别,我们可以发散一下思维,struts2里面action如何能自动注入request对象?Spring如何让service监听事件?答案是一样的,那就是让你的类实现某个接口,你要的东西就给你了,对的,这里也一样,此servlet实现了ContainerServlet接口,初始的时候setWrapper方法才会被调用。
是JAVA新手的看这里,我提出上面这些问题,不是想卖什么关子,只是想启发JAVA初学者们,当某天你们做设计时,可以参考这种方法,一句概括就是:只要你实现我的接口,我就可以让你做某事,而不需要任何额外的配置。当然这种设计的缺点就是入侵、偶合。举个简单的应用场景:每天晚上,我用一个定时器通过Spring搜索所有实现了“GarbageCleaner”接口的service bean,并调用其clean方法清理对应模块的垃圾数据,那么任何模块的service只要实现了此接口,就会被调用。
回到正题,我也自已写个servlet并且实ContainerServlet接口吧,使用静态方法取得所有的session,具体代码如下:
1 package manager.session.http.servlet; 2 3 import java.io.IOException; 4 import java.util.LinkedHashMap; 5 import java.util.Map; 6 7 import javax.servlet.ServletException; 8 import javax.servlet.http.HttpServlet; 9 import javax.servlet.http.HttpServletRequest; 10 import javax.servlet.http.HttpServletResponse; 11 import javax.servlet.http.HttpSession; 12 13 import org.apache.catalina.ContainerServlet; 14 import org.apache.catalina.Context; 15 import org.apache.catalina.Session; 16 import org.apache.catalina.Wrapper; 17 18 public class TomcatWrapperServlet extends HttpServlet implements 19 ContainerServlet { 20 private static final long serialVersionUID = 1L; 21 22 // 弄个静态变量,初始化后就记下来,以备随时使用 23 private static Wrapper wrapper = null; 24 25 public Wrapper getWrapper() { 26 return wrapper; 27 } 28 29 public void setWrapper(Wrapper wrapper) { 30 TomcatWrapperServlet.wrapper = wrapper; 31 } 32 33 // doGet不做任何事情,只需要接收第一次请求,触发初始动作就完成它的使命了 34 @Override 35 protected void doGet(HttpServletRequest req, HttpServletResponse resp) 36 throws ServletException, IOException { 37 resp.getWriter().println("Hello world!"); 38 resp.getWriter().flush(); 39 resp.getWriter().close(); 40 } 41 42 // 初始化后可通过此静态方法取得所有session 43 public static Map<String, HttpSession> fillSessions() { 44 if (wrapper == null) {// 没有初始化 45 throw new RuntimeException( 46 "本servlet未被初始化,您必须先通过URL访问本servlet后,才可以调用这个方法"); 47 } 48 Map<String, HttpSession> sessions = new LinkedHashMap<String, HttpSession>(); 49 50 // 取得本应用 51 Context context = (Context) wrapper.getParent(); 52 // 取得Session[]数组 53 Session[] temps = context.getManager().findSessions(); 54 if (temps != null && temps.length > 0) { 55 for (int j = 0; j < temps.length; j++) { 56 // Map<sessionId,session> 57 sessions.put(temps[j].getSession().getId(), temps[j] 58 .getSession()); 59 } 60 } 61 return sessions; 62 } 63 64 }
在web.xml配置一下,然后启动应用,访问之,结果出现异常,是一个安全异常:TomcatWrapperServlet is privileged and cannot be loaded by this web application(想想如下),说我的类是个特权类,不能被普通的web应用加载,为何manager这个应用又可以呢?把manager/META-INF/context.xml复制到我的应用,再加载,再访问,一切搞定,此文件内容只有一句
Xml代码
<Context antiResourceLocking="false" privileged="true" />
type Exception report
message Error allocating a servlet instance
description The server encountered an internal error that prevented it from fulfilling this request.
exception
javax.servlet.ServletException: Error allocating a servlet instance org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103) org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:293) org.apache.coyote.http11.Http11AprProcessor.process(Http11AprProcessor.java:879) org.apache.coyote.http11.Http11AprProtocol$Http11ConnectionHandler.process(Http11AprProtocol.java:617) org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1760) java.lang.Thread.run(Thread.java:722)
root cause
java.lang.SecurityException: Servlet of class manager.session.http.servlet.TomcatWrapperServlet is privileged and cannot be loaded by this web application org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103) org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:293) org.apache.coyote.http11.Http11AprProcessor.process(Http11AprProcessor.java:879) org.apache.coyote.http11.Http11AprProtocol$Http11ConnectionHandler.process(Http11AprProtocol.java:617) org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1760) java.lang.Thread.run(Thread.java:722)
note The full stack trace of the root cause is available in the Apache Tomcat/6.0.37 logs.
来自:http://www.jspspace.com/ResearchTopics/Art-1757-17.html