监听器(Listener)在开发中的应用(一)

来源:互联网 发布:stc89c52单片机资料 编辑:程序博客网 时间:2024/04/25 12:57

监听器在JavaWeb开发中用得比较多,下面说一下监听器(Listener)在开发中的常见应用。

统计当前在线人数

在JavaWeb应用开发中,有时候我们需要统计当前在线的用户数,此时就可以使用监听器技术来实现这个功能了。

  1. 编写监听器,代码如下:

    public class CountNumListener implements HttpSessionListener {    @Override    public void sessionCreated(HttpSessionEvent se) {        ServletContext context = se.getSession().getServletContext();        Integer count = (Integer) context.getAttribute("count");        if (count == null) {            count = 1;            context.setAttribute("count", count);        } else {            count++;            context.setAttribute("count", count);        }    }    @Override    public void sessionDestroyed(HttpSessionEvent se) {        ServletContext context = se.getSession().getServletContext();        Integer count = (Integer) context.getAttribute("count");        count--;        context.setAttribute("count", count);    }}
  2. 在web.xml文件中注册监听器。

    <listener>    <listener-class>cn.itcast.web.listener.example.CountNumListener</listener-class></listener>
  3. 我们写一个index.jsp页面来测试上面编写好的监听器。index.jsp页面内容如下:

    <%@ page language="java" contentType="text/html; charset=UTF-8"    pageEncoding="UTF-8"%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>统计当前在线用户人数</title></head><body>    当前在线用户人数:${applicationScope.count }人</body></html>

    注意统计出来的当前在线人数只是一个近似值

自定义Session扫描器

当一个Web应用创建的Session很多时,为了避免Session占用太多的内存,我们可以选择手动将这些内存中的session销毁,那么此时也可以借助监听器技术来实现。

编写监听器

编写一个SessionScanner类,实现HttpSessionListener接口,去监听HttpSession对象。
为了管理服务器创建的session,就需要将其加到自己的一个容器里面去管理起来。即需要定义一个集合存储服务器创建的session。
这时选用ArrayList容器行不行呢?如下:

public class SessionScanner implements HttpSessionListener {    private List<HttpSession> list = new ArrayList<HttpSession>();    @Override    public void sessionCreated(HttpSessionEvent se) {        HttpSession session = se.getSession();        list.add(session);    }    @Override    public void sessionDestroyed(HttpSessionEvent se) {    }}

答:显然不行。假设我选用这个容器,等一会儿我要管理这个容器里面的所有session,发现哪个session5分钟没人用了,就把这个session删除,那就涉及到对这个容器的大量增删,ArrayList底层是数组,性能不好,所以这个时候选用这个容器是不合适的,应选用底层为链表的LinkedList容器。
这时将SessionScanner监听器的代码修改为:

public class MySessionScanner implements HttpSessionListener {    private List<HttpSession> list = new LinkedList<HttpSession>();    @Override    public void sessionCreated(HttpSessionEvent se) {        HttpSession session = se.getSession();        list.add(session);    }    @Override    public void sessionDestroyed(HttpSessionEvent se) {    }}

虽然修改了,但是像上面那样写会涉及到线程安全问题,SessionScaner对象在内存中只有一个。sessionCreated方法可能会被多个人同时调用,当有多个人并发访问站点时,服务器同时为这些并发访问的人创建session,那么sessionCreated方法在某一时刻内会被几个线程同时调用,几个线程并发调用sessionCreated方法。sessionCreated方法的内部处理是往一个集合中添加创建好的session,那么在加session的时候就会涉及到几个Session同时抢夺集合中一个位置的情况,所以往集合中添加session时,一定要保证集合是线程安全的才行。
用自己的话说就是:只要有一个session创建了,就往list这个容器里面加,还会有线程并发的问题。sessionCreated方法里面这句代码list.add(session);有可能被多线程并发访问。若有2个哥们同时访问服务器,服务器会针对这2个哥们同时创建session,那这句代码list.add(session);会同时被调用,那这时就会有2个session同时被加到容器里面去,就会出现2个session在容器里面抢同一个位置的情况。又由于LinkedList集合不是线程安全的,现在有2个线程同时调用add方法往这个集合里面加,这时完全有可能出现2个东西抢一个位置的情况,就有可能出现有一个session加到容器里面的第一个位置了,第二个session就把第一个位置的session覆盖掉了。LinkedList集合内部的源代码可能类似于:

class MyList {    Object[] arr = new Object[10];    public void add(Object obj) { // session        if (arr[0] == null) {            第一个线程来了...            第二个线程来了...    这时出现两个线程抢一个位置的情况            arr[0] = obj;        }    }}

对于上述问题,解决办法为:

  1. 可以把这句代码list.add(session);放在同步代码块里面。
  2. 也可以把这个容器做成线程安全的。

我们选择第二种方法,即将一个集合做成线程安全的集合。可以使用 Collections.synchronizedList(List<T> list)方法将不是线程安全的list集合包装线程安全的list集合。
此时SessionScanner监听器的代码修改为:

public class MySessionScanner implements HttpSessionListener {    private List<HttpSession> list = Collections.synchronizedList(new LinkedList<HttpSession>());    @Override    public void sessionCreated(HttpSessionEvent se) {        HttpSession session = se.getSession();        list.add(session);    }    @Override    public void sessionDestroyed(HttpSessionEvent se) {    }}

为了实现某个session5分钟没人用了,就把这个session删除的需求,我们可以在web应用程序启动的时候就启动一个定时器,该定时器每隔5分钟执行一个任务,即扫描list集合,如果某个session5分钟没人用了,就把这个session删除。这时SessionScanner类需实现ServletContextListener接口,只要把启动定时器的代码写到contextInitialized(ServletContextEvent sce)方法里面,这个web应用一启动,定时器就启动了。
此时SessionScanner监听器的代码修改为:

public class MySessionScanner implements HttpSessionListener, ServletContextListener {    private List<HttpSession> list = Collections.synchronizedList(new LinkedList<HttpSession>());    @Override    public void sessionCreated(HttpSessionEvent se) {        HttpSession session = se.getSession();        list.add(session);    }    @Override    public void sessionDestroyed(HttpSessionEvent se) {    }    @Override    public void contextInitialized(ServletContextEvent sce) {        Timer timer = new Timer();        timer.schedule(new MyTask1(list), 0, 30*1000); // 定时器每隔30秒执行一个任务    }    @Override    public void contextDestroyed(ServletContextEvent sce) {    }}//任务(每隔30秒干什么事情,即扫描list集合)class MyTask1 extends TimerTask {    // 存储HttpSession的list集合    private List<HttpSession> list;    public MyTask1(List<HttpSession> list) {        this.list = list;    }    // run方法指明了任务要做的事情    @Override    public void run() {        Iterator<HttpSession> it = list.iterator();        while (it.hasNext()) {            HttpSession session = it.next();            if ((System.currentTimeMillis() - session.getLastAccessedTime()) > 30*1000) {                session.invalidate();                list.remove(session); // 从集合中移除摧毁的session。注意:在集合迭代的过程中,删除集合的元素,就会抛一个并发修改异常。            }        }    }}

提示:有关Timer类和TimerTask类怎么使用可以参考JDK API帮助文档。
如果程序像上面这样写,是不是万事大吉了呢?想得美啊!现在主要是MyTask1类有错误。现在我们来思考这样一个问题:在迭代过程中,准备添加或者删除元素。

public class Demo {    public static void main(String[] args) {        List list = new ArrayList();        list.add("aaa");        list.add("bbb");        list.add("ccc");        Iterator it = list.iterator();         while (it.hasNext()) {            it.next();            list.add("ggg"); // java.util.ConcurrentModificationException        }    }}

运行以上程序,发现报异常:java.util.ConcurrentModificationException(并发修改异常)。说明在集合迭代的过程中,删除集合的元素,就会抛一个并发修改异常,所以在迭代集合时,不可以通过集合对象的方法操作集合中的元素。如果想要在集合迭代的过程中,删除集合的元素,那该怎么办呢?这时就需要使用其子接口listIterator——List集合特有的迭代器(listIterator)。该接口只能通过List集合的listIterator()获取。

public class Demo4 {    public static void main(String[] args) {        List list = new ArrayList();        list.add("aaa");        list.add("bbb");        list.add("ccc");        ListIterator it = list.listIterator();        while (it.hasNext()) {            it.next();            it.add("ggg");        }    }}

此时SessionScanner监听器的代码修改为:

public class MySessionScanner implements HttpSessionListener, ServletContextListener {    private List<HttpSession> list = Collections.synchronizedList(new LinkedList<HttpSession>());    @Override    public void sessionCreated(HttpSessionEvent se) {        HttpSession session = se.getSession();        list.add(session);    }    @Override    public void sessionDestroyed(HttpSessionEvent se) {    }    @Override    public void contextInitialized(ServletContextEvent sce) {        Timer timer = new Timer();        timer.schedule(new MyTask1(list), 0, 30*1000); // 定时器每隔30秒执行一个任务    }    @Override    public void contextDestroyed(ServletContextEvent sce) {    }}//任务(每隔30秒干什么事情,即扫描list集合)class MyTask1 extends TimerTask {    // 存储HttpSession的list集合    private List<HttpSession> list;    public MyTask1(List<HttpSession> list) {        this.list = list;    }    // run方法指明了任务要做的事情    @Override    public void run() {        Iterator<HttpSession> it = list.listIterator();        while (it.hasNext()) {            HttpSession session = it.next();            if ((System.currentTimeMillis() - session.getLastAccessedTime()) > 30*1000) {                session.invalidate();                // 为了不抛并发修改异常                it.remove();             }        }    }}

如果程序这样写,并发布到网上去,别人来访问,只要访问的频率比较高,这时候又会出现并发线程安全异常。为什么呢?——在迭代list集合中的session的过程中可能有别的用户来访问,用户一访问,服务器就会为该用户创建一个session,此时就会调用sessionCreated往list集合中添加新的session,然而定时器在定时执行扫描遍历list集合中的session时是无法知道正在遍历的list集合又添加新的session进来了的,这样就导致了往list集合添加新的session和遍历list集合中的session这两个操作无法达到同步。那么解决的办法就是把

list.add(session)

Iterator<HttpSession> it = list.listIterator();while (it.hasNext()) {    HttpSession session = it.next();    if ((System.currentTimeMillis() - session.getLastAccessedTime()) > 30*1000) {        session.invalidate();        // 为了不抛并发修改异常        it.remove();     }}

这两段代码做成同步,保证当有一个线程在访问

list.add(session)

这段代码时,另一个线程就不能访问

Iterator<HttpSession> it = list.listIterator();while (it.hasNext()) {    HttpSession session = it.next();    if ((System.currentTimeMillis() - session.getLastAccessedTime()) > 30*1000) {        session.invalidate();        // 为了不抛并发修改异常        it.remove();     }}

这段代码。为了能够将这两段不相干的代码做成同步,只能定义一把锁(Object lock),然后给这两步操作加上同一把锁,用这把锁来保证往list集合添加新的session和遍历list集合中的session这两个操作达到同步,当在执行往list集合添加新的session操作时,就必须等添加完成之后才能够对list集合进行迭代操作,当在执行对list集合进行迭代操作时,那么必须等到迭代操作结束之后才能够往list集合添加新的session。
所以最终SessionScanner监听器的代码修改为:

public class MySessionScanner implements HttpSessionListener, ServletContextListener {    private List<HttpSession> list = Collections.synchronizedList(new LinkedList<HttpSession>());    // 定义一个对象,让这个对象充当一把锁,用这把锁来保证往list集合添加新的session和遍历list集合中的session这两个操作达到同步    private Object lock = new Object(); // 在不同位置的代码怎么做到同步,可以利用一个对象锁就可实现    @Override    public void sessionCreated(HttpSessionEvent se) {        HttpSession session = se.getSession();        System.out.println(session + "被创建了!!!");        // 将该操作加锁进行锁定        synchronized (lock) { // 锁旗标            list.add(session);        }    }    @Override    public void sessionDestroyed(HttpSessionEvent se) {        System.out.println(se.getSession() + "被销毁了!!!");    }    @Override    public void contextInitialized(ServletContextEvent sce) {        Timer timer = new Timer();        timer.schedule(new MyTask1(list, lock), 0, 30*1000); // 定时器每隔30秒执行一个任务    }    @Override    public void contextDestroyed(ServletContextEvent sce) {    }}//任务(每隔30秒干什么事情,即扫描list集合)class MyTask1 extends TimerTask {    // 存储HttpSession的list集合    private List<HttpSession> list;    // 存储传递过来的锁    private Object lock;    public MyTask1(List<HttpSession> list, Object lock) {        this.list = list;        this.lock = lock;    }    // run方法指明了任务要做的事情    @Override    public void run() {        System.out.println("定时器执行!!!");        // 将该操作加锁进行锁定        synchronized (this.lock) {            Iterator<HttpSession> it = list.listIterator();            while (it.hasNext()) {                HttpSession session = it.next();                if ((System.currentTimeMillis() - session.getLastAccessedTime()) > 30*1000) {                    session.invalidate();                    // 为了不抛并发修改异常                    it.remove();                 }            }        }    }}

在web.xml文件中注册监听器

<listener>    <listener-class>cn.itcast.web.listener.example.SessionScanner</listener-class></listener>

以上就是监听器的两个简单应用场景。

一道作业

写一个定时器,完成每天一到11:40就向用户发邮件的任务。

  1. 编写一个监听器,具体代码如下:

    public class SendMailTimer implements ServletContextListener {    @Override    public void contextInitialized(ServletContextEvent sce) {        Timer timer = new Timer();        Calendar c = Calendar.getInstance();        c.set(2016, 8, 8, 18, 42, 55);        timer.schedule(new TimerTask() {            @Override            public void run() {                System.out.println("发邮件!!!");            }        }, c.getTime());    }    @Override    public void contextDestroyed(ServletContextEvent sce) {        // TODO Auto-generated method stub    }}
  2. 在web.xml文件中注册监听器

    <listener>    <listener-class>cn.itcast.web.listener.example.SendMailTimer</listener-class></listener>

    运行结果就是:启动应用程序,到2016-09-08 18:42:55这一刻,在Eclipse的控制台打印一句话:发邮件!!!
    要想你这个网站能定时执行某些任务的话,应该怎么做呢?——在实际开发里面,数据库里面有一张任务表,平时可以往任务表里面添加一些任务,整个应用程序启动的时候,定时器定时扫描这个任务表,只要扫描到你添加的一条任务,就用你这个任务new一个定时器出来执行。

0 0
原创粉丝点击