前言:

  之前写Java服务端处理POST请求时遇到了请求体转换成字符流所用编码来源的疑惑,在doPost方法里通过request.getReader()获取的BufferedReader对象内部的

Reader用的是什么编码将字节流转换成字符流的呢?又是在哪里设置呢和什么时候生效的呢?通过查找资料,我了解到通过HttpServletRequest对象获得请求体数据

有三种方法,其中两种是不管HTTP请求头设置Content-Type为何值都能够在不重复获取输入流的前提下获取到数据的,一个是request.getInputStream(),一个是request.getReader();

对于前者我们可以在其上面套一个InputStreamReader并设置编码便能正确读取出字符数据,但是对于后者猜测是通过request.setCharacterEncoding(charsetName);来设置;但是当时

挺想知道这两句代码是怎么关联起来的,于是就开始了读源码的过程。

步骤:

  最开始的时候我是想通过request.getReader()来找出答案,于是通过打印request.getClass().toString(),知道了request对象真正的类是org.apache.catalina.connector.RequestFacade,

通过名字最终找出这个类是Tomcat安装目录中lib目录下的catalina.jar,导入到项目找出RequestFacade.getReader()的源码为:

public BufferedReader getReader() throws IOException {
if (this.request == null) {
throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
} else {
return this.request.getReader();
}
}

然后找出this.request的类是org.apache.catalina.connector.Request,通过RequestFacade构造方法初始化,接着找到org.apache.catalina.connector.Request.getReader()的代码为:

public BufferedReader getReader() throws IOException {
if (this.usingInputStream) {
throw new IllegalStateException(sm.getString("coyoteRequest.getReader.ise"));
} else {
this.usingReader = true;
this.inputBuffer.checkConverter();
if (this.reader == null) {
this.reader = new CoyoteReader(this.inputBuffer);
}
return this.reader;
}
}

这里注意this.inputBuffer.checkConverter();这里将会把request.setCharacter(charsetName)设置的编码应用在字节流转换为字符串的过程上,这个过程后面再讲。

我们先看new CoyoteReader(this.inputBuffer);由于CoyoteBuffer是继承自BufferedReader,故真正将字节流转换为字符流的应该是this.inputBuffer,

查看代码得知它的类型为:org.apache.catalina.connector.InputBuffer,类定义为:

public class InputBuffer extends Reader implements ByteInputChannel, CharInputChannel, CharOutputChannel {
。。。。。
}

由于它和InputStreamReader有共同的父类Reader,故我猜测将字节流转换成字符流的应该就是InputBuffer类了,但是线索到了就断了我不知道接下来该看哪里了(后来理清思路后发现其实应该往上找看InputBuffer是在哪创建及赋值的),

于是我回到最初的猜测,request.getReader()是通过request.setCharacterEncoding(charsetName)来实现的;通过查看request.setCharacterEncoding(charsetName)源码

得知RequestFacade设置字符编码是通过内部的org.apache.catalina.connector.Request,而这个Request又是通过内部的org.apache.coyote.Request来实现的,导入所需jar包:tomcat-coyote.jar

其中coyoteRequest.setCharacterEncoding(charsetName)的代码为:

public void setCharacterEncoding(String enc) {
this.charEncoding = enc;
}

到了这里后线索又断了,我只知道最初RequestFacade设置的编码最终是保存在org.apache.catalina.connector.Request里,但是这个编码是什么时候用到了InputBuffer上就不知道了。

趁着这阶段还弄清楚了RequestFacade无论是设置编码、获得编码、getContentLength()等方法本质上都是通过org.apache.coyote.Request来最终实现的。

回到正题,线索断了以后我后来通过找到是哪里new了InputBuffer及是哪里给InputBuffer设置编码和字节流等思考继续回到了org.apache.catalina.connector.Request类的定义里,

通过搜索发现org.apache.catalina.connector.Request内部的this.inputBuffer是在构造方法里创建的,但是只有一个空壳,而RequestFacade.getInputStream()最终也是以this.inputBuffer作为了

字节流的参数new CoyoteInputStream(this.inputBuffer);故它可能本身既能读取字符流又能读取字节流,即它是存储着第一手的数据。

接着找到了org.apache.catalina.connector.Request中的一个方法:

public void setCoyoteRequest(org.apache.coyote.Request coyoteRequest) {
this.coyoteRequest = coyoteRequest;
this.inputBuffer.setRequest(coyoteRequest);
}

我之前一直钻进找InputBuffer编码的巷道里,忘了找coyoteRequest这么重要的属性是从哪赋值的了,经过搜查org.apache.catalina.connector.Request里只有这个set方法可以给this.coyoteRequest赋值,故这个set方法

一定会执行,也就是说this.inputBuffer.setRequest(coyoteRequest);会执行,而coyoteRequest里保存着RequestFacade设置的编码,故而InputBuffer里需要的编码来源有了。

接着看InputBuffer里哪里会用到这个coyoteRequest,找了一下InputBuffer里一大堆方法都用到了coyoteRequest,经过一番思考想到外部程序是通过BufferedReader来读取字符流的,而BufferedReader读取字符流又是

通过构造方法初加载的的Reader来读取的,即是通过InputBuffer的Read(char[]....)方法读取数据的,故找到InputBuffer中的这个方法,定义如下:

public int read(char[] cbuf, int off, int len) throws IOException {
if (this.closed) {
throw new IOException(sm.getString("inputBuffer.streamClosed"));
} else {
return this.cb.substract(cbuf, off, len);
}
}

可见InputBuffer读取字符流又是通过this.cb的substract方法读取的,查找代码得知cb是CharChunk类,导入jar包:tomcat-util.jar,CharChunk.substract的源码为:

public int substract(char[] src, int off, int len) throws IOException {
int n;
if (this.end - this.start == 0) {
if (this.in == null) {
return -1;
} n = this.in.realReadChars(this.buff, this.end, this.buff.length - this.end);
if (n < 0) {
return -1;
}
} n = len;
if (len > this.getLength()) {
n = this.getLength();
} System.arraycopy(this.buff, this.start, src, off, n);
this.start += n;
return n;
}

这里面的this.in.realReadChars(...)很关键,从名字可以猜测这个是真正读取字符数组的方法,然后通过查找,this.in就是之前的InputBuffer对象。

然后我通过看CharChunk的代码,发现this.start和this.end最初值为0,故第一次调用此方法时会执行this.in.realReadChars(...),我们来看这个方法定义:

public int realReadChars(char[] cbuf, int off, int len) throws IOException {
if (!this.gotEnc) {
this.setConverter();
} boolean eof = false;
if (this.bb.getLength() <= 0) {
int nRead = this.realReadBytes(this.bb.getBytes(), 0, this.bb.getBytes().length);
if (nRead < 0) {
eof = true;
}
} if (this.markPos == -1) {
this.cb.setOffset(0);
this.cb.setEnd(0);
} else {
this.cb.makeSpace(this.bb.getLength());
if (this.cb.getBuffer().length - this.cb.getEnd() == 0 && this.bb.getLength() != 0) {
this.cb.setOffset(0);
this.cb.setEnd(0);
this.markPos = -1;
}
} this.state = 1;
this.conv.convert(this.bb, this.cb, eof);
return this.cb.getLength() == 0 && eof ? -1 : this.cb.getLength();
}

通过查看代码发现this.goEnc初始为false,只有this.setConverter()后才变为true,故第一次会执行setConverter(),再来看setConverter()的源码:

protected void setConverter() throws IOException {
if (this.coyoteRequest != null) {
this.enc = this.coyoteRequest.getCharacterEncoding();
} this.gotEnc = true;
if (this.enc == null) {
this.enc = "ISO-8859-1";
} this.conv = (B2CConverter)this.encoders.get(this.enc);
if (this.conv == null) {
if (SecurityUtil.isPackageProtectionEnabled()) {
try {
this.conv = (B2CConverter)AccessController.doPrivileged(new PrivilegedExceptionAction<B2CConverter>() {
public B2CConverter run() throws IOException {
return new B2CConverter(InputBuffer.this.enc);
}
});
} catch (PrivilegedActionException var3) {
Exception e = var3.getException();
if (e instanceof IOException) {
throw (IOException)e;
}
}
} else {
this.conv = new B2CConverter(this.enc);
} this.encoders.put(this.enc, this.conv);
} }

有代码:this.enc = this.coyoteRequest.getCharacterEncoding();

并且通过this.enc初始化了一个B2CConverter对象,从名字可猜测这个类是将字节流转换成字符流的转换器;

我们回到realReadChars(...)的源码里有必执行的代码:this.conv.convert(this.bb, this.cb, eof);

这个代码是将this.bb转换生成字符流数据到this.cb里(bb是ByteChunk对象),至此可知将字节流转换成字符流是通过InputBuffer的this.conv.convert(...)转换,而字符编码则是通过setConverter()来获取coyoteRequest的编码进行设置在this.conv里,且

setConverter()只执行一次,因为setConverter()内部会将this.gotEnc = true;,故我们需要找出最早执行setConverter()地方,发现除了realReadChars()还有checkConverter()方法也会执行setConverter()方法,而

checkConverter()方法在org.apache.catalina.connector.Request.getReader()方法里就会执行,故可以得知必需先调用RequestFacade.setCharacterEncoding(charsetName)方法再执行getReader()方法,顺序错了设置的编码将不会起效于Reader中,

对于ResponseFacade.getWriter()也是一样。

记录一下通过分析Tomcat内部jar包找出request.getReader()所用的字符编码在哪里设置和起效的完整分析流程的更多相关文章

  1. 为什么项目的jar包会和tomcat的jar包冲突?

    为什么项目的jar包会和tomcat的jar包冲突? 碰到这个问题,猜测tomcat启动时会将自己的lib和项目的lib在逻辑上归并为一个大的lib,但是并没有做版本区分以及去重,这样相同的包可能就有 ...

  2. 如何在Eclipse中添加Tomcat的jar包

    原文:如何在Eclipse中添加Tomcat的jar包 右键项目工程,点击Java Build Path 点击Add Library,选择Server Runtime 选择Tomcat版本 此时就看到 ...

  3. 禁止tomcat扫描jar包的tld文件

    禁止tomcat扫描jar包的tld文件tomcat/conf/logging.properties 取消注释org.apache.jasper.compiler.TldLocationsCache. ...

  4. Java_java动态编译整个项目,解决jar包找不到问题

    java动态编译整个项目,解决jar包找不到问题原文:http://itzyx.com/index.php/javac/ 动态将java文件编译为class文件解决方案:将temp\sdl\src目录 ...

  5. maven项目发布到Tomcat丢失jar包

    昨天看了一篇tomcat设置的文章,说要把第一个勾上,这样不需要更新到tomcat.  一启动tomcat就发现丢包.后来在网上看了许多文章,说要update maeven项目,然后你就会发现启动过程 ...

  6. 记录添加mvn命令,以及安装jar包到本地仓库

    安装版的maven,没有mvn命令,需要先设置环境变量,添加%MAVEN_HOME% =D:\apache-maven-3.3.9path 中添加 %MAVEN_HOME%/bin即可 安装下载好的j ...

  7. pom文件中引入依赖成功了,但是jar包找不着

    编写代码的时候总是会碰到各种奇奇怪怪的问题,最近引入依赖的时候发现依赖虽然引入成功了,而且查看仓库,仓库中也存在该jar包,但是项目代码中并没有找到该jar包,重新导入reimport各种都试了还是不 ...

  8. android studio导jar包找不到类的解决方法

    今天更新了universal-image-loader-1.9.5.jar文件,studio死活找不到其中相关的类,上网百度试了很多办法,总算解决了,具体方法跟一篇老外的帖子的差不多,这里分享下. 解 ...

  9. 从jar包还原出java源码(项目文件)

    原文转载至:https://blog.csdn.net/mxmxz/article/details/73043156 上周接到个新任务,一个遗留的接口工程需要改造,然而根据前任开发留下的文档看,这个工 ...

随机推荐

  1. ORALCE PL/SQL学习笔记

    ORALCE  PL/SQL学习笔记 详情见自己电脑的备份数据资料

  2. WPF加载程序集中字符串资源

    WPF资源 WPF资源使用其实的也是resources格式嵌入资源,默认的资源名称为"应用程序名.g.resources",不过WPF资源使用的pack URI来访问资源. 添加图 ...

  3. NOIP初赛 之 哈夫曼树

    哈夫曼树 种根据我已刷的初赛题中基本每套的倒数第五或第六个不定项选择题就有一个关于哈夫曼树及其各种应用的题,占:0-1.5分:然而我针对这个类型的题也多次不会做,so,今晚好好研究下哈夫曼树: 概念: ...

  4. Unity 使用 陀螺仪 实现 《王者荣耀》 登入界面 背景动态效果

    在 <王者荣耀> 登入界面 左右上下晃动手机(有些手机不支持)可以看到背景在变化 我使用的是iPhone SE 效果如下: 对比两张图片的左下角 可以看到差异 至于为什么要这么做: 1.使 ...

  5. SQLyog快捷键汇总

    Ctrl+M   创建一个新的连接Ctrl+N   使用当前设置新建连接Ctrl+F4   断开当前连接 对象浏览器 F5   刷新对象浏览器(默认)Ctrl+B   设置焦点于对象浏览器 SQL 窗 ...

  6. python抓取zabbix图形,并发送邮件

    最近十九大非常烦,作为政府网站维护人员,简直是夜不能寐.各种局子看着你,内保局,公安部,360,天融信,华胜天成,中央工委,政治委员会... 360人员很傻X,作为安全公司,竟然不能抓到XX网站流量, ...

  7. win10 UWP Hmac

    HMAC是密钥相关的哈希运算消息认证码,输入密钥和信息. 在uwp,Hmac在很多网络使用,我最近写qiniu SDK,把原来C#改为UWP,需要使用HMAC. 上传文件 <form metho ...

  8. C# 6.0 $"Hello {csdn}"

    "hello $world"的格式化字符串是指把字符串中一个单词,以一个标示开头.可以代换为单词所指的变量. 这个在jq有,而C#string的格式只能用格式的字符占位符,格式的字 ...

  9. STM32F10XX存储器细节

    >> STM32F10XX系统架构 >>  程序存储器.数据存储器.寄存器和输入输出端口被组织在同一个4GB的线性地址空间内. >>  数据字节以小端格式存放在存储 ...

  10. kafka 的 createDirectStream

    一入大数据深似海,脑袋不够用了,先留下只言片语. kafka api中给出2类直接获取流的接口:createStream和createDirectStream. createStream比较简单,只需 ...