Java后端自顶向下方法——过滤器与监听器

Java后端自顶向下方法——过滤器与监听器

(一)过滤器有什么用

当客户端发出Web资源的请求时,Web服务器根据应用程序配置文件设置的过滤规则进行检查,若客户请求满足过滤规则,则对客户请求/响应进行拦截,对请求头和请求数据进行检查或改动,并依次通过过滤器链,最后把请求/响应交给请求的Web资源处理。请求信息在过滤器链中可以被修改,也可以根据条件让请求不发往资源处理器,并直接向客户机发回一个响应。当资源处理器完成了对资源的处理后,响应信息将逐级逆向返回。同样在这个过程中,用户可以修改响应信息,从而完成一定的任务。

在这里插入图片描述

很显然,过滤器就像一个挡在客户端和Servlet之间的一个筛子,这个筛子很特别,他不仅可以筛选请求和响应(拦下不符合我们定义的规则的请求和响应),还可以对请求和响应的内容进行修改。总之,只要记住过滤器就像一个筛子就行,然后记住这个筛子的位置:客户端和Servlet之间。一个过滤器可以附加到一个或者多个Servlet上,一个Servlet可以附加一个或者多个过滤器。

当我们的业务逻辑越来越复杂时,一种筛子可能就不太够用了,我们可能就需要多种筛子,帮助我们依据不同的规则进行筛选。那么我们的多种过滤器之间的顺序改如何确定呢?(PS:初学者可能会感觉过滤器顺序的不同不会造成不同的筛选结果,其实这种想法不完全对,只能说存在不造成影响结果的情况,但是也有可能会造成影响,所以过滤器之间的顺序是要好好考虑来确定的)我们就要用到过滤器链。

(二)过滤器与过滤器链

在这里插入图片描述
过滤器链十分好理解,服务器会按照过滤器定义的先后顺序组装成一条链。执行过程如下:执行第一个过滤器的chain.doFilter()之前的代码,执行第二个过滤器的chain.doFilter()之前的代码,执行Servlet中的service方法,执行第二个过滤器的chain.doFilter()之后的代码,执行第一个过滤器的chain.doFilter()之后的代码,最后返回响应给客户端。

这边的执行过程非常关键,核心就是这个chain.doFilter()方法,简单来说,这个方法的作用就是跳转到下一个过滤器,这个很重要。因此我们就很容易理解,我们的请求只经历了chain.doFilter方法前面的代码的筛选,而chain.doFilter()方法后面的代码是来筛选响应的,就像图上画的那样。

另外还有一点要注意,就是筛选请求和筛选响应的顺序是相反的,有点像对栈操作的先进后出(FILO)原则,这一点在开发时一定要格外注意。相信了解Koa的朋友一看就发现(没听说过Koa的就当我没说。。。),这不是Koa中的中间件吗?确实,这两个东西基本上是一样的功能,而Koa中间件的最著名的一张图,就是洋葱模型图:

在这里插入图片描述
有了这张图我们就能更好的理解过滤器的执行过程了,众所周知,洋葱是一层一层的,每一层就相当于一个过滤器,请求穿过一层层(一旦遇到chain.doFilter()方法就会穿到下一层)的过滤器到达洋葱中心(Servlet),然后响应再穿过一层层的过滤器到达外面(客户端),很明显,处理请求的时的第一层在处理响应时已经是最后一层了,是不是很直观?放个gif动图给用过Koa的朋友们复习一下:

在这里插入图片描述
这里有四个中间件,顺序为从上到下,里面的yield next就相当于我们过滤器中的chain.doFilter()方法,我们可以很清晰的看出跳转的顺序,这与我们的过滤器链的顺序完全一致。

(三)过滤器的注解配置

一个过滤器必须实现javax.servlet.Filter接口并实现其中的三个方法:

  1. init()方法:这个方法在实例化过滤器时被调用,容器为这个方法传递一个FilterConfig对象,其中包含配置信息。
  2. doFilter()方法:这个方法用于处理请求和相应,是过滤器的核心。它接受3个输入参数:ServletRequest、ServletReponse、FilterChain对象。
  3. destroy()方法:该方法由容器在销毁过滤器实例之前调用。

下面我们来看一个例子,我们用到了一个Servlet和两个过滤器:

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

@WebServlet(name = "TestServlet", value = "/test")
public class TestServlet extends HttpServlet {
    
    
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
    
        System.out.println("Servlet调用了");
    }
}
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(filterName = "Filter1", servletNames = "TestServlet")
public class Filter1 implements Filter {
    
    
    public void destroy() {
    
    
        System.out.println("Filter1销毁了");
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
    
    
        System.out.println("Filter1过滤请求...");
        chain.doFilter(req, resp);
        System.out.println("Filter1过滤响应...");
    }

    public void init(FilterConfig config) throws ServletException {
    
    
        System.out.println("Filter1初始化了");
    }
}
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter(filterName = "Filter2", servletNames = "TestServlet")
public class Filter2 implements Filter {
    
    
    public void destroy() {
    
    
        System.out.println("Filter2销毁了");
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
    
    
        System.out.println("Filter2过滤请求...");
        chain.doFilter(req, resp);
        System.out.println("Filter2过滤响应...");
    }

    public void init(FilterConfig config) throws ServletException {
    
    
        System.out.println("Filter2初始化了");
    }
}

我们这里的配置都是基于注解的,我们会发现我们的过滤器的注解里面写了两个参数,一个是过滤器名称,一个是挂载的Servlet的名称。过滤器名称没啥好讲的,这个挂载的Servlet名称是指我们的过滤器要挂载在哪个Servlet上,也就是说发送到这个Servlet的请求会被我们指定的过滤器过滤。那么问题来了,如果我们的Servlet很多,比如有三百个,是不是要把这三百个Servlet的名字都写上去呢?当然不是!

我们可以将注解中的servletName属性换成urlPattern属性,在里面我们可以用正则表达式来指定我们需要过滤的API,比如我们可以让他过滤全部的API,相当于挂载到所有的Servlet上,我们就可以指定urlPattern = “/*”,这样就简单多了。

至于过滤器在过滤器链中的顺序,@WebFilter注解没有提供对应的属性可以设置(网传的通过字母序来排序那个是假的。。。大家千万不要相信,如果遇到成功的案例仅为巧合),也就是说我们只有通过传统的XML配置文件方式才能指定过滤器顺序。

(四)过滤器的XML配置

虽然我个人不是特别喜欢XML配置文件的方式,但是为了指定过滤器的顺序,我们只能这样做。(希望早日能给@WebFilter加上指定顺序的属性)

我们把上面那个例子的过滤器的注解去掉,然后编辑web.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
	http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

  <filter>
    <filter-name>Filter1</filter-name>
    <filter-class>Filter1</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>Filter1</filter-name>
    <servlet-name>TestServlet</servlet-name>
  </filter-mapping>

  <filter>
    <filter-name>Filter2</filter-name>
    <filter-class>Filter2</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>Filter2</filter-name>
    <servlet-name>TestServlet</servlet-name>
  </filter-mapping>
</web-app>

配置Filter很简单,和注解类似,我们先要有过滤器,然后再指定他要过滤的请求对应的Servlet

在配置文件中指定顺序非常简单,配置在前面的就会排在前面,就像这里,Filter1就排在Filter前面。我们调用这个Servlet,我们就会得到我们想要的结果:

Filter1初始化了
Filter2初始化了
Filter1过滤请求...
Filter2过滤请求...
Servlet调用了
Filter2过滤响应...
Filter1过滤响应...

注意两个过滤器调用的顺序,是不是和前面讲的一样?还有一点要强调的是,许多人会把过滤器和拦截器傻傻分不清,其实这两个东西区别真的很大的,虽然实现的功能好像看起来差不多,但是过滤器基于Servlet容器,底层是回调函数实现的,而拦截器基于Spring容器,底层是反射实现的。因此他们两个有本质的区别,不能混为一谈,相比较而言,拦截器的功能更强,他可以做所有过滤器能做的事。

(五)监听器有什么用

监听器就是一个实现特定接口的普通java程序,这个程序专门用于监听另一个java对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法将立即被执行。

听起来好像很厉害,我们先来看看Servlet给我们提供的常用的六个监听器,分别是HttpSessionListener、ServletContextListener、ServletRequestListener、ServletContextAttributeListener、HttpSessionAttributeListener和ServletRequestAttributeListener。其实我们从他们的名字上就能看出一点规律,我们下面先分一下类:

监听器 监听内容
ServletRequestListener、ServletRequestAttributeListener request(请求)
HttpSessionListener、HttpSessionAttributeListener session(会话)
ServletContextListener、ServletContextAttributeListener context(上下文)

首先我们来理解一下这三个名词——请求、会话和上下文。首先,请求是这三个里面最小的单位,就是指我们发送HTTP请求然后收到响应的过程。其次,稍微大一点的是会话,比如我们登陆了一个网页,服务器会保存你的一些信息在一个session对象中,然后退出登录之后,这个会话就会被服务器清除。很显然,在一个会话中我们可以多次发出请求,而每一个用户都会获得一个单独的会话。最后,上下文是最大的单位,代表了整个应用程序,里面包含了多个会话,就像一个网页可以被多个用户同时访问一样。

这三个对象对应了三个作用域,也就是说他们都是用来保存一些我们需要用到的属性的存储空间而已,存储方式是常见的key-value对。请求对象保存的属性仅在本请求中可见,会话对象保存的属性在会话内所有请求均可见,上下文对象保存的属性在本应用程序所有的会话和请求中均可见。

然后我们再来看这些监听器,一共三类,每一类里面有两个,其中一个只是比另一个多了“Attribute”(属性)。其实不带“Attribute”的监听的东西比较宏观,带“Attribute”的监听的东西比较微观,甚至还可以修改属性。下面我们来一一介绍:

(六)监听器分类

1. Request监听器

ServletRequestListener接口:用于对Request请求进行监听(创建、销毁)。

public void requestInitialized(ServletRequestEvent sre);//request初始化
public void requestDestroyed(ServletRequestEvent sre);//request销毁
public ServletRequest getServletRequest();//取得一个ServletRequest对象
public ServletContext getServletContext();//取得一个ServletContext(application)对象

ServletRequestAttributeListener接口:对Request属性的监听(增删改属性)。

public void attributeAdded(ServletRequestAttributeEvent srae);//增加属性
public void attributeRemoved(ServletRequestAttributeEvent srae);//属性删除
public void attributeReplaced(ServletRequestAttributeEvent srae);//属性替换(第二次设置同一属性)
public String getName();//得到属性名称
public Object getValue();//取得属性的值

2. Session监听器

HttpSessionListener接口:对Session的整体状态的监听。

public void sessionCreated(HttpSessionEvent se);//session创建
public void sessionDestroyed(HttpSessionEvent se);//session销毁
public HttpSession getSession();//取得当前操作的session

HttpSessionAttributeListener接口:对session的属性监听。

public void attributeAdded(HttpSessionBindingEvent se);//增加属性
public void attributeRemoved(HttpSessionBindingEvent se);//删除属性
public void attributeReplaced(HttpSessionBindingEvent se);//替换属性
public String getName();//取得属性的名称
public Object getValue();//取得属性的值
public HttpSession getSession();//取得当前的session

3. Context监听器

ServletContextListener:用于对Servlet整个上下文进行监听(创建、销毁)。

public void contextInitialized(ServletContextEvent sce);//上下文初始化
public void contextDestroyed(ServletContextEvent sce);//上下文销毁
public ServletContext getServletContext();//取得一个ServletContext(application)对象

ServletContextAttributeListener:对Servlet上下文属性的监听(增删改属性)。

public void attributeAdded(ServletContextAttributeEvent scab);//增加属性
public void attributeRemoved(ServletContextAttributeEvent scab);//属性删除
public void attributeRepalced(ServletContextAttributeEvent scab);//属性替换(第二次设置同一属性)
public String getName();//得到属性名称
public Object getValue();//取得属性的值

监听器的使用和Servlet和Filter的使用非常类似,都是实现接口然后重写对应的方法,就可以实现相应的功能了,上面列出了六个接口中的方法。

(七)监听器实际案例

下面我们来做一个小案例,将监听器和三个作用域(request、session、context)的内容来总结一下。首先我们先来看看我们要做什么,我们的需求是统计网页当前在线的人数,并且记录用户的sessionID,IP地址和登录时间这三个信息。因此,我们需要一个类来保存上述的信息,我们就先写这个实体类。

package entity;

public class User {
    
    
    private String sessionID;
    private String ip;
    private String firstTime;

    public String getSessionID() {
    
    
        return sessionID;
    }

    public void setSessionID(String sessionID) {
    
    
        this.sessionID = sessionID;
    }

    public String getIp() {
    
    
        return ip;
    }

    public void setIp(String ip) {
    
    
        this.ip = ip;
    }

    public String getFirstTime() {
    
    
        return firstTime;
    }

    public void setFirstTime(String firstTime) {
    
    
        this.firstTime = firstTime;
    }

    @Override
    public String toString(){
    
    
        return sessionID + "\t" + ip + "\t" + firstTime;
    }
}

然后我们要用一个链表在存储这些保存了用户数据的对象,但是有一点要注意,我们如何保证链表内不会有重复的对象?我们判断多次请求是否来自同一个用户的依据是什么?前面讲过,每个用户的一次会话中的所有请求用的是同一个session对象,并且有相同的sessionID。用户的请求中正好带了这个sessionID,也就是说我们可以以这个sessionID为依据,所有sessionID相同的请求都被认为来自于同一个用户。我们可以写一个帮助类,帮我们找到链表内指定sessionID的User对象。

package util;

import java.util.ArrayList;
import entity.User;

public class SessionUtil {
    
    
    public static User getUserBySessionID(ArrayList<User> userList, String sessionID) {
    
    
        for (User user : userList) {
    
    
            if (user.getSessionID().equals(sessionID)) {
    
    
                return user;
            }
        }
        return null;
    }
}

根据上面讲到的思路,我们可以写下监听请求的监听器:

package listenner;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import entity.User;
import util.SessionUtil;

@WebListener
public class MyServletRequestListener implements ServletRequestListener {
    
    
    @SuppressWarnings("unchecked")
    public void requestInitialized(ServletRequestEvent servletRequestEvent) {
    
    
        ArrayList<User> userList = null;
        //获取上下文对象中的链表,若不存在,则创建一个链表
        userList= (ArrayList<User>) servletRequestEvent.getServletContext().getAttribute("userList");
        if (userList == null) {
    
    
            userList = new ArrayList<User>();
        }
        //获取请求对象
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequestEvent.getServletRequest();
        //通过获取会话对象拿到sessionID
        String sessionID = httpServletRequest.getSession().getId();
        //通过sessionID在链表中查找对应的User对象,若不存在,则创建并加入到链表中
        if (SessionUtil.getUserBySessionID(userList, sessionID) == null) {
    
    
            User user = new User();
            user.setSessionID(sessionID);
            user.setFirstTime(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
            user.setIp(httpServletRequest.getRemoteAddr());
            userList.add(user);
        }
        //将链表保存到上下文对象中,便于多地使用
        servletRequestEvent.getServletContext().setAttribute("userList", userList);
    }
}

另外,光是添加User对象肯定是不行的,因为用户可能还会登出下线,因此我们就需要把这些人从在线人数中扣除,相应的用户信息也要一并删除。因此,我们还需要监听session的销毁(我们这个案例中没有提供手动销毁session的方法,只能依赖系统根据session过期时间自动销毁,Tomcat中默认的session过期时间是三十分钟)。

package listenner;

import java.util.ArrayList;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import entity.User;
import util.SessionUtil;

@WebListener
public class MyHttpSessionListener implements HttpSessionListener {
    
    
    private int num = 0; //统计人数

    public void sessionCreated(HttpSessionEvent httpSessionEvent) {
    
    
    	//创建一个session,人数加一
        num++;
        httpSessionEvent.getSession().getServletContext().setAttribute("number", num);
    }

    @SuppressWarnings("unchecked")
    public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
    
    
    	//销毁一个session,人数减一
        num--;
        httpSessionEvent.getSession().getServletContext().setAttribute("number", num);
        //在此用户被销毁的时候,将链表中对应的用户对象删除
        ArrayList<User> userList = (ArrayList<User>) httpSessionEvent.getSession().getServletContext().getAttribute("userList");
        if (SessionUtil.getUserBySessionID(userList, httpSessionEvent.getSession().getId()) != null) {
    
    
            userList.remove(SessionUtil.getUserBySessionID(userList, httpSessionEvent.getSession().getId()));
        }
    }
}

其实你会发现,如果我们只要当前在线人数,只监听session就可以了。因为这里的session创建和销毁已经可以满足我们的需要,但我们还需要用户IP地址等信息,所以光监听session是不够的,我们还需要监听请求。

最后就是我们的Servlet,Servlet干的事很简单,接收请求后打印当前在线人数和用户信息:

package controller;

import entity.User;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

@WebServlet(name = "MyServlet", value = "/test")
public class MyServlet extends HttpServlet {
    
    
    @SuppressWarnings("unchecked")
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
    
        ArrayList<User> userList = (ArrayList<User>) request.getSession().getServletContext().getAttribute("userList");
        int number = (int) request.getSession().getServletContext().getAttribute("number");
        System.out.println("当前在线人数:" + number);
        for (User user : userList) {
    
    
            System.out.println(user);
        }
    }
}

我们来测试一下,首先我用我的手机请求,控制台上打印了如下内容:

当前在线人数:4
CA2B5BF8BABBA786717411F6731E7B4F	127.0.0.1	2020-05-25 09:48:37
E40ACB667614F7C249C41F205960576A	127.0.0.1	2020-05-25 09:48:37
222DAF40FB4AC0FF83FB1CA872EC7C03	0:0:0:0:0:0:0:1	2020-05-25 09:48:37
773BE8633271CA60A03B1F7F92B43A64	192.168.1.104	2020-05-25 09:48:55

然后我用另一台电脑再次请求,控制台上打印了如下内容:

当前在线人数:5
CA2B5BF8BABBA786717411F6731E7B4F	127.0.0.1	2020-05-25 09:48:37
E40ACB667614F7C249C41F205960576A	127.0.0.1	2020-05-25 09:48:37
222DAF40FB4AC0FF83FB1CA872EC7C03	0:0:0:0:0:0:0:1	2020-05-25 09:48:37
773BE8633271CA60A03B1F7F92B43A64	192.168.1.104	2020-05-25 09:48:55
88EB3378C10EBB84BC3C9131B9BAC6A9	192.168.1.106	2020-05-25 09:49:14

前三条信息我们暂时先不管。我们可以看到,这样我们就实现了我们需求的基本的功能,IP地址和时间也是正确的,我的手机的IP地址是192.168.1.104,我的另一台电脑的IP地址是192.168.1.106。然后可以再试试,用我的手机和电脑重复发出请求,上述内容无变化,证明我们成功的识别了来自同一个用户的多次请求。

以上就是过滤器和监听器的全部内容了。这一章内容好像讲的有点多了(早知道我把过滤器和监听器分开讲了),不过代码部分还是要仔细摸索的,尤其是监听器部分和三个作用域,内容可能会有一点抽象,仔细的思考一下也不算是太难。

2020年5月25日

猜你喜欢

转载自blog.csdn.net/weixin_43907422/article/details/106177545