Java servlet 下载文件名包含特殊字符

10
我正在编写一个简单的文件下载Servlet,但无法获得正确的文件名。尝试使用现有答案中所见的URLEncoding和MimeEncoding文件名,但它们都没有起作用。
以下代码段中的fileData对象包含mime类型、byte[]内容和文件名,需要至少ISO-8859-2字符集,ISO-8859-1不够用。
如何使浏览器正确显示下载的文件名?
这是一个文件名的例子:árvíztűrőtükörfúrógép.xls,结果为:árvíztqrptükörfúrógép.xls
  protected void renderMergedOutputModel(Map model, HttpServletRequest req, HttpServletResponse res) throws Exception {

    RateDocument fileData = (RateDocument) model.get("command.retval");
    OutputStream out = res.getOutputStream();
    if(fileData != null) {
        res.setContentType(fileData.getMime());
        String enc = "utf-8"; //tried also: ISO-8859-2

        String encodedFileName = fileData.getName();
            // also tried URLencoding and mime encoding this filename without success

        res.setCharacterEncoding(enc); //tried with and without this
        res.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName);
        res.setContentLength(fileData.getBody().length);
        out.write(fileData.getBody());
    } else {
        res.setContentType("text/html");
        out.write("<html><head></head><body>Error downloading file</body></html>"
                .getBytes(res.getCharacterEncoding()));
    }
    out.flush();
  }

请举例说明文件名的样子以及实际得到的内容。 - BalusC
árvíztűrőtükörfúrógép.xls --> árvíztqrptükörfúrógép.xls - jabal
1
是的,你说得对。这两个字符不在ISO-8859-1中,只在ISO-8859-2中出现,给每个匈牙利开发人员带来了很多问题.. :-) - jabal
6个回答

20
我找到了一种在我安装的所有浏览器中都有效的解决方案(IE8,FF16,Opera12,Chrome22)。 它基于这样一个事实:如果没有指定[不同的]编码方式,浏览器期望在文件名参数中使用浏览器本地编码的值。 通常,浏览器的本地编码是utf-8(FireFox,Opera,Chrome)。但是IE的本地编码是Win-1250。 因此,如果我们将根据用户浏览器编码为utf-8/win-1250编码的值放入文件名参数中,它应该可以工作。至少对我来说是这样的。
String fileName = "árvíztűrőtükörfúrógép.xls";

String userAgent = request.getHeader("user-agent");
boolean isInternetExplorer = (userAgent.indexOf("MSIE") > -1);

try {
    byte[] fileNameBytes = fileName.getBytes((isInternetExplorer) ? ("windows-1250") : ("utf-8"));
    String dispositionFileName = "";
    for (byte b: fileNameBytes) dispositionFileName += (char)(b & 0xff);

    String disposition = "attachment; filename=\"" + dispositionFileName + "\"";
    response.setHeader("Content-disposition", disposition);
} catch(UnsupportedEncodingException ence) {
    // ... handle exception ...
}

当然,这只是在上述浏览器上测试过,并且我不能保证它在所有浏览器中都能100%地工作。

注1(@fallen): 不正确的是使用URLEncoder.encode()方法。尽管该方法的名称为URL编码,但它实际上是将字符串编码为表单编码。(表单编码与URL编码非常相似,在许多情况下会产生相同的结果。但是有一些差异。例如,空格字符“ ”被编码为'+'而不是'%20')

要获得正确的URL编码字符串,您应该使用URI类:

URI uri = new URI(null, null, "árvíztűrőtükörfúrógép.xls", null);
System.out.println(uri.toASCIIString());

我认为如果你的文件名包含“”,那么你仍然会遇到问题,但除此之外,这很棒 - 谢谢! - teedyay
3
IE的本地编码是中欧/东欧代码页?你一定在开玩笑。它只表明IE使用本地浏览器的系统区域设置。可悲的是,我认为没有可靠的方法从服务器端检测它。 - Yongwei Wu
1
为什么这个代码能够正常工作?如果原始的“fileName”只是一个单一字符,例如“ő”,那么“fileName.getBytes('UTF-8')”将返回一个包含两个元素“0xC5 0x91”的字节数组。上述解决方案循环遍历这两个字节并将它们附加到一个新字符串中。这个新字符串将会是两个字符长和四个字节长。这到底是怎么回事?顺便说一句,虽然它能够正常工作,但我无法理解其中的原因。 - Kohányi Róbert

3

不幸的是,这取决于浏览器。请参阅讨论主题以解决该问题。要解决您的问题,请查看此站点,其中包含不同浏览器中不同标头及其行为的示例。


3

根据这里提供的优秀答案,我已经开发了一个扩展版本并已经投入生产使用。基于RFC 5987这个测试套件。

String filename = "freaky-multibyte-chars";
StringBuilder contentDisposition = new StringBuilder("attachment");
CharsetEncoder enc = StandardCharsets.US_ASCII.newEncoder();
boolean canEncode = enc.canEncode(filename);
if (canEncode) {
    contentDisposition.append("; filename=").append('"').append(filename).append('"');
} else {
    enc.onMalformedInput(CodingErrorAction.IGNORE);
    enc.onUnmappableCharacter(CodingErrorAction.IGNORE);

    String normalizedFilename = Normalizer.normalize(filename, Form.NFKD);
    CharBuffer cbuf = CharBuffer.wrap(normalizedFilename);

    ByteBuffer bbuf;
    try {
        bbuf = enc.encode(cbuf);
    } catch (CharacterCodingException e) {
        bbuf = ByteBuffer.allocate(0);
    }

    String encodedFilename = new String(bbuf.array(), bbuf.position(), bbuf.limit(),
            StandardCharsets.US_ASCII);

    if (StringUtils.isNotEmpty(encodedFilename)) {
        contentDisposition.append("; filename=").append('"').append(encodedFilename)
                .append('"');
    }

    URI uri;
    try {
        uri = new URI(null, null, filename, null);
    } catch (URISyntaxException e) {
        uri = null;
    }

    if (uri != null) {
        contentDisposition.append("; filename*=UTF-8''").append(uri.toASCIIString());
    }

}

1

我最近解决了我的应用程序中的这个问题。 这是仅适用于 Firefox 的解决方案,但在 IE 上遗憾地失败了。

response.addHeader("Content-Disposition", "attachment; filename*='UTF-8'" + URLEncoder.encode("árvíztűrőtükörfúrógép", "UTF-8") + ".xls");


谢谢,但我仍在寻找最终解决方案.. :-) 目前我将文件名中的每个ű更改为u,ő更改为o,这比?标记要好。 - jabal
有人可以告诉我如果我使用Safari 5.1.7会有什么结果吗?我遇到了相同的问题。上面的代码在Firefox、 Chrome和IE上都能正常工作,但在Safari上无法正常工作。 - vermaraj

0

根据我所读的所有内容,这对我有效:

    URI uri = new URI( null, null, fileName, null);
    String fileNameEnc = uri.toASCIIString(); //URL 编码。
    String contDisp = String.format( "attachment; filename=\"%s\";filename*=utf-8''%s", fileName, fileNameEnc);
    response.setHeader( "Content-disposition", contDisp);


0
private void setContentHeader(HttpServletResponse response, String userAgent, String fileName) throws UnsupportedEncodingException {
    fileName = URLEncoder.encode(fileName, "UTF-8");
    boolean isFirefox = (userAgent.indexOf("Firefox") > -1);
    if (isFirefox) {
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + fileName);
    } else {
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接