最早
我是在看struts2处理静态资源时,看到代码中对取得的资源路径进行了url解码,具体的代码路径如下(struts2版本是2.5.20):
- org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter第128行,
- org.apache.struts2.dispatcher.ExecuteOperations第59行,
- org.apache.struts2.dispatcher.DefaultStaticContentLoader第200行,
- org.apache.struts2.dispatcher.DefaultStaticContentLoader第301行,
struts2在这里是先判断了资源是否存在,存在的话再往输出流里面写数据。
后来
我通过xxx.class.getClassLoader().getResource(“文件名”).getPath()来获取相对于classes路径的文件路径时,发现路径存在中文时会有问题,此时的中文变成了%xy%xy%xy%xy%xy%xy%xy%xy%xy这样的格式,百度后发现我前面实际上获取的是一个被url编码后的路径,必须通过url解码来获取我要的中文路径,这时我找到了一个java已有的接口:
- java.net.URLDecoder.decode(String s, String enc)
我查阅了jdk1.6的中文文档,对它有这样的描述(实际上就是源码注释的翻译):
上面的注释有提到一个类似编码相关的东西:application/x-www-form-urlencoded,搜了一番后我又去制定Web技术标准的中文网站发现了这个专栏:HTML URL 编码参考手册,专栏里面有这样的描述:
-
URL 编码会将字符转换为可通过因特网传输的格式。
-
URL 编码只能使用 ASCII 字符集来通过因特网进行发送。
由于 URL 常常会包含 ASCII 集合之外的字符,URL 必须转换为有效的 ASCII 格式。
URL 编码使用 “%” 其后跟随两位的十六进制数来替换非 ASCII 字符。
URL 不能包含空格。URL 编码通常使用 + 来替换空格。
上面的十六进制一般指通过UTF-8编码后生成的数字的十六进制格式,再结合上面的jdk注释,大家是不是有点感觉了呢?为了进一步加深印象,我点进java.net.URLDecoder带两个参数的decode方法,看它做了点啥。我使用的是jdk1.8,由于源码没法在一张图里面完整展现,大家先自行查看。
对于这个方法的逻辑,我的简单解读如下:
- 定义了一个boolean类型的变量needToChange,看名字应该是确定参数是否应该被改变,默认是false
- 使用StringBuffer保存解码后的字符,初始化时StringBuffer的容量是根据第一个字符串参数的长度来确定
- 长度大于500时,取长度的一半的整数位作为StringBuffer的容量
- 长度不足500时,直接把长度作为StringBuffer的容量
- 一个字节数组,存放被UTF-8编码的16进制数字
后面在一个while循环里面进行字符的转换和拼接:
while里面通过循环变量i从头依次获取字符串入参的每个位置的字符
-
当字符是 + 时,使用一个空格来替换,将needToChange标识为true,即碰到加号就必须进行转换了——加号转空格,
-
当字符是%时,每一组%xy这样的三个字符将生成一个字节(byte),在转换时会把%后面的xy进行十六进制转十进制的处理,通常来说一个中文字符使用UTF-8编码时会占用三个字节,比如:
%E4%B8%AD%E5%9B%BD,这是6组%xy,除去所有的%,最终它会被转成6个字节,前面的三个字节(E4,B8,AD)实际上是对汉字"中"进行UTF-8编码后得出的,后面的则代表汉字“国”。大家可以在bejson进行验证,输入“中国”转16进制得到e4b8ade59bbd。每转换一组,变量i会加3,表示在while循环时会跳过两次,同时将needToChange标识为true。在碰到类似%x这样不完整的编码时,decode方法会直接抛出异常,因为这不是一个完整的十六进制的字节编码。最后通过new String的形式将字节数组还原成真实的字符。 -
当碰到其他直接是ASCII 集合内的字符时,不做转换。
总结
对于url中包含中文这样的ASCII 集合之外的字符,需要进行解码处理。通常url中一个中文使用三组%xy来表示,除去特殊字符%后,一个xy实际上是被UTF-8编码后的两个十六进制格式数字,可表示为一个字节,三组xy即三个字节。最终通过new String的形式被还原成真实的字符。