• JavaWeb过滤器(Filter)详解,是时候该把过滤器彻底搞懂了(万字说明)


    注意:这篇文章很长,学习完后将会解决你对于过滤器(Filter)的所有疑惑,下面将通过理论和代码的结合来进行讲解演示…


    目录

    基本介绍

    过滤器原理

    过滤器(Filter)接口

    使用过滤器(Filter)

    创建过滤器(Fliter)

    使用过滤器(Filter)

    配置过滤器(Filter)拦截路径?

    注解方式

    xml方式?

    过滤器(Filter)生命周期

    理论说明?

    代码演示

    FilterConfig和FilterChain说明

    FilterConfig

    FilterConfig实例运用

    FilterChain?

    FilterChain应用实例

    多个Filter的执行顺序

    执行顺序验证

    Filter应用实例(实现敏感词汇过滤)?

    代码实现

    代码验证

    总结?


    基本介绍

    过滤器,顾名思义就是对事物进行过滤的,在Web中的过滤器,当然就是对请求进行过滤,我们使用过滤器,就可以对请求进行拦截,然后做相应的处理,实现许多特殊功能。如登录控制,权限管理,过滤敏感词汇等.

    过滤器原理

    当我们使用过滤器时,过滤器会对游览器的请求进行过滤,过滤器可以动态的分为3个部分,1.放行之前的代码,2.放行,3.放行后的代码,这3个部分分别会发挥不同作用。

    • 第一部分代码会对游览器请求进行第一次过滤,然后继续执行
    • 第二部分代码就是将游览器请求放行,如果还有过滤器,那么就继续交给下一个过滤器
    • 第三部分代码就是对返回的Web资源再次进行过滤处理

    我们使用过滤器,也就是说,不止请求会经过过滤器,我们的响应也会经过过滤器。


    过滤器(Filter)接口

    我们学习过滤器,肯定就要先看一下官方给我们提供的过滤器接口。下面我们使用Idea来查看Filter。

    我们通过官方提供的过滤器可以看出过滤器(Filter)使用起来还是比较简单的,下面我们就来学习如何使用过滤器(Filter)


    使用过滤器(Filter)

    我们使用过滤器肯定要导入相应的jar包才行,Filter就在servlet-api.jar中,我们将该jar包放到WEB-INF下的lib目录下面,然后加入项目。

    创建过滤器(Fliter)

    我们创建Filter,只需要继承Filter接口就行。

    import javax.servlet.*;
    import java.io.IOException;
    
    public class MyFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Filter接口有3个方法,但是只有一个方法没有实现,我们只需要实现这个方法就行。我们可以发现,我们实现了一个doFilter方法,这个方法就是我们写过滤代码的地方,具体逻辑就是和上面介绍的过滤器原理一样的。


    使用过滤器(Filter)

    我们先来感受一下如何使用过滤器,细节我们后面慢慢说明。我们在上面创建的类中写入以下代码,并且加一个WebFIlter注解

    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import java.io.IOException;
    
    @WebFilter("/*")
    public class MyFilter implements Filter {
        
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("对request进行过滤");
            //下面这行代码就是放行
            filterChain.doFilter(servletRequest,servletResponse);
            System.out.println("对response进行过滤");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    我简单介绍下上面的代码,WebFilter(“/*”)表示对所有请求进行过滤,而在doFilter中的放行代码,也就是filterChain.doFilter(servletRequest,servletResponse);这行代码就是对拦截进行放行,细节我们后面讲,现在先怎么理解就行。

    启动服务器,然后我们在游览器中输入http://localhost:8080/filter/abc,注意,filter是我们自己配置的web工程路径,后面的abc随便输入的。我们下面来查看游览器后控制台输出。

    游览器输出

    控制台输出

    现在,我们就已经可以得出两个结论了,过滤器并不会管资源是否存在,而只会对配置的拦截路径进行拦截。拦截不仅会对请求进行拦截,而且还会对相应进行拦截。


    配置过滤器(Filter)拦截路径

    配置Filter的拦截路径有2种方式,一种是注解,一种是xml方式,我们分别进行讲解。

    注解方式

    我们如果使用注解来进行配置,那么我们就需要使用@WebFilter,我们不说废话,直接看该注解的源码。

    里面的配置项还是有很多的,下面我对常用配置项进行说明:

    • filterName:该filter的名字
    • initParams:初始化参数
    • displayName:filter显示名称
    • servletNames:指定对哪些servlet进行过滤
    • asyncSupported:是否支持异步模式
    • urlPatterns:指定拦截路径
    • value:指定拦截路径

    注意:urlPatterns和value是一样的。urlPatterns和value只能配置一个,不能两个都配置,两个都配置就会报错。

    对于使用**@WebFilter**,里面的多个参数用 , 进行分隔。

    说明:如果我们仅仅需要配置一个拦截路径,那么我们可以直接简写@WebLister(“拦截路径”),如@WebFilter(“/*”)就是拦截所有请求。


    xml方式

    xml方式可以说是和Servlet使用xml配置方式一样了,这里就不废话,直接配置一个。

        
            myFilter
            com.clucky.filter.MyFilter
        
        
            myFilter
            /*
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这个就是xml配置方式,只不过把注解换成了xml标签来配置,里面属性都是一样的,这个和Servlet的配置方式基本一样,这里就不再赘述了。


    过滤器(Filter)生命周期

    我们都知道Servlet有一个生命周期,当然Filter也有一个生命周期,下面我们就来探讨一下Filter的生命周期。

    Filter的生命周期和Servlet也十分相似,如果大家对Servlet的生命周期不怎么熟悉,那么可以看一下这篇文章Servlet生命周期

    我们创建一个类,实现Filter的所有方法。

    import javax.servlet.*;
    import java.io.IOException;
    
    public class LifeCycleFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        }
    
        @Override
        public void destroy() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    理论说明

    Filter有3个阶段,分别是初始化,拦截和过滤,销毁。

    1. 初始化阶段:当服务器启动时,我们的服务器(Tomcat)就会读取配置文件,扫描注解,然后来创建我们的Filter。
    2. 拦截和过滤阶段:只要请求资源的路径和拦截的路径相同,那么过滤器就会对请求进行过滤,这个阶段在服务器运行过程中会一直循环。
    3. 销毁阶段:当服务器(Tomcat)关闭时,服务器创建的Filter也会随之销毁。

    代码演示

    Filter的三个阶段就对应着Filter的3个方法,init方法会在Filter创建时调用,doFilter方法会在请求和拦截匹配时调用,destroy方法会在Filter销毁时调用。我们来对这些方法进行编写验证。

    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import java.io.IOException;
    
    @WebFilter("/*")
    public class LifeCycleFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            //这个方法就是初始化方法,在Filter创建时调用
            System.out.println("调用了init()方法");
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            //这个方法就是过滤和拦截的方法,当请求和拦截匹配时调用
            System.out.println("调用了doFilter()方法");
        }
    
        @Override
        public void destroy() {
            //这个方法就是销毁方法,在Filter销毁前调用
            System.out.println("调用了destroy()方法");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    启动服务器控制台输出

    进行拦截时控制台输出

    关闭服务器控制台输出

    都和我们预想的一样,到此,我们就成功验证了Filter的生命周期。


    FilterConfig和FilterChain说明

    FilterConfig和FilterConfig这2个对象是由服务器(Tomcat)在创建和调用Filter对象时所传入的,这2个对象十分有用,FilterConfig对象可以读取我们配置的初始参数,FilterChain可以实现多个Filter之间的连接。


    FilterConfig

    老规矩,我们要学习一个对象,首先查看类图和源代码

    里面的方法就4个,下面我们分别进行讲解

    • getFilterName():获取filter的名称
    • getServletContext():获取ServletContext
    • getInitparamter(String var1):获取配置的初始参数的值
    • getInitParamterNames():获取配置的所有参数名称

    FilterConfig实例运用

    我们在init方法中使用FilterConfig来读取配置的数据库的信息,然后输出。

    java代码

    import javax.servlet.*;
    import java.io.IOException;
    import java.util.Enumeration;
    
    public class MyFilterConfig implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            System.out.println("-----------获取全部key:value------------");
            //得到所有配置参数的名字
            Enumeration names = filterConfig.getInitParameterNames();
            while (names.hasMoreElements()) {
                //得到每一个名字
                String name = names.nextElement();
                System.out.println(name+" = "+filterConfig.getInitParameter(name));
            }
            System.out.println("-----------end.....------------");
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        }
    
        @Override
        public void destroy() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    xml配置

    
            myFilterConfig
            com.clucky.filter.MyFilterConfig
            
                driver
                com.mysql.jdbc.Driver
            
            
                url
                jdbc:mysql://localhost:3306/equip_employ_manage?serverTimezone=GMT
            
            
                username
                root
            
            
                password
                root
            
        
        
            myFilterConfig
            /*
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    启动服务器,控制台输出

    我们使用FilterConfig提供的方法就成功实现了功能,FilterConfig就是用来读取配置文件的。


    FilterChain

    一样,我们还是先来查看源代码以及类图

    我们查看类图,可以发现FilterChain就只有一个方法,其实这个方法就是用来对拦截进行放行的,如果有多个拦截器,那么就会继续调用下一个Filter进行拦截。doFilter方法需要传入个参数,一个是ServletRequest,一个是ServletResponse参数,这个直接传入进行。

    Tomcat在调用过滤器时,默认就会传入Request和Response,这个参数封装了请求和响应,我们直接使用就行。ServletResquest和ServletResponse可以直接强转成HttpServletRequest和HttpServletResponse,然后使用相应的方法。

    将ServletRequest转成HttpServletRequest


    FilterChain应用实例

    我们前面一直都是一个Filter,现在我们来配置2个Filter,通过FilterChain来进行多个过滤。

    第一个Filter

    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import java.io.IOException;
    
    @WebFilter("/*")
    public class Filter01 implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("调用过滤器01对请求进行过滤~~~~");
            //放行,如果还有过滤器,那么就执行下一个过滤器
            filterChain.doFilter(servletRequest,servletResponse);
            System.out.println("调用过滤器01对响应进行过滤~~~~");
        }
    
        @Override
        public void destroy() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    第二个过滤器

    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import java.io.IOException;
    
    @WebFilter("/*")
    public class Filter02 implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("调用过滤器02对请求进行过滤~~~~");
            //放行,如果还有过滤器,那么就执行下一个过滤器
            filterChain.doFilter(servletRequest,servletResponse);
            System.out.println("调用过滤器02对响应进行过滤~~~~");
        }
    
        @Override
        public void destroy() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    启动服务器,然后我们游览器输入http://localhost:8080/filter/abc(filter是我配置的web工程路径)来进行访问,查看控制台输出。

    我们可以看见Filter01先进行过滤,然后交给Filter02,然后访问资源,然后Filter02对响应进行过滤,然后Filter01对响应进行过滤。图示如下:

    我们先使用Filter01对请求进行过滤,那么很自然的,我们就是使用Filter02先对响应进行过滤。


    多个Filter的执行顺序

    上面我们配置了2个过滤器,那么我们怎么知道那个过滤器先执行呢?其实大家可以直接使用代码进行验证,培养独立思考的习惯,这里我就直接给出答案了。

    • 如果我们是在web.xml中配置的过滤器,那么过滤器的执行顺序就是在web配置的顺序,配置在上面那么就会先执行。
    • 如果我们是使用@WebFilter进行配置的,那么执行顺序就是字符比较顺序来执行,例如有2个过滤器,一个是AFilter,一个是BFilter,那么AFilter就会先执行。
    • 如果注解和xml混用,那么在web.xml中配置的会先执行。

    执行顺序验证

    我这里就验证第一条,也就是web.xml中配置的顺序和顺序一样,其他大家感兴趣自己验证。

    xml配置顺序 3->1->2

        
            filter03
            com.clucky.filter.Filter03
        
        
            filter03
            /*
        
        
            filter01
            com.clucky.filter.Filter01
        
        
            filter01
            /*
        
        
            filter02
            com.clucky.filter.Filter02
        
        
            filter02
            /*
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    Filter01

    import javax.servlet.*;
    import java.io.IOException;
    
    public class Filter01 implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("调用过滤器01对请求进行过滤~~~~");
            //放行,如果还有过滤器,那么就执行下一个过滤器
            filterChain.doFilter(servletRequest,servletResponse);
            System.out.println("调用过滤器01对响应进行过滤~~~~");
        }
    
        @Override
        public void destroy() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    Filter02

    import javax.servlet.*;
    import java.io.IOException;
    
    public class Filter02 implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("调用过滤器02对请求进行过滤~~~~");
            //放行,如果还有过滤器,那么就执行下一个过滤器
            filterChain.doFilter(servletRequest,servletResponse);
            System.out.println("调用过滤器02对响应进行过滤~~~~");
        }
    
        @Override
        public void destroy() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    Filter03

    import javax.servlet.*;
    import java.io.IOException;
    
    public class Filter03 implements Filter{
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            System.out.println("调用过滤器03对请求进行过滤~~~~");
            //放行,如果还有过滤器,那么就执行下一个过滤器
            filterChain.doFilter(servletRequest,servletResponse);
            System.out.println("调用过滤器03对响应进行过滤~~~~");
        }
    
        @Override
        public void destroy() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    我们启动服务器,游览器访问,然后查看控制台输出是不是我们配置的3->1->2的顺序

    发现执行顺序果然是这样,另外2个的验证大家感兴趣可以自己验证,我这里就不验证了,如果嫌麻烦,那么记住就行了。


    Filter应用实例(实现敏感词汇过滤)

    我们学了那么多,现在来做一个实例,我们写一个评论页面,可以进行评论,如果评论中含有我们定义的敏感词汇,那么我们就进行过滤,使用**来进行代替。

    代码实现

    jsp页面

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

    输入评论内容

    ${requestScope.get("name")}${requestScope.get("comment")}

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Filter代码

    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.annotation.WebInitParam;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    
    @WebFilter(servletNames = {"comment"},initParams = {@WebInitParam(name = "sensitiveWord", value = "zz")})
    public class CommentFilter implements Filter {
    
        private List sensitiveWords = new ArrayList<>();
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            //得到敏感词汇
            String word = filterConfig.getInitParameter("sensitiveWord");
            //加入集合
            sensitiveWords.add(word);
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            //设置编码
            servletRequest.setCharacterEncoding("utf-8");
            servletResponse.setContentType("text/html;charset=utf-8");
            //得到评论
            String message = servletRequest.getParameter("message");
            for (String sensitiveWord : sensitiveWords) {
                //对所有敏感词汇进行过滤
                if (message.contains(sensitiveWord)){
                    //替换敏感词汇
                    message = message.replace(sensitiveWord, "**");
                }
            }
            //存入request域
            servletRequest.setAttribute("comment",message);
            //放行
            filterChain.doFilter(servletRequest,servletResponse);
        }
    
        @Override
        public void destroy() {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    Servlet代码

    import javax.servlet.*;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.*;
    import java.io.IOException;
    import java.util.HashSet;
    
    @WebServlet(name = "comment",value = "/comment")
    public class CommentServlet extends HttpServlet {
    
        //记录评论敏感词汇的ip
        private HashSet hashSet = new HashSet<>();
    
        @Override
        protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            String message = request.getParameter("message");
            String comment = (String) request.getAttribute("comment");
            if (message.equals(comment)){
                System.out.println("没有敏感词汇.....");
                //设置名字
                request.setAttribute("name","good boy:");
            }else {
                //有敏感词汇,记录IP
                String localAddr = request.getLocalAddr();
                System.out.println(localAddr);
                hashSet.add(localAddr);
                //设置名字
                request.setAttribute("name","bad boy:");
            }
            //转发到comment.jsp页面
            request.getRequestDispatcher("/comment.jsp").forward(request,response);
        }
    
        @Override
        protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            this.doGet(request, response);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    代码验证

    我们输入http://localhost:8080/filter/comment.jsp,来访问jsp页面.

    输入评论内容:你好啊!!!

    下面显示了我们的评论,我们再来输入:你真是个zz

    显示你真是个**,我们的评论过滤就成功了。


    总结

    通过上面的学习,相信大家对Filter的掌握肯定也上了一个台阶,但是理论知识的掌握并不能代码大家已经真正学会了,打开编译器,开始练习吧,技术就就是靠一行行代码堆起来的!!!

    如果觉得讲的还不错,那么就点赞评论支持一下吧

  • 相关阅读:
    【杰理AC696X】外挂收音IC RDA5807的频段设置
    从电大搜题到上海开放大学,广播电视大学引领学习新风尚
    java-net-php-python-jsp罪犯信息管理系统mp4计算机毕业设计程序
    工业物联网:基于数字孪生的车间物流与制造智能同步制造系统
    Linux学习-49-列出进程调用或打开的文件信息(lsof命令)
    Lyx使用bib插入参考文献
    FPGA可以转行数字IC验证吗?
    【Java】基于微服务架构的智慧工地监管云平台源码带APP
    创业思维和商业模式的区别,从0到1:构建到突破,专业新零售全案策划
    理解C语言中的位操作和整数类型:一个温度传感器读取问题的解析
  • 原文地址:https://blog.csdn.net/m0_67390379/article/details/126020107