JAVA中下载文件名含有中文乱码一种少见的解决方案

原始代码(未处理含中文和空格的文件名)

@RestController
public class FileController {
    @RequestMapping(value = "/download", method = {RequestMethod.GET})
    public void download(HttpServletResponse response) throws IOException {
        String filename = "中国 ABC.txt";
        response.setHeader("Content-Disposition", "attachment; filename=" + filename);

        response.setCharacterEncoding("UTF-8");
        byte[] b = "你好,世界。Hello, world.".getBytes("UTF-8");
        response.setContentLength(b.length);

        response.getOutputStream().write(b);
        response.getOutputStream().flush();
    }
}
这段代码的关键行是:
response.setHeader("Content-Disposition", "attachment; filename=" + filename);

在这里既没有考虑文件名中含有中文的情况,也没有考虑空格的情况。

如果文件名是 test.txt,那倒没问题。但当文件名是 中国.txt,则各个浏览器都出了问题,下载文件名变成了 __.txt。而一旦文件名中有了空格,变成 中国 ABC.txt,则各个浏览器的表现五花八门:

Chrome:download.sql (这是什么?)

Firefox:__

Internet Explorer:__ ABC.txt

处理空格
先解决空格问题。方法很简单,只要在响应头 Content-Disposition 的 filename 指令值前后加上英文双引号 " 即可:

response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");

这样处理后:

Chrome:download.sql (真的无语)

Firefox:__ ABC.txt

Internet Explorer:__ ABC.txt

按照 HTTP 协议的规范,filename 指令值前后是必须加英文双引号 " 的,也就是 filename=“abc.txt” 的形式。但是大多数开发者不太喜欢阅读规范文档,而喜欢看帖子抄答案,“遂致谤书流于后世”,不规范的写法层出不穷。
处理中文
按照网上很多文章的做法,我们先把原文件名字符串用 UTF-8 编码成字节数组,再用 ISO-8859-1 解码回到字符串:

filename = new String(filename.getBytes("UTF-8"), "ISO-8859-1");
response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");

这样处理后:
Chrome:中国 ABC.txt

Firefox:中国 ABC.txt

Internet Explorer:涓浗 ABC.txt (IE 总是拖后腿啊)

于是看到网上还有一种方法:

filename = java.net.URLEncoder.encode(filename,"UTF-8").replace("+", "%20");
response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");

注意上面代码中 URLEncoder.encode() 方法会把空格转换成 + 号,但这样浏览器会认为文件名中就是含有一个 + 号。所以需要再用 replace(“+”, “%20”) 把 + 号转换成 %20,这个 %20 才是浏览器 JavaScript 对于 URL 编码中空格的转义表示。

不过这样处理后,Firefox 又不行了,真是按下葫芦起了瓢:

Chrome:中国 ABC.txt

Firefox:%E4%B8%AD%E5%9B%BD%20ABC.txt

Internet Explorer:中国 ABC.txt

对于以上两难境地,网上多数文章的推荐方式是读取请求头 User-Agent 的值,判断里面是否含有 Firefox 字样,然后对 Firefox 浏览器做分支专门处理。

不过笔者不太喜欢这种“分情况讨论”的代码,生怕以后再跳出一种新的浏览器来,又要针对性地写代码。所以在这里介绍一种新的处理方式。

放弃 filename 指令,改用 filename* 指令
改用这种写法:

filename = java.net.URLEncoder.encode(filename,"UTF-8").replace("+", "%20");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + filename);

结果令人满意,三种浏览器都提示了正确的下载文件名 中国 ABC.txt。

解释
根据 HTTP 协议(RFC2626)的默认规则,请求或响应头中的指令(parameter)值只能使用 ISO-8859-1 字符集。但是 ISO-8859-1 是单字节编码的,对西欧语言支持还比较好,但对中日韩等国的语言就无能为力了。

为了解决上述问题,在 RFC2231 规范中规定了当一个指令名字后面跟了一个 * 号时,表示指令值中提供了字符集和语言信息。其格式为: 字符集'语言'使用前述字符集编码后的字符串 。注意其中有两个单引号 ',其作用是栏位之间的分隔符,都是必须存在的。即使 字符集 和 语言 栏为空白,也必须写上单引号 ' 作为分隔符。

举个例子:

Content-Type: application/x-stuff; title*=us-ascii'en-us'This%20is%20fun
这表示 title 这个参数值是用 us-ascii 字符集编码的,语言是 en-us,编码后的字符串是 This%20is%20fun。解码后的原字符串是 This is fun。

具体见 RFC2231 文档的这一小节: https://tools.ietf.org/html/rfc2231#section-4

所以,

Content-Disposition: attachment; filename*=UTF-8''%E4%B8%AD%E5%9B%BD%20ABC.txt
就表示 filename 参数值是用 UTF-8(这里用小写 utf-8 也可以)编码的,语言不限,编码后的文件名是 %E4%B8%AD%E5%9B%BD%20ABC.txt。解码后的原文件名就是 中国 ABC.txt 了。
  • 23
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
Java使用Zip压缩中文文件名可能会出现乱码的问题,这是因为Zip文件格式默认使用的是ASCII编码,而中文字符需要使用其他编码方式进行处理。解决这个问题的方法有两种: 1. 使用Apache Commons Compress库 可以使用Apache Commons Compress库来压缩文件,这个库支持多种编码方式,可以解决中文文件名乱码的问题。示例代码如下: ```java import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.apache.commons.compress.utils.IOUtils; import java.io.*; public class ZipUtil { public static void zip(String sourcePath, String destPath) throws IOException { FileOutputStream fos = new FileOutputStream(destPath); ZipArchiveOutputStream zos = new ZipArchiveOutputStream(fos); File file = new File(sourcePath); String basePath = file.getParent(); compress(file, zos, basePath); zos.close(); fos.close(); } private static void compress(File file, ZipArchiveOutputStream zos, String basePath) throws IOException { if (file.isDirectory()) { File[] files = file.listFiles(); if (files.length == 0) { String path = file.getPath().substring(basePath.length() + 1) + "/"; ArchiveEntry entry = zos.createArchiveEntry(new File(file.getPath()), path); zos.putArchiveEntry(entry); zos.closeArchiveEntry(); } else { for (File f : files) { compress(f, zos, basePath); } } } else { String path = file.getPath().substring(basePath.length() + 1); ArchiveEntry entry = zos.createArchiveEntry(new File(file.getPath()), path); zos.putArchiveEntry(entry); InputStream is = new FileInputStream(file); IOUtils.copy(is, zos); is.close(); zos.closeArchiveEntry(); } } } ``` 2. 使用JavaZipOutputStream类 Java自带的ZipOutputStream类也可以用于压缩文件,但是需要手动设置编码方式,示例代码如下: ```java import java.io.*; import java.nio.charset.Charset; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class ZipUtil { private static final int BUFFER_SIZE = 2 * 1024; public static void zip(String sourcePath, String destPath) throws IOException { FileOutputStream fos = new FileOutputStream(destPath); ZipOutputStream zos = new ZipOutputStream(fos, Charset.forName("GBK")); File file = new File(sourcePath); String basePath = file.getParent(); compress(file, zos, basePath); zos.close(); fos.close(); } private static void compress(File file, ZipOutputStream zos, String basePath) throws IOException { if (file.isDirectory()) { File[] files = file.listFiles(); if (files.length == 0) { String path = file.getPath().substring(basePath.length() + 1) + "/"; ZipEntry entry = new ZipEntry(path); zos.putNextEntry(entry); zos.closeEntry(); } else { for (File f : files) { compress(f, zos, basePath); } } } else { String path = file.getPath().substring(basePath.length() + 1); ZipEntry entry = new ZipEntry(path); zos.putNextEntry(entry); InputStream is = new FileInputStream(file); byte[] buffer = new byte[BUFFER_SIZE]; int len; while ((len = is.read(buffer)) != -1) { zos.write(buffer, 0, len); } is.close(); zos.closeEntry(); } } } ``` 以上两种方法都可以解决中文文件名乱码的问题,具体使用哪一种方法可以根据实际需要选择。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值