一、事件监听机制

       事件监听机制涉及到三个组件:事件源、事件监听器、事件对象。当事件源上发生操作时,事件源会调用事件处理器的一个方法响应操作,并且在调用方法时还会把事件对象传递给事件处理器。事件处理器由程序员编写,程序员通过事件对象可以知道哪个事件源上发生了操作,从而可以对操作进行处理。

 

二、Servlet 监听器

       在 Servlet 规范中定义了多种类型的监听器,它们用于监听的事件源分别为:ServletContext、HttpSession、ServletRequest 这三个域对象。Servlet 规范针对这三个对象上的操作,又把这多种类型的监听器划分为三种类型:

         1、监听三个域对象创建和销毁的事件监听器

         2、监听域对象中属性的增加和删除的事件监听器

         3、监听绑定到 HttpSession 域中的某个对象的状态的事件监听器。


三、监听 ServletContext 域对象创建和销毁

       ServletContextListener 接口用于监听 ServletContext 对象的创建和销毁事件。当 ServletContext 对象被创建时会激发 contextInitialized 方法。当 ServletContext 对象被销毁时,会激发 contextDestroyed 方法。

       ServletContextListener 监听器用于对 Web 应用的初始化和终结操作,例如当 Web 应用启动时加载一个数据库连接池,当 Web 应用关闭时释放数据库连接池。

package cn.dk.listener.context;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class ContextListener implements ServletContextListener {

	public void contextDestroyed(ServletContextEvent sce) {
		System.out.println("web应用结束");
	}

	public void contextInitialized(ServletContextEvent sce) {
		System.out.println("web应用开始");
	}
}

四、监听 HttpSession 域对象创建和销毁

       HttpSessionListener 接口用于监听 HttpSession 的创建和销毁。创建一个 Session 时,sssionCreated 方法会被调用。销毁一个 Session 时,sessionDestroyed 方法会被调用。Session 域对象创建和销毁:用户每一次访问时,服务器会创建一个 session(在 getSession()时),销毁:用户的 session 30分钟没有被使用,服务器会销毁 session。如果 Jsp 页面中设置了 session=false,那么只有当 request.getSession() 时,Session 对象才会被创建。

package cn.dk.listener.context;

import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class SessionListener implements HttpSessionListener {

	public void sessionCreated(HttpSessionEvent se) {
		System.out.println("创建session");
	}

	public void sessionDestroyed(HttpSessionEvent se) {
		System.out.println("销毁session");
	}
}

五、监听 HttpRequest 域对象创建和销毁

       ServletRequestListener 接口用于监听 ServletRequest 对象的创建和销毁。Request 对象被创建时,监听器的 requestInitialized 方法会被调用。Request 对象被销毁时,监听器的 requestDestroyed 方法会被调用。ServletRequest 域对象创建和销毁的时机:用户每一次访问都是一次新的请求,都会创建爱你一个新的 request。当访问结束,也就是请求结束时 request 会立即销毁。所以刷新一次,监听器的创建和销毁事件会被激发一次。

package cn.dk.listener.context;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;

public class RequestListener implements ServletRequestListener {

	public void requestDestroyed(ServletRequestEvent sre) {
		System.out.println("request销毁了");
	}

	public void requestInitialized(ServletRequestEvent sre) {
		System.out.println("request创建了");
	}
}

 

六、利用 HttpSessionListener 编写粗略在线人数统计

package cn.dk.count;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class CountListener implements HttpSessionListener {

	public void sessionCreated(HttpSessionEvent se) {
		ServletContext servletContext = se.getSession().getServletContext();
		Integer count = (Integer) servletContext.getAttribute("count");
		if(count == null)
			count = 1;
		else
			count ++;
		servletContext.setAttribute("count", count);
	}

	public void sessionDestroyed(HttpSessionEvent se) {
		ServletContext servletContext = se.getSession().getServletContext();
		Integer count = (Integer) servletContext.getAttribute("count");
		servletContext.setAttribute("count", --count);
	}
}

七、自定义 Session 扫描器

       难点分析:我们为了精确的统计出当前在线人数,就必须及时将不活动的 Session 对象销毁掉。所以我们就必须定时的扫描当前 ServletContext 中所有的 Session。当服务器为一个会话创建一个 Session 时,我们需要将这个 Session 对象存储到集合中,利用定时器来定时遍历这个集合中的所有 Session 得到 Session 的 lastAccessedTime 从而进行判断。原理很简单,但是我们必须考虑到线程安全的问题。

         首先,如果有多个用户并发访问服务器,那么服务器极有可能同时执行 list.add(session) 的方法,那么就可能造成后进来的 session 将前面的 session 覆盖掉,导致丢失了其他 session 的管理。这就是集合并发访问的问题,为了解决线程安全问题,我们可以将这个 add 方法放到 synchronized 代码块中,也可以在创建集合的时候指定一个线程安全的集合。Collections.synchronizedList(new LinkedList<HttpSession>()); 这样就保证了并发访问安全。

         其次,当我们在遍历集合,发现有不活动的 Session 时,需要将 Session 摧毁掉,并从集合中移除。这时就会引发集合并发访问异常。(因此我们在迭代集合的时候不要轻易的调用集合的方法。)我们可以通过迭代器的 remove() 方法来移除集合中的元素。另外还可以使用 ListIterator 迭代器,ListIterator 迭代器可以在迭代的时候对元素进行增删改查。

         最后,当我们在遍历集合的时候,刚好有一个用户进来,list.add(session) 这时候又会引发集合并发访问的异常。我们可以调用迭代器的增加方法,还可以将增加方法和迭代方法放在一个同步代码块中,两个同步代码块有着相同的锁旗标对象。这样也可以解决问题。

         总结:1、并发向集合中增加元素,我们可以使用同步代码块也可以使用 Collections.synchronized...构建线程安全的集合。

                   2、在对集合进行迭代的时候,谨慎调用集合的操作。如果有对集合的操作我们应该用迭代器来操作集合。

                   3、对同一集合在不同地方操作时,也要注意集合并发访问的问题,这时我们需要将这些方法同步,以保证线程安全。

package cn.dk.sessionScanner;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

public class SessionScanner implements HttpSessionListener,
		ServletContextListener {

	private static final List<HttpSession> SESSIONS = Collections.synchronizedList(new LinkedList<HttpSession>());
	private final static Object LOCK = new Object();;

	public void sessionCreated(HttpSessionEvent se) {
		System.out.println("Session创建了");
		synchronized (LOCK) {
			SESSIONS.add(se.getSession());
		}
	}

	public void sessionDestroyed(HttpSessionEvent se) {
		System.out.println("Session摧毁了");
	}

	public void contextInitialized(ServletContextEvent sce) {
		Timer timer = new Timer();
		timer.schedule(new TimerTask() {
			public void run() {
				synchronized (LOCK) {
					Iterator<HttpSession> iterator = SESSIONS.iterator();
					while (iterator.hasNext()) {
						HttpSession session = iterator.next();
						System.out.println(session.getLastAccessedTime());
						System.out.println(System.currentTimeMillis());
						if (System.currentTimeMillis()
								- session.getLastAccessedTime() > 1000 * 60) {
							session.invalidate();
							iterator.remove();
						}
					}
				}
			}
		}, 0, 1000 * 10);
	}

	public void contextDestroyed(ServletContextEvent sce) {
	}
}

八、观察者设计模式

       上面我们一直是在写监听器来监听别人写的对象。我们自己写的对象也可以被别人监听。这就是观察者设计模式,就是说我们写的对象可以被别人观察和监听,要想写出被别人监听的对象,我们需要事件源、监听器、事件对象。在事件源中维护这一个事件监听器对象,当我们调用事件源的某个方法时,会调用事件监听器中约定的方法,事件监听器是一个接口,约定了事件源中所有的行为,事件对象用来描述事件源,封装事件源,以便监听器中可以获取事件源相关信息。在事件源中的每个方法中,将事件对象中的事件源指向自己。

package cn.dk.observer;

public class Person {

	private PersonListener listener;

	public Person(PersonListener listener) {
		super();
		this.listener = listener;
	}

	public void eat() {
		Event source = new Event(this);
		listener.eat(source);
	}

	public void run() {
		Event source = new Event(this);
		listener.run(source);
	}
}

interface PersonListener {
	public void eat(Event source);

	public void run(Event source);
}

class Event {
	private Person source;

	public Person getSource() {
		return this.source;
	}

	public Event(Person source) {
		super();
		this.source = source;
	}
}
package cn.dk.observer;

public class PersonObserver {

	public static void main(String[] args) {
		Person person = new Person(new PersonListener(){

			public void eat(Event source) {
				System.out.println(source.getSource() + "吃了");
				
			}

			public void run(Event source) {
				System.out.println(source.getSource() + "跑了");
				
			}});
		
		person.eat();
		person.run();
	}
}

九、监听三个域对象属性变化

       ServletContextAttributeListener

         HttpSessionAttributeListener

         ServletRequestAttributeListener

         这三个接口中定义了三个方法来吃力被监听对象中的属性增加,删除和替换的时间,同一个时间在这三个接口中对应的方法名称完全相同,只是接受的参数类型不同。


十、感知 Session 绑定的事件监听器

       保存在 Session 域中的对象可以有多种状态:绑定到 Session 中,从 Session 中解除绑定,随 Session 对象持久化到存储设备中(钝化),随 Session 对象从一个存储设备中恢复(活化)。Servlet 规范中定义了两个特殊的监听器接口来感知以上两种状态。

       1、HttpSessionBindingListener 某个 Bean 对象实现了这个接口,就可以感知自己被绑定到 Session 中或自己从 Session 中解除绑定。

package cn.dk.binding;

import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;

public class Person implements HttpSessionBindingListener{

	public void valueBound(HttpSessionBindingEvent event) {
		System.out.println("绑定到session");
	}

	public void valueUnbound(HttpSessionBindingEvent event) {
		System.out.println("解除绑定");
	}
}

         2、HttpSessionActivationListener 某个 Bean 对象实现了这个接口,就可以感知自己是否随 Session 一起序列化到硬盘,或者从硬盘恢复到内存中去。服务器默认为一个 Session 保存半小时,这是非常耗资源的,所以我们可以采取将短时间不用的 Session 序列化到硬盘,以减轻服务器内存压力,当再使用的时候再从硬盘恢复到内存中去。



package cn.dk.binding;

import java.io.Serializable;

import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionEvent;

public class Person implements HttpSessionBindingListener,HttpSessionActivationListener,Serializable{

	public void valueBound(HttpSessionBindingEvent event) {
		System.out.println("绑定到session");
	}

	public void valueUnbound(HttpSessionBindingEvent event) {
		System.out.println("解除绑定");
	}

	public void sessionDidActivate(HttpSessionEvent se) {
		System.out.println("活化了");
	}

	public void sessionWillPassivate(HttpSessionEvent se) {
		System.out.println("钝化了");
	}
}

九、HttpSessionAttributeListener 监听器实现网站踢人功能

       思路分析:每个用户登录后,会将用户信息存放在 session 中,当向 session 中增加属性时,会被 HttpSessionAttributeListener 监听器捕获到。首先判断添加的属性是否为 User 类型,如果是,则从 ServletContext 拿出 key 为用户名 value 为相对应的用户 session 的 Map 集合,将此用户登录信息保存于 Map 中。管理员点击踢人超链接,将用户名以参数带给 Servlet,在 Servlet 中取出 ServletContext 中的 Map 获取到被踢用户的 Session,将用户登录信息从 Session 属性中移除。当移除 Session 属性时,又会被 HttpSessionAttributeListener 监听器捕获到,在 HttpSessionAttributeListener 监听器内部,获取到被踢用户对象,再从 ServletContext 中的 Map 集合中移除掉。


package cn.dk.kick;

import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;

public class SessionListener implements HttpSessionAttributeListener {

	@SuppressWarnings("unchecked")
	public void attributeAdded(HttpSessionBindingEvent se) {
		ServletContext servletContext = se.getSession().getServletContext();
		Map<String, HttpSession> map = (Map<String, HttpSession>) servletContext.getAttribute("map");
		Object value = se.getValue();
		if(value instanceof User){
			if(map == null)
				map = new HashMap<String, HttpSession>();
			map.put(((User)value).getUsername(), se.getSession());
		}
		servletContext.setAttribute("map", map);
	}

	@SuppressWarnings("unchecked")
	public void attributeRemoved(HttpSessionBindingEvent se) {
		System.out.println("remove");
		ServletContext servletContext = se.getSession().getServletContext();
		Map<String, HttpSession> map = (Map<String, HttpSession>) servletContext.getAttribute("map");
		map.remove(((User)se.getValue()).getUsername());
	}

	public void attributeReplaced(HttpSessionBindingEvent se) {
	}
}

 

package cn.dk.kick;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginServlet extends HttpServlet {

	@SuppressWarnings("unchecked")
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		Service service = new Service();
		String username = request.getParameter("username");
		String password = request.getParameter("password");
		User user = service.login(username, password);
		request.getSession().setAttribute("user", user);
		response.sendRedirect(request.getContextPath() + "/index.jsp");
	}

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}
}

 

package cn.dk.kick;

import java.io.IOException;
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;

public class KickServlet extends HttpServlet {

	@SuppressWarnings("unchecked")
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		Map<String, HttpSession> map = (Map<String, HttpSession>) getServletContext().getAttribute("map");
		HttpSession session = map.get(request.getParameter("username"));
		session.removeAttribute("user");
		response.sendRedirect(request.getContextPath() + "/index.jsp");
	}

	public void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		doGet(request, response);
	}
}