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

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

JavaWeb的三大组件

组件 作用 实现接口
Servlet 小应用程序,在JavaWeb中主要做为控制器来使用  可以处理用户的请求并且做出响应 javax.servlet.Servlet
Filter 过滤器,对用户发送的请求或响应进行集中处理,实现请求的拦截 javax.servlet.Filter
Listener 监听器,在某些框架中会使用到监听器(比如spring),在Web执行过程中,引发一些事件,对相应事件进行处理 javax.servlet.XxxListener  每个事件有一个接口

一、 概述

生活中的过滤器

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

web中的过滤器

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

应用场景

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

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

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

二、Filter过滤器 – 快速入门

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

29. Filter 过滤器 以及 Listener 监听器
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接口

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
image-20210305000354097

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

2.2 注解配置

2.2.1  编写java类,实现filter接口

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

29. Filter 过滤器 以及 Listener 监听器
image-20210305000516205

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

29. Filter 过滤器 以及 Listener 监听器
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 的效果

29. Filter 过滤器 以及 Listener 监听器
image-20210305000842746

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

2.3 Filter模板设置

2.3.1 设置 Filter 模板

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

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

29. Filter 过滤器 以及 Listener 监听器
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 测试创建模板

29. Filter 过滤器 以及 Listener 监听器
image-20210305001446860
29. Filter 过滤器 以及 Listener 监听器
image-20210305001503238
29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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 ,查看声明周期函数打印如下:

29. Filter 过滤器 以及 Listener 监听器
image-20210305004139979

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

29. Filter 过滤器 以及 Listener 监听器
image-20210305004324089

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

4.2 拦截路径

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

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

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

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

4.2.1 精准匹配的案例

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

29. Filter 过滤器 以及 Listener 监听器
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,专门来进行精确匹配,如下:

29. Filter 过滤器 以及 Listener 监听器
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 查看拦截的情况,如下:

29. Filter 过滤器 以及 Listener 监听器
image-20210305005524070

4.2.2 目录匹配的案例

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

29. Filter 过滤器 以及 Listener 监听器
image-20210305005901515

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

29. Filter 过滤器 以及 Listener 监听器
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() {

    }

}

测试请求如下:

29. Filter 过滤器 以及 Listener 监听器
image-20210305010029599

4.2.3 后缀名匹配的案例

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

29. Filter 过滤器 以及 Listener 监听器
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() {

    }

}

测试如下:

29. Filter 过滤器 以及 Listener 监听器
image-20210305010342602

4.2.4 匹配所有的案例

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

29. Filter 过滤器 以及 Listener 监听器
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 如下:
29. Filter 过滤器 以及 Listener 监听器
image-20210305072234706
  • 再访问 LifeServlet 如下:
29. Filter 过滤器 以及 Listener 监听器
image-20210305072327860
  • 最后再试试 /abc/hello.jsp 如下:
29. Filter 过滤器 以及 Listener 监听器
image-20210305072448207

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

4.2.5 匹配多个路径的案例

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

29. Filter 过滤器 以及 Listener 监听器
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 的请求,如下:
29. Filter 过滤器 以及 Listener 监听器
image-20210305073336152
  • 访问匹配 /LifeServlet 的请求,如下:
29. Filter 过滤器 以及 Listener 监听器
image-20210305073401834

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

4.3 拦截方式

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

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

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

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

4.3.1 xml版本

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

29. Filter 过滤器 以及 Listener 监听器
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 如下:

29. Filter 过滤器 以及 Listener 监听器
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>

测试请求如下:

29. Filter 过滤器 以及 Listener 监听器
image-20210305074512188

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

4.3.2 注解版本

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

  • 拦截器 DispatcherFilter
29. Filter 过滤器 以及 Listener 监听器
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
29. Filter 过滤器 以及 Listener 监听器
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
29. Filter 过滤器 以及 Listener 监听器
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, 看看会不会拦截两次
29. Filter 过滤器 以及 Listener 监听器
image-20210305080138845

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

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

29. Filter 过滤器 以及 Listener 监听器
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, 确认是否拦截请求转发:

29. Filter 过滤器 以及 Listener 监听器
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先执行...
29. Filter 过滤器 以及 Listener 监听器
1592017660642

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

4.4.1 创建访问资源 ServletA

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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配置拦截器

29. Filter 过滤器 以及 Listener 监听器
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,查看拦截器链的执行顺序

29. Filter 过滤器 以及 Listener 监听器
image-20210305225533705

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

五、Filter案例

5.1 统一网站编码

需求

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

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

5.1.1 需求分析

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

5.1.2 中文乱码的问题

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

注册页面

29. Filter 过滤器 以及 Listener 监听器
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>

登录页面

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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);
    }
}

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

输入英文注册:

29. Filter 过滤器 以及 Listener 监听器
image-20210307001123826

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

29. Filter 过滤器 以及 Listener 监听器
image-20210307001225494

输入中文注册:

29. Filter 过滤器 以及 Listener 监听器
image-20210307001302046
29. Filter 过滤器 以及 Listener 监听器
image-20210307001324575

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

29. Filter 过滤器 以及 Listener 监听器
image-20210307001451657
29. Filter 过滤器 以及 Listener 监听器
image-20210307001518275

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

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

  • 真实场景中,过滤器不会统一响应,因为响应的mime类型可能不同(有些返回html页面,有些返回JSON格式字符串)
29. Filter 过滤器 以及 Listener 监听器
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() {

    }

}

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

  • 请求登录
29. Filter 过滤器 以及 Listener 监听器
image-20210307002927068
29. Filter 过滤器 以及 Listener 监听器
image-20210307003002203
  • 请求注册
29. Filter 过滤器 以及 Listener 监听器
image-20210307003025324
29. Filter 过滤器 以及 Listener 监听器
image-20210307003050291

5.2 非法字符拦截

需求

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

5.2.1 需求分析

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

5.2.2 代码实现

非法词库

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

29. Filter 过滤器 以及 Listener 监听器
image-20210307082253457
keyword=傻叉,大爷,二大爷的

注意: properties文件编码问题

29. Filter 过滤器 以及 Listener 监听器
image-20210307082502147

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

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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);
    }
}

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

29. Filter 过滤器 以及 Listener 监听器
image-20210307083724710
29. Filter 过滤器 以及 Listener 监听器
image-20210307083739197

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

非法字符过滤器 WordsFilter

29. Filter 过滤器 以及 Listener 监听器
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() {

    }

}

再次测试如下:

29. Filter 过滤器 以及 Listener 监听器
image-20210307084545234
29. Filter 过滤器 以及 Listener 监听器
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 如下:

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
image-20210307090205518
<!--  配置监听器      -->
<listener>
    <listener-class>com.listener.MyServletContextListener</listener-class>
</listener>

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

29. Filter 过滤器 以及 Listener 监听器
image-20210307090355698

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

29. Filter 过滤器 以及 Listener 监听器
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 配置全局参数

29. Filter 过滤器 以及 Listener 监听器
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 编写监听器读取配置信息

29. Filter 过滤器 以及 Listener 监听器
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 需求分析

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

1.4.3 代码实现

初始化计数的监听器: InitCountServletContextListener

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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

29. Filter 过滤器 以及 Listener 监听器
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 如下:

29. Filter 过滤器 以及 Listener 监听器
image-20210307092509074

多个浏览器访问:

29. Filter 过滤器 以及 Listener 监听器
image-20210307092622764

退出登录:

29. Filter 过滤器 以及 Listener 监听器
image-20210307092733541


原文始发于微信公众号(海洋的渔夫):29. Filter 过滤器 以及 Listener 监听器

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/34093.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!