JavaWeb学习笔记

监听器 Listener

哔哩哔哩蛙课网【动力节点】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

1、监听器相关设计模式

在 Servlet 规范中存在三大组件:Servlet 接口、Listener 接口、Filter 接口。我们在这里要学习监听器接口 Listener。监听器是一种设计模式,是观察者设计模式的一种实现。所以我们需要先学习观察者设计模式,再学习监听器设计模式。

1.1、设计模式

设计模式是指,可以重复利用的解决方案。由 GoF(Gang of Four,四人组)于 1995年提出。他们提出了三类 23 种设计模式。这三类分别为:

1.1.1、创建型

通过特定方式创建特定对象的设计模式。例如,工厂方法模式、单例模式等。

1.1.2、结构性

为了解决某一特定问题所搭建的特定代码结构的设计模式。例如,适配器模式(实现接口的一部分方法)、代理模式等。

1.1.3 、行为型

通过构建不同的角色来完成某一特定功能的设计模式。例如,模板方法模式、观察者模式等。

1.2、观察者设计模式

从现实角度来说,我们每一个人都是一个观察者,同时也是一个被观察者。

  • 作为被观察者,我们会发出一些信息,观察者在接收到这些信息后,会做出相应的反映;

  • 而作为观察者,我们是可以被“被观察者”所发出的信息影响的。

  • 一个被观察者,可能存在多个观察者。也就是说,一个被观察者所发出的信息,可能会影响到多个观察者。

观察者设计模式,定义了一种一对多的关联关系。一个对象 A 与多个对象 B、C、D 之间建立“被观察与观察关系”。当对象 A 的状态发生改变时,通知所有观察者对象 B、C、D。当观察者对象 B、C、D 在接收到 A 的通知后,根据自身实际情况,做出相应改变。

当然,观察者与被观察者指的都是具有某一类功能的对象,所以这里的观察者与被观察者都是指的接口,而真正的观察者对象与被观察者对象,是指实现了这些接口的类的对象。

1.2.1 定义观察者接口


1.2.2 定义被观察者接口

1.2.3 定义观察者

这里定义了两个观察者:1 号与 2 号观察者。

1.2.4、定义被观察者

被观察者类除了要实现观察者接口 IObservable 外,还需要在类中声明并创建一个观察者集合,用于向其中添加观察者。

1.2.5、定义测试类

1.2.6、运行结果

同时,观察者还可以通过收到的不同信息进行解析,然后做出不同的动作

1.3、监听器设计模式

监听器设计模式,是观察者设计模式的一种实现,它并不是 23 种设计模式之一。这里的监听器实际对应的就是观察者,而被监听对象,则是指被观察者。当被监听对象的状态发生改变时,也需要通知监听器,监听器在收到通知后会做出相应改变。

与观察者设计模式不同的是,被监听者的状态改变被定义为了一个对象,称为事件

  • 被监听对象有了个新的名子,称为事件源

  • 对监听器的通知,称为触发监听器

其实质与观察者设计模式是相同的。下面以对被监听者所执行的增删改查 CURD 操作进行监听为例,来演示监听器设计模式的用法。

1.3.1、定义事件接口

一般情况下,监听器对象被事件触发后,都是需要从事件中获取到事件源对象,然后再从事件源中获取一些数据。也就是说,在事件对象中一般是需要提供获取事件源对象的方法的。当然,除了获取事件源的方法外,根据业务需求,事件对象一般还需要提供一些其它数据,以便让监听器获取。

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();
}

公共静态变量使用大写字母

1.3.2、定义监听器接口
package com.bjpowernode.listeners;import com.bjpowernode.events.ICurdEvent;// 监听器接口
public interface IListener {// 处理事件void handle(ICurdEvent event);
}
1.3.3、定义事件源接口

因为只有一个监听器,所以使用set,不是add

1.3.4 、定义事件类
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;}@Overridepublic IListenerable getEventSource() {return eventSource;}// 根据事件源所执行的不同的方法,返回不同的事件类型@Overridepublic 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;}}
1.3.5、定义监听器
package com.bjpowernode.listeners;import com.bjpowernode.events.ICurdEvent;// 定义监听器类
public class CurdListener implements IListener {@Overridepublic 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("事件源执行了 查询 操作");}}}
1.3.6、定义事件源

1.3.7、定义测试类

1.3.8、运行结果

1.3.9、再定义事件源

事件源是拥有自己的业务方法的,本例的业务方法为增删改查。应该是事件源对象在执行这些业务方法时触发监听器,而并非是前面测试类那么使用监听器。

所以这里需要为事件源添加业务方法,在业务方法中触发监听器。

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;// 注册监听器@Overridepublic void setListener(IListener listener) {this.listener = listener;}// 触发监听器@Overridepublic 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);}}
1.3.10、再定义测试类
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();}}

2、监听器 Listener

2.1、Servlet 规范中的监听器

Servlet 规范中已经定义好了八个监听器接口,它们要监听的对象分别是 request、session、servletContext 对象,触发监听器的事件是这三个对象的创建与销毁,它们的域属性空间中属性的添加、删除、修改,及 session 的钝化与活化操作。

在 JavaWeb 项目中使用监听器,需要在 web.xml 文件中对监听器进行注册。

下面分别对这八个监听器进行学习。

2.1.1、ServletRequestListener

该监听器用于完成对 Request 对象的创建及销毁的监听,即当 Request 对象被创建或被销毁时,会触发该监听器中相应方法的执行。

|定义监听器

注册监听器

2.1.2、ServletRequestAttributeListener

该监听器用于完成对 request 域属性空间中属性的添加、修改、删除操作的监听。

定义监听器

注意 ServletRequestAttributeEvent 事件的方法 getName()可以获取到被操作的属性名,方法 getValue()可以获取到被操作的属性的值。

package com.bjpowernode.listeners;import javax.servlet.ServletRequestAttributeEvent;
import javax.servlet.ServletRequestAttributeListener;public class MyRequestAttributeListener implements ServletRequestAttributeListener {// 当向request域中 添加 属性时会触发该方法的执行@Overridepublic void attributeAdded(ServletRequestAttributeEvent srae) {System.out.println("向request域中添加了一个属性:" + srae.getName() + " = " + srae.getValue());}// 当向request域中 删除 属性时会触发该方法的执行@Overridepublic void attributeRemoved(ServletRequestAttributeEvent srae) {System.out.println("从request域中删除了一个属性:" + srae.getName() + " = " + srae.getValue());}// 当向request域中 重置 属性时会触发该方法的执行//获得的是修改前的值@Overridepublic void attributeReplaced(ServletRequestAttributeEvent srae) {System.out.println("修改了request域中的一个属性:" + srae.getName() + " = " + srae.getValue());}
}

注册监听器

修改 index.jsp 页面

2.1.3、HttpSessionListener

该监听器用于完成对 Session 对象的创建及销毁的监听。

定义监听器

package com.bjpowernode.listeners;import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;public class MySessionListener implements HttpSessionListener {// 当Session被创建时触发该方法的执行@Overridepublic void sessionCreated(HttpSessionEvent se) {System.out.println("Session被创建");}// 当Session被销毁时触发该方法的执行@Overridepublic void sessionDestroyed(HttpSessionEvent se) {System.out.println("Session被销毁");}
}

只有内置页面一创建,Session同时也被创建了,监听器就会触发

注册监听器

修改 index.jsp 页面

Session的销毁,可以通过两种方式:

  • 一种是设置时效,时间到自动销毁

  • 还有是通过代码强制销毁 invalidate()

2.1.4、HttpSessionAttributeListener

该监听器用于完成对 session 域属性空间中属性的添加、修改、删除操作的监听。

定义监听器

package com.bjpowernode.listeners;import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;public class MySessionAttributeListener implements HttpSessionAttributeListener {// 当向Session域中添加属性时触发该方法的执行@Overridepublic void attributeAdded(HttpSessionBindingEvent se) {System.out.println("向Session中添加了属性:" + se.getName() + " = " + se.getValue());}// 当从Session域中删除属性时触发该方法的执行@Overridepublic void attributeRemoved(HttpSessionBindingEvent se) {System.out.println("从Session中删除了属性:" + se.getName() + " = " + se.getValue());}// 当重置Session域中属性值时触发该方法的执行@Overridepublic void attributeReplaced(HttpSessionBindingEvent se) {System.out.println("重置了Session中的属性:" + se.getName() + " = " + se.getValue());}
}

注册监听器

修改 index.jsp 页面

2.1.5、ServletContextListener

​ 该监听器用于完成对 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被初始化时会触发该方法的执行@Overridepublic void contextInitialized(ServletContextEvent sce) {System.out.println("ServletContext被创建");}// 当ServletContext被销毁时会触发该方法的执行@Overridepublic void contextDestroyed(ServletContextEvent sce) {System.out.println("ServletContext被销毁");}as
}

注册监听器

2.1.6、ServletContextAttributeListener

定义监听器

package com.bjpowernode.listeners;import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;public class MyServletContextAttributeListener implements ServletContextAttributeListener {// 当向ServletContext域中添加属性时会触发该方法的执行@Overridepublic void attributeAdded(ServletContextAttributeEvent scae) {System.out.println("向ServletContext中添加了属性:" + scae.getName() + " = " + scae.getValue());}// 当从ServletContext域中删除属性时会触发该方法的执行@Overridepublic void attributeRemoved(ServletContextAttributeEvent scae) {System.out.println("从ServletContext中删除了属性:" + scae.getName() + " = " + scae.getValue());}// 当重置ServletContext域中属性时会触发该方法的执行@Overridepublic void attributeReplaced(ServletContextAttributeEvent scae) {System.out.println("重置了ServletContext中的属性:" + scae.getName() + " = " + scae.getValue());}
}

注册监听器

修改 index.jsp 页面

2.1.7、HttpSessionBindingListener

该监听器用于监听指定类型对象与 Session 的绑定与解绑,即该类型对象被放入到Session 域中,或从 Session 域中删除该类型对象,均会引发该监听器中相应方法的执行。

它与 HttpSessionAttributeListener 的不同之处是,该监听器监听的是指定类型的对象在Session 域中的操作, HttpSessionAttributeListener 监听的是 Session 域属性空间的变化,而无论是什么类型的对象。

​ 另外,需要强调两点:

  • 该监听器是由实体类实现
  • 该监听器无需在 web.xml 中注册

定义实现监听器接口的实体类

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;}@Overridepublic String toString() {return "Student [name=" + name + ", age=" + age + "]";}// 当当前类的对象绑定到Session时(放入到Session域中)会触发该方法的执行@Overridepublic void valueBound(HttpSessionBindingEvent event) {System.out.println("student对象放入到了Session域");System.out.println(event.getName() + " = " + event.getValue());}// 当当前类的对象与Session解绑时(从Session域中删除)会触发该方法的执行@Overridepublic void valueUnbound(HttpSessionBindingEvent event) {System.out.println("student对象从Session域中删除");System.out.println(event.getName() + " = " + event.getValue());}
}

修改 index.jsp 页面

运行结果

2.1.8、HttpSessionActivationListener

该监听器用于监听在 Session 中存放的指定类型对象的钝化活化

钝化是指将内存中的数据写入到硬盘中,而活化是指将硬盘中的数据恢复到内存

当用户正在访问的应用或该应用所在的服务器由于种种原因被停掉,然后在短时间内又重启,此时用户在访问时 Session 中的数据是不能丢掉的,在应用关闭之前,需要将数据写入到硬盘,在重启后应可以立即重新恢复 Session 中的数据。这就称为 Session 的钝化与活化。

那么 Session 中的哪些数据能够钝化呢?只有存放在 JVM 堆内存中的实现了 Serializable类的对象能够被钝化。也就是说,对于字符串常量、基本数据类型常量等存放在 JVM 方法区中常量池中的常量,是无法被钝化的。

对于监听 Session 中对象数据的钝化与活化,需要注意以下几点:

  • 实体类除了要实现 HttpSessionActivationListener 接口外,还需要实现 Serializable 接口。
  • 钝化指的是 Session 中对象数据的钝化,并非是 Session 的钝化。所以 Session 中有几个可以钝化的对象,就会发生几次钝化。
  • HttpSessionActivationListener 监听器是不需要在 web.xml 中注册的。

定义实现监听器接口的实体类

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;}@Overridepublic String toString() {return "Student [name=" + name + ", age=" + age + "]";}// 当当前类的对象被活化(硬盘中的数据恢复到内存)时会触发该方法的执行@Overridepublic void sessionDidActivate(HttpSessionEvent se) {System.out.println("Student已经活化");}// 当当前类的对象被钝化(内存中的数据写入到硬盘)时会触发该方法的执行@Overridepublic void sessionWillPassivate(HttpSessionEvent se) {System.out.println("Student将要被钝化");}
}

修改 index.jsp 页面

2.2、监听器应用举例

2.2.1、在线客户端统计

统计连接在应用上的客户端数量。客户端的唯一标识就是 IP,只需要将连接到服务器上的 IP 数量进行统计,就可统计出客户端的数量。这里需要注意一些细节:

  • 从 Request 中可以获取到请求的 IP,而从 Session 中是获取不到的。
  • 从 Session 中是无法获取到 Request 对象的,因为 session 与 request 的关系是 1:n,即一个会话中可以包含多个请求。
  • 一个客户端可以发出很多请求与会话,但从这些请求中获取到的 IP 都是相同的。可以将获取到的 IP 放入到 Map 集合中,且以 IP 为 key,可以保证集合中没有重复的 IP。而value 则为该 IP 的机器上所发出的会话对象所组成的 List。
  • 当一个客户端的 Session 被销毁时,应从 map 的当前 ip 所对应的 value 中,即 List 中删除当前的 Session 对象。然后再查看 Map 中该客户端 IP 所发出的会话 List 长度,若为 0,则可将该 IP 所对应的 Entry 对象从 Map 中删除。
1、获取访问请求次数

从简单开始

MyRequestListener.java 请求监控器

package com.ssxxz.listeners;import javax.servlet.ServletContext;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;//监听器
public class MyRequestListener implements ServletRequestListener {//获取请求的对象,并记录@Overridepublic 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"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body><!-- applicationScope.在全局中查找 -->本页面浏览量为 ${applicationScope.count} 次
</body>
</html>

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<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>clientCount</display-name><listener><listener-class>com.ssxxz.listeners.MyServletContextListener</listener-class></listener><listener><listener-class>com.ssxxz.listeners.MyRequestListener</listener-class></listener><welcome-file-list><welcome-file>index.jsp</welcome-file></welcome-file-list>
</web-app>
2、获取浏览器会话次数

将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 {//浏览器,开启一个新浏览器会话@Overridepublic void sessionCreated(HttpSessionEvent se) {System.out.println("=============");//利用全局域变量,就算访问次数//获取全局域ServletContext sc = se.getSession().getServletContext();//获取全局域中指定的访问值Integer count = (Integer)sc.getAttribute("count");count++;sc.setAttribute("count", count);}
}
3、获取IP次数,初次编写

因为 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 {//获取请求的对象,并记录@Overridepublic void requestInitialized(ServletRequestEvent sre) {//请求里获取ipServletRequest request = sre.getServletRequest();//getRemoteAddr() 获取远程地址IPString 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集合,记录进来的IPList<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/

4、id退出,让 session 失效,并且删除id

index 添加一个退出功能

<%@ page language="java" contentType="text/html; charset=UTF-8"pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body><!-- applicationScope.ips.size()在全局中获得集合的大小数 -->您是第 ${applicationScope.ips.size()} 位访客 <br><a href="${pageContext.request.contextPath}/logoutServlet">安全退出</a>
</body>
</html>

创建一个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也不创建sessionHttpSession 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失效@Overridepublic 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 {//获取请求的对象,并记录@Overridepublic void requestInitialized(ServletRequestEvent sre) {//请求里获取ipHttpServletRequest request = (HttpServletRequest)sre.getServletRequest();//getRemoteAddr() 获取远程地址IPString 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个。

5、解决4号问题,最终版

是因为,当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 {@Overridepublic 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 监听器主要完成以下功能:

  • 获取当前请求的客户端的 IP
  • 获取当前 IP 所对应的全局域中的 List。若这个 List 为空,则创建一个 List。
  • 将当前 IP 放入到 List 中,并将当前 IP 与 List 写入到 Map 中,然后再重新将 Map 写回ServletContext 中
  • 将当前 IP 存放到当前请求所对应的 Session 域中,以备在 Session 销毁时使用
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中@Overridepublic void requestInitialized(ServletRequestEvent sre) {// 获取当前requestHttpServletRequest request = (HttpServletRequest) sre.getServletRequest();// 获取当前ipString clientIp = request.getRemoteAddr();// 获取当前session对象HttpSession currentSession = request.getSession();//获取全局域ServletContext sc = sre.getServletContext();// 从全局域中获取mapMap<String, List<HttpSession>> map = (Map<String, List<HttpSession>>) sc.getAttribute("map");// 从Map中获取由当前IP所发出的所有Session组成的ListList<HttpSession> sessions = map.get(clientIp);// 判断当前的List是否为null。若为null,则创建List。if(sessions == null) {sessions = new ArrayList<>();}// 遍历List。若List中存在当前Session对象,则不用操作Listfor (HttpSession session : sessions) {if(session == currentSession) {return;}}// 将当前Session放入到这个Listsessions.add(currentSession);// 将变化过的List重新写回到mapmap.put(clientIp, sessions);// 将变化过的map重新写回到全局域sc.setAttribute("map", map);// 将当前ip放入到当前SessioncurrentSession.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中删除@Overridepublic void sessionDestroyed(HttpSessionEvent se) {// 获取当前Session对象HttpSession currentSession = se.getSession();// 从当前Session中获取当前ipString clientIp = (String) currentSession.getAttribute("clientIp");// 获取全局域ServletContext sc = currentSession.getServletContext();// 从全局域中获取mapMap<String, List<HttpSession>> map = (Map<String, List<HttpSession>>) sc.getAttribute("map");// 从Map中获取ListList<HttpSession> sessions = map.get(clientIp);// 从List中删除当前Session对象sessions.remove(currentSession);// 若List中没有了元素,则说明当前IP所发出的会话全部关闭,那么就可以从map中将当前ip所对应的Entry对象删除// 若List中仍有元素,则说明当前IP所发出的会话还存在,那么将这个变化过的List写回到mapif(sessions.size() == 0) {map.remove(clientIp);} else {map.put(clientIp, sessions);}// 将变化过的map写回到全局域sc.setAttribute("map", map);}
}

注册监听器

<?xml version="1.0" encoding="UTF-8"?>
<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.MyRequestListener</listener-class></listener><listener><listener-class>com.bjpowernode.listeners.MyServletContextListener</listener-class></listener><listener><listener-class>com.bjpowernode.listeners.MySessionListener</listener-class></listener><welcome-file-list><welcome-file>index.jsp</welcome-file></welcome-file-list><servlet><description></description><display-name>LogoutServlet</display-name><servlet-name>LogoutServlet</servlet-name><servlet-class>com.bjpowernode.servlets.LogoutServlet</servlet-class></servlet><servlet-mapping><servlet-name>LogoutServlet</servlet-name><url-pattern>/logoutServlet</url-pattern></servlet-mapping>
</web-app>

定义 index.jsp 页面

遍历map导入jar包

<%@ page pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<!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>Insert title here</title>
</head>
<body>您是第${map.size() }位访客。<br>安全<a href="${pageContext.request.contextPath }/logoutServlet">退出</a><br><c:forEach items="${map }" var="entry">${entry.key } = ${entry.value.size() } <br></c:forEach></body>
</html>

定义 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();}}}
2.2.2、管理员踢除用户

论坛管理员对于一些不守规矩的登录用户可以进行踢除。这里要完成的就是这个功能。

这里有些细节需要注意:

  • 什么是用户已经登录?就是用户信息写入到了 Session 中

  • 什么是对用户的踢除?就是使该用户信息所绑定的 Session 失效

  • 若要完成这个踢除功能,管理员首先应该可以看到所有在线用户。那么在线用户信息就应该保存在一个集合中。

  • 这个集合既应该有用户信息,又应该有与用户信息绑定的 Session 对象。这样便于管理员获取到某一用户的 Session 后,将其失效。所以这个集合选用 Map,key 为用户名(各站点都要求用户名是不能重复的,原因就是这个),value 为与该用户绑定的 Session。

  • 这个 Map 集合中的数据应该在什么时候放进去?只要发生 User 对象与 Session 的绑定操作,就说明有用户登录。此时就应将数据放入到 Map 中。也就是说,应该为实体类User 实现 Session 绑定监听器 HttpSessionBindingListener

  • 这个 Map 集合应该什么时候创建?应该在应用启动时就创建,即在 ServletConetxt 被初始化时被创建。所以应该定义一个 ServletContext 监听器 ServletContextListener,在ServletContext 被初始化时创建这个 Map 集合。

1、文案代码

定义用户登录页面 index.jsp

定义实体类 User

只要发生 User 对象与 Session 的绑定操作,就说明有用户登录。此时就应将数据放入到Map 中。也就是说,实体类 User 应实现 Session 绑定监听器 HttpSessionBindingListener。

定义 ServletContext 监听器

注册 ServletContext 监听器

定义 LoginServlet

用户提交登录表单后,马上与 Session 进行绑定,以便将信息写入到 Map 中

注册 LoginServlet

定义 welcome.jsp 页面

定义用户列表页面 userList.jsp

这其中用到了显示序号、隔行着色。而这些功能的完成,是由<c:forEach/>的 varStatus属性配合着完成的。

定义 DeleteServlet

这里需要注意的是,当将当前 user 的 Session 失效后,还需要将当前 user 对应的 Entry对象从 Map 中删除,否则,userList 页面的列表中仍然会显示这个用户。因为 Map 仍存在,只不过该 session 失效而已。

将指定用户踢除后,重新返回到 userList 页面,显示更新过的列表。

注册 DeleteServlet

2、视频代码

login.jsp 登录界面

<%@ page 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>Insert title here</title>
</head>
<body><form action="${pageContext.request.contextPath }/loginServlet" method="POST">姓名:<input type="text" name="name"/><br>年龄:<input type="text" name="age"/><br><input type="submit" value="登录"/></form>
</body>
</html>

welcome 欢迎页面

<%@ page language="java" contentType="text/html; charset=UTF-8"pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>welcome page
</body>
</html>

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;}@Overridepublic String toString() {return "User [name=" + name + ", age=" + age + "]";}//当User对象和Session对象进行绑定时,将用户名和Session对象放到map里存储起来@Overridepublic void valueBound(HttpSessionBindingEvent event) {//获取当前的SessionHttpSession session = event.getSession();//获取全局域ServletContext sc = session.getServletContext();//从全局域中获取mapMap<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,存储用户信息@Overridepublic 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);//获取当前请求对应的SessionHttpSession 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" %>
<!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>Insert title here</title>
</head>
<body><table border="1"><caption>已登录用户</caption> <tr><th>用户名</th><th>Session</th><th>踢除</th></tr><c:forEach items="${map }" var="entry"><tr><td>${entry.key }</td><td>${entry.value }</td><td><a href="${pageContext.request.contextPath }/kickServlet?name=${entry.key }">踢除</a></td></tr></c:forEach></table>
</body>
</html>

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中获取当前用户所对应的SessionHttpSession session = map.get(name);//然后使map中的session失效session.invalidate();//将该用户对应的Entry从map中删除map.remove(name);//返回 index 页面request.getRequestDispatcher("/index.jsp").forward(request, response);}
}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<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>kickUser</display-name><listener><listener-class>com.ssxxz.listeners.MyServletContextListener</listener-class></listener><welcome-file-list><welcome-file>index.jsp</welcome-file></welcome-file-list><servlet><description></description><display-name>LoginServlet</display-name><servlet-name>LoginServlet</servlet-name><servlet-class>com.ssxxz.Servlet.LoginServlet</servlet-class></servlet><servlet-mapping><servlet-name>LoginServlet</servlet-name><url-pattern>/loginServlet</url-pattern></servlet-mapping><servlet><description></description><display-name>KickServlet</display-name><servlet-name>KickServlet</servlet-name><servlet-class>com.ssxxz.Servlet.KickServlet</servlet-class></servlet><servlet-mapping><servlet-name>KickServlet</servlet-name><url-pattern>/kickServlet</url-pattern></servlet-mapping>
</web-app>

测试时注意,输入往一个用户时,你如果返回重写输入另外一个用户,此刻两个用户就会出现同时使用一个session的情况。当你要删除完第一个用户,session会同时失效,再删除另一个时,此时session已经失效,系统会报错。所以我们测试这个时需要用其他浏览器登录另外一个用户。

JavaWeb—监听器—Eclipse版动力节点学习笔记相关推荐

  1. Spring Boot入门篇,动力节点学习笔记整理

    什么是Spring Boot? 多年来,随着新功能的增加,spring变得越来越复杂.只需访问https://spring.io/projects页面,我们就会看到可以在我们的应用程序中使用的所有Sp ...

  2. 《python基础教程(第二版)》学习笔记 基础部分(第1章)

    <python基础教程(第二版)>学习笔记 基础部分(第1章) python常用的IDE: Windows: IDLE(gui), Eclipse+PyDev; Python(comman ...

  3. 《游戏设计艺术(第2版)》——学习笔记(17)第17章 有种体验叫作故事

    <游戏设计艺术(第2版)>学习笔记(17) 第17章 有种体验叫作故事 故事/游戏的二象性 被动娱乐的迷思 梦想 事实 真实世界方法1:珍珠串 真实世界方法2:故事机 问题 问题1:好的故 ...

  4. 《游戏设计艺术(第2版)》——学习笔记(11)第11章 玩家的动机驱使着玩家的脑

    <游戏设计艺术(第2版)>学习笔记(11) 第11章 玩家的动机驱使着玩家的脑 需求 更多需求 内在动机.外在动机 想做与得做 创新 评断 第11章 玩家的动机驱使着玩家的脑 我们先来面对 ...

  5. 《游戏设计艺术(第2版)》——学习笔记(30)第30章 设计师要向客户推销自己的想法

    <游戏设计艺术(第2版)>学习笔记(30) 为什么是我 权力的谈判 想法的层次 成功推销的12条建议 建议1:敲开客户的门 建议2:展示你的认真 建议3:条理分明 建议4:充满激情 建议5 ...

  6. 《游戏设计艺术(第2版)》——学习笔记(12)第12章 有些元素是游戏机制

    <游戏设计艺术(第2版)>学习笔记(12) 第12章 有些元素是游戏机制 机制1:空间 互相嵌套的空间 零维度 机制2:时间 离散与连续的时间 时钟与竞赛 操控时间 机制3:对象.属性和状 ...

  7. 《游戏设计艺术(第2版)》——学习笔记(24)第24章 其他玩家有时会形成社群

    <游戏设计艺术(第2版)>学习笔记(24) 第24章 其他玩家有时会形成社群 不仅仅是其他玩家 强大社群的10条建议 社群建议1:培养友谊 社群建议2:牢记矛盾 社群建议3:运用建筑学去形 ...

  8. 《游戏设计艺术(第2版)》——学习笔记(8)第8章 游戏通过迭代提高

    <游戏设计艺术(第2版)>学习笔记(8) 第8章 游戏通过迭代提高 选择创意 八项测试 迭代规则 软件工程的简短历史 危险--瀑布--保留 巴里·伯姆爱你 敏捷宣言 风险评估与原型设计 制 ...

  9. 《游戏设计艺术(第2版)》——学习笔记(7)第7章 游戏始于一个创意

    <游戏设计艺术(第2版)>学习笔记(7) 第7章 游戏始于一个创意 灵感 陈述问题 如何睡眠 你的无声伙伴 潜意识建议1:给予关注 潜意识建议2:记录你的创意 潜意识建议3:(明智的)满足 ...

最新文章

  1. 怎么判断日出时间早晚_珠海最全防堵攻略,知道这些上班时间或缩短半小时!有大数据分析!...
  2. java程序设计第一次作业
  3. 完成users中的models
  4. 深智云 让企业在物联网时代实现数据价值
  5. cv::imread导致段错误_网络诊断举例LSO导致的网络性能问题
  6. 40种网站设计常用方法
  7. CVPR2021 最佳论文候选—提高图像风格迁移的鲁棒性
  8. 数据分析的5层解读,报表仍是有效的落地实践!
  9. 洛谷 P3049园林绿化 题解
  10. Leaning perl 第2章练习题答案
  11. 记录一次elastic-job分片查询及基础概念理解
  12. Win32 改变鼠标的光标图片
  13. 【报表福利大放送】100余套报表模板免费下
  14. java连连看代码_Java版连连看
  15. 小程序遵循的语法_2020年遵循的最佳应用程序开发实践
  16. express图片上传
  17. Android项目源码分享
  18. Windows安装乌班图(Ubuntu)教程及错误解决办法
  19. 到底什么是“被动收入”?
  20. 用Python实现无条件重复循环

热门文章

  1. ASP.NETIT产品网上物流管理信息系统的设计与实现(源代码+论文)
  2. linux服务篇-Samba服务
  3. 典型聚类——K-means算法原理及python实战
  4. Brats2020数据集的读取—>python中对.nii格式数据读取
  5. IM通讯协议专题学习(八):金蝶随手记团队的Protobuf应用实践(原理篇)
  6. JavaScript闭包和this
  7. 《上海市城镇私营企业职工养老保险办法》
  8. Python中的List知识
  9. utf-8 中的一个汉字占几个字节
  10. Kafka 深度剖析