在工作流的一张表单里可能会有多个步骤上传附件,在用户的待办中往往会存在多条带有附件的任务,如果一一打开并且点击下载链接下载,不仅费时,而且繁琐,用户体验较差。
OA系统采用的是FastDFS做为文件服务器,FastDFS的Java客户端提供了上传、下载等功能供调用。
在我之前的文章里对此有描述,目前已有的代码有对文件的批量上传功能,但下载的参数往往是针对单个文件。比如单个文件的下载方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
/**
* 文件下载
* @author chao.gao
* @date 2014-2-17 下午5:28:23
* @see com.gaochao.platform.components.upload.IUploadService#download(java.lang.String,
* java.lang.String)
* @param id
* @param fileName
*/
@Override
public
void
download(String id, String fileName) {
InputStream in=
null
;
FileOutputStream out =
null
;
try
{
Resource[] resources = RESOLVER
.getResources(
"classpath*:com/fx/**/META-INF/upload/*.conf"
);
if
(resources ==
null
|| resources.length <
1
) {
LOGGER.error(
"下载文件失败,失败原因 : "
,
"client.conf不存在!"
);
}
else
{
in = resources[
0
].getInputStream();
File file =
new
File(
"client.conf"
);
out =
new
FileOutputStream(file);
byte
[] buf =
new
byte
[
1024
];
while
(
true
) {
int
r = in.read(buf);
if
(r == -
1
) {
break
;
}
out.write(buf,
0
, r);
}
ClientGlobal.init(file.getAbsolutePath());
LOGGER.info(
"下载中:"
+
"client.conf初始化成功!"
);
TrackerClient trackerClient =
new
TrackerClient();
TrackerServer trackerServer = trackerClient.getConnection();
if
(trackerServer !=
null
) {
LOGGER.info(
"下载中:"
+
"成功获得均衡器"
);
}
else
{
LOGGER.error(
"下载失败:"
+
"均衡器获取失败"
);
}
StorageServer storageServer =
null
;
StorageClient storageClient =
new
StorageClient(trackerServer,
storageServer);
// groupName and remoteFileName should exist
String group_name =
"group1"
;
AttachmentEntity aEntity = attachmentDao.queryById(id);
String remote_filename = aEntity.getPosition();
String[] paths = remote_filename.split(
"/"
);
FileInfo fi = storageClient.get_file_info(paths[
0
],
remote_filename.replace(paths[
0
] +
"/"
,
""
));
LOGGER.error(
"下载中..."
+
"得到文件信息"
);
HttpServletResponse response = ResponseContext
.geRequestContext().getResponse();
response.reset();
response.setContentType(
"application/download;charset=UTF-8"
);
// response.setHeader("Content-Disposition","attachment;" +
// "filename=" + new String(fileName.getBytes("ISO_8859_1"),
// "UTF-8"));
// 如果使用上面编码格式,否则附件名称若为中文,则乱码,下载下来的附件名称,显示不全.或者乱乱码 .应该使用下面的编码格式.
response.setHeader(
"Content-Disposition"
,
"attachment;"
+
"filename="
+
new
String(fileName.getBytes(
"gbk"
),
"ISO8859-1"
));
// String basepath =
// System.getProperties().getProperty("user.home");
// String path = basepath + File.separator + new
// String(fileName.getBytes("GBK"), "ISO_8859_1");
ServletOutputStream oOutput = response.getOutputStream();
byte
[] inputStream = storageClient.download_file(paths[
0
],
remote_filename.replace(paths[
0
] +
"/"
,
""
));
try
{
oOutput.write(inputStream);
oOutput.flush();
}
catch
(IOException ioe) {
LOGGER.error(
"下载失败:"
+
"写入本地文件失败!"
);
}
finally
{
IOUtils.close(oOutput);
}
String sourceIpAddr = fi.getSourceIpAddr();
long
size = fi.getFileSize();
LOGGER.info(
"ip:"
+ sourceIpAddr +
",size:"
+ size);
}
}
catch
(FileNotFoundException fnfex) {
LOGGER.error(
"下载失败,文件未找到:"
+ fnfex.getMessage());
}
catch
(IOException ioex) {
LOGGER.error(
"下载失败,IO 错误 :"
+ ioex.getMessage());
}
catch
(MyException e) {
LOGGER.error(
"下载失败,其他错误 :"
+ e.getMessage());
}
finally
{
try
{
if
(in!=
null
)
in.close();
if
(out!=
null
)
out.close();
}
catch
(IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
|
我尝试向后端传入多个文件的id,然后循环调用下载方法。我的目的是在循环调用中多个文件依次下载。但测试结果表明这种方法是行不通的。在第一个文件下载结束后,该方法在
1
2
3
4
|
HttpServletResponse response = ResponseContext
.geRequestContext().getResponse();
response.reset();
response.setContentType(
"application/download;charset=UTF-8"
);
|
前后会报:java.lang.IllegalStateException
最初我是怀疑跟我的页面提交方式有关,我是采用ajax提交,参数直接用json格式。action中返回的是String类型的SUCCESS,struts.xml中配置的<result name="success" type="json" >。又借鉴单个成功下载的经验,我将struts.xml中的action的result配置修改为如下:
1
2
3
|
<
result
name
=
"success"
type
=
"json"
>
<
param
name
=
"contentType"
>text/html</
param
>
</
result
>
|
同时根据网上有关该异常的处理办法,本文记为方法1,以下类推。这些方法在某些场合或某些情况下是有效的,大家遇到类似问题可以逐一尝试。
方法一、将返回值修改为null,而不是使用SUCCESS;
该方法在我的应用下未起作用。然后我查找了资料,又借鉴了我之前所写的批量导出的实现,在批量导出时也遇到了很多问题,最后采用动态组装form提交的方式解决了。我将我的逻辑也参考修改,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
function
download(){
var
selected = $(
"#proposalDataGrid"
).selectRow();
if
(selected.length == 0){
alert(
"未选择提案!"
);
}
else
{
var
data = {proposalVo : {ids : selected}};
var
url =
'../patent/dowloadAttByIds.action'
var
name =
"proposalVo.ids"
;
download_(url, name,selected);
}
}
function
download_(url, name, uuidArray) {
var
tempForm = document.createElement(
"form"
);
tempForm.action = url;
tempForm.method =
"post"
;
tempForm.contentType =
"application/x-www-form-urlencoded;charset=utf-8"
;
document.body.appendChild(tempForm);
for
(
var
i = 0; i < uuidArray.length; i++) {
var
tempInput = document.createElement(
"input"
);
tempInput.type =
"hidden"
;
tempInput.name = name;
var
uuids =
new
Array();
uuids[i] = uuidArray[i];
tempInput.value = uuids[i];
tempForm.appendChild(tempInput);
}
tempForm.submit();
}
|
这是一个动态的js组装,非常巧妙,我使用这种方法传递参数以及向后端发起下载请求。事实表明这种方法与第一种方案完全相同,只下载了第一个,在下载第二个时即抛出同样的错误。
现在矛盾聚集在了该exception的解决上。我上网搜索了很多资料,有一篇文章介绍的情况与本文类似,如下:
该异常表示,当前对客户端的响应已经结束,不能在响应已经结束(或说消亡)后再向客户端(实际上是缓冲区)输出任何内容。具体分析:
首先解释下flush(),我们知道在使用读写流的时候数据先被读入内存这个缓冲区中,然后再写入文件,但是当数据读完时不代表数据已经写入文件完毕,因为可能还有一部分仍未写入文件而留在内存中,这时调用flush()方法就会把缓冲区的数据强行清空输出,因此flush()的作用就是保证缓存清空输出。 response是服务端对客户端请求的一个响应,其中封装了响应头、状态码、内容等, 服务端在把response提交到客户端之前,会向缓冲区内写入响应头和状态码,然后将所有内容flush。这就标志着该次响应已经committed(提交)。对于当前页面中已经committed(提交)的response,就不能再使用这个response向缓冲区写任何东西(注:同一个页面中的response.XXX()是同一个response的不同方法,只要其中一个已经导致了committed,那么其它类似方式的调用都会导致 IllegalStateException异常)。
我的代码里存在flush,flush导致了第一次的提交,我将flush语句去掉,问题依然。看来flush还不是最终的原因。但最终的原因肯定是第一个文件下载结束后,response已经返回给前端,状态已经失效,第二个文件下载的时候当然会出问题。
我在百度云网盘有账号,我记得其支持多个文件下载。登陆网盘后多选下载,百度是将多个文件打包后发给了用户。参考该方法,我最终的实现方案是循环FastDFS下载方法,将字节流转化为文件,再将文件添加进zip文件中,然后将zip转化为字节流写入response的输出流中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
for
(
int
i =
0
;i<ids.length; i++){
AttachmentEntity aEntity = attachmentDao.queryById(ids[i]);
String remote_filename = aEntity.getPosition();
String[] paths = remote_filename.split(
"/"
);
FileInfo fi = storageClient.get_file_info(paths[
0
],
remote_filename.replace(paths[
0
] +
"/"
,
""
));
LOGGER.error(
"下载中..."
+
"得到文件信息"
);
byte
[] inputStream = storageClient.download_file(paths[
0
],
remote_filename.replace(paths[
0
] +
"/"
,
""
));
File fileTemp = byte2File(inputStream, fileNames[i]);
fileList.add(fileTemp);
}
File[] files = (File[])fileList.toArray(
new
File[fileList.size()]);
File zipFile = genZip(files);
HttpServletResponse response = ResponseContext
.geRequestContext().getResponse();
response.reset();
response.setContentType(
"application/download;charset=UTF-8"
);
// response.setHeader("Content-Disposition","attachment;" +
// "filename=" + new String(fileName.getBytes("ISO_8859_1"),
// "UTF-8"));
// 如果使用上面编码格式,否则附件名称若为中文,则乱码,下载下来的附件名称,显示不全.或者乱乱码 .应该使用下面的编码格式.
response.setHeader(
"Content-Disposition"
,
"attachment;"
+
"filename="
+
new
String(
"download.zip"
.getBytes(
"gbk"
),
"ISO8859-1"
));
ServletOutputStream oOutput = response.getOutputStream();
try
{
oOutput.write(File2byte(zipFile));
oOutput.flush();
zipFile.delete();
}
catch
(IOException ioe) {
LOGGER.error(
"下载失败:"
+
"写入本地文件失败!"
);
}
finally
{
IOUtils.close(oOutput);
}
|
该方案涉及到了文件转字节流:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
/**
*
* file to byte
* @author chao.gao
* @date 2015-6-25 下午5:08:14
* @param filePath
* @return
*/
public
static
byte
[] File2byte(File file)
{
byte
[] buffer =
null
;
try
{
FileInputStream fis =
new
FileInputStream(file);
ByteArrayOutputStream bos =
new
ByteArrayOutputStream();
byte
[] b =
new
byte
[
1024
];
int
n;
while
((n = fis.read(b)) != -
1
)
{
bos.write(b,
0
, n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
}
catch
(FileNotFoundException e)
{
e.printStackTrace();
}
catch
(IOException e)
{
e.printStackTrace();
}
return
buffer;
}
|
字节流写入文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
/**
*
* byte to file
* @author chao.gao
* @date 2015-6-25 下午4:56:50
* @param buf
* @param filePath
* @param fileName
*/
public
static
File byte2File(
byte
[] buf, String fileName)
{
BufferedOutputStream bos =
null
;
FileOutputStream fos =
null
;
File file =
null
;
try
{
file =
new
File(fileName);
fos =
new
FileOutputStream(file);
bos =
new
BufferedOutputStream(fos);
bos.write(buf);
return
file;
}
catch
(Exception e)
{
e.printStackTrace();
}
finally
{
if
(bos !=
null
)
{
try
{
bos.close();
}
catch
(IOException e)
{
e.printStackTrace();
}
}
if
(fos !=
null
)
{
try
{
fos.close();
}
catch
(IOException e)
{
e.printStackTrace();
}
}
}
return
file;
}
|
以及文件的zip打包:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public
static
File genZip(File[] file1){
try
{
File filezip =
new
File(
"download.zip"
);
ZipOutputStream out =
new
ZipOutputStream(
new
FileOutputStream(
filezip));
byte
[] buffer =
new
byte
[
1024
];
for
(
int
i =
0
; i < file1.length; i++) {
FileInputStream fis =
new
FileInputStream(file1[i]);
out.putNextEntry(
new
ZipEntry(file1[i].getName()));
int
len;
// 读入需要下载的文件的内容,打包到zip文件
while
((len = fis.read(buffer)) >
0
) {
out.write(buffer,
0
, len);
}
out.closeEntry();
fis.close();
file1[i].delete();
}
out.close();
return
filezip;
}
catch
(Exception e) {
}
return
null
;
}
|