【走近Spring】从OncePerRequestFilter去了解Spring内置的Filter的特别之处以及常见过滤器使用介绍

先上图:

在这里插入图片描述

从截图上可以看到我标红的我们比较熟悉,配置过的一些Filter,他们都继承自OncePerRequestFilter。

该Filter从字面上理解:只执行一次的Filter。可能有人会问了,我们自己写的Filter不都只执行一次吗?为何Spring还要专门提供这么一个类来处理呢?

在Spring中,Filter默认继承OncePerRequestFilter

关于OncePerRequestFilter

OncePerRequestFilter:顾名思义,它能够确保在一次请求中只通过一次filter,而不需要重复的执行。大家常识上都认为,一次请求本来就只filter一次,为什么还要由此特别限定呢。

往往我们的常识和实际的实现并不真的一样,经过一番资料的查阅,此方法是为了兼容不同的web container,也就是说并不是所有的container都是我们期望的只过滤一次,servlet版本不同,执行过程也不同,我们可以看看Spring的javadoc怎么说:

 *
 * <p>As of Servlet 3.0, a filter may be invoked as part of a
 * {@link javax.servlet.DispatcherType#REQUEST REQUEST} or
 * {@link javax.servlet.DispatcherType#ASYNC ASYNC} dispatches that occur in
 * separate threads. A filter can be configured in {@code web.xml} whether it
 * should be involved in async dispatches. However, in some cases servlet
 * containers assume different default configuration. 

简单的说就是去适配了不同的web容器,以及对异步请求,也只过滤一次的需求。另外打个比方:如:servlet2.3与servlet2.4也有一定差异:

在servlet2.3中,Filter会经过一切请求,包括服务器内部使用的forward转发请求和<%@ include file=”/login.jsp”%>的情况
servlet2.4中的Filter默认情况下只过滤外部提交的请求,forward和include这些内部转发都不会被过滤。

因此建议:若是在Spring环境下使用Filter的话,还是继承OncePerRequestFilter吧,而不是直接实现Filter接口,这是一个比较稳妥的选择。

需要注意的是:

@Override
public final void init(FilterConfig filterConfig) throws ServletException {}

它final掉了init方法,因此若我们继承它,无法使用init方法了。但我们可以复写initFilterBean这个方法,实现我们比init方法更强大的一些逻辑,可以直接使用容器对象了,如下:~

@Component("helloFilter")
public class HelloFilter extends OncePerRequestFilter {

    @Override
    protected void initFilterBean() throws ServletException {
        System.out.println("Filter初始化...");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        FilterConfig filterConfig = super.getFilterConfig();
        ServletContext servletContext = super.getServletContext();
        Environment environment = super.getEnvironment();

        filterChain.doFilter(request, response);
    }
}

需要注意的是,这个方法在初始化的时候,会被执行两次。虽然没什么影响,但个人认为这是Spring的Bug吧,执行两次的地方在GenericFilterBean这个类:

@Override
public void afterPropertiesSet() throws ServletException {
	initFilterBean();
}

以及它的init方法:

@Override
public final void init(FilterConfig filterConfig) throws ServletException {
	...
	// Let subclasses do whatever initialization they like.
	initFilterBean();
	...
}

但是,继承自OncePerRequestFilter的Filter采用@WebFilter以及Spring Bean的方式都是可以的。若采用@WebFilter,则initFilterBean方法就只会被执行一次,但是此时@Autowired自动注入就不好使了,需要自己去容器里拿:

super.getServletContext();

所以看自己需求,哪个方便选哪个。(比如我的Filter只需要记录一下请求日志,没必要注入Spring的Bean,那么我觉得知己@WebFilter就更方便了)

OncePerRequestFilter源码解读

/**
* 过滤器基类,旨在确保每个请求调度在任何servlet容器上执行一次执行。 
* 它提供了一个带有HttpServletRequest和HttpServletResponse参数的{@link #doFilterInternal}方法。
*/
public abstract class OncePerRequestFilter extends GenericFilterBean { ... }

我们知道filter最主要的是doFilter方法:

// 已过滤过的过滤器的固定后缀名
public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";

// 相当于生成已过滤的过滤器的名字(全局唯一的)
protected String getAlreadyFilteredAttributeName() {
	String name = getFilterName();
	if (name == null) {
		name = getClass().getName();
	}
	return name + ALREADY_FILTERED_SUFFIX;
}

//判断该请求是否是异步请求(Servlet 3.0后有异步请求,Spring MVC3.2开始)
protected boolean isAsyncDispatch(HttpServletRequest request) {
	return WebAsyncUtils.getAsyncManager(request).hasConcurrentResult();
}

//是否需要不过滤异步的请求(默认是不多次过滤异步请求的)
//javadoc:javax.servlet.DispatcherType.ASYNC的请求方式意味着可能在一个请求里这个过滤器会被多个不同线程调用多次,而这里返回true,就能保证只会被调用一次
protected boolean shouldNotFilterAsyncDispatch() {
	return true;
}

//原理基本同上
protected boolean shouldNotFilterErrorDispatch() {
	return true;
}

//可以人工直接返回true  那这个请求就肯定不会被过滤了~~~~
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
	return false;
}

//这里很清楚的记录着  需要被跳过的请求们,这种请求直接就放行了
private boolean skipDispatch(HttpServletRequest request) {
	if (isAsyncDispatch(request) && shouldNotFilterAsyncDispatch()) {
		return true;
	}
	if (request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null && shouldNotFilterErrorDispatch()) {
		return true;
	}
	return false;
}

介绍了上面这些基础方法,那么问题接下来我们就可以看doFilter方法了:

@Override
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
		throws ServletException, IOException {
	
	//只处理http请求
	if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
		throw new ServletException("OncePerRequestFilter just supports HTTP requests");
	}
	HttpServletRequest httpRequest = (HttpServletRequest) request;
	HttpServletResponse httpResponse = (HttpServletResponse) response;

	//判断这个请求是否需要执行过滤
	String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
	boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;

	if (hasAlreadyFilteredAttribute || skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {

		// 直接放行,不执行此过滤器的过滤操作
		filterChain.doFilter(request, response);
	} else {
		// 执行过滤,并且向请求域设置一个值,key就是生成的全局唯一的·alreadyFilteredAttributeName·
		request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
		try {
			//由子类自己去实现拦截的逻辑  注意 自己写时,filterChain.doFilter(request, response);这句代码不要忘了
			doFilterInternal(httpRequest, httpResponse, filterChain);
		}
		finally {
			// Remove the "already filtered" request attribute for this request.
			request.removeAttribute(alreadyFilteredAttributeName);
		}
	}
}

最后有必要提一下,父类中提供的一个方法,可以获取到该Filter的名字:

// 如果在Spring应用程序上下文中初始化为bean,那么它将返回到bean工厂中定义的bean名称。
// 需要注意的是,如果是以bean的形式加入了。(比如Boot环境下),此时FilterConfig还未null的,所以有这个判断
@Nullable
protected String getFilterName() {
	return (this.filterConfig != null ? this.filterConfig.getFilterName() : this.beanName);
}

Spring内置OncePerRequestFilter实现

开始我们已经截图了,Spring内部了好些个该Filter的实现,我们只需要轻松配置一下即可使用了。下面介绍几个最常用的:

CharacterEncodingFilter

这个类是专门来解决body编码(乱码问题的),在我们还是web.xml时代的时候,大家肯定都熟悉这样的配置:

<!-- characterEncodingFilter字符编码过滤器 -->
<filter>
	<filter-name>characterEncodingFilter</filter-name>
	<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
	<init-param>
		<!--要使用的字符集,一般我们使用UTF-8(保险起见UTF-8最好)-->
		<param-name>encoding</param-name>
		<param-value>UTF-8</param-value>
	</init-param>
	<init-param>
		<!--是否强制设置request的编码为encoding,默认false,不建议更改-->
		<param-name>forceRequestEncoding</param-name>
		<param-value>false</param-value>
	</init-param>
	<init-param>
		<!--是否强制设置response的编码为encoding,建议设置为true,下面有关于这个参数的解释-->
		<param-name>forceResponseEncoding</param-name>
		<param-value>true</param-value>
	</init-param>
</filter>
<filter-mapping>
	<filter-name>characterEncodingFilter</filter-name>
	<!--这里不能留空或者直接写 ' / ' ,否者不起作用-->
	<url-pattern>/*</url-pattern>
</filter-mapping>

只需要这么处理一下,我们就不再需要处理body体里面的编码问题了。

它的doFilterInternal方法的实现也比较简单。

HiddenHttpMethodFilter

浏览器form表单只支持GET与POST请求,而DELETE、PUT等method并不支持,spring3.0添加了这个过滤器,可以让我们的form表达拥有发任何标准的http请求的能力了。

在ajax rest编程风格大行其道的今天,可能这个使用场景比较少了。

HttpPutFormContentFilter

有些人可能遇到过,用ajax发送一个put请求给后台的Spring MVC,发现request.getParameter()的时候拿不到值,很是纳闷。

其实,是因为对于表单提交,tomcat默认只解析POST的表单,对于PUT和DELETE的不处理,所以Spring拿不到。

解决办法一:修改tomcat的server.xml(极其不推荐)

<Connector port="8080" protocol="HTTP/1.1" 
           connectionTimeout="20000"
           redirectPort="8443"
           parseBodyMethods="POST,PUT,DELETE"
           URIEncoding="UTF-8" />

解决办法二:使用Spring提供的HttpPutFormContentFilter

<filter>
	<filter-name>httpPutFormContentFilter</filter-name>
	<filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>
<filter-mapping>
	<filter-name>httpPutFormContentFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

FormContentFilter:处理PUT请求等的请求参数

它是一个新贵。Spring 5.1后才推出。该过滤器针对DELETE,PUT和PATCH这三种HTTP method分析其FORM表单参数,将其暴露为Servlet请求参数。

缺省情况下,Servlet规范仅针对HTTP POST做这样的要求。

因为FormContentFilter依赖的是Spring MVC的消息转换器:FormHttpMessageConverter,所以它支持的MediaType也必须只能是application/x-www-form-urlencoded的。

从FormContentFilter的效果也能想到,它肯定会调用request.getInputStream();,所以后续我们不能再使用getInputStream()了。另外它要想getParameter系列方法有效果,必须包装一下request。它用的是自己的静态内部类:

private static class FormContentRequestWrapper extends HttpServletRequestWrapper { ... }

RequestContextFilter

这个过滤器有点意思,字面理解:请求上下文过滤器。

作用:让你在一个请求的线程内,任意地方都可以获取到请求参数的相关信息,非常的方便。

这里面有两个Spring里面非常重要的类:

  • org.springframework.context.i18n.LocaleContextHolder
  • org.springframework.web.context.request.RequestContextHolder

这样当前请求随后的处理过程中,就可以在当前线程中获取的当前请求的信息,而无需把请求对象作为参数到处传递 。

这里注意一个概念,缺省情况下,Servlet容器对一个请求的整个处理过程,是由同一个线程完成的,中途不会切换线程。但这个线程在处理完一个请求后,会被放回到线程池用于处理其他请求。

Spring还有两个类,也做了同样的事情,也会达到此种效果:RequestContextListener和DispatcherServlet。因此可以看出,如果我们已经配置了DispatcherServlet是正常的Spring MVC环境,是没必要在配置此Filter的。

DispatcherServlet中往当前线程中设置请求的逻辑已经足够了,但是在一个Web应用中,并不是所有的请求都最终会被DispatcherServlet处理,比如匿名用户访问一个登录用户才能访问的资源,此时请求只会被安全过滤器处理,而不会到达DispatcherServlet,在这种情况下,该过滤器RequestContextFilter就起了担当了相应的职责。

Springboot 提供了一个OrderedRequestContextFilter继承自RequestContextFilter应用在基于Springboot的Servlet Web应用中。OrderedRequestContextFilter在RequestContextFilter的功能上仅仅增加了接口OrderedFilter定义的过滤器顺序,并且缺省使用优先级(-105)。在整个Servlet过滤器链中,过滤器的顺序数字越小,表示越先被调用。

MultipartFilter

和文件上传有关。当我们需要自定义文件上传解析器的时候,需要用到它来切换。

Spring内置了两个上传处理器:

在这里插入图片描述

CommonsMultipartResolver:使用commons Fileupload来处理multipart请求,使用时需导入jar包。

StandardServletMultipartResolver:是基于Servlet3.0来处理multipart请求的,所以不需要引用其他jar包,但是必须使用支持Servlet3.0的容器。

CorsFilter

跨域相关的过滤器。

跨域:当一个资源从与该资源本身所在的服务器不同的域或端口不同的域或不同的端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

出于安全考虑,浏览器会限制从脚本内发起的跨域HTTP请求。跨域资源共享机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。浏览器支持在 API 容器中使用 CORS,以降低跨域 HTTP 请求所带来的风险。

针对于JAVA开发而言,为了更好的做业务分层,经常会将前后端代码分离开来,发布在不同的服务器上,此时,便会遇到跨域的问题。

若在Spring MVC环境,解决此问题就更简单了~

AbstractRequestLoggingFilter

记录请求日志的一个过滤器,非常好用有木有。Spring默认给我们提供两个实现:

CommonsRequestLoggingFilter:

@Override
protected boolean shouldLog(HttpServletRequest request) {
	return logger.isDebugEnabled();
}
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
	logger.debug(message);
}
@Override
protected void afterRequest(HttpServletRequest request, String message) {
	logger.debug(message);
}

它调用初始化时候设置的GenericFilterBean中的logger进行记录,并且默认记录debug级别日志。

ServletContextRequestLoggingFilter:

@Override
protected void beforeRequest(HttpServletRequest request, String message) {
	getServletContext().log(message);
}

@Override
protected void afterRequest(HttpServletRequest request, String message) {
	getServletContext().log(message);
}

使用ServletContext来记录日志,【不会输出到控制台,ServletContext.log()日志输出tomcat的目录下,具体位置和tomcat的配置有关】

内置的两个实现,我们一般都用不着,此处主要是我们自己去实现的意义非常大。

参数如下:

public static final String DEFAULT_BEFORE_MESSAGE_PREFIX = "Before request [";
public static final String DEFAULT_BEFORE_MESSAGE_SUFFIX = "]";
public static final String DEFAULT_AFTER_MESSAGE_PREFIX = "After request [";
public static final String DEFAULT_AFTER_MESSAGE_SUFFIX = "]";
// 默认body体里只会打印出50个字符,自己可以自定义修改
private static final int DEFAULT_MAX_PAYLOAD_LENGTH = 50;

//true表示包含查询参数  形如:[  uri=xxx?a=xx&b=xxx  ]
private boolean includeQueryString = false;
//true表示包含客户端相关信息
private boolean includeClientInfo = false;
//true表示包含请求头信息
private boolean includeHeaders = false;
//true表示包含body体性息
private boolean includePayload = false;

一些方法我们可以复写,来改变一些默认行为(大部分情况下都不需要复写~)

public void setMaxPayloadLength(int maxPayloadLength) {
	Assert.isTrue(maxPayloadLength >= 0, "'maxPayloadLength' should be larger than or equal to 0");
	this.maxPayloadLength = maxPayloadLength;
}

//自定义请求前、后的前缀、后缀等等  这个其实是建议定制的  个性化点比较好
public void setBeforeMessagePrefix(String beforeMessagePrefix) {
	this.beforeMessagePrefix = beforeMessagePrefix;
}

//是否过滤异步的请求  这里false表示要过滤
@Override
protected boolean shouldNotFilterAsyncDispatch() {
	return false;
}

//生成日志消息的方法  这个强烈不建议调用者自己去做(当然,你要个性化,你随意)  getMessagePayload
protected String createMessage(HttpServletRequest request, String prefix, String suffix) { ... }

//这个非常的重要,判断哪些请求输出日志,哪些不需要输出。比如get请求,我们一般都不需要输出日志的 (至于能够实现标注指定注解接口采取输出日志呢?这个就做不到了,因为还没有到方法呢,没有办法拿到方法元信息)  但是若通过HandlerInterceptor来拦截,是可以处理方法注解的
protected boolean shouldLog(HttpServletRequest request) {
	return true;
}

//在系统默认的message的基础上,你自己再去个性化吧。或者不用此message变量都成~  比如自己可以计算出请求耗时~
protected abstract void beforeRequest(HttpServletRequest request, String message);
protected abstract void afterRequest(HttpServletRequest request, String message);

下面书写一个我们最实用的例子,也是微服务项目中的一个例子,此处贴出来供以参考:

@ResponseBody
@PostMapping(value = "/hello")
public String helloPost(@RequestBody Map<String, Object> object) {
	//System.out.println(object);
	return "hello...Post";
}

get请求,不输出日志,来一个put请求如下:

在这里插入图片描述

日志输出如下:

日志输出里有个小细节,我们看到请求开始里没有payload,输出的时候却有了,怎么回事呢?原因其实是Spring提供的ContentCachingRequestWrapper具有懒加载的特性,所以才会出现这种现象。我个人觉得没什么影响,payload输出一遍反倒是我最希望的效果。

来看个Filter例子:

/**
 * 打印请求日志,默认只打印POST、PUT、DELETE方法的请求日志哦~
 * 会计算出请求耗时、client请求的ip地址等等  contentType也会记录打印出来 信息比较全 方便查找问题~
 *
 */
@Slf4j
@WebFilter(urlPatterns = "/*")
public class RequestLogFilter extends AbstractRequestLoggingFilter {

    //这些配置都可以在init-param中进行设置,但是基于注解的,这里就不要这么麻烦了,统一在初始化的时候设置值吧
    //private boolean includeQueryString = false;
    //private boolean includeClientInfo = false;
    //private boolean includeHeaders = false;
    //private boolean includePayload = false;
    private static final String PROCESS_START_TIME_SUFFIX = ".PROCESS_START_TIME";

    @Override
    protected void initFilterBean() throws ServletException {
        super.setIncludeQueryString(true);
        super.setIncludeClientInfo(true);
        //因为headers里面的信息太多了 所以这里不输出了,否则过于干扰,只手动把content-type等关键信息输出即可
        super.setIncludeHeaders(false);
        super.setIncludePayload(true);
        super.setMaxPayloadLength(1000); //最大支持到1000个字符

        //头信息
        super.setBeforeMessagePrefix("请求开始 [");
        super.setBeforeMessageSuffix("]");
        super.setAfterMessagePrefix("请求结束 [");
        super.setAfterMessageSuffix("]");

    }

    @Override
    protected boolean shouldLog(HttpServletRequest request) {
        String method = request.getMethod();
        return HttpMethod.POST.matches(method) || HttpMethod.PUT.matches(method) || HttpMethod.DELETE.matches(method);
    }

    @Override
    protected void beforeRequest(HttpServletRequest request, String message) {
        logger.info(calcRequestTime(request)
                .concat(getConfigTypeLog(request))
                .concat(getThreadId())
                .concat(message));
    }

    @Override
    protected void afterRequest(HttpServletRequest request, String message) {
        logger.info(calcRequestTime(request)
                .concat(getConfigTypeLog(request))
                .concat(getThreadId())
                .concat(message));
    }

    //拼装contentType
    private String getConfigTypeLog(HttpServletRequest request) {
        String contentType = request.getContentType();
        String method = request.getMethod();
        return "[contentType=" + contentType + "] " + method.toUpperCase() + " ";
    }

    //计算请求耗时
    private String calcRequestTime(HttpServletRequest request) {
        long mills = 0;
        String requestTimeUniqueName = getRequestTimeUniqueName();

        Object processStartTime = request.getAttribute(requestTimeUniqueName);
        if (processStartTime == null) { //首次 放置值
            request.setAttribute(requestTimeUniqueName, Instant.now());
        } else { //请求结束的处理  开始计算吧
            Instant start = (Instant) processStartTime;
            Instant now = Instant.now();
            mills = Duration.between(start, now).toMillis();
			
            request.removeAttribute(requestTimeUniqueName);
        }
        return mills == 0 ? "" : ("[耗时:" + mills + "ms] ");

    }

    private String getRequestTimeUniqueName() {
        return this.getClass().getName().concat(PROCESS_START_TIME_SUFFIX);
    }

    private String getThreadId() {
        return "[ThreadId:" + Thread.currentThread().getId() + "] ";
    }

}

 若你结束请求时还想输出response里面的的内容。比如状态码,返回body里面的内容等等(其实我倒觉得还挺重要的),自己可以去加工实现。

 这里比较坑爹的是,Spring提供给我们复写的两个方法,都没返回给我们response,所以若有需要,各位小伙伴自己实现哈。

此请求日志Filter一般放在TokenFilter后面执行(如果业务特殊,放在前面执行也可)

 拦截器(Interceptor)是基于Java的反射机制,而过滤器(Filter)是基于函数回调。

参考链接:

【小家Spring】从OncePerRequestFilter的源码解读去了解Spring内置的Filter的特别之处以及常见过滤器使用介绍

 

posted @ 2021-12-10 20:15  残城碎梦  阅读(2983)  评论(1编辑  收藏  举报