tomcat源码研究之参数编码格式处理
一,我们知道tomcat作为web服务器以后,我们编写的Servlet 请求中经常出现 中文乱码问题,而出现这些中文乱码,则是以下三种情况
1),来自浏览器地址栏uri携带的中文参数
2),来自页面链接跳转携带的中文参数
3 ),来自表单form中提交成参数
而这些提交方式一般以get和Post提交,那么tomcat是怎么按照什么编码格式解析这些请求参数的呢? 下面请看我的分析,
我们知道不管从get还是post来的请求参数 我们都是以reqest.getParameter(xxx) 和request.getParameterValues(xxx) 来的获取值的
所以我们先看request这两个方法:(这里的request对象是org.apache.catalina.connector.Requsest 我们知道tomcat对象我们用HttpServletRequest实现对象时RequestFacade这个对象,而这个对象却是使用设计模式中外观模式对org.apache.catalina.connector.Requsest这个对象进行了一定的封装,本质上还是看org.apache.catalina.connector.Requsest此对象)
public String getParameter(String name) { if (!parametersParsed) { parseParameters(); } return coyoteRequest.getParameters().getParameter(name); } public String[] getParameterValues(String name) { if (!parametersParsed) { parseParameters(); } return coyoteRequest.getParameters().getParameterValues(name); } // 从上可知,在没有解析之前都是先调用 parseParameters()这个方法,也就是乱码问题可能出现在这儿方法中
下面是 parseParameters 此对象
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 String enc = getCharacterEncoding(); 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); } } //处理uri中参数解析 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(); } //如果消息体是文件流 则用parseParts()解析 if ("multipart/form-data".equals(contentType)) { parseParts(); success = true; return; } //处理body中的参数解析 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 { //如果消息体是form参数 则用ReadPostBody解析 if (readPostBody(formData, len) != len) { return; } } catch (IOException e) { // Client disconnect if (context.getLogger().isDebugEnabled()) { context.getLogger().debug( sm.getString("coyoteRequest.parseParameters"), e); } return; } //存放到Parametes对象中 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)从这个方法中 parameters.handleQueryParameters(); 解析uri参数 也就是get方式的参数
2)从这个方法中 parameters.processParameters(formData, 0, len);解析请求post的消息体参数 formdata是一个未解析的字节数组
最终 而parameters最终就是处理这两个方法调用所在(org.apache.tomcat.util.http.Parameters此对象)
(1)下面请看此对象org.apache.tomcat.util.http.Parameters的handlerQueryParameters()方法
public void handleQueryParameters() { if( didQueryParameters ) { return; } didQueryParameters=true; if( queryMB==null || queryMB.isNull() ) { return; } if(log.isDebugEnabled()) { log.debug("Decoding query " + decodedQuery + " " + queryStringEncoding); } try { decodedQuery.duplicate( queryMB ); } catch (IOException e) { // Can't happen, as decodedQuery can't overflow e.printStackTrace(); } //在这儿可知我们处理uri的参数编码是用的queryStringEncoding这个属性的编码 processParameters( decodedQuery, queryStringEncoding ); }
(2)下面请看此对象org.apache.tomcat.util.http.Parameters的processParameters()方法
public void processParameters( byte bytes[], int start, int len ) { //在这儿可知我们处理post请求消息体的bytes字节编码用的是encoding这个属性的编码 processParameters(bytes, start, len, getCharset(encoding)); }
所以综上所述我们可知处理get和post请求分别是用的org.apache.tomcat.util.http.Parameters对象中queryStringEncoding 和encoding 这两个成员变量存储的编码
String encoding=null; String queryStringEncoding=null;
两个属性
继续上述方法
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 String enc = getCharacterEncoding(); 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); } }
有上述代码可知 getCharacterEncoding()和useBodyEncodingForURI 这两个会覆盖 encoding和queryStringEncoding两属性的值 从而影响解码
若使用默认编码格式 则是"ISO-8859-1”格式
public final class Constants { // -------------------------------------------------------------- Constants public static final String Package = "org.apache.coyote"; public static final String DEFAULT_CHARACTER_ENCODING="ISO-8859-1"; public static final int MAX_NOTES = 32;
若不是采用默认格式编码 ,那么我们有哪些方式可以设置呢
1)可以从 getCharacterEncoding()(也就是 org.apache.coyote.Request.getCharacterEncoding() 注意最终到了org.apache.coyote.Request.getCharacterEncoding()此方法)
public String getCharacterEncoding() { if (charEncoding != null) return charEncoding; charEncoding = ContentType.getCharsetFromContentType(getContentType()); return charEncoding; }
上述代码可知 如果charEncoding 为空 ,那么我们采取的是页面http头中的ContentType:text/html;charset=xxx 这样的编码格式, 所有我们要是设置好charEncoding就会采用我们自己的编码格式
而设置charEncoding属性却是org.apache.catalina.connector.Requsest
public void setCharacterEncoding(String enc) throws UnsupportedEncodingException { if (usingReader) { return; } // Ensure that the specified encoding is valid byte buffer[] = new byte[1]; buffer[0] = (byte) 'a'; // Confirm that the encoding name is valid B2CConverter.getCharset(enc); // Save the validated encoding //org.apache.coyote.Request.setCharacterEncoding(xxx)方法 coyoteRequest.setCharacterEncoding(enc); }
这时回到了 org.apache.catalina.connector.Requsest的setCharacterEncoding此方法 此方法是不是我们很熟悉啊, 它的调用就是我们上面所说的RequestFacde对象的setCharacterEncoding方法 也就是我们应用程序中HttpServletRequest的setCharacterEncoding的方法
2)从org.apache.tomcat.util.http.Parameters的queryStringEncoding属性设置
而此处设置却是来源于connector的U
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { 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()); } //省略代码
从最后一行代码可知其实queryStringEncoding的设置还是来源于connector的URIEncoding 属性的设置
3)可以从 connector的useBodyEncodingForURI
二,综上所述 我们可以设置的编码格式地方有以下两点:
(1),connector的URIEncoding和useBodyEncodingForURI属性
也就是修改tomcat下conf/server.xml中的connector节点 如:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000" redirectPort="8443" useBodyEncodingForURI='true' URIEncoding='UTF-8' />
(2) 以及代码中 request.setsetCharacterEncoding
所以我们再次根据 这段代码可以总结
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 String enc = getCharacterEncoding(); 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); } }
(1)默认情况下get请求 按照 queryStringEncoding 属性解码 即默认为org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING="ISO-8859-1" post 按照 encoding解码 也是"ios-8859-1"
当设置了 useBodyEncodingForURI=true uri 解析就根据页面http消息头的Content-type解析
(2)如果设置了 request.setsetCharacterEncoding 编码 那么 post请求的encoding一定是根据这个设置的结果,
get请求 如下
1,useBodyEncodingForURI=false 还是则看URIEncoding='UTF-8' 属性的设置 如果这个属性还是为空 则使用默认的 "ISO-8859-1" 如果设置了 则用 URIEncoding这个值
2,如果 useBodyEncodingForURI=true 则根据request.setsetCharacterEncoding 这个设置