`

SpringMVC源码总结(五)Tomcat的URIEncoding、useBodyEncodingForURI和CharacterEncodingFilter

阅读更多
继续上一章节的乱码问题。上一篇文章仅仅说了设置Tomcat的URIEncoding可以解决乱码问题,这篇文章便会讲述这一背后的内容。首先说明下,光看是没用的,要多实验实验。

目前我的tomcat版本为:7.0.55,spring所有文章的版本始终为4.0.5

本文章会从tomcat的源码角度来解析Tomcat的两个参数设置URIEncoding和useBodyEncodingForURI。

对于一个请求,常用的有两种编码方式,如下:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<title></title>
	</head>
	<body>
		<form action="http://127.0.0.1:8080/string?name=中国" method="post">
			<input type="text" name="user" value="张三"/>
			<input type="submit" value="提交"/>
		</form>
	</body>
</html>

首先说说结论:
上述请求有两处含有中文,一处是请求参数中,即?name='中国',另一处是请求体中,即user='张三'。对于这两处tomcat7是分两种编码方式的。URIEncoding就是针对请求参数的编码设置的,而filter的request.setCharacterEncoding('UTF-8')或者请求header中的content-type中的编码都是针对请求体的。不要把他们搞混了。

useBodyEncodingForURI=true是说,请求参数的编码方式要采用请求体的编码方式。当useBodyEncodingForURI=true时,若请求体采用utf-8解析,则请求参数也要采用utf-8来解析。这两个属性值的设置在tomcat的conf/server.xml文件中配置,如下:

 <Service name="Catalina">

    <!--The connectors can use a shared executor, you can define one or more named thread pools-->
    <!--
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="150" minSpareThreads="4"/>
    -->


    <!-- A "Connector" represents an endpoint by which requests are received
         and responses are returned. Documentation at :
         Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)
         Java AJP  Connector: /docs/config/ajp.html
         APR (HTTP/AJP) Connector: /docs/apr.html
         Define a non-SSL HTTP/1.1 Connector on port 8080
    -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" useBodyEncodingForURI='true' URIEncoding='UTF-8' />
    <!-- A "Connector" using the shared thread pool-->

这样写只是说明这两者的配置位置,并不是两个属性要同时配置,不要理解错了。
继续说说CharacterEncodingFilter的作用。
使用方式,将如下代码加入web.xml文件中:

<filter>
		<filter-name>encoding</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>

	<filter-mapping>
		<filter-name>encoding</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

作用是,当forceEncoding为false的前提下(默认为false),当request没有指定content-type或content-type不含编码时,该filter将会为这个request设定请求体的编码为filter的encoding值。
当forceEncoding为true的前提下,就会为request的请求体和response都设定为这个filter的encoding值。
CharacterEncodingFilter源码如下:

public class CharacterEncodingFilter extends OncePerRequestFilter {

	private String encoding;

	private boolean forceEncoding = false;


	/**
	 * Set the encoding to use for requests. This encoding will be passed into a
	 * {@link javax.servlet.http.HttpServletRequest#setCharacterEncoding} call.
	 * <p>Whether this encoding will override existing request encodings
	 * (and whether it will be applied as default response encoding as well)
	 * depends on the {@link #setForceEncoding "forceEncoding"} flag.
	 */
	public void setEncoding(String encoding) {
		this.encoding = encoding;
	}

	/**
	 * Set whether the configured {@link #setEncoding encoding} of this filter
	 * is supposed to override existing request and response encodings.
	 * <p>Default is "false", i.e. do not modify the encoding if
	 * {@link javax.servlet.http.HttpServletRequest#getCharacterEncoding()}
	 * returns a non-null value. Switch this to "true" to enforce the specified
	 * encoding in any case, applying it as default response encoding as well.
	 * <p>Note that the response encoding will only be set on Servlet 2.4+
	 * containers, since Servlet 2.3 did not provide a facility for setting
	 * a default response encoding.
	 */
	public void setForceEncoding(boolean forceEncoding) {
		this.forceEncoding = forceEncoding;
	}


	@Override
	protected void doFilterInternal(
			HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		if (this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) {
			request.setCharacterEncoding(this.encoding);
			if (this.forceEncoding) {
				response.setCharacterEncoding(this.encoding);
			}
		}
		filterChain.doFilter(request, response);
	}

}

这个filter有两个属性,encoding和forceEncoding,我们可以在web.xml文件中来设定这两个属性值。
每次request请求到来执行方法doFilterInternal,首先调用request.getCharacterEncoding(),本质就是从请求header content-type中获取编码值,如果没有,则调用request.setCharacterEncoding(this.encoding)将该filter的encoding值设置为请求体的编码方式,记住该编码方式只对请求体,不针对请求参数。当forceEncoding=true时,不管请求header content-type有没有编码方式,始终将该filter的encoding值设置到request和response中,同样只针对request的请求体。

以上的结论说完了,下面就要看看源代码了。不想看的就算了不影响使用,想看看原理的请继续:

首先是三个名词:
org.apache.coyote.Request:这是一个最底层的request,包含各种参数信息。暂且称为coyoteRequest。
org.apache.catalina.connector.Request:实现了HttpServletRequest接口,称它为request,同时包含了一个coyoteRequest,一个connector,待会你就会发现这个connector的编码传递作用。
org.apache.catalina.connector.RequestFacade:同样实现了HttpServletRequest接口,它仅仅是一个装饰类,称它为requestFacade,构造函数为:

/**
     * Construct a wrapper for the specified request.
     *
     * @param request The request to be wrapped
     */
    public RequestFacade(Request request) {

        this.request = request;

    }

该构造函数将一个org.apache.catalina.connector.Request传进来,requestFacade的工作全是靠它内部的org.apache.catalina.connector.Request来完成的,org.apache.catalina.connector.Request又是依据它所包含的org.apache.coyote.Request这个最底层的类来完成的。通过org.apache.catalina.connector.Request,我们可以设定org.apache.coyote.Request的一些工作方式,如通过什么编码来解析数据。

org.apache.coyote.Request含有的属性:
String charEncoding:针对请求体的编码(在第一次解析参数时会传递给Parameters的encoding)
Parameters :用于处理和存放请求参数和请求体参数的类
            (1)含String encoding:针对请求体的编码
            (2)含String queryStringEncoding:针对请求参数的编码
            (3)含Map<String,ArrayList<String>> paramHashValues:存放解析后的参数
Parameters的两个编码是最最重要的编码,直接参与解析数据的编码,不像其他对象的编码大部分都是起传递作用,最终作用到Parameters的两个编码上


public class MyCharacterEncodingFilter extends CharacterEncodingFilter{

	@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		request.setCharacterEncoding("UTF-8");
		String name=request.getParameter("user");
		System.out.println(name);
		request.setCharacterEncoding("UTF-8");
		String name1=request.getParameter("user");
		System.out.println(name1);
		super.doFilterInternal(request, response, filterChain);
	}
}

传给过滤器filter的HttpServletRequest request其实是org.apache.catalina.connector.RequestFacade类型的,我们看下获取参数的具体过程:
requestFacade.getParameter("user")会传递到org.apache.catalina.connector.Request的相应方法,如下:

public String getParameter(String name) {

        if (!parametersParsed) {
            parseParameters();
        }

        return coyoteRequest.getParameters().getParameter(name);

    }

parametersParsed是org.apache.catalina.connector.Request的属性,用于标示是否已经解析过参数,如果解析过,便不再解析,直接从coyoteRequest的Parameters参数中取出。所以当已经解析过后,你再去设置编码,会无效的,因为它会直接返回第一次的解析结果。并且解析过程仅仅发生在第一次获取参数的时候。
我们来看下parseParameters()这个解析参数的过程:

/**
     * Parse request parameters.
     */
    protected void parseParameters() {

        //解析发生后,便将是状态置为已解析
        parametersParsed = true;

        Parameters parameters = coyoteRequest.getParameters();
        boolean success = false;
        try {
            // Set this every time in case limit has been changed via JMX
            parameters.setLimit(getConnector().getMaxParameterCount());

            // getCharacterEncoding() may have been overridden to search for
            // hidden form field containing request encoding
            //重点1
            String enc = getCharacterEncoding();
            //重点2
            boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
            if (enc != null) {
                parameters.setEncoding(enc);
                if (useBodyEncodingForURI) {
                    parameters.setQueryStringEncoding(enc);
                }
            } else {
                parameters.setEncoding
                    (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
                if (useBodyEncodingForURI) {
                    parameters.setQueryStringEncoding
                        (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
                }
            }
            //重点3
            parameters.handleQueryParameters();

            if (usingInputStream || usingReader) {
                success = true;
                return;
            }

            if( !getConnector().isParseBodyMethod(getMethod()) ) {
                success = true;
                return;
            }

            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();
                success = true;
                return;
            }

            if (!("application/x-www-form-urlencoded".equals(contentType))) {
                success = true;
                return;
            }

            int len = getContentLength();

            if (len > 0) {
                int maxPostSize = connector.getMaxPostSize();
                if ((maxPostSize > 0) && (len > maxPostSize)) {
                    if (context.getLogger().isDebugEnabled()) {
                        context.getLogger().debug(
                                sm.getString("coyoteRequest.postTooLarge"));
                    }
                    checkSwallowInput();
                    return;
                }
                byte[] formData = null;
                if (len < CACHED_POST_LEN) {
                    if (postData == null) {
                        postData = new byte[CACHED_POST_LEN];
                    }
                    formData = postData;
                } else {
                    formData = new byte[len];
                }
                try {
                    if (readPostBody(formData, len) != len) {
                        return;
                    }
                } catch (IOException e) {
                    // Client disconnect
                    if (context.getLogger().isDebugEnabled()) {
                        context.getLogger().debug(
                                sm.getString("coyoteRequest.parseParameters"), e);
                    }
                    return;
                }
               //重点4
                parameters.processParameters(formData, 0, len);
            } else if ("chunked".equalsIgnoreCase(
                    coyoteRequest.getHeader("transfer-encoding"))) {
                byte[] formData = null;
                try {
                    formData = readChunkedPostBody();
                } catch (IOException e) {
                    // Client disconnect or chunkedPostTooLarge error
                    if (context.getLogger().isDebugEnabled()) {
                        context.getLogger().debug(
                                sm.getString("coyoteRequest.parseParameters"), e);
                    }
                    return;
                }
                if (formData != null) {
                    parameters.processParameters(formData, 0, formData.length);
                }
            }
            success = true;
        } finally {
            if (!success) {
                parameters.setParseFailed(true);
            }
        }

    }


上面有四处我们需要关注的重点。

重点1:getCharacterEncoding()其实是通过底层的coyoteRequest来获取header content-type中的编码。
如下:

/**
     * Return the character encoding for this Request.
     */
    @Override
    public String getCharacterEncoding() {
      return coyoteRequest.getCharacterEncoding();
    }

 public String getCharacterEncoding() {

        if (charEncoding != null)
            return charEncoding;

        charEncoding = ContentType.getCharsetFromContentType(getContentType());
        return charEncoding;

    }

若无,则返回空。

重点2:
boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();这里就是我们在tomcat的server中配置的useBodyEncodingForURI属性的值。

常量值org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING="ISO-8859-1";

当重点1中的enc为空时,则会设置底层coyoteRequest的parameters对象的encoding=s上述"ISO-8859-1",即请求体采用"ISO-8859-1"来解析。当useBodyEncodingForURI=true时,请求参数和请求体的编码设置的都是同一个值,即"ISO-8859-1"。当useBodyEncodingForURI=false时,不改变queryStringEncoding即请求参数的编码,queryStringEncoding默认是为null的,当解析时碰见queryStringEncoding也会采用默认的编码"ISO-8859-1",然而我们可以通过org.apache.catalina.connector.Request所包含的connector配置来给queryStringEncoding赋值。如下:
当你在tomcat的server.xml文件中加入URIEncoding="UTF-8"时,它将会为queryStringEncoding赋值此值。
在tomcat的server.xml中配置此值

<Service name="Catalina">

    <!--The connectors can use a shared executor, you can define one or more named thread pools-->
    <!--
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
        maxThreads="150" minSpareThreads="4"/>
    -->


    <!-- A "Connector" represents an endpoint by which requests are received
         and responses are returned. Documentation at :
         Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)
         Java AJP  Connector: /docs/config/ajp.html
         APR (HTTP/AJP) Connector: /docs/apr.html
         Define a non-SSL HTTP/1.1 Connector on port 8080
    -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" URIEncoding='UTF-8'/>

connector将这个值为queryStringEncoding赋值的过程如下:
 public void log(org.apache.coyote.Request req,
            org.apache.coyote.Response res, long time) {

        Request request = (Request) req.getNote(ADAPTER_NOTES);
        Response response = (Response) res.getNote(ADAPTER_NOTES);

        if (request == null) {
            // Create objects
            request = connector.createRequest();
            request.setCoyoteRequest(req);
            response = connector.createResponse();
            response.setCoyoteResponse(res);

            // Link objects
            request.setResponse(response);
            response.setRequest(request);

            // Set as notes
            req.setNote(ADAPTER_NOTES, request);
            res.setNote(ADAPTER_NOTES, response);

            // Set query string encoding
            //重点重点重点重点重点重点重点重点重点重点重点重点重点重点重点
            req.getParameters().setQueryStringEncoding
                (connector.getURIEncoding());
        }

connector.getURIEncoding()便是我们配置的URIEncoding值
req.getParameters().setQueryStringEncoding
                (connector.getURIEncoding());
这句代码便是将我们在tomcat的server.xml文件中配置的URIEncoding值设置进最重要的Parameters的queryStringEncoding中。

当重点1中的enc不为空时,为Parameters请求体的的编码encoding设置为enc。
至此,Parameters的encoding和queryStringEncoding都有相应的值了,然后便按照对应的编码来解析字节数组。

重点3和4:有个相应的编码方式,分别执行请求参数的解析过程和请求体的解析过程。

总结下一些设置的作用:

request.setCharacterEncoding(encoding) :暴漏给我们的request为requestFacade,最终调用request->调用coyoteRequest->设置到coyoteRequest的charEncoding,所以coyoteRequest的charEncoding有两种来源,一种可能是content-type中的编码,另一种就是调用request.setCharacterEncoding(encoding) 方法。此方法最好在第一次解析参数之前调用,不然就无效。

URIEncoding:直接设置Parameters的queryStringEncoding的值。即针对请求参数的编码。

useBodyEncodingForURI:设置queryStringEncoding的值=encoding的值,即请求参数采用请求体的编码方式。
分享到:
评论
1 楼 zpkbtzmbw 2016-02-23  
看懂了,原理

相关推荐

    springMvc源码分析

    springMvc源码分析springMvc源码分析springMvc源码分析springMvc源码分析springMvc源码分析springMvc源码分析

    1、手写springmvc框架及分析springmvc源码.zip

    1、手写springmvc框架及分析springmvc源码.zip1、手写springmvc框架及分析springmvc源码.zip1、手写springmvc框架及分析springmvc源码.zip1、手写springmvc框架及分析springmvc源码.zip1、手写springmvc框架及分析...

    分析springmvc源码(2).zip

    分析springmvc源码(2).zip分析springmvc源码(2).zip分析springmvc源码(2).zip分析springmvc源码(2).zip分析springmvc源码(2).zip分析springmvc源码(2).zip分析springmvc源码(2).zip分析springmvc源码(2).zip分析...

    分析springmvc源码(5).zip

    分析springmvc源码(5).zip分析springmvc源码(5).zip分析springmvc源码(5).zip分析springmvc源码(5).zip分析springmvc源码(5).zip分析springmvc源码(5).zip分析springmvc源码(5).zip分析springmvc源码(5).zip分析...

    springmvc源码

    - 小项目源码:可以直接运行和调试,加深对实际开发流程的理解。 - 整合 Hibernate:理解如何在 Spring MVC 中配置和使用 Hibernate,实现数据访问。 在学习过程中,你可以逐步理解 Spring MVC 的组件和工作原理...

    SpringMVC+JNDI+Tomcat配置数据源

    SpringMVC框架结合Java Naming and Directory Interface (JNDI) 和Apache Tomcat服务器进行数据源配置,能够有效地管理和复用数据库连接资源。JNDI作为一组API,提供了Java应用程序访问命名和目录服务的能力,通过...

    springMVC源码详解

    SpringMVC是Spring框架的重要组成部分,广泛应用于企业级Web应用开发,对于提升开发效率和维护性具有重要作用。 SpringMVC的运行流程主要包括以下步骤: 1. **请求接收**:当一个HTTP请求到达服务器时,...

    最新springmvc源码分析与实战

    最新springmvc源码分析与实战,绝对超值,亲测,高清,完整标签,已读。

    spring源码、springMVC源码、springboot源码资料,轻松应对金三银四Java面试 99% Spring面试题

    本文将深入探讨Spring、SpringMVC和SpringBoot的源码,帮助你理解和掌握这三大框架的关键知识点,从而在金三银四的Java面试中脱颖而出。 首先,让我们从Spring框架开始。Spring的核心是依赖注入(Dependency ...

    SpringMVC源码jar包

    SpringMVC源码jar包,方便查看源码。学习起来更加方便

    SpringMVC源码分析系列

    SpringMVC是目前主流的WebMVC框架之一,其源码分析涉及到一系列重要的接口和类,这些组件共同协作实现请求与Controller方法之间的映射关系。 首先,HandlerMethod类是Spring3.1版本之后引入的,它封装了方法参数、...

    jetbrick-springmvc jar包(包含源码)

    10. **源码分析**:由于此jar包包含了源码,开发者可以通过阅读源码学习Jetbrick-SpringMVC的设计模式和最佳实践,提升自己的编程技能。 了解这些知识点后,开发者可以在实际项目中充分利用Jetbrick-SpringMVC的...

    springmvc框架源码.zip

    本压缩包包含SpringMVC的源码,对于学习和理解SpringMVC的工作原理及其内部机制非常有价值。 1. **DispatcherServlet**:SpringMVC的核心组件,它作为前端控制器,负责接收HTTP请求,并根据请求信息(如URL、HTTP...

    基于SpringMVC+Tomcat搭建的WEB工程

    在本文中,我们将深入探讨如何基于SpringMVC和Tomcat搭建一个WEB工程,这是一个常见的Web开发实践,尤其适合初学者入门。我们将从编程环境的设置、SpringMVC框架的介绍、Tomcat服务器的使用,以及项目目录结构等方面...

    springmvc源码流程解析

    springmvc源码流程解析

    jar包直接当做web服务,netty负责http协议,配合springMVC,再也不用tomcat了

    在没有Tomcat的情况下,Spring MVC仍然可以处理路由、请求处理和视图渲染,但是由于不支持JSP,我们需要寻找其他方式来呈现视图,例如使用Thymeleaf、Freemarker或者纯HTML静态文件。 4. **jar包部署**: 传统的Web...

    SpringMVC源码

    基于Spring3.0的SpringMVC,调试通过. 数据库为mysql,内附建数据库和表语句. 是学习入门的好架构程序. 也可以做开发SpringMVC项目的基本包. 我也在学习中,有问题可以练习我 共同学习,一起成长.

    springboot + springMVC整合源码

    SpringBoot和SpringMVC是两个在Java开发领域广泛使用的框架,SpringBoot简化了Spring应用程序的初始设置和配置,而SpringMVC则是Spring框架的一部分,专门用于处理Web应用程序的请求和响应。在这里,我们将深入探讨...

    SpringMvc源码

    应用控制器其实拆为处理器映射器(Handler Mapping)进行处理器管理和视图解析器(View Resolver)进行视图管理;页面控制器/动作/处理器为Controller接口(仅包含ModelAndView handleRequest(request, response) 方法)...

    基于SpringMVC+MyBatis+tomcat+mysql的java web开发环境Demo.zip

    在这个基于SpringMVC+MyBatis+Tomcat+MySQL的开发环境中,首先需要安装并配置MySQL数据库,创建对应的表结构和数据。然后,搭建Tomcat服务器,确保其正常运行。接着,利用Maven或Gradle等构建工具,创建一个Java Web...

Global site tag (gtag.js) - Google Analytics