对于一个Java Web应用来说,如果需要指定客户端下载的文件名,那就需要设置Content-Disposition,指定前由于Web容器的限制,需要对指定的fileName进行URL编码编码。
举个栗子,如果文件名叫’测试.txt’,那么需要指定Content-Disposition: attachment; filename=”%e6%b5%8b%e8%af%95.txt”; filename*=UTF-8”%e6%b5%8b%e8%af%95.txt
理论上这没有任何问题,但是如果这个文件名叫’测试 1.txt’呢?根据commons-codec的实现(当前最新的1.11),文件名被编码后的文件名是’%e6%b5%8b%e8%af%95+1.txt’;
气氛顿时尴尬了起来,如果用FireFox对这个文件进行下载,会被FireFox把文件名改成’测试+1.txt’;
What? 我凭本事指定的文件名,凭什么改我的?
抱着探求问题本质(避免QA提BUG)的态度,找了一下URL编码的规范:RFC3986
里面有这样一段文字:
重点是第三句:举个栗子,”%20”是US-ASCII码中’ ‘字符的URL编码。
我怕是看了个假标准吧,为啥commons-codec会把’ ‘编码成’+’?翻了翻源码,好像还是刻意那么做的:
可以看到,如果碰到’ ‘就把它转成’+’,这怕是玩我吧?这又是遵守的什么标准?看了一眼注释,遵守的标准是www-form-urlencoded(RFC1866)
原来,RFC3986针对的是URI指向的内容的编码,RFC1866针对的是HTML相关的www-form-urlencoded相关内容的编码;
本来泾渭分明的两个标准,在某一天RFC1866宣布,GET请求参数也算www-form-urlencoded的时候被打破了….
于是StackOverFlow上也有了相关问题的讨论: ‘space’什么时候应该被编码成’+’什么时候该编码成’%20’;
大家的意见比较统一,URL相关的用RFC1866,其他用RFC3986,拿不准的时候就试试….. 制定标准的大佬们,不能给底层Coder一条活路么…
之后改用自己定制的符合RFC3986的编码方式,问题终于解决,代码修改如下:
public String encodeWithoutSpaceSkip(String content) {
if (StringUtils.isEmpty(content)) {
return null;
}
byte[] bytes = content.getBytes();
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
for (final byte c : bytes) {
int b = c;
if (b < 0) {
b = 256 + b;
}
if (WWW_FORM_URL.get(b)) {
if (b == ' ') {
// ' '按URL编码标准会被编码成'+', 但是此时浏览器兼容可能有问题, 需要编码成%20
buffer.write('%');
buffer.write('2');
buffer.write('0');
} else {
buffer.write(b);
}
} else {
buffer.write(ESCAPE_CHAR);
final char hex1 = hexDigit(b >> 4);
final char hex2 = hexDigit(b);
buffer.write(hex1);
buffer.write(hex2);
}
}
return new String(buffer.toByteArray());
}
private char hexDigit(final int b) {
return Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
}