下载zip InputStream Struts2 Java

3

首先,抱歉使用英语。 我想知道你是否能帮我解决这个问题。 我正在尝试使用Struts2和流结果从多个byte[]创建一个zip并下载它。

我使用ZipOutputStream,并成功创建了一个文件。使用FileInputStream读取和下载它,但我的问题是我不想创建一个文件。我只想将ZipOutputStream转换为InputStream(例如ZipIntputStream),并下载该ZipInputStream。 为此,我使用以下代码:

public void downloadZip() {
    contentType = "application/octet-stream";
    filename="myZip.zip";
    ByteArrayOutputStream baos = new ByteArrayOutputStream();       
    byte[] bytes;
    try {
        ZipOutputStream zos = new ZipOutputStream(baos);
        ZipEntry ze;
        bytes = otherClass.getBytes("File1");
        ze = new ZipEntry("File1.pdf");
        zos.putNextEntry(ze);
        zos.write(bytes);
        zos.closeEntry();

        bytes = otherClass.getBytes("File2");
        ze = new ZipEntry("File2.pdf");
        zos.putNextEntry(ze);
        zos.write(bytes);
        zos.closeEntry();

        zos.flush();
        inputStream = new ZipInputStream(new ByteArrayInputStream(baos.toByteArray()));
        zos.close();
}
catch(Exception e){...}
}

我的 action struts.xml

<action...>
    <result name="success" type="stream">
        <param name="contentType">${contentType}</param>
        <param name="inputName">inputStream</param>
        <param name="contentDisposition">attachment;filename="${filename}"</param>
        <param name="bufferSize">1024</param>
    </result>
</action>

问题在于浏览器显示了一条消息,告诉我文件不是有效的zip文件,并且文件大小为0字节。
希望我已经清楚地解释了,非常感谢您的帮助。
编辑:正如我所评论的,最终我找到了解决方案,它与leonbloy的回复非常相似。除了返回ByteArrayInputStream之外,我应该在创建ByteArrayInputStream之前关闭ZipOutputStream。下面是结果代码,也许对其他人有用:
    ...
    zos.closeEntry();                       
    zos.close();            
    inputStream = new ByteArrayInputStream(baos.toByteArray());
}
catch(Exception e){...}
}

感谢您的帮助。
3个回答

3
参数<param name="inputName">inputStream</param>告诉Struts2从哪里获取将发送给客户端的原始字节。在您的情况下,您想发送压缩的字节。相反,您正在设置inputStream=ZipInputStream,这是一个流,需要压缩源 - 来解压缩。您不想那样做,您想发送原始压缩字节。
请用以下内容替换:
inputStream = new ZipInputStream(new ByteArrayInputStream(baos.toByteArray()))

by

inputStream = new ByteArrayInputStream(baos.toByteArray())

它应该可以正常工作。


谢谢您的回答,但对我来说它并不起作用。也许我应该将contentType =“application/octet-stream”更改为其他contentType? - Airenin
你好。经过更多的测试,我找到了解决方案。实际上,在阅读您的回复之前,我已经尝试过您的解决方案,但在阅读后再次尝试时,它对我没有起作用。但现在我知道原因了,所以我发布解决方案,因为它可能对其他人有用。 问题是在创建byteArrayInputStream之前无法关闭ZipOutputStream。非常感谢您的回复,因为它让我再试了几次。 - Airenin

1

你很幸运,这正是你所需要的

在这种特定情况下,我通过直接向响应写入内容来绕过框架结果系统。这样,Zip 将立即在客户端系统中创建,并逐步进行馈送,而不是等待处理结束并使用流结果一次性输出所有内容。

然后没有定义结果:

<action name="createZip" class="foo.bar.CreateZipAction" />

而在动作中:

public String execute() {
    try {        
        /* Read the amount of data to be streamed from Database to File System,
           summing the size of all Oracle's BLOB, PostgreSQL's ABYTE etc: 
           SELECT sum(length(my_blob_field)) FROM my_table WHERE my_conditions
        */          
        Long overallSize = getMyService().precalculateZipSize();

        // Tell the browser is a ZIP
        response.setContentType("application/zip"); 
        // Tell the browser the filename, and that it needs to be downloaded instead of opened
        response.addHeader("Content-Disposition", "attachment; filename=\"myArchive.zip\"");        
        // Tell the browser the overall size, so it can show a realistic progressbar
        response.setHeader("Content-Length", String.valueOf(overallSize));      

        ServletOutputStream sos = response.getOutputStream();       
        ZipOutputStream zos = new ZipOutputStream(sos);

        // Set-up a list of filenames to prevent duplicate entries
        HashSet<String> entries = new HashSet<String>();

        /* Read all the ID from the interested records in the database, 
           to query them later for the streams: 
           SELECT my_id FROM my_table WHERE my_conditions */           
        List<Long> allId = getMyService().loadAllId();

        for (Long currentId : allId){
            /* Load the record relative to the current ID:         
               SELECT my_filename, my_blob_field FROM my_table WHERE my_id = :currentId            
               Use resultset.getBinaryStream("my_blob_field") while mapping the BLOB column */
            FileStreamDto fileStream = getMyService().loadFileStream(currentId);

            // Create a zipEntry with a non-duplicate filename, and add it to the ZipOutputStream
            ZipEntry zipEntry = new ZipEntry(getUniqueFileName(entries,fileStream.getFilename()));
            zos.putNextEntry(zipEntry);

            // Use Apache Commons to transfer the InputStream from the DB to the OutputStream
            // on the File System; at this moment, your file is ALREADY being downloaded and growing
            IOUtils.copy(fileStream.getInputStream(), zos);

            zos.flush();
            zos.closeEntry();

            fileStream.getInputStream().close();                    
        }

        zos.close();
        sos.close();    

    } catch (Exception e) {
        logError(e);
    finally {
        IOUtils.closeQuietly(sos);
    }

    return NONE;
}

谢谢您的帮助,但就像Sarael的回答中所发生的那样,我无法使用response.getOutputStream(),因为我的代码中存在对response.getWriter()的调用,这会导致异常。据我所知,我只能使用InputStream通过Struts2将其发送到客户端。 - Airenin
如果您只需要下载ZIP文件(而不是其他数据),则不要调用response.getWriter()。上面的示例是我使用的代码的一部分,并且在Struts2上完全有效。 - Andrea Ligios
另外,response.getWriter()是一个PrintWriter,它只处理字符,而不是二进制数据(例如ZIP内容):http://docs.oracle.com/javaee/6/api/javax/servlet/ServletResponse.html#getWriter%28%29。这就是为什么你应该使用OutputStream而不是PrintWriter的原因。 - Andrea Ligios

0

ZipInputStream用于解压缩文件并访问未压缩的zip内容。您只想将压缩字节发送到浏览器。

创建zip文件后,只需使用response.getOutputStream(baos.toByteArray())发送文件的字节即可。

如果您正在压缩大文件,则在内存中执行所有操作会导致内存不足的风险。最好限制输入文件的大小,并在文件过大时向用户显示错误。

此外,看起来inputStream是一个类变量。这对于servlet来说是非常糟糕的想法。基本上,您的类不是线程安全的,当两个用户同时访问页面时,一个用户可能会获得另一个用户的zip文件。相反,将响应对象作为参数传递给方法或将baos返回给调用者,以便它可以将字节写入正确的响应流。摆脱存储特定于各个用户的信息的类变量,并在调用之间传递它们。


1
谢谢您的快速回答,但问题是我无法调用response.getOutputStream,因为显然我在.jsp中使用了getWriter。几天前我就遇到过这个问题。您知道其他解决方法吗? - Airenin
不要同时调用 getWriter 和 getOutputStream,如果要发送文本,请调用 getWriter;如果要发送 zip 文件,请调用 getOutputStream。 - Sarel Botha
问题在于这个调用是在程序的其他部分执行的,我无法更改它,这就是为什么我想发送一个InputStream的原因。我试图删除那个调用,但似乎它是对内部类或类似东西的调用,因为我没有调用它,所以我放弃了,并开始使用inputStream来显示内容。问题是,如果我发送一个pdf文件来显示它,它可以工作,但是zip文件不行,我不知道为什么。 - Airenin

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