哔哩哔哩蛙课网【动力节点】JavaWeb-Eclipse版学习视频网址
解释 | 归属 | 备注 | |
---|---|---|---|
ServletRequestListener | Request 创建及销毁的监听 | Servlet | |
ServletRequestAttributeListener | request 域属性的添加、修改、删除的监听 | Servlet | |
HttpSessionListener | Session 对象的创建及销毁的监听 | Servlet | |
HttpSessionAttributeListener | Session 域属性的添加、修改、删除的监听 | Servlet | |
ServletContextListener | ServletContext 对象的创建及销毁的监听 | Servlet | |
ServletContextAttributeListener | ServletContext 域属性的添加、修改、删除的监听 | Servlet | |
HttpSessionBindingListener | 监听指定类型对象与 Session 的绑定与解绑 | Servlet | 不需要注册 |
HttpSessionActivationListener | 监听在 Session 中存放的指定类型对象的钝化与活化 | Servlet | 钝化:出内存入硬盘,活化:出硬盘入内存 |
Serializable | 监控钝化与活化同时要实现的接口 | Servlet | |
.getRemoteAddr() | 获得远程地址IP | Servlet |
在 Servlet 规范中存在三大组件:Servlet 接口、Listener 接口、Filter 接口。我们在这里要学习监听器接口 Listener。监听器是一种设计模式,是观察者设计模式的一种实现。所以我们需要先学习观察者设计模式,再学习监听器设计模式。
设计模式是指,可以重复利用的解决方案。由 GoF(Gang of Four,四人组)于 1995年提出。他们提出了三类 23 种设计模式。这三类分别为:
通过特定方式创建特定对象的设计模式。例如,工厂方法模式、单例模式等。
为了解决某一特定问题所搭建的特定代码结构的设计模式。例如,适配器模式(实现接口的一部分方法)、代理模式等。
通过构建不同的角色来完成某一特定功能的设计模式。例如,模板方法模式、观察者模式等。
从现实角度来说,我们每一个人都是一个观察者,同时也是一个被观察者。
作为被观察者,我们会发出一些信息,观察者在接收到这些信息后,会做出相应的反映;
而作为观察者,我们是可以被“被观察者”所发出的信息影响的。
一个被观察者,可能存在多个观察者。也就是说,一个被观察者所发出的信息,可能会影响到多个观察者。
观察者设计模式,定义了一种一对多的关联关系。一个对象 A 与多个对象 B、C、D 之间建立“被观察与观察关系”。当对象 A 的状态发生改变时,通知所有观察者对象 B、C、D。当观察者对象 B、C、D 在接收到 A 的通知后,根据自身实际情况,做出相应改变。
当然,观察者与被观察者指的都是具有某一类功能的对象,所以这里的观察者与被观察者都是指的接口,而真正的观察者对象与被观察者对象,是指实现了这些接口的类的对象。
被观察者类除了要实现观察者接口 IObservable 外,还需要在类中声明并创建一个观察者集合,用于向其中添加观察者。
同时,观察者还可以通过收到的不同信息进行解析,然后做出不同的动作
监听器设计模式,是观察者设计模式的一种实现,它并不是 23 种设计模式之一。这里的监听器实际对应的就是观察者,而被监听对象,则是指被观察者。当被监听对象的状态发生改变时,也需要通知监听器,监听器在收到通知后会做出相应改变。
与观察者设计模式不同的是,被监听者的状态改变,被定义为了一个对象,称为事件。
被监听对象有了个新的名子,称为事件源
对监听器的通知,称为触发监听器
其实质与观察者设计模式是相同的。下面以对被监听者所执行的增删改查 CURD 操作进行监听为例,来演示监听器设计模式的用法。
一般情况下,监听器对象被事件触发后,都是需要从事件中获取到事件源对象,然后再从事件源中获取一些数据。也就是说,在事件对象中一般是需要提供获取事件源对象的方法的。当然,除了获取事件源的方法外,根据业务需求,事件对象一般还需要提供一些其它数据,以便让监听器获取。
package com.bjpowernode.events;
import com.bjpowernode.listenerable.IListenerable;
// 定义增删改查事件
// C:Create,增加
// U:Update,修改
// R:Retrieve,检索
// D:Delete,删除
// 通常,对于事件对象,我们一般是需要从事件对象中获取到事件源对象的
public interface ICurdEvent {
// 声明事件类型
String CRE_EVENT = "create event";
String UPD_EVENT = "update event";
String RET_EVENT = "retrieve event";
String DEL_EVENT = "delete event";
// 获取事件源对象
IListenerable getEventSource();
// 获取事件类型
String getEventType();
}
公共静态变量使用大写字母
package com.bjpowernode.listeners;
import com.bjpowernode.events.ICurdEvent;
// 监听器接口
public interface IListener {
// 处理事件
void handle(ICurdEvent event);
}
因为只有一个监听器,所以使用set,不是add
package com.bjpowernode.events;
import com.bjpowernode.listenerable.IListenerable;
// 定义事件类
public class CurdEvent implements ICurdEvent {
private IListenerable eventSource; // 事件源
private String methodName; // 事件源所执行的方法名称
public CurdEvent(IListenerable eventSource, String methodName) {
super();
this.eventSource = eventSource;
this.methodName = methodName;
}
@Override
public IListenerable getEventSource() {
return eventSource;
}
// 根据事件源所执行的不同的方法,返回不同的事件类型
@Override
public String getEventType() {
String eventType = null;
if(methodName.startsWith("save")) {
eventType = CRE_EVENT;
} else if(methodName.startsWith("remove")) {
eventType = DEL_EVENT;
} else if(methodName.startsWith("modify")) {
eventType = UPD_EVENT;
} else if(methodName.startsWith("find")) {
eventType = RET_EVENT;
} else {
eventType = "have not this event type";
}
return eventType;
}
}
package com.bjpowernode.listeners;
import com.bjpowernode.events.ICurdEvent;
// 定义监听器类
public class CurdListener implements IListener {
@Override
public void handle(ICurdEvent event) {
String eventType = event.getEventType();
if(ICurdEvent.CRE_EVENT.equals(eventType)) { // 若事件类型为“添加”
System.out.println("事件源执行了 添加 操作");
} else if(ICurdEvent.DEL_EVENT.equals(eventType)) { // 若事件类型为“删除”
System.out.println("事件源执行了 删除 操作");
} else if(ICurdEvent.UPD_EVENT.equals(eventType)) { // 若事件类型为“修改”
System.out.println("事件源执行了 修改 操作");
} else if(ICurdEvent.RET_EVENT.equals(eventType)) { // 若事件类型为“查询”
System.out.println("事件源执行了 查询 操作");
}
}
}
事件源是拥有自己的业务方法的,本例的业务方法为增删改查。应该是事件源对象在执行这些业务方法时触发监听器,而并非是前面测试类那么使用监听器。
所以这里需要为事件源添加业务方法,在业务方法中触发监听器。
package com.bjpowernode.listenerable;
import com.bjpowernode.events.CurdEvent;
import com.bjpowernode.events.ICurdEvent;
import com.bjpowernode.listeners.IListener;
// 定义事件源类
public class Some implements IListenerable {
private IListener listener;
// 注册监听器
@Override
public void setListener(IListener listener) {
this.listener = listener;
}
// 触发监听器
@Override
public void triggerListener(ICurdEvent event) {
listener.handle(event);
}
// 下面的方法中事件源类真正的业务逻辑,而监听器监听的就是这些业务方法的执行
public void saveStudent() {
System.out.println("向DB中插入了一条数据");
ICurdEvent event = new CurdEvent(this, "saveStudent");
this.triggerListener(event);
}
public void removeStudent() {
System.out.println("从DB中删除了一条数据");
ICurdEvent event = new CurdEvent(this, "removeStudent");
this.triggerListener(event);
}
public void mofidyStudent() {
System.out.println("修改了DB中的一条数据");
ICurdEvent event = new CurdEvent(this, "mofidyStudent");
this.triggerListener(event);
}
public void findStudent() {
System.out.println("从DB中执行了查询");
ICurdEvent event = new CurdEvent(this, "findStudent");
this.triggerListener(event);
}
}
package com.bjpowernode.test;
import com.bjpowernode.listenerable.Some;
import com.bjpowernode.listeners.CurdListener;
import com.bjpowernode.listeners.IListener;
public class MyTest {
public static void main(String[] args) {
// 定义监听器
IListener listener = new CurdListener();
// 定义事件源
Some some = new Some();
// 事件源注册监听器
some.setListener(listener);
// 事件源执行自己的业务方法
some.saveStudent();
some.removeStudent();
some.mofidyStudent();
some.findStudent();
}
}
Servlet 规范中已经定义好了八个监听器接口,它们要监听的对象分别是 request、session、servletContext 对象,触发监听器的事件是这三个对象的创建与销毁,它们的域属性空间中属性的添加、删除、修改,及 session 的钝化与活化操作。
在 JavaWeb 项目中使用监听器,需要在 web.xml 文件中对监听器进行注册。
下面分别对这八个监听器进行学习。
该监听器用于完成对 Request 对象的创建及销毁的监听,即当 Request 对象被创建或被销毁时,会触发该监听器中相应方法的执行。
|定义监听器
注册监听器
该监听器用于完成对 request 域属性空间中属性的添加、修改、删除操作的监听。
定义监听器
注意 ServletRequestAttributeEvent 事件的方法 getName()可以获取到被操作的属性名,方法 getValue()可以获取到被操作的属性的值。
package com.bjpowernode.listeners;
import javax.servlet.ServletRequestAttributeEvent;
import javax.servlet.ServletRequestAttributeListener;
public class MyRequestAttributeListener implements ServletRequestAttributeListener {
// 当向request域中 添加 属性时会触发该方法的执行
@Override
public void attributeAdded(ServletRequestAttributeEvent srae) {
System.out.println("向request域中添加了一个属性:" + srae.getName() + " = " + srae.getValue());
}
// 当向request域中 删除 属性时会触发该方法的执行
@Override
public void attributeRemoved(ServletRequestAttributeEvent srae) {
System.out.println("从request域中删除了一个属性:" + srae.getName() + " = " + srae.getValue());
}
// 当向request域中 重置 属性时会触发该方法的执行
//获得的是修改前的值
@Override
public void attributeReplaced(ServletRequestAttributeEvent srae) {
System.out.println("修改了request域中的一个属性:" + srae.getName() + " = " + srae.getValue());
}
}
修改 index.jsp 页面
该监听器用于完成对 Session 对象的创建及销毁的监听。
定义监听器
package com.bjpowernode.listeners;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class MySessionListener implements HttpSessionListener {
// 当Session被创建时触发该方法的执行
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println("Session被创建");
}
// 当Session被销毁时触发该方法的执行
@Override
public void sessionDestroyed(HttpSessionEvent se) {
System.out.println("Session被销毁");
}
}
只有内置页面一创建,Session同时也被创建了,监听器就会触发
注册监听器
Session的销毁,可以通过两种方式:
一种是设置时效,时间到自动销毁
还有是通过代码强制销毁 invalidate()
该监听器用于完成对 session 域属性空间中属性的添加、修改、删除操作的监听。
定义监听器
package com.bjpowernode.listeners;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
public class MySessionAttributeListener implements HttpSessionAttributeListener {
// 当向Session域中添加属性时触发该方法的执行
@Override
public void attributeAdded(HttpSessionBindingEvent se) {
System.out.println("向Session中添加了属性:" + se.getName() + " = " + se.getValue());
}
// 当从Session域中删除属性时触发该方法的执行
@Override
public void attributeRemoved(HttpSessionBindingEvent se) {
System.out.println("从Session中删除了属性:" + se.getName() + " = " + se.getValue());
}
// 当重置Session域中属性值时触发该方法的执行
@Override
public void attributeReplaced(HttpSessionBindingEvent se) {
System.out.println("重置了Session中的属性:" + se.getName() + " = " + se.getValue());
}
}
注册监听器
修改 index.jsp 页面
该监听器用于完成对 ServletContext 对象的创建及销毁的监听。不过需要注意,由于ServletContext 在一个应用中只有一个,且是在服务器启动时创建。另外,ServletConetxt 的生命周期与整个应用的相同,所以当项目重新部署,或 Tomcat 正常关闭(通过 stop service关闭,不能是 terminate 关闭)时,可以销毁 ServletContext。
定义监听器
package com.bjpowernode.listeners;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class MyServletContextListener implements ServletContextListener {
// 当ServletContext被初始化时会触发该方法的执行
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("ServletContext被创建");
}
// 当ServletContext被销毁时会触发该方法的执行
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("ServletContext被销毁");
}as
}
注册监听器
定义监听器
package com.bjpowernode.listeners;
import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
public class MyServletContextAttributeListener implements ServletContextAttributeListener {
// 当向ServletContext域中添加属性时会触发该方法的执行
@Override
public void attributeAdded(ServletContextAttributeEvent scae) {
System.out.println("向ServletContext中添加了属性:" + scae.getName() + " = " + scae.getValue());
}
// 当从ServletContext域中删除属性时会触发该方法的执行
@Override
public void attributeRemoved(ServletContextAttributeEvent scae) {
System.out.println("从ServletContext中删除了属性:" + scae.getName() + " = " + scae.getValue());
}
// 当重置ServletContext域中属性时会触发该方法的执行
@Override
public void attributeReplaced(ServletContextAttributeEvent scae) {
System.out.println("重置了ServletContext中的属性:" + scae.getName() + " = " + scae.getValue());
}
}
注册监听器
修改 index.jsp 页面
该监听器用于监听指定类型对象与 Session 的绑定与解绑,即该类型对象被放入到Session 域中,或从 Session 域中删除该类型对象,均会引发该监听器中相应方法的执行。
它与 HttpSessionAttributeListener 的不同之处是,该监听器监听的是指定类型的对象在Session 域中的操作, HttpSessionAttributeListener 监听的是 Session 域属性空间的变化,而无论是什么类型的对象。
另外,需要强调两点:
定义实现监听器接口的实体类
package com.bjpowernode.beans;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
// 实体类实现HttpSessionBindingListener接口
// 该监听器是不需要注册的
public class Student implements HttpSessionBindingListener {
private String name;
private int age;
public Student() {
super();
// TODO Auto-generated constructor stub
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
// 当当前类的对象绑定到Session时(放入到Session域中)会触发该方法的执行
@Override
public void valueBound(HttpSessionBindingEvent event) {
System.out.println("student对象放入到了Session域");
System.out.println(event.getName() + " = " + event.getValue());
}
// 当当前类的对象与Session解绑时(从Session域中删除)会触发该方法的执行
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
System.out.println("student对象从Session域中删除");
System.out.println(event.getName() + " = " + event.getValue());
}
}
修改 index.jsp 页面
运行结果
该监听器用于监听在 Session 中存放的指定类型对象的钝化与活化。
钝化是指将内存中的数据写入到硬盘中,而活化是指将硬盘中的数据恢复到内存。
当用户正在访问的应用或该应用所在的服务器由于种种原因被停掉,然后在短时间内又重启,此时用户在访问时 Session 中的数据是不能丢掉的,在应用关闭之前,需要将数据写入到硬盘,在重启后应可以立即重新恢复 Session 中的数据。这就称为 Session 的钝化与活化。
那么 Session 中的哪些数据能够钝化呢?只有存放在 JVM 堆内存中的实现了 Serializable类的对象能够被钝化。也就是说,对于字符串常量、基本数据类型常量等存放在 JVM 方法区中常量池中的常量,是无法被钝化的。
对于监听 Session 中对象数据的钝化与活化,需要注意以下几点:
定义实现监听器接口的实体类
package com.bjpowernode.beans;
import java.io.Serializable;
import javax.servlet.http.HttpSessionActivationListener;
import javax.servlet.http.HttpSessionEvent;
// 实体类实现HttpSessionActivationListener接口,同时还要实现Serializable接口
// 该监听器是不需要注册的
public class Student implements HttpSessionActivationListener, Serializable {
private String name;
private int age;
public Student() {
super();
// TODO Auto-generated constructor stub
}
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
// 当当前类的对象被活化(硬盘中的数据恢复到内存)时会触发该方法的执行
@Override
public void sessionDidActivate(HttpSessionEvent se) {
System.out.println("Student已经活化");
}
// 当当前类的对象被钝化(内存中的数据写入到硬盘)时会触发该方法的执行
@Override
public void sessionWillPassivate(HttpSessionEvent se) {
System.out.println("Student将要被钝化");
}
}
修改 index.jsp 页面
统计连接在应用上的客户端数量。客户端的唯一标识就是 IP,只需要将连接到服务器上的 IP 数量进行统计,就可统计出客户端的数量。这里需要注意一些细节:
从简单开始
MyRequestListener.java 请求监控器
package com.ssxxz.listeners;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
//监听器
public class MyRequestListener implements ServletRequestListener {
//获取请求的对象,并记录
@Override
public void requestInitialized(ServletRequestEvent sre) {
//利用全局域变量,就算访问次数
//获取全局域
ServletContext sc = sre.getServletContext();
//获取全局域中指定的访问值
Integer count = (Integer)sc.getAttribute("count");
count++;
sc.setAttribute("count", count);
}
}
MyServletContextListener.java 全局域监控器,创建变量记录访问次数
package com.ssxxz.listeners;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
//全局域变量,计算访问次数,方便调用
public class MyServletContextListener implements ServletContextListener {
@Override //初始化方法
public void contextInitialized(ServletContextEvent sce) {
Integer count = 0; //统计值初始化
ServletContext sc = sce.getServletContext(); //获取域
sc.setAttribute("count", count); //将统计值传入域中
}
}
index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
Insert title here
本页面浏览量为 ${applicationScope.count} 次
web.xml
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://JAVA.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>clientCountdisplay-name>
<listener>
<listener-class>com.ssxxz.listeners.MyServletContextListenerlistener-class>
listener>
<listener>
<listener-class>com.ssxxz.listeners.MyRequestListenerlistener-class>
listener>
<welcome-file-list>
<welcome-file>index.jspwelcome-file>
welcome-file-list>
web-app>
将MyRequestListener.java 的计算内容放到新建的 MyRequestListener.java 内
新建 MyRequestListener.java 会话监控器
package com.ssxxz.listeners;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class MySessionListener implements HttpSessionListener {
//浏览器,开启一个新浏览器会话
@Override
public void sessionCreated(HttpSessionEvent se) {
System.out.println("=============");
//利用全局域变量,就算访问次数
//获取全局域
ServletContext sc = se.getSession().getServletContext();
//获取全局域中指定的访问值
Integer count = (Integer)sc.getAttribute("count");
count++;
sc.setAttribute("count", count);
}
}
因为 IP 只能从请求里获取,所以删除MyRequestListener.java 会话监听器,在MyRequestListener请求监听器 中编写代码
package com.ssxxz.listeners;
import java.util.Iterator;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
//监听器
public class MyRequestListener implements ServletRequestListener {
//获取请求的对象,并记录
@Override
public void requestInitialized(ServletRequestEvent sre) {
//请求里获取ip
ServletRequest request = sre.getServletRequest();
//getRemoteAddr() 获取远程地址IP
String clientIp = request.getRemoteAddr();
//获取全局域
ServletContext sc = sre.getServletContext();
//获取全局域集合总数
List<String> ips =(List<String>)sc.getAttribute("ips");
//遍历比对获得的Ip在集合中的是否存在
for (String ip : ips) {
if (clientIp.equals(ip)) {
return;
}
}
//遍历没有的,加入ips内
ips.add(clientIp);
//更新全局域中的集合
sc.setAttribute("ips", ips);
}
}
MyServletContextListener 全局域,将总数从int类型改成list集合
package com.ssxxz.listeners;
import java.util.List;
import java.util.ArrayList;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
//全局域变量,计算访问次数,方便调用
public class MyServletContextListener implements ServletContextListener {
@Override //初始化方法
public void contextInitialized(ServletContextEvent sce) {
//将int类型换成list集合,记录进来的IP
List<String> ips = new ArrayList<>();
ServletContext sc = sce.getServletContext(); //获取域
sc.setAttribute("ips", ips); //将统计值传入域中
}
}
同一电脑测试多ip方式:
http://localhost:8080/clientCount/
http://127.0.0.1:8080/clientCount/
http://以太网地址:8080/clientCount/
index 添加一个退出功能
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
Insert title here
您是第 ${applicationScope.ips.size()} 位访客
安全退出
创建一个session失效的方法LogoutServlet
package com.ssxxz.Servlets;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
//使session失效
public class LogoutServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//获取session,没有session也不创建session
HttpSession session = request.getSession(false);
if (session != null) {
//强行失效
session.invalidate();
}
}
}
session 失效后删除id
但是 session无法直接获得id,所有需要使用request将ip存于session域中,再从域中调取删除
package com.ssxxz.listeners;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class MySessionListener implements HttpSessionListener {
//如果session失效
@Override
public void sessionDestroyed(HttpSessionEvent se) {
//Requeest里将Ip加入到session域中,再从域中获取
HttpSession session = se.getSession();
String clientIp = (String)session.getAttribute("clientIp");
ServletContext sc = se.getSession().getServletContext();
List<String> ips = (List<String>)sc.getAttribute("ips");
//ip在请求里,而因为session里有无数个请求,无法获取到指定的ip,所以只能在请求里删除ip
//域中获取后再删除
ips.remove(clientIp);
}
}
利用 request 调用ip存入域中
package com.ssxxz.listeners;
import java.util.Iterator;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.websocket.Session;
//监听器
public class MyRequestListener implements ServletRequestListener {
//获取请求的对象,并记录
@Override
public void requestInitialized(ServletRequestEvent sre) {
//请求里获取ip
HttpServletRequest request = (HttpServletRequest)sre.getServletRequest();
//getRemoteAddr() 获取远程地址IP
String clientIp = request.getRemoteAddr();
//Request里可以获取ip也可以获取Session,所以在这里将ip放入session域
//getHeader(), getMethod() , getSession() 需要HttpServletRequest接口
HttpSession session = request.getSession();
session.setAttribute("clientIp", clientIp);
System.out.println("clientIp =" + clientIp);
//获取全局域
ServletContext sc = sre.getServletContext();
//获取全局域集合总数
List<String> ips =(List<String>)sc.getAttribute("ips");
//遍历比对获得的Ip在集合中的是否存在
for (String ip : ips) {
if (clientIp.equals(ip)) {
return;
}
}
//遍历没有的,加入ips内
ips.add(clientIp);
//更新全局域中的集合
sc.setAttribute("ips", ips);
}
}
但是有个问题,比如:同时3个ip在线,其中有一个ip有2个在线会话。当失效一个,其他ip的显示数会为2(3-1个),但是它的另一个ip还是显示3个ip在线,此时就出现了一个bug。实际上还是3个ip在线,但是其他ip显示为2个。
是因为,当session失效时,程序立刻删除了ip,并没有查询该ip是否还有其他会话存在
定义 ServletContext 监听器
在 ServletContext 初始化时创建用于存放 IP 信息的 Map 集合,并将创建好的 Map 存放到 ServletContext 域中。
Map 的 key 为客户端 IP,而 value 则为该客户端 ip 所发出的会话对象组成的 List。
package com.bjpowernode.listeners;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSession;
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 创建一个Map,key为ip,value为该ip上所发出的会话对象
Map<String, List<HttpSession>> map = new HashMap<>();
// 获取ServletContext,即全局域对象
ServletContext sc = sce.getServletContext();
// 将map放入到全局域中
sc.setAttribute("map", map);
}
}
定义 Request 监听器
Request 监听器主要完成以下功能:
package com.bjpowernode.listeners;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
public class MyRequestListener implements ServletRequestListener {
// 主要目标将当前Session对象存放到List中
@Override
public void requestInitialized(ServletRequestEvent sre) {
// 获取当前request
HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
// 获取当前ip
String clientIp = request.getRemoteAddr();
// 获取当前session对象
HttpSession currentSession = request.getSession();
//获取全局域
ServletContext sc = sre.getServletContext();
// 从全局域中获取map
Map<String, List<HttpSession>> map = (Map<String, List<HttpSession>>) sc.getAttribute("map");
// 从Map中获取由当前IP所发出的所有Session组成的List
List<HttpSession> sessions = map.get(clientIp);
// 判断当前的List是否为null。若为null,则创建List。
if(sessions == null) {
sessions = new ArrayList<>();
}
// 遍历List。若List中存在当前Session对象,则不用操作List
for (HttpSession session : sessions) {
if(session == currentSession) {
return;
}
}
// 将当前Session放入到这个List
sessions.add(currentSession);
// 将变化过的List重新写回到map
map.put(clientIp, sessions);
// 将变化过的map重新写回到全局域
sc.setAttribute("map", map);
// 将当前ip放入到当前Session
currentSession.setAttribute("clientIp", clientIp);
}
}
定义 Session 监听器
该监听器的功能主要是,当 Session 被销毁时,将当前 Session 对象从 List 中删除。在从List 删除后,若 List 中没有了元素,则说明这个 IP 所发出的会话已全部关闭,则可以将该 IP所对应的 Entry 从 map 中删除了。 List 中仍有元素,若则将变化过的 List 重新再写入回 map。
package com.bjpowernode.listeners;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class MySessionListener implements HttpSessionListener {
// 将当前Session对象从List中删除
@Override
public void sessionDestroyed(HttpSessionEvent se) {
// 获取当前Session对象
HttpSession currentSession = se.getSession();
// 从当前Session中获取当前ip
String clientIp = (String) currentSession.getAttribute("clientIp");
// 获取全局域
ServletContext sc = currentSession.getServletContext();
// 从全局域中获取map
Map<String, List<HttpSession>> map = (Map<String, List<HttpSession>>) sc.getAttribute("map");
// 从Map中获取List
List<HttpSession> sessions = map.get(clientIp);
// 从List中删除当前Session对象
sessions.remove(currentSession);
// 若List中没有了元素,则说明当前IP所发出的会话全部关闭,那么就可以从map中将当前ip所对应的Entry对象删除
// 若List中仍有元素,则说明当前IP所发出的会话还存在,那么将这个变化过的List写回到map
if(sessions.size() == 0) {
map.remove(clientIp);
} else {
map.put(clientIp, sessions);
}
// 将变化过的map写回到全局域
sc.setAttribute("map", map);
}
}
注册监听器
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<listener>
<listener-class>com.bjpowernode.listeners.MyRequestListenerlistener-class>
listener>
<listener>
<listener-class>com.bjpowernode.listeners.MyServletContextListenerlistener-class>
listener>
<listener>
<listener-class>com.bjpowernode.listeners.MySessionListenerlistener-class>
listener>
<welcome-file-list>
<welcome-file>index.jspwelcome-file>
welcome-file-list>
<servlet>
<description>description>
<display-name>LogoutServletdisplay-name>
<servlet-name>LogoutServletservlet-name>
<servlet-class>com.bjpowernode.servlets.LogoutServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>LogoutServletservlet-name>
<url-pattern>/logoutServleturl-pattern>
servlet-mapping>
web-app>
定义 index.jsp 页面
遍历map导入jar包
<%@ page pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
Insert title here
您是第${map.size() }位访客。
安全退出
${entry.key } = ${entry.value.size() }
定义 LogoutServlet
package com.bjpowernode.servlets;
import java.io.IOException;
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 LogoutServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession(false);
if(session != null) {
session.invalidate();
}
}
}
论坛管理员对于一些不守规矩的登录用户可以进行踢除。这里要完成的就是这个功能。
这里有些细节需要注意:
什么是用户已经登录?就是用户信息写入到了 Session 中。
什么是对用户的踢除?就是使该用户信息所绑定的 Session 失效。
若要完成这个踢除功能,管理员首先应该可以看到所有在线用户。那么在线用户信息就应该保存在一个集合中。
这个集合既应该有用户信息,又应该有与用户信息绑定的 Session 对象。这样便于管理员获取到某一用户的 Session 后,将其失效。所以这个集合选用 Map,key 为用户名(各站点都要求用户名是不能重复的,原因就是这个),value 为与该用户绑定的 Session。
这个 Map 集合中的数据应该在什么时候放进去?只要发生 User 对象与 Session 的绑定操作,就说明有用户登录。此时就应将数据放入到 Map 中。也就是说,应该为实体类User 实现 Session 绑定监听器 HttpSessionBindingListener。
这个 Map 集合应该什么时候创建?应该在应用启动时就创建,即在 ServletConetxt 被初始化时被创建。所以应该定义一个 ServletContext 监听器 ServletContextListener,在ServletContext 被初始化时创建这个 Map 集合。
定义用户登录页面 index.jsp
定义实体类 User
只要发生 User 对象与 Session 的绑定操作,就说明有用户登录。此时就应将数据放入到Map 中。也就是说,实体类 User 应实现 Session 绑定监听器 HttpSessionBindingListener。
注册 ServletContext 监听器
定义 LoginServlet
用户提交登录表单后,马上与 Session 进行绑定,以便将信息写入到 Map 中
定义用户列表页面 userList.jsp
这其中用到了显示序号、隔行着色。而这些功能的完成,是由
的 varStatus属性配合着完成的。
定义 DeleteServlet
这里需要注意的是,当将当前 user 的 Session 失效后,还需要将当前 user 对应的 Entry对象从 Map 中删除,否则,userList 页面的列表中仍然会显示这个用户。因为 Map 仍存在,只不过该 session 失效而已。
将指定用户踢除后,重新返回到 userList 页面,显示更新过的列表。
注册 DeleteServlet
login.jsp 登录界面
<%@ page pageEncoding="UTF-8"%>
Insert title here
welcome 欢迎页面
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
Insert title here
welcome page
User.java
用户属性的实现类,进行User对象和Session对象的绑定
package com.ssxxz.beans;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
public class User implements HttpSessionBindingListener {
//实体类用户属性
private String name;
private int age;
public User() {
super();
// TODO Auto-generated constructor stub
}
public User(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}
//当User对象和Session对象进行绑定时,将用户名和Session对象放到map里存储起来
@Override
public void valueBound(HttpSessionBindingEvent event) {
//获取当前的Session
HttpSession session = event.getSession();
//获取全局域
ServletContext sc = session.getServletContext();
//从全局域中获取map
Map<String, HttpSession> map = (Map<String, HttpSession>)sc.getAttribute("map");
//将当前用户名与Session放到map中
map.put(name, session);
//将map写回到全局域中
sc.setAttribute("map", map);
}
}
MyServletContextListener.java
创建一个存储用户信息的map,并放到全局域中方便调用
package com.ssxxz.listeners;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSession;
public class MyServletContextListener implements ServletContextListener {
//应用启动时,创建map,存储用户信息
@Override
public void contextInitialized(ServletContextEvent sce) {
//创建一个Map,key为用户名,value为与当前用户绑定的Session对象
Map<String, HttpSession> map = new HashMap<>();
//获取到全局域
ServletContext sc = sce.getServletContext();
//将map放入到全局域中,方便其他地方调用
sc.setAttribute("map",map);
}
}
LoginServlet.java
中文解码,并将用户放入map中
package com.ssxxz.Servlet;
import java.io.IOException;
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 com.ssxxz.beans.User;
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//解决乱码问题,获得用户提交的请求参数
request.setCharacterEncoding("UTF-8");
String name = request.getParameter("name");
String ageStr = request.getParameter("age");
Integer age = Integer.valueOf(ageStr); //String转int
//创建User对象
User user = new User(name,age);
//获取当前请求对应的Session
HttpSession session = request.getSession();
// 将User与Serssion绑定
session.setAttribute("user", user);
//登录跳转到欢迎页面
response.sendRedirect(request.getContextPath()+"/welcome.jsp");
}
}
List.jsp 管理员页面
实现查看及剔除功能
<%@ page pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
Insert title here
已登录用户
用户名
Session
踢除
${entry.key }
${entry.value }
踢除
KickServlet.java
管理员剔除动作
package com.ssxxz.Servlet;
import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletContext;
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 {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//注意不能直接失效本地Session,需要失效map里的Session
//获取全局域,获得map
ServletContext sc = request.getSession().getServletContext();
Map<String, HttpSession> map = (Map<String, HttpSession>) sc.getAttribute("map");
//获取请求中传入的要剔除的用户名
String name = request.getParameter("name");
//从map中获取当前用户所对应的Session
HttpSession session = map.get(name);
//然后使map中的session失效
session.invalidate();
//将该用户对应的Entry从map中删除
map.remove(name);
//返回 index 页面
request.getRequestDispatcher("/index.jsp").forward(request, response);
}
}
web.xml
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://JAVA.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>kickUserdisplay-name>
<listener>
<listener-class>com.ssxxz.listeners.MyServletContextListenerlistener-class>
listener>
<welcome-file-list>
<welcome-file>index.jspwelcome-file>
welcome-file-list>
<servlet>
<description>description>
<display-name>LoginServletdisplay-name>
<servlet-name>LoginServletservlet-name>
<servlet-class>com.ssxxz.Servlet.LoginServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>LoginServletservlet-name>
<url-pattern>/loginServleturl-pattern>
servlet-mapping>
<servlet>
<description>description>
<display-name>KickServletdisplay-name>
<servlet-name>KickServletservlet-name>
<servlet-class>com.ssxxz.Servlet.KickServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>KickServletservlet-name>
<url-pattern>/kickServleturl-pattern>
servlet-mapping>
web-app>
测试时注意,输入往一个用户时,你如果返回重写输入另外一个用户,此刻两个用户就会出现同时使用一个session的情况。当你要删除完第一个用户,session会同时失效,再删除另一个时,此时session已经失效,系统会报错。所以我们测试这个时需要用其他浏览器登录另外一个用户。