网上关于Struts2文件下载的文章太多了,但对于新手来说,要查阅到使用完整的Demo却是非常困难,或者说实现“成功”下载后便置之不理了,但当项目投入生产环境后,出了问题却无法解决,笔者根据自己的经验,下面介绍较为完整的可用于实际项目的Struts2文件下载Demo。
1.Struts文件下载常见配置(基础配置)
<!-- 处理文件下载请求的action -->
<action name="fileDownload" class="my.pro.FileDownloadAction">
<!-- 对于文件下载,这里的type必须是stream -->
<result type="stream" name="downLoad">
<!-- 文档类型,下面这种是通用类型,适用于任何文档类型 -->
<param name="contentType">application/octet-stream</param>
<!-- 输入流名称 类my.pro.DownLoadFile中与之对应的方法为getTarget(),且返回值为InputStream -->
<param name="inputName">targetFile</param>
<!-- 下载时的文件名 -->
<param name="contentDisposition">attachment;filename="${fileName}"</param>
<!-- 缓冲区大小 -->
<param name="bufferSize">4096</param>
</result>
</action>
2.下载类FileDownloadAction的写法
public class FileDownloadAction extends ActionSupport{
private static final long serialVersionUID = 1L;
//该属性是依赖注入的属性,该属性可以在配置文件中动态指定该属性值
private String fileName;
//依赖注入该属性值的setter方法
public void setFileName(String fileName) {
this.fileName = fileName;
}
/*
下载用的Action应该返回一个InputStream实例,
该方法对应在result里的inputName属性值为targetFile
*/
public InputStream getTargetFile() throws Exception
{
/*可以放入业务逻辑处理*/
return ServletActionContext.getServletContext().getResourceAsStream(fileName);
}
//处理用户请求的execute方法,该方法返回success字符串
@Override
public String execute() throws Exception
{
return SUCCESS;
}
public String getFileName() {
return fileName;
}
}
这里得介绍下ServletActionContext.getServletContext().getResourceAsStream( )了,这里用不用此方法得取决了你的文件存放位置,这里不是指绝对路径,而是相对于classpath环境变量的相对路径。在开发环境下,一般src目录包含在classpath下的,在tomcat下,一般指向WEB-INF/classes目录,不过通常情况下,开发者们都不想将待下载的文档置于网站目录下,而是放在服务器的某个路径下便于管理,这样就必须在tomcat中设置context参数指向这个路径。如果读者不想这样做,而是想在下载时直接打开服务器上的文件绝对路径,就必须换个方法了,如下:
public InputStream getTargetFile() throws Exception
{
/*可以放入业务逻辑处理*/
File file = new File("文件绝对路径");
/*这里可考虑算法的健壮性,如文件不存在时的处理,当然用try catch了*/
return new FileInputStream(file);
}
通过上面的介绍,读者可以实现的下载了,当然很多初学者弄到这步也就沾沾自喜了。不过等待着的将是一个让人头疼的异常,有人为此奋战许久。那是什么异常呢,先卖个关子。我们先说一个用户行为,当我们在下载文件时,如果发现网速太慢或是其他情况,通常会取消下载。下面要说的就是这个问题,用Struts2实现文件下载时点击取消带来的异常(保存或打开不会出现),如下:
严重: Servlet.service() for servlet default threw exception
java.lang.IllegalStateException
at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:407)
at javax.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:108)
at com.opensymphony.module.sitemesh.filter.PageResponseWrapper.sendError(PageResponseWrapper.java:176)
或者:Broken pipe
为什么会出现此异常呢?下面讲讲原理(理解原理,对于解决问题是必要的),在Struts2的配置文件中,对于下载的配置参数用的是stream,stream对应的类是org.apache.struts2.dispatcher.StreamResult,其处理过程是:
(1)配置其中result标签下的各个参,如:
<param name="contentType">application/octet-stream</param>
<!-- 输入流名称 类my.pro.DownLoadFile中与之对应的方法为getTarget(),且返回值为InputStream -->
<param name="inputName">targetFile</param>
<!-- 下载时的文件名 -->
<param name="contentDisposition">attachment;filename="${fileName}"</param>
<!-- 缓冲区大小 -->
<param name="bufferSize">4096</param>
(2)当请求文件下载后,服务器响应后开始获取输入流,并同时与客户端建立输出流,服务器与客户端链接通过Socket进行连接。
(3)数据开始传输。在传输的过程中,如果点击了“取消”,代表关闭了所有的流,但实际上Socket没有断开,就是流还没有关闭,所以在JSP容器通过Response获取输出流之前,前面的流并没有关闭,故而出现异常。
下面给出解决办法。
3. Struts2下载文件点击取消出现的异常解决办法
(1)逃避办法
这是一种异常拦截方法,当发生ClientAbortException异常时,跳转到指定的页面,而这个页面什么都不用输出,实际上异常依然发生,只不过不显示,当看不见,有点“掩耳盗铃”的意思,所以叫“逃避办法”,笔者不推荐这个方,毕竟治标不治本。示例如下:
<package name="default" extends="struts-default">
<global-results>
<result name="client-abort-exception">err.jsp</result>
</global-results>
</package>
<package name="mypck" extends="struts-default">
<exception-mapping result="client-abort-exception" exception="org.apache.catalina.connector.ClientAbortException"/>
<action name="download" class="my.pro.FileDownloadAction">
<result name="success" type="stream">
<para mname="inputName">targetFile</param>
<param name="contentDisposition">filename=""</param>
<param name="buffersize">4096</param>
</result>
</action>
</package>
(2)根治方法
前面分析了tream对应的类是org.apache.struts2.dispatcher.StreamResult的执行过程,要彻底解决这个问题,还需从它入手。如果将其换成能代替的类,并且这个类不会出现这个异,原因是点击“取消”的同时关闭流,不会再报出该异常,就可以根治了。这里用struts2-sunspoter-stream-1.0.jar代替之,注意是1.0。方法如下:
第一步:先下载struts2-sunspoter-stream-1.0.jar,并复制到项目的/WEB-INF/lib下。下载地址为:http://download.youkuaiyun.com/detail/xiangchengguan/8010633
第二步:在原有的struts.xml的基础上进行相应的配置,先在〈package〉包的第一个节点前(否则会出错)插入<result-types>,如下:
<package name="default" namespace="/"extends="struts-default">
<!-- 添加如下内容 -->
<result-types>
<result-type name="streamx"class="com.sunspoter.lib.web.struts2.dispatcher.StreamResultX"/>
</result-types>
……
</package>
接下来就是将下载类配置中的stream换成streamx啦,共他不变,如下:
<!-- 处理文件下载请求的action -->
<action name="fileDownload" class="my.pro.FileDownloadAction">
<!-- 对于文件下载,这里的type必须是stream -->
<result type="streamx" name="downLoad">
<!-- 文档类型,下面这种是通用类型,适用于任何文档类型 -->
<param name="contentType">application/octet-stream</param>
<!-- 输入流名称 类my.pro.DownLoadFile中与之对应的方法为getTarget(),且返回值为InputStream -->
<param name="inputName">targetFile</param>
<!-- 下载时的文件名 -->
<param name="contentDisposition">attachment;filename="${fileName}"</param>
<!-- 缓冲区大小 -->
<param name="bufferSize">4096</param>
</result>
</action>
这样就算大功告成了,点击“取消”的同时也关闭了流,不会再报出该异常。如果配置了log4j.properties,当用户点击“取消”时,将出现如下警告,Socket非正常中断,但流已经关闭。如下:
WARN StreamResult:45 - StreamResultX Warn : socket write error