zl程序教程

您现在的位置是:首页 >  其他

当前栏目

29. Filter 过滤器 以及 Listener 监听器

2023-03-15 22:04:09 时间

29. Filter 过滤器 以及 Listener 监听器

JavaWeb的三大组件

组件

作用

实现接口

Servlet

小应用程序,在JavaWeb中主要做为控制器来使用 可以处理用户的请求并且做出响应

javax.servlet.Servlet

Filter

过滤器,对用户发送的请求或响应进行集中处理,实现请求的拦截

javax.servlet.Filter

Listener

监听器,在某些框架中会使用到监听器(比如spring),在Web执行过程中,引发一些事件,对相应事件进行处理

javax.servlet.XxxListener 每个事件有一个接口

一、 概述

生活中的过滤器

净水器、空气净化器、地铁安检

web中的过滤器

当用户访问服务器资源时,过滤器将请求拦截下来,完成一些通用的操作

应用场景

如:登录验证、统一编码处理、敏感字符过滤

1592011832218

从上图可以简单说明一下,Filter 过滤器就是用来拦截 请求 或者 响应 的。下面我们首先来写一个快速入门的案例,从代码的角度来认识一下。

二、Filter过滤器 - 快速入门

首先我们创建一个HelloServlet,用来下面提供Filter进行拦截,如下:

image-20210304235335136

@WebServlet("/HelloServlet")
public class HelloServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("HelloServlet 被访问了...");
    }
}

好了,下面我们来开始写 Filter 过滤器。而 Filter 跟 Servlet 一样,具有两种写法,一种是 xml 配置的方式,另一种是注解的方式。

下面我们首先来写 xml 配置的方式。

2.1 xml配置

2.1.1 编写java类,实现filter接口

image-20210304235727090

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * 过滤器入门案例
 * 1. 编写一个类 , 实现Filter接口,重写抽象方法
 * 2. 配置web.xml / 注解
 *
 * @author Aron.li
 * @date 2021/3/4 23:55
 */
public class MyFilter implements Filter {
    public void destroy() {
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 处理拦截器的相关拦截业务
        System.out.println("filter执行了,然后放行");

        // 放行拦截,执行后续Servlet程序
        chain.doFilter(req, resp);
    }

    public void init(FilterConfig config) throws ServletException {

    }

}

2.1.2 配置web.xml

image-20210304235853975

<?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"
 version="2.5">

 <!--
  filter的web.xml配置
  1. filter和filter-mapping的子标签filter-name必须一致(可以自定义,通常与类名相同)
  2. url-pattern : 当前filter要拦截的虚拟路径
 -->
 <filter>
  <filter-name>MyFilter</filter-name>
  <filter-class>com.filter.MyFilter</filter-class>
 </filter>
 <filter-mapping>
  <filter-name>MyFilter</filter-name>
  <url-pattern>/HelloServlet</url-pattern>
 </filter-mapping>

</web-app>

2.1.3 启动服务,测试 filter 的效果

启动服务后,浏览器访问 http://localhost:8082/HelloServlet

image-20210305000354097

好了,我们已经知道 xml 如何配置 filter 了,那么下面再来看看注解的配置方式。

2.2 注解配置

2.2.1 编写java类,实现filter接口

首先我们将上面的 xml 配置进行注释,如下:

image-20210305000516205

然后给 MyFilter 设置注解,在注解中的参数就是配置需要拦截的请求路径,如下:

image-20210305000650221

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * 过滤器入门案例
 * 1. 编写一个类 , 实现Filter接口,重写抽象方法
 * 2. 配置web.xml / 注解
 *
 * @author Aron.li
 * @date 2021/3/4 23:55
 */
// 注解配置
@WebFilter("/HelloServlet")
public class MyFilter implements Filter {
    public void destroy() {
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 处理拦截器的相关拦截业务
        System.out.println("注解的方式: filter执行了,然后放行");

        // 放行拦截,执行后续Servlet程序
        chain.doFilter(req, resp);
    }

    public void init(FilterConfig config) throws ServletException {

    }

}

2.2.2 重新部署服务,测试 filter 的效果

image-20210305000842746

可以看到注解的方式的 filter 过滤器也成功拦截了请求了。

2.3 Filter模板设置

2.3.1 设置 Filter 模板

上面我们已经成功编写了拦截器的示例,为了可以快速编写代码,我们还可以修改拦截器的自动生成模板,如下:

搜索 file and code template ,选择 Other 如下:

image-20210305001334388

模板如下:

#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")
@javax.servlet.annotation.WebFilter(urlPatterns = "/${Entity_Name}")
public class ${Class_Name} implements javax.servlet.Filter {

    public void init(javax.servlet.FilterConfig config) throws javax.servlet.ServletException {

    }

    public void doFilter(javax.servlet.ServletRequest req, javax.servlet.ServletResponse resp, javax.servlet.FilterChain chain) throws javax.servlet.ServletException, java.io.IOException {
        chain.doFilter(req, resp);
    }

    public void destroy() {
    
    }

}

2.3.2 测试创建模板

image-20210305001446860

image-20210305001503238

image-20210305001517508

三、Filter过滤器 - 工作原理

1. 用户发送请求,请求Web资源(包括html,jsp,servlet等)
2. 如果Web资源的地址,匹配filter的地址,请求将先经过filter,并执行doFilter()
3. doFilter()方法中如果调用chain.doFilter(),则放行执行下一个Web资源。
4. 访问Web资源,响应回来会再次经过filter,执行过滤器中的代码,到达浏览器端。

四、Filter过滤器 - 使用细节

4.1 生命周期

生命周期:指的是一个对象从生(创建)到死(销毁)的一个过程

// filter 过滤器的声明周期主要有三个方法: init、doFilter、destory,分别如下:

// 初始化方法
public void init(FilterConfig config);

// 执行拦截方法
public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain);

// 销毁方法
public void destroy();
* 创建
  服务器启动项目加载,创建filter对象,执行init方法(只执行一次)
  
* 运行(过滤拦截)
  用户访问被拦截目标资源时,执行doFilter方法

* 销毁
  服务器关闭项目卸载时,销毁filter对象,执行destroy方法(只执行一次)
  
* 补充:
 过滤器一定是优先于servlet创建的,后于Servlet销毁

下面个声明周期的案例。

4.1.1 编写一个演示声明周期的过滤器 LifeFilter

image-20210305003635786

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 *     #Filter的生命周期
 *         0. 先于Servlet创建,后于Servlet销毁
 *         1. init方法
 *             filter 自动启动加载的,执行一次
 *             (先于Servlet的init方法执行)
 *         2. doFilter 方法
 *             (先于Servlet的service方法执行)
 *             浏览器每访问一次,就会执行一次
 *
 *             chain.doFilter(req, resp); // 放行
 *
 *        3.  destroy 方法
 *             (后于Servlet的destroy方法执行)
 *             tomcat关闭,会随之销毁, 只执行一次
 *
 * @author Aron.li
 * @date 2021/3/5 0:31
 */
@WebFilter(urlPatterns = "/LifeServlet")
public class LifeFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {
        System.out.println("lifeFilter init");
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        /*
         *   此方法决定了,后续资源是否被访问到
         *   1. 如果调用此方法,后续资源就会被访问到
         *   2. 如果没有调用,后续资源就不会被访问到
         *       -> 放行
         *
         *       类似于请求转发
         *       不仅拦截对资源的请求,还拦截资源的响应
         * */
        System.out.println("lifeFilter doFilter before");
        chain.doFilter(req, resp);
        System.out.println("lifeFilter doFilter after");
    }

    public void destroy() {
        System.out.println("lifeFilter destroy");
    }

}

4.1.2 编写提供访问的 LifeServlet

image-20210305003841971

package com.web;

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

/**
 * @author Aron.li
 * @date 2021/3/5 0:37
 */
@WebServlet("/LifeServlet")
public class LifeServlet implements Servlet {

    @Override
    public void init(ServletConfig config) throws ServletException {
        System.out.println("LifeServlet init");
    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        System.out.println("LifeServlet service");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
        System.out.println("LifeServlet destroy");
    }

}

4.1.3 启动服务,测试请求 LifeServlet,查看声明周期函数的调用

首先访问 http://localhost:8082/LifeServlet ,查看声明周期函数打印如下:

image-20210305004139979

从上面我结果来看,filter 过滤器的声明周期 都是在 servlet 程序的前面执行。下面我们关闭 tomcat,看看结束时候的生命周期。

image-20210305004324089

在这里我们已经知道了 Filter 和 Servlet 之间的执行顺序,下面再来看看 Filter 的拦截路径。

4.2 拦截路径

在开发时,我们可以指定过滤器的拦截路径来定义拦截目标资源的范围

* 精准匹配
  用户访问指定目标资源(/show.jsp)时,过滤器进行拦截
  
* 目录匹配
  用户访问指定目录下(/user/*)所有资源时,过滤器进行拦截

* 后缀匹配
  用户访问指定后缀名(*.html)的资源时,过滤器进行拦截

* 匹配所有
  用户访问该网站所有资源(/*)时,过滤器进行拦截

4.2.1 精准匹配的案例

首先我们写一个 show.jsp ,如下:

image-20210305004855960

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>show jsp</h1>
</body>
</html>

下面再来创建一个 PathFilter,专门来进行精确匹配,如下:

image-20210305005432652

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
     1. 精准匹配
     @WebFilter(urlPatterns = "/LifeServlet")
 *
 * @author Aron.li
 * @date 2021/3/5 0:52
 */
@WebFilter(urlPatterns = "/show.jsp")
public class PathFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 拦截业务
        System.out.println("被 PathFilter 拦截了 ....");
        // 拦截放行
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

测试访问 show.jsp 查看拦截的情况,如下:

image-20210305005524070

4.2.2 目录匹配的案例

上面通过精确匹配,我们已经匹配拦截到了 show.jsp, 下面我们再来创建一个路径,拦截这个路径下的请求,如下:

image-20210305005901515

下面再修改 PathFilter 为目录匹配的方式,如下:

image-20210305005942404

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
     1. 精准匹配
     @WebFilter(urlPatterns = "/LifeServlet")

     2. 目录匹配
     @WebFilter(urlPatterns = "/abc/*")
 *
 * @author Aron.li
 * @date 2021/3/5 0:52
 */
@WebFilter(urlPatterns = "/abc/*")
public class PathFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 拦截业务
        System.out.println("目录匹配: 被 PathFilter 拦截了 ....");
        // 拦截放行
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

测试请求如下:

image-20210305010029599

4.2.3 后缀名匹配的案例

这里我们只要修改 PathFilter 的匹配路径就可以了,如下:

image-20210305010311534

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
     1. 精准匹配
     @WebFilter(urlPatterns = "/LifeServlet")

     2. 目录匹配
     @WebFilter(urlPatterns = "/abc/*")

     3. 后缀名匹配
     @WebFilter(urlPatterns = "*.jsp")
 *
 * @author Aron.li
 * @date 2021/3/5 0:52
 */
@WebFilter(urlPatterns = "*.jsp")
public class PathFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 拦截业务
        System.out.println("后缀名匹配: 被 PathFilter 拦截了 ....");
        // 拦截放行
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

测试如下:

image-20210305010342602

4.2.4 匹配所有的案例

匹配所有也只需要修改 PathServlet 的匹配路径即可,如下:

image-20210305072039366

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
     1. 精准匹配
     @WebFilter(urlPatterns = "/LifeServlet")

     2. 目录匹配
     @WebFilter(urlPatterns = "/abc/*")

     3. 后缀名匹配
     @WebFilter(urlPatterns = "*.jsp")

     4. 匹配所有 (html,css,js,servlet...)
     @WebFilter(urlPatterns = "/*")
 *
 * @author Aron.li
 * @date 2021/3/5 0:52
 */
@WebFilter(urlPatterns = "/*")
public class PathFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 拦截业务
        System.out.println("匹配所有: 被 PathFilter 拦截了 ....");
        // 拦截放行
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

测试如下:

  • 首先访问 index.jsp 如下:

image-20210305072234706

  • 再访问 LifeServlet 如下:

image-20210305072327860

  • 最后再试试 /abc/hello.jsp 如下:

image-20210305072448207

可以从上面的测试中,匹配所有可以拦截所有路径的请求。

4.2.5 匹配多个路径的案例

在上面我们已经尝试了四种匹配路径的方式,在最后我们再来测试多个路径拦截。

image-20210305073004675

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
     1. 精准匹配
     @WebFilter(urlPatterns = "/LifeServlet")

     2. 目录匹配
     @WebFilter(urlPatterns = "/abc/*")

     3. 后缀名匹配
     @WebFilter(urlPatterns = "*.jsp")

     4. 匹配所有 (html,css,js,servlet...)
     @WebFilter(urlPatterns = "/*")
 *
 * @author Aron.li
 * @date 2021/3/5 0:52
 */
@WebFilter(urlPatterns = {"*.jsp", "/LifeServlet"})
public class PathFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 拦截业务
        System.out.println("多个路径匹配: 被 PathFilter 拦截了 ....");
        // 拦截放行
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

测试如下:

  • 访问匹配 *.jsp 的请求,如下:

image-20210305073336152

  • 访问匹配 /LifeServlet 的请求,如下:

image-20210305073401834

好了,写到这里我们已经知道了拦截器的拦截路径该如何匹配了,下面我们再来看拦截器如何来拦截不同的请求,例如:request 请求、forward 请求转发。

4.3 拦截方式

在开发时,我们可以指定过滤器的拦截方式来处理不同的应用场景,比如:只拦截从浏览器直接发送过来的请求,或者拦截内部转发的请求

总共有五种不同的拦截方式,我们这里学习常见的两种

1. request(默认拦截方式)
  浏览器直接发送请求时,拦截
2. forward
  请求转发的时候,拦截
  比如: 资源A转发到资源B时
  
我们可以配置 二个同时存在...

下面的案例,我们还是分为 xml 版本 和 注解版本 两种来分开演示一下。

4.3.1 xml版本

下面我们简单演示一下 xml 版本该如何配置,那么首先来创建一个拦截器:

image-20210305074037955

public class MethodFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 拦截业务代码
        System.out.println("被 method 拦截了");
        // 放行
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

配置 xml 如下:

image-20210305074300734

<!--
   filter的web.xml配置
   1. filter和filter-mapping的子标签filter-name必须一致(可以自定义,通常与类名相同)
   2. url-pattern : 当前filter要拦截的虚拟路径
-->
<filter>
   <filter-name>MethodFilter</filter-name>
   <filter-class>com.filter.MethodFilter</filter-class>
</filter>
<filter-mapping>
   <filter-name>MethodFilter</filter-name>
   <url-pattern>/HelloServlet</url-pattern>
   <!--
      dispatcher : 用来指定拦截方式的
      1. REQUEST(默认) : 浏览器直接发送过来的请求
      2. FORWARD: 请求转发过来的请求

      可以同时设置
   -->
   <dispatcher>REQUEST</dispatcher>
   <dispatcher>FORWARD</dispatcher>
</filter-mapping>

测试请求如下:

image-20210305074512188

在这里我们只演示该如何配置,下面我们在注解版本的案例中,来演示拦截请求转发的请求。

4.3.2 注解版本

4.3.2.1 首先创建一个注解版本的拦截器 DispatcherFilter,然后创建两个Servlet,其中为 AServlet 和 BServlet,访问 AServlet 的时候请求转发至 BServlet ,查看 DispatcherFilter 是否会拦截请求转发。

  • 拦截器 DispatcherFilter

image-20210305075446687

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * @author Aron.li
 * @date 2021/3/5 7:49
 */
@WebFilter(urlPatterns = "*.do")
public class DispatcherFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 拦截
        System.out.println("DispatcherFilter 拦截 *.do 请求..");
        // 放行
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

在这里设置拦截路径 *.do, 下面我们将两个Servlet 的路径都加上 .do 路径,那么就可以被拦截了。但是请求转发的 forward请求 会被拦截么?

  • AServlet

image-20210305075939890

package com.web;

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;

/**
 * @author Aron.li
 * @date 2021/3/5 7:47
 */
@WebServlet("/AServlet.do")
public class AServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("访问 AServlet.....");
        // 请求转发至 BServlet。。。
        request.getRequestDispatcher("/BServlet.do").forward(request, response);
    }
}
  • BServlet

image-20210305080009243

package com.web;

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;

/**
 * @author Aron.li
 * @date 2021/3/5 7:48
 */
@WebServlet("/BServlet.do")
public class BServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("访问 BServelet...");
    }
}
  • 最后,我们测试一下,只访问 AServlet,然后 AServlet 自动转发 BServlet, 看看会不会拦截两次

image-20210305080138845

可以从拦截的结果来看,默认只拦截了 request 请求,而不会去拦截 请求转发的 forward 请求。那么如果我们需要拦截 forward 请求,则需要配置 拦截方式。

4.3.2.2 配置同时拦截 forward 和 request 请求

image-20210305080408797

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * 拦截方式
 * 1. REQUEST(默认) : 浏览器发起的请求
 * 2. FORWARD : 请求转发
 * 可以同时设置
 *
 * @author Aron.li
 * @date 2021/3/5 7:49
 */
@WebFilter(urlPatterns = "*.do", dispatcherTypes = {DispatcherType.FORWARD, DispatcherType.REQUEST})
public class DispatcherFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 拦截
        System.out.println("DispatcherFilter 拦截 *.do 请求..");
        // 放行
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

再次测试访问 AServlet, 确认是否拦截请求转发:

image-20210305080518449

4.4 过滤器链

在一次请求中,若我们请求匹配到了多个filter,通过请求就相当于把这些filter串起来了,形成了过滤器链

* 需求
 用户访问目标资源 show.jsp时,经过 FilterA  FilterB
 
* 过滤器链执行顺序 (先进后出)
 1.用户发送请求
 2.FilterA拦截,放行
 3.FilterB拦截,放行
 4.执行目标资源 show.jsp
 5.FilterB增强响应
 6.FilterA增强响应
 7.封装响应消息格式,返回到浏览器
 
* 过滤器链中执行的先后问题....
 配置文件
  谁先声明,谁先执行
   <filter-mapping>
 注解【不推荐】
  根据过滤器类名进行排序,值小的先执行
   FilterA  FilterB  进行比较, FilterA先执行...

1592017660642

下面我们来基于上面的需求来写一个案例。

4.4.1 创建访问资源 ServletA

image-20210305224731864

@WebServlet("/ServletA")
public class ServletA extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("访问 ServletA.....");
    }
}

4.4.2 创建拦截器 FilterA

image-20210305224844070

public class FilterA implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        System.out.println("FilterA执行了");
        /*
         *  放行: 允许请求往后继续传递
         *     1. 等价于请求转发
         *     2. 如果后续还有过滤器,先执行过滤器, 知道过滤器执行完毕,才到资源里去
         * */
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

4.4.3 创建拦截器 FilterB

image-20210305225107452

public class FilterB implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        System.out.println("FilterB执行了");
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

4.4.4 在xml配置拦截器

image-20210305225444671

        <!--  过滤器链是按照xml从上到下的配置顺序进行逐步拦截的      -->
        <!--  配置过滤器FilterB      -->
        <filter>
            <filter-name>FilterB</filter-name>
            <filter-class>com.filter.FilterB</filter-class>
        </filter>

        <filter-mapping>
            <filter-name>FilterB</filter-name>
            <url-pattern>/ServletA</url-pattern>
        </filter-mapping>

        <!--  配置过滤器 FilterA  -->
        <filter>
            <filter-name>FilterA</filter-name>
            <filter-class>com.filter.FilterA</filter-class>
        </filter>

        <filter-mapping>
            <filter-name>FilterA</filter-name>
            <url-pattern>/ServletA</url-pattern>
        </filter-mapping>

4.4.5 访问 ServletA,查看拦截器链的执行顺序

image-20210305225533705

可以从结果来看,首先 FilterB 进行了拦截,然后 FilterA 拦截,最后访问到了 ServletA

五、Filter案例

5.1 统一网站编码

需求

tomcat8.5版本中已经将get请求的中文乱码解决了,但是post请求还存在中文乱码

浏览器发出的任何请求,通过过滤器统一处理中文乱码

5.1.1 需求分析

1592020764180

5.1.2 中文乱码的问题

首先我们来写注册页面、登录页面,还有对应的 Servlet,查看相关的中文乱码问题。

注册页面

image-20210306234155527

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册页面</title>
</head>
<body>

    <h1>注册页面</h1>
    <form action="RegisterServlet" method="post">
        <input type="text" name="username" placeholder="请输入要注册的用户名"> <br>
        <input type="password" name="pwd" placeholder="请输入要注册的密码"> <br>
        <input type="submit">
    </form>

</body>
</html>

登录页面

image-20210306234847655

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>

    <h2>登录页面</h2>
    <form action="LoginServlet" method="post">
        <input type="text" name="username" placeholder="请输入用户名"> <br>
        <input type="password" name="pwd" placeholder="请输入的密码"> <br>
        <input type="submit">
    </form>

</body>
</html>

注册 RegisterServlet

image-20210307000228838

@WebServlet("/RegisterServlet")
public class RegisterServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取注册用户所需的字段参数
        String username = request.getParameter("username");
        String pwd = request.getParameter("pwd");
        System.out.println("RegisterServlet:" + username + "," + pwd);
    }
}

登录 LoginServlet

image-20210307000809869

@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取登录参数
        String username = request.getParameter("username");
        String pwd = request.getParameter("pwd");
        System.out.println("LoginServlet:" + username + "," + pwd);
    }
}

测试请求注册,查看中文乱码的情况

输入英文注册:

image-20210307001123826

点击提交,之后查看 RegisterServlet 的后台打印信息,如下:

image-20210307001225494

输入中文注册:

image-20210307001302046

image-20210307001324575

测试请求登录,查看中文乱码的情况

image-20210307001451657

image-20210307001518275

可以看到在 RegisterServlet 和 LoginServlet 都出现了中文乱码的请求参数问题,下面我们使用拦截器来统一解决。

5.1.2 定义解决中文乱码请求的过滤器 PostFilter

  • 真实场景中,过滤器不会统一响应,因为响应的mime类型可能不同(有些返回html页面,有些返回JSON格式字符串)

image-20210307002746257

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @author Aron.li
 * @date 2021/3/7 0:16
 */
@WebFilter(urlPatterns = "/*") // 1. 使用 /* 拦截所有请求
public class PostFilter implements Filter {

    public void init(FilterConfig config) throws ServletException {

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        // 拦截请求,设置中文编码格式,解决中文乱码问题
        // 1. 获取请求 request
        HttpServletRequest request = (HttpServletRequest) req;
        // 2. 判断请求的方法类型
        String method = request.getMethod();
        if ("POST".equalsIgnoreCase(method)) {
            //3. 当请求的方法为 POST,则设置编码格式
            //解决请求参数的中文乱码
            request.setCharacterEncoding("UTF-8");
        }

        // 4. 打印 req 和 request
        System.out.println("req:" + req);
        System.out.println("request:" + request);

        // 放行
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

再次测试请求,查看是否还会出现中文乱码的情况:

  • 请求登录

image-20210307002927068

image-20210307003002203

  • 请求注册

image-20210307003025324

image-20210307003050291

5.2 非法字符拦截

需求

当用户发出非法言论的时候,提示用户言论非法警告信息

5.2.1 需求分析

1587622441357

5.2.2 代码实现

非法词库

创建一个 words.properties的配置文件,如下:

image-20210307082253457

keyword=傻叉,大爷,二大爷的

“注意: properties文件编码问题 ”

image-20210307082502147

模拟发送留言的页面 bbs.jsp

image-20210307082937485

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <form action="WordServlet" method="get">
        <input type="text" name="word" placeholder="请发言">
        <input type="submit" value="发送"> <br>
    </form>
</body>
</html>

接收留言信息的 WordServlet

image-20210307083315002

package com.web;

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.List;

/**
 * @author Aron.li
 * @date 2021/3/7 8:30
 */
@WebServlet("/WordServlet")
public class WordServlet extends HttpServlet {

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

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 接收留言信息
        String word = request.getParameter("word");

        // 响应留言信息
        response.setContentType("text/html;charset=utf-8");
        response.getWriter().print(word);
    }
}

此时我们启动服务,留个脏话来看看效果,如下:

image-20210307083724710

image-20210307083739197

可以看到脏话的词汇直接返回到浏览器,为了避免这种情况,下面我们用过滤器就行过滤

非法字符过滤器 WordsFilter

image-20210307084509519

package com.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

/**
 * @author Aron.li
 * @date 2021/3/7 8:38
 */
@WebFilter(urlPatterns = "/WordServlet")
public class WordsFilter implements Filter {

    // 非法字符词库集合
    List<String> words;

    public void init(FilterConfig config) throws ServletException {
        // 读取非法字符词库的单词
        InputStream is = WordsFilter.class.getClassLoader().getResourceAsStream("words.properties");
        Properties properties = new Properties();
        try {
            properties.load(is); // 加载输入流
            String keyword = properties.getProperty("keyword"); // 读取属性
            String[] strings = keyword.split(","); // 根据逗号 , 拆分字符串, 获取字符串数组
            this.words = Arrays.asList(strings); // 将数组转为list集合

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        //1. 获取用户的发言
        String word = req.getParameter("word");
        //2. 是否包含敏感词
        for (String dirty : this.words) {
            //word字符串是否包含dirty
            // 小瘪三 包含 瘪三, 返回true
            if(word.contains(dirty)){
                resp.setContentType("text/html;charset=utf-8");
                resp.getWriter().print("发言不友善,扣分10");
                return;
            }
        }
        //如果都不包含脏话, 放行
        chain.doFilter(req, resp);
    }

    public void destroy() {

    }

}

再次测试如下:

image-20210307084545234

image-20210307084657080

六、 Listener

1.1 概述

生活中的监听器

我们很多商场有摄像头,监视着客户的一举一动。如果客户有违法行为,商场可以采取相应的措施。

javaweb中的监听器

在我们的java程序中,有时也需要监视某些事情,一旦被监视的对象发生相应的变化,我们应该采取相应的操作。

监听web三大域对象:HttpServletRequest、HttpSession、ServletContext (创建和销毁)

场景

历史访问次数、统计在线人数、系统启动时初始化配置信息

监听器的接口分类

事件源

监听器接口

时机

ServletContext

ServletContextListener

上下文域创建和销毁

ServletContext

ServletContextAttributeListener

上下文域属性增删改的操作

**HttpSession **

HttpSessionListener

会话域创建和销毁

**HttpSession **

HttpSessionAttributeListener

会话域属性增删改的操作

HttpServletRequest

ServletRequestListener

请求域创建和销毁

HttpServletRequest

ServletRequestAttributeListener

请求域属性增删改的操作

1.2 快速入门

监听器在web开发中使用的比较少,见的机会就更少了,下面我们使用ServletContextListenner来学习下监听器,因为这个监听器是监听器中使用率最高的一个,且监听器的使用方式都差不多。

我们使用这个监听器可以在项目启动和销毁的时候做一些事情,例如,在项目启动的时候加载配置文件。

步骤分析

1. 创建一个普通类,实现 ServletContextListener

2. 重写抽象方法
 监听ServletContext创建
 监听ServletContext销毁
 
3. 配置
 web.xml
 注解

① xml版本

创建 MyServletContextListener 如下:

image-20210307085949685

package com.listener;

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

/**
 *
 *   Listener
 *   1. 创建一个类,实现相应接口(6个)
 *
 *   2. web.xml配置/注解配置
 *
 *   举例:
 *       ServletContextListener
         上下文域创建和销毁

         问题:
         tomcat启动,加载项目时创建
         tomcat关闭,销毁项目时消失

         监听器:  ServletContextListener
         1. 创建
         tomcat启动时,创建
         (早于相应的域对象 ServletContext)
         2. 运行
         (监听对应的域对象 ServletContext)
         ServletContext创建的时候, 这个监听器contextInitialized就会执行
         ServletContext销毁的时候, 这个监听器contextDestroyed就会执行

         3. 销毁
         tomcat关闭时,销毁
         (后于相应的域对象 ServletContext)
 *
 * @author Aron.li
 * @date 2021/3/7 8:56
 */
public class MyServletContextListener implements ServletContextListener {
    //当上下文域对象 初始化的时候
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("serlvetContext 创建了");
    }

    //当上下文域对象 销毁的时候
    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("serlvetContext 销毁了");
    }

}

配置 web.xml

image-20210307090205518

<!--  配置监听器      -->
<listener>
    <listener-class>com.listener.MyServletContextListener</listener-class>
</listener>

启动 tomcat,查看监听效果如下:

image-20210307090355698

停止 tomcat,查看监听效果如下:

image-20210307090434422

② 注解版本

@WebListener
public class MyServletContextListener implements ServletContextListener {
    //当上下文域对象 初始化的时候
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println("serlvetContext 创建了");
    }
    //当上下文域对象 销毁的时候
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        System.out.println("serlvetContext 销毁了");
    }
}

监听属性增删改的监听器

import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
import javax.servlet.annotation.WebListener;

/*
*   监听: ServletContext的属性变化
* */
@WebListener
public class MyServletContextAttributeListener implements ServletContextAttributeListener {
    @Override
    public void attributeAdded(ServletContextAttributeEvent servletContextAttributeEvent) {
        System.out.println("ServletContext 属性增加了");
    }

    @Override
    public void attributeRemoved(ServletContextAttributeEvent servletContextAttributeEvent) {
        System.out.println("ServletContext 属性被移除了");
    }

    @Override
    public void attributeReplaced(ServletContextAttributeEvent servletContextAttributeEvent) {
        System.out.println("ServletContext 属性替换了");
    }
}

1.3 案例:模拟spring框架

需求:可以在项目启动时读取配置文件

1.3.1 在 xml 配置全局参数

image-20210307090803792

<?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"
 version="2.5">
    
    <!--全局配置参数-->
    <context-param>
        <param-name>configLocation</param-name>
        <param-value>words.properties</param-value>
    </context-param>
</web-app>

1.3.1 编写监听器读取配置信息

image-20210307091245950

package com.filter;

import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;

/**
 * 监听: ServletContext 的创建
 *
 * @author Aron.li
 * @date 2021/3/7 9:09
 */
@WebListener
public class MySpringListener implements ServletContextListener {

    // ServletContext对象创建就会执行
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        /*
         *   1. 获取servletContext对象
         *       a. 小的域对象可以获取大的域对象(Servlet,Filter)
         *       b. 监听器: xxxEvent 就会 xxx 域对象
         * */
        ServletContext context = sce.getServletContext();
        //2. 读取全局配置参数
        String configLocation = context.getInitParameter("configLocation");
        System.out.println("读取全局配置参数: " + configLocation);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {

    }
}

1.4 案例:统计在线人数

需求

有用户使用网站,在线人数就+1;用户退出网站,在线人数就-1

# 分析:
1. 怎么判定用户在线还是离线?
 a. 在线: 一个用户访问,应该给他生成一个session
  假设第一个访问页面: index.jsp
   (jsp底层: 默认获取session)
  如果有其他页面: html之类的
   (Filter进行拦截: request.getSession();)
 b. 离线: 用户点击退出 , 把对应的session销毁  -> LogoutServlet
   (如果用户直接X掉网页, 服务器默认等待30分钟,才会销毁session -> 时间太长了)
   (长连接: 心跳包, 每隔30秒发一个请求 空包)
   
2. 监听session的创建和销毁
 - > HttpSessionListener
  1. 监听到创建  number + 1
  2. 监听到销毁  number - 1
  
3. number 存在哪里?  
  -> ServletContext 域对象 (全局)
  
4. 何时初始化number? 
  -> ServletContextListener
  1. 监听到创建 : 初始化number,然后存进ServletContext

1.4.1 技术分析

使用 ServletContext域对象 存储在线总人数

使用 ServletContextListener监听器,在项目启动时,初始化总人数为0

使用 HttpSessionListener监听器,用户访问,人数+1,用户退出,人数-1

使用 LogoutServlet控制器,对当前会话的session销毁

1.4.2 需求分析

1587779813977

1.4.3 代码实现

初始化计数的监听器: InitCountServletContextListener

image-20210307091745955

@WebListener
public class InitCountServletContextListener implements ServletContextListener {
    //监听到ServletContext创建就会执行
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        //初始化number,然后存进ServletContext
        int number = 0;
        sce.getServletContext().setAttribute("number",number);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {

    }
}

基于session计数的监听器: CountSessionListener

image-20210307092043325

package com.listener;

import javax.servlet.ServletContext;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

/**
 * @author Aron.li
 * @date 2021/3/7 9:18
 */
@WebListener
public class CountSessionListener implements HttpSessionListener {
    // session创建,就会执行
    @Override
    public void sessionCreated(HttpSessionEvent se) {
        HttpSession session = se.getSession(); // 获取HttpSession域对象

        ServletContext servletContext = session.getServletContext(); // 获取 ServletContext
        int number = (int) servletContext.getAttribute("number"); // 增加number值
        number++;
        servletContext.setAttribute("number", number);

        System.out.println("有人上线了");
    }

    // session销毁,就会执行
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        HttpSession session = se.getSession(); // 获取HttpSession域对象

        ServletContext servletContext = session.getServletContext(); // 获取 ServletContext
        int number = (int) servletContext.getAttribute("number"); // 减少number值
        number--;
        servletContext.setAttribute("number", number);

        System.out.println("有人下线了....");
    }
}

显示在线人数的页面:count.jsp

image-20210307092320206

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>当前在线人数:</h1>
    <div> ${number} 人 </div>

    <a href="LogoutServlet">退出登录</a>
</body>
</html>

用户退出: LogoutServlet

image-20210307092421035

@WebServlet("/LogoutServlet")
public class LogoutServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 销毁session
        request.getSession().invalidate();
        response.getWriter().write("logout");
    }
}

1.4.4 测试效果

启动服务,访问 count.jsp 如下:

image-20210307092509074

多个浏览器访问:

image-20210307092622764

退出登录:

image-20210307092733541