精华帖 (7) :: 良好帖 (9) :: 新手帖 (4) :: 隐藏帖 (1)
|
|
---|---|
作者 | 正文 |
发表时间:2010-03-13
最后修改:2010-03-14
今天突然回想起tomcat下的manager应用上面就能看到session数和session的内容,于是本文的实现原理就是,做一个类似这样的servlet,此servlet把tomcat上负责管理应用的对象保存下来,供我任意使用。在tomcat上看应用的信息时,使用的是http://localhost:8080/manager/html/list这个路径,页面信息见下图: 于是把源码下来看看(最新版原码下载地址http://apache.etoak.com/tomcat/tomcat-6/v6.0.26/src/apache-tomcat-6.0.26-src.zip),细看tomcat下webapps/manager/WEB-INF/web.xml文件配置,发现原来tomcat是通过org.apache.catalina.manager.ManagerServlet这个类来提供以上服务的,跟踪此类doGet方法代码 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { ...... //取得访问路径, String command = request.getPathInfo(); if (command == null) command = request.getServletPath(); ...... ...... if (command == null) { writer.println(sm.getString("managerServlet.noCommand")); } else if (command.equals("/deploy")) { if (war != null || config != null) { deploy(writer, config, path, war, update); } else { deploy(writer, path, tag); } } else if (command.equals("/install")) { // Deprecated deploy(writer, config, path, war, false); } else if (command.equals("/list")) {//找到了,就是这个路径,往下看list方法 list(writer); } else if ...... ...... } else if (command.equals("/findleaks")) { findleaks(writer); } else { writer.println(sm.getString("managerServlet.unknownCommand", command)); } // Finish up the response writer.flush(); writer.close(); } //就是这个方法生成上面的那个页面 protected void list(PrintWriter writer) { ...... //host就当是当前的tomcat吧,那么contexts就此tomcat下的所有应用 Container[] contexts = host.findChildren(); for (int i = 0; i < contexts.length; i++) {//循环每个应用 Context context = (Context) contexts[i]; //应用路径 String displayPath = context.getPath(); if( displayPath.equals("") ) displayPath = "/"; if (context != null ) { if (context.getAvailable()) {//如果应用已启动 /*打印出一行关于此应用的信息,应用的URL,当前状态,session数等,具体见上图 */ writer.println(sm.getString("managerServlet.listitem", displayPath, "running", "" + context.getManager().findSessions().length, context.getDocBase())); } else { writer.println(sm.getString("managerServlet.listitem", displayPath, "stopped", "0", context.getDocBase())); } } } } 注意:context.getManager().findSessions()可以取得所有session,但这是个org.apache.catalina.Session[]数组,不是HttpSession[]数组,但这个Session接口里面有个getSession方法,返回结果正是HttpSession类型,没错,就是循环这个数组并调用其getSession方法就可以取得所有在线用户了 上面的Session[]数组是从context对象里面来的,而context是从host对象来的,host是个初始值为NULL的成员变量,是什么时候赋上值的?是在init方法执行前,setWrapper方法执行时赋的值,请看setWrapper方法代码 public void setWrapper(Wrapper wrapper) { this.wrapper = wrapper; if (wrapper == null) { context = null; host = null; oname = null; } else { //这里所有需要的对象都有了,其实下面我们需要拿到wrapper就够了 context = (Context) wrapper.getParent(); host = (Host) context.getParent(); Engine engine = (Engine) host.getParent(); try { oname = new ObjectName(engine.getName() + ":type=Deployer,host=" + host.getName()); } catch (Exception e) { // ? } } // Retrieve the MBean server mBeanServer = Registry.getRegistry(null, null).getMBeanServer(); } setWrapper会在初始化时被调用,怎么实现的,首先看web.xml中对此servlet的配置,没什么特别,我们可以发散一下思维,struts2里面action如何能自动注入request对象?Spring如何让service监听事件?答案是一样的,那就是让你的类实现某个接口,你要的东西就给你了,对的,这里也一样,此servlet实现了ContainerServlet接口,初始的时候setWrapper方法才会被调用。 是JAVA新手的看这里,我提出上面这些问题,不是想卖什么关子,只是想启发JAVA初学者们,当某天你们做设计时,可以参考这种方法,一句概括就是:只要你实现我的接口,我就可以让你做某事,而不需要任何额外的配置。当然这种设计的缺点就是入侵、偶合。举个简单的应用场景:每天晚上,我用一个定时器通过Spring搜索所有实现了“GarbageCleaner”接口的service bean,并调用其clean方法清理对应模块的垃圾数据,那么任何模块的service只要实现了此接口,就会被调用。 回到正题,我也自已写个servlet并且实ContainerServlet接口吧,使用静态方法取得所有的session,具体代码如下: import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.catalina.ContainerServlet; import org.apache.catalina.Context; import org.apache.catalina.Session; import org.apache.catalina.Wrapper; import com.esc.common.exception.BusinessException; public class TomcatWrapperServlet extends HttpServlet implements ContainerServlet { private static final long serialVersionUID = 1L; //弄个静态变量,初始化后就记下来,以备随时使用 private static Wrapper wrapper = null; public Wrapper getWrapper() { return wrapper; } public void setWrapper(Wrapper wrapper) { TomcatWrapperServlet.wrapper = wrapper; } //doGet不做任何事情,只需要接收第一次请求,触发初始动作就完成它的使命了 @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().println("Hello world!"); resp.getWriter().flush(); resp.getWriter().close(); } //初始化后可通过此静态方法取得所有session public static Map<String,HttpSession> fillSessions() { if(wrapper==null){//没有初始化 throw new BusinessException("本servlet未被初始化,您必须先通过URL访问本servlet后,才可以调用这个方法"); } Map<String,HttpSession> sessions = new LinkedHashMap<String, HttpSession>(); //取得本应用 Context context = (Context) wrapper.getParent(); //取得Session[]数组 Session[] temps = context.getManager().findSessions(); if(temps!=null && temps.length>0){ for (int j = 0; j < temps.length; j++) { //Map<sessionId,session> sessions.put(temps[j].getSession().getId(), temps[j].getSession()); } } return sessions; } } 在web.xml配置一下,然后启动应用,访问之,结果出现异常,是一个安全异常:TomcatWrapperServlet is privileged and cannot be loaded by this web application,说我的类是个特权类,不能被普通的web应用加载,为何manager这个应用又可以呢?把manager/META-INF/context.xml复制到我的应用,再加载,再访问,一切搞定,此文件内容只有一句 <Context antiResourceLocking="false" privileged="true" /> 其实在doGet方法中还可以学到很多管理tomcat应用的方法,也许自动构建系统也可以从这里入手,如果找到什么好的点子,再续下一篇。 小弟出道几年,很不幸一直没有碰到很牛的师傅,一切都靠自已,找到这个方法未必是正道(也许能监听session复制,或者使用单点登陆等其它的办法),如果哪位有好的解决方案,欢迎拍砖! ----后记,session的复制是可以用HttpSessionListner监听到的,之前可能是因为某些集群配置属性未能配置完全,具体的配置见http://lgdlgd.iteye.com/blog/615307。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2010-03-13
这个问题我们也遇到过,不过解决的方法是扩张org.apache.catalina.session.StandardManager类,然后部署到Tomcat的classpath上面,并且在Context元素添加Manager.
|
|
返回顶楼 | |
发表时间:2010-03-13
mercyblitz 写道 这个问题我们也遇到过,不过解决的方法是扩张org.apache.catalina.session.StandardManager类,然后部署到Tomcat的classpath上面,并且在Context元素添加Manager.
扩张?是改写了源码吗? |
|
返回顶楼 | |
发表时间:2010-03-13
继承这个类~
|
|
返回顶楼 | |
发表时间:2010-03-14
lgdlgd 写道 tomcat集群时,原来通过HttpSessionListener实现类监听session的创建和销毁来统计在线人数的方法不再有效,因为不是每个人登陆都会在同一个tomcat服务器上,而在另一台tomcat上登陆的人的session是通过session复制创建的,而复制过程不会调用HttpSessionListener接口的方法,也一直没找着如何监听session复制的方法,所以就没法统计在线人了。
不知道你是用哪个Tomcat版本, Tomcat6.0在进行session复制时是会触发HttpSessionListener的, 在org\apache\catalina\ha\session\DeltaManager.java类 的“handleSESSION_CREATED(SessionMessage msg,Member sender)”方法 会通过“session.setId(msg.getSessionID())”这一行代码间接触发所有的HttpSessionListener。 当然,在配置方面要注意三点: 1:在你的应用程序的WEB-INF\web.xml文件中要加入"<distributable/>" 2.在配置conf\server.xml文件时要加入<Cluster>配置项, <Cluster>配置项还有一个<Manager>子项, 在<Manager>中可以设置一个"notifySessionListenersOnReplication"属性, 这个"notifySessionListenersOnReplication"属性默认是"true", 不能把它设为"false". 3.最好不要像下面这样简单的配置<Cluster>: <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/> 而是要一个一个的配置<Manager>、<Channel>、<Channel>\<Membership>... 否则有时会出现奇怪的问题, 问题主要是由下面两个类产生的 org\apache\catalina\tribes\membership\McastService.java org\apache\catalina\tribes\membership\McastServiceImpl.java 因为<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/> 默认情况下没有为“<Channel>\<Membership>”子项设置"bind"属性。 比如在我的电脑上测试<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>时, 只要连上宽带,当我启动第二个Tomcat实例时,并不能得到第一个Tomcat实例的地址信息, 因此两个Tomcat实例之间并不能相互收发信息, 断开宽带后又正常了。 如果显示加入<Membership>子项后总是能正常工作: <Membership className="org.apache.catalina.tribes.membership.McastService" address="228.0.0.4" bind="127.0.0.1" port="45564" frequency="500" dropTime="3000"/> 更具体的细节可以看看McastService类的start()方法, 以及McastServiceImpl类的setupSocket()方法。 有关Tomcat集群、Session复制的东西都在 org.apache.catalina.tribes、org.apache.catalina.ha这两个包中 |
|
返回顶楼 | |
发表时间:2010-03-14
非常感谢ZHH2009的回复,一会实践一下,我用的是6.014版本,不过我是参照两三年前别人配置5.0版本的教程配的集群,原来在想,我发这些贴,高不承低不就的,很少人会回复。
另外,不知道ZHH2009或哪位高人有没有配置过程的文档,有的话麻烦发我一份,再次感谢。邮箱:leegd@126.com |
|
返回顶楼 | |
发表时间:2010-03-14
最简单的办法,建立一个在线用户表,在session创建和销毁的时候实时更新这张表即可。SpringSecutiry打开会话保护的话,登录的时候,是先销毁session(很变态,但就是这样子的),然后再创建一个session,此种情况,如果要统计在线人数,甚至管理员要具体查看究竟哪些人在线,唯一的方案就是在线用户表。可以设置为内存表,加快读写速度。
|
|
返回顶楼 | |
发表时间:2010-03-14
还有一点,如果不用tomcat,换作其他服务器呢,所以啊,还是在线用户表通用
|
|
返回顶楼 | |
发表时间:2010-03-14
用cookie来统计
|
|
返回顶楼 | |
发表时间:2010-03-14
清晨阳光 写道 可以设置为内存表,加快读写速度。
内存表集群时又一堆同步的麻烦事,想过没? |
|
返回顶楼 | |