servlet过滤器使用、分析、总结

1,237 阅读8分钟

SpringMVC+自定义过滤器+方式一

1.自定义过滤器

package com.demo.thymeleafstudy.filter;

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

public class FilterTest implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("----FilterTest过滤器初始化----");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        // 对request和response进行一些预处理,比如说对报文的加解密、字符集的变更
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");

        System.out.println("FilterTest执行前!!!");
        // 让目标资源执行,放行
        filterChain.doFilter(request, response);
        System.out.println("RequestFilter执行后!!!");
    }

    @Override
    public void destroy() {
        System.out.println("----FilterTest过滤器销毁----");
    }
}

2.web.xml配置

<!-- 配置过滤器 -->
<filter>
    <filter-name>filterTest</filter-name>
    <filter-class>com.demo.thymeleafstudy.filter.FilterTest</filter-class>
</filter>

<!-- 映射过滤器 -->
<filter-mapping>
    <!-- <filter-mapping>和<filter>标签里面的<filter-name>必须要一致!! -->
    <filter-name>filterTest</filter-name>
    <!-- “/*”表示拦截所有的请求 -->
    <url-pattern>/*</url-pattern>
</filter-mapping>

3.缺点

采用这种方式配置,如果FilterTest类需要一些Sring容器中的一些实例(bean),无法通过Spring直接注入!

SpringMVC+自定义过滤器+方式二(自定义过滤器交给spring管理)

1.自定义过滤器

package com.demo.thymeleafstudy.filter;

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

public class FilterTest implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("----FilterTest过滤器初始化----");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        // 对request和response进行一些预处理,比如说对报文的加解密、字符集的变更
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");

        System.out.println("FilterTest执行前!!!");
        // 让目标资源执行,放行
        filterChain.doFilter(request, response);
        System.out.println("RequestFilter执行后!!!");
    }

    @Override
    public void destroy() {
        System.out.println("----FilterTest过滤器销毁----");
    }
}

2.自定义过滤器注册为bean

<bean name="filterTest" class="com.demo.thymeleafstudy.filter.FilterTest"></bean>

3.web.xml配置

<filter>
    <!-- Spring中配置的bean的name要和这里<filter-name>一样 -->
    <filter-name>filterTest</filter-name>
    <!-- <filter-class>使用DelegatingFilterProxy -->
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <!-- 如果要保留自定义Filter原有的init,destroy方法的调用,需要配置初始化参数targetFilterLifecycle为true,该参数默认为false -->
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
    <!-- 其它的初始化参数 -->
    <init-param>
        <param-name>excludedPages</param-name>
        <param-value>/login</param-value>
    </init-param>
</filter>

<!-- 映射过滤器 -->
<filter-mapping>
    <!-- <filter-mapping>和<filter>标签里面的<filter-name>必须要一致!! -->
    <filter-name>filterTest</filter-name>
    <!-- “/*”表示拦截所有的请求 -->
    <url-pattern>/*</url-pattern>
</filter-mapping>

4.注意

  • 1.<filter-class>使用 org.springframework.web.filter.DelegatingFilterProxy
  • 2.如果自定义filter要使用initdestroy方法,需要配置web.xml中的targetFilterLifecycle参数为true
  • 3.自定义filter要注册为bean

SpringBoot+自定义过滤器+方式一

1.新建一个TimeFilter类,加入注解

@WebFilter(urlPatterns = "/*", filterName = "FilterTest")
public class FilterTest implements Filter {
}

2.在SpringBoot加上如下注解,@ServletComponentScan

@SpringBootApplication
@ServletComponentScan
public class ThymeleafstudyApplication {
}

3.注意:多个自定义filter使用@Order无法指定顺序

经过测试,发现 @Order 注解指定 int 值没有起作用,是无效的。为啥?因为看源码发现 @WebFilter 修饰的过滤器在加载时,没有使用 @Order 注解,而是使用的类名来实现自定义Filter顺序。详情参考这个博客博客

SpringBoot+自定义过滤器+方式二(多个过滤器并且指定顺序)

1.新建一个或者多个自定义过滤器类,不要加任何注解

2.项目中增加一个配置类

@Configuration
public class MyFilterConfig {
    @Bean
    public FilterRegistrationBean<FilterTest> filterTestRegistration() {
        FilterRegistrationBean<FilterTest> registration = new FilterRegistrationBean<FilterTest>();
        registration.setFilter(new FilterTest());
        registration.addUrlPatterns("/*");
        registration.setAsyncSupported(true);
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        registration.setOrder(1);
        return registration;
    }

    @Bean
    public FilterRegistrationBean<AsyncFilterTest> asyncFilterTestRegistration() {
        FilterRegistrationBean<AsyncFilterTest> registration = new FilterRegistrationBean<AsyncFilterTest>();
        registration.setFilter(new AsyncFilterTest());
        registration.addUrlPatterns("/*");
        registration.setAsyncSupported(true);
        registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
        registration.setOrder(2);
        return registration;
    }
}

setDispatcherType属性详解请点我

异步servlet+异步Filter

参考这篇文章点我

拓展

SpringMVC和Servlet区别

  • 1.SpringMVC是框架,或者说SpringMVC封装了Servlet;

  • 2.Servlet可以认为是一套处理网络请求的规范

    比如说我们要实现一个能够响应“Hello Word”的servlet,那么我们最终是实现Servlet这个接口。我们自己写的servlet只有部署到容器中(最常用的是tomcat)才能起作用。tomcat才是与客户端直接打交道的家伙,他监听了端口,请求过来后,根据url等信息,确定要将请求交给哪个servlet去处理,然后调用那个servlet的service方法,service方法返回一个response对象,tomcat再把这个response返回给客户端。参考1参考2

Controller和Servlet区别

SpringMVC中经常看到如下配置

<servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 可以自定义servlet.xml配置文件的位置和名称,默认为WEB-INF目录下,名称为[<servlet-name>]-servlet.xml,如spring-servlet.xml-->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/spring-mvcServlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

Spring MVC是基于servlet的,它有一个DispatherServlet,然后DispatherServlet负责处理请求,并且调用了你的controller。

HTTP请求获取请求内容的方法

在平时开发过程中,在过滤器filter中如果我们想要获取http post请求的报文主要有三种方式,request.getParameter()、request.getInputStream()和request.getReader()。参考资料资料1

  • 1.request.getParameter()

    主要参考资料request.getParameter()源码分析1request.getParameter()源码分析2tomcat的Request源码

    该方法只能用于post请求并且content-type=application/x- www-form-urlencoded。下面看一下request.getParameter()的源码,其具体实现类是org.apache.catalina.connector.Request

        /**
     * @return the value of the specified request parameter, if any; otherwise,
     * return <code>null</code>.  If there is more than one value defined,
     * return only the first one.
     *
     * @param name Name of the desired request parameter
     */
    @Override
    public String getParameter(String name) {
    
        if (!parametersParsed) {
            parseParameters();
        }
    
        return coyoteRequest.getParameters().getParameter(name);
    
    }
    

    parametersParsed默认为false的一个全局变量,针对当前线程parseParameters()方法只会执行一次,下面看下parseParameters的关键代码。

    String contentType = getContentType();
    if (contentType == null) {
        contentType = "";
    }
    int semicolon = contentType.indexOf(';');
    if (semicolon >= 0) {
        contentType = contentType.substring(0, semicolon).trim();
    } else {
        contentType = contentType.trim();
    }
    
    if ("multipart/form-data".equals(contentType)) {
        parseParts(false);
        success = true;
        return;
    }
    
    if( !getConnector().isParseBodyMethod(getMethod()) ) {
        success = true;
        return;
    }
    
    if (!("application/x-www-form-urlencoded".equals(contentType))) {
        success = true;
        return;
    }
    

    1.1当contentType有多种时,只会取第一种。比如contentType为application/json;application/x-www-form-urlencoded,最终是取application/json

    1.2当contentType不为application/x-www-form-urlencoded退出,不在执行下面的关键代码

    1.3当contenttype为multipart/form-data时,parseParts()方法里使用的解析文件的框架是apache自带的fileupload

    1.4当contentType为application/x-www-form-urlencoded时,关键代码

    try {
        if (readPostBody(formData, len) != len) {
            parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
            return;
        }
    }
    

    readPostBody()代码如下

    /**
     * Read post body in an array.
     *
     * @param body The bytes array in which the body will be read
     * @param len The body length
     * @return the bytes count that has been read
     * @throws IOException if an IO exception occurred
     */
    protected int readPostBody(byte[] body, int len) throws IOException {
        int offset = 0;
        do {
            int inputLen = getStream().read(body, offset, len - offset);
            if (inputLen <= 0) {
                return offset;
            }
            offset += inputLen;
        } while ((len - offset) > 0);
        return len;
    }
    

    readPostBody()方法中用到了getStream()方法,很明显这里读了一次流(详细源码解读可以参考这篇博客点我,因为流不能重复读取(流不能回写),所以使用了request.getParameter()方法后再使用request.getinputstream()就无法获取报文了。

    那么同学可能会有这个疑问,那为什么我可以多次使用request.getParameter()方法呢?接着看下面代码parameters.processParameters(formData, 0, formData.length);processParameters调用了同名的重载方法processParameters(bytes, start, len, charset);,这个方法的工作就是将key和value拆分,然后调用java/org/apache/tomcat/util/http/Parameters.javaaddParameter()方法将key和value保存到一个变量名为paramHashValues的map中,所以调用多少次request.getParameter()获取参数都没有问题,因为它已经将请求流中的数据封装到了map中了。

    addParameter()关键代码如下:

    ArrayList<String> values = paramHashValues.get(key);
    if (values == null) {
        values = new ArrayList<>(1);
        paramHashValues.put(key, values);
    }
    values.add(value);
    

    总结: 1.当contentType为application/x-www-form-urlencoded时,request.getParameter()才能获取到参数。为其他contentType时,不执行下面的关键代码,不会读request的输入流;

    2.request.getParameter()可以多次重复使用的原因是此方法将request的输入流数据封装到map中去了,所以可以重复获取;

    3.request.getParameter()、request.getInputStream、request.getReader()三个方法之间是互斥的,原因是流是不能重复读取的。而不仅request.getInputStream会读取request的inputStream,request.getParameter()方法内部也会读取request的inputStream。

  • 2.如何解决request.getInputStream流只能读取一次的问题

    实际开发过程中,需要读去流最多的就是在过滤器和拦截器中。如果多个人开发同一个项目,每个人都写了一个过滤器,都使用了request.getInputStream获取报文显然是不行的。

    网上有很多中解决办法,这里提供一种简单思路,但是具体情况要具体分析,即读取一次流后把报文放到request.setAttribute()中,使用有局限性。

多个filter的初始化顺序

如果定义了多个filter,并且指定了多个filter的执行顺序,但是并不代表多个filter的初始化顺序(filter执行init()方法的顺序)与执行顺序相同

filter开发过程中遇到的坑

  • 1.doFilter之后再设置setHeader无效,具体原因有时间再分析,可暂时参考这篇文章点我