[IBM DW] GAE 应用文件上传的三种方法

红薯 发布于 2010/11/01 11:01
阅读 1K+
收藏 3

简介: 文件上传 / 下载是应用开发中非常常见的需求,尤其是 CMS、博客系统。各种语言都有自己的解决方法。Google App Engine(以下简称 GAE)作为云计算的平台,为应用提供了广阔的扩展空间,运行于其上的应用也越来越多。那么在 GAE 上的应用如何实现文件的上传 / 下载呢?本文将向读者介绍三种实现方式,以及相关注意事项。

在应用开发中,常会需要上传 / 下载各种类型的文件,尤其是 CMS、博客系统。各种语言都有自己的解决方法。Google App Engine(以下简称 GAE)作为云计算的平台,为应用提供了广阔的扩展空间,运行于其上的应用也越来越多。那么在 GAE 上的应用如何实现文件的上传 / 下载呢?本文将给读者介绍三种解决方法,及其使用时需要注意的内容。

本文使用的开发环境如下:

  • Grails 1.3.3;
  • Google App Engine 1.3.1;
  • app-engine-0.8.10;
  • gorm-jpa-0.7.1;
  • GAEvfs 0.3

非 GAE 应用中的文件上传

如果你的 Grails 应用不需要部署在 GAE 上,那么要实现文件上传就非常简单。在你的 GSP 页面中,使用 <g:form> 或者 <g:uploadform> 就能完成,示例代码如下:


清单 1. 文件上传 GSP 示例代码

				
<g:uploadForm action="save" method="post" >
<input type="file" name="filename" />
</g:uploadForm>
<!-- 或者 -->
<g:form action="save" method="post" enctype="multipart/form-data">
<input type="file" name="filename" />
</g:form>

 

在 Controller 中,通过 request 获得上传文件的类型为 org.springframework.web.multipart.commons.CommonsMultipartFile。对于文件内容,可以 以 byte 数组或者 BLOB 的形式保存在数据库中,或者干脆保存在文件系统中。


清单 2. 将文件保存至文件系统

 def file=request.getFile('filename') 
def filename= file.originalFilename
uploadInstance.filename=filename
def dir= new File(System.getProperty("user.dir")+"/attachments");
if(!dir.exists()){
dir.mkdirs()
}
def destFile= new File("${dir}\\"+filename)
if(destFile.exists()){
destFile.delete()
}
file.transferTo(destFile)

至于将文件内容以 byte 数组或者 BLOB 的形式保存至数据库,那就再简单不过了。这里就不赘述。

MultipartResolver

如果你心急,直接将上述代码应用在 GAE 环境下。抱歉,文件无法上传!在 GSP 页面提交后,就会遇到 java.lang.NoClassDefFoundError: java.rmi.server.UID is a restricted class 这样的错误。显然,java.rmi.server.UID 不在 GAE 的类白名单之列。

查看 CommonsMultipartFile源代码, 你会发现,它除了使用 UID 之外,还将上传的文件保存在一个临时目录中,这又犯了 GAE 的大忌 -- 不能使用本地文件系统。看来,CommonsMultipartFile 在 GAE 这边是没法通过审查啊!那么要让 GSP 上传的文件得到 GAE 的认可,就必须对 CommonsMultipartFile 进行改造:不使用 UID 和文件系统。

我们都知道,Grails 使用 Spring 作为其 MVC 框架。Spring 中,在文件上传后,使用 MultipartResolver 对文件进行预处理,并将文件生成为 CommonsMultipartFile。如果你熟悉 MultipartResolver 和 CommonsMultipartFile,可以对它们的代码进行改造,使其使用 GAE 认可的类。如果不熟悉,也不用着急,有现成的 Spring 扩展项目:pjesi/springextras,已经给我们提供了合适的 MultipartResolver 和 MultipartFile,这个项目将 Request 中文件的相关属性保存在内存中,这对于 GAE 是允许的。该项目包含如下两个文件:

  • StreamingMultipartResolver:对 request 中的内容进行处理,比如设置上传文件大小的最大值、编码等;
  • StreamingMultipartFile:这就是 type=file 的输入框中提交的文件,其中包含了文件名、文件大小以及文件内容的字节数组。

如下是 StreamingMultipartFile 中获取文件内容的代码:

清单 3. StreamingMultipartFile 中获取文件内容的代码

 public byte[] getBytes() throws IOException { 
if(bytes == null) {
bytes = IOUtils.toByteArray(item.openStream());
}
return bytes;
}

从上述代码,可以看出。文件内容是以 byte 数组的形式保存在内存当中。这是 GAE 所接受的形式。要使用这个项目,需要对 Grails 应用进行如下配置:

  • 将下载的 springextras-1.1.jar 保存至 grailsApp\lib 目录下;
  • 在 grailsApp\grails-app\conf\spring 下的 resources.groovy 文件中,添加如下代码:

清单 4. 示例代码

 beans = { 
multipartResolver(
is.hax.spring.web.multipart.StreamingMultipartResolver)
}

这个时候在 GAE 环境中重新运行 Grails 程序,文件上传时就会不出现 restricted class 的错误了。

使用 Blob 存储文件

好,上传的文件已经可以通过 Request 访问了。那么上传的文件在 GAE 环境中如何保存呢?这也是最关键的部分。在 GAE 中有多种方法。首先从最简单的讲起— Blob 字段。

GAE 中提供了两种 Blob 类型:com.google.appengine.api.datastore.ShortBlobcom.google.appengine.api.datastore.Blob。二者的主要区别是,前者保存的最大数组长度为 500 字节,后者没有大小限制。在开发过程中,可根据实际需要选择使用。下面以 com.google.appengine.api.datastore.Blob 为例,介绍文件的存储与下载。

首先在 Domain class 中,使用 com.google.appengine.api.datastore.Blob 来定义保存文件内容的字段。


清单 5. 定义 Domain class

				
import com.google.appengine.api.datastore.Blob
class File implements Serializable {
......
String filename
Blob content
......
}

 

在保存文件时,通过 Request 获得上传文件,之后获得文件内容数组,构建一个新的 Blob,将这个新的 blob 保存即可。参见如下代码:


清单 6. 保存文件

				
import com.google.appengine.api.datastore.Blob
def save={
......
def fileInstance = new File()
def file = request.getFile( 'myfile' )
File.withTransaction {
fileInstance.filename=file.getName()
Blob blob=new Blob(file.getBytes())
fileInstance.content=blob
fileInstance.save(flush:true)
......
}
}

 

由于文件内容保存为 blob,那么在下载文件的时候,可以通过 byte 数组从输出字节流中下载文件。


清单 7. 下载文件

				
def fileInstance = File.get( params.id )
response.setContentType( "application-xdownload;charset=UTF-8")
response.setHeader("Content-Disposition",
"attachment;filename="+new String(
fileInstance.filename.getBytes("gb2312"), "ISO8859-1" ))
response.getOutputStream() << new ByteArrayInputStream(
fileInstance.content.getBytes())

 

当然,文件的内容也可以直接保存在 byte[] 中,那么 Domain class 就变更为:


清单 8. 修改后的 Domain class

				
class File implements Serializable {
......
String filename
byte[] content
......
}

 

byte[] 相关的保存 / 下载文件的代码跟 blob 类似,这里就不赘述,详细代码可参见参考资料(Easy file uploading in Grails)。

注意事项

使用 Blob 或者 byte 数组保存文件,直接而简单。

多用于上传文件均小于 10K 以及文件之间关系松散的情景中。在使用的时候需要注意如下内容:

  1. 对上传文件的类型没有限制;
  2. 虽然上传文件的大小没有限制,但是对于文件过大,比如兆或百兆的文件不建议采用此法。如果文件过大,在上传时可能会时间太长而引起超时。

GAEvfs

GAEvfsApache Commons VFS的插件,实现了一个可写的分布式虚拟文件系统,目前版本为 0.3。在这个虚拟文件系统中,可以对文件分文件夹保存,文件夹下可以有子文件夹。

名词解释

Block

在这个虚拟的文件系统中,有一个关键概念— Block。Block 是文件保存的最小单元。每个 Block 在 GAE 的数据存储区表现为一个实体,所以每个 Block 理论的最大值为 1M。缺省情况下,Block 的大小为 128k。如下是 Block 的注意事项:

  • 可以使用 GaeVFS.setBlockSize 设置 Block 的大小,单位为 K,范围 1~1023;
  • Block 大小设定后,所有文件都使用这个值,但是可以在每个文件创建之前,单独的为这个文件设置它所使用的 Block 大小,如:GaeVFS.setBlockSize( filename, 8 );
  • 在文件创建完成后,无法更改 Block 的大小。如果执意要执行,则会出现 IO 异常;
  • Block 是文件保存得最小单元,即使文件只有 1K 大小,也会给这个文件分配一个 128K 的 Block,所以在开发时可以根据实际情况,来设置合适的 Block 大小。

GaeFileObject

继承自 AbstractFileObject ,是 GAEvfs 的 Domain class。在 GAEvfs 中,file、folder、Block 都属于 GaeFileObject。如下是各自使用的字段列表。


表 1. 文件(File)用到的字段

字段 类型 说明
Id Key
filetype String File
last-modified Int 最近修改的时间
block-keys List 文件使用的 Block 的 key 值列表
block-size Int 可通过程序设置,缺省值为 128K
content-size Int 文件的大小



表 2. 文件夹(Folder)用到的字段

字段 类型 说明
Id Key
Filetype String Folder
last-modified Int 最近修改的时间
childe-Keys List 文件夹下的所有内容(文件以及文件夹)的 key 的列表



表 3. Block 用到的字段

字段 类型 说明
Id Key
content-blob Blob 用于保存文件内容

 

文件路径

在 GAEvfs 的 jar 文件中 META-INF 目录下有一个名为 vfs-providers.xml 的文件,在这个文件中配置了 GAEvfs 使用的模式名称:gae,那么可以通过如下目录保存和访问文件:gae://path,比如 gae://myfile/,gae://myfile/temp.txt。

使用 GAEvf s

对 GAEvfs 有了大概的了解之后,就来看看如何使用吧!

这里下载 GAEvfs 的 jar 文件,将其放入 grailsApp\web-app\WEB-INF\lib 中。

要使用 GaeVFS 必须先设置 GAEvfs 使用的根目录,通常是设置为工程根目录。示例代码如下:


清单 9. 设置工作目录

				
def rootPath=servletContext.getRealPath( "/" )
GaeVFS.setRootPath( servletContext.getRealPath( "/" ) )

 

工作目录设置好之后,就能将上传的文件保存至虚拟路径下。


清单 10. 保存文件

				
def rootPath=servletContext.getRealPath( "/" )
GaeVFS.setRootPath( servletContext.getRealPath( "/" ) )
try {
def file=params.myfile
def filename=file.getName()
FileObject fileObject =
GaeVFS.resolveFile( "gae://myfile/" + filename )
def out=fileObject.getContent().getOutputStream()
out.write(file.getBytes())
out.close()
}finally {
GaeVFS.clearFilesCache()
}

 

获得 FileObject 之后,可以通过 Delete 方法删除文件。参见示例代码:


清单 11. 删除文件

				
FileObject fileObject =
GaeVFS.resolveFile( "gae://myfile/" + filename )
if(fileObject.exists()){
fileObject.delete()
}

 

在 GAE 中查看数据,你会发现 GAEvfs 中 Block 其实使用的也是 com.google.appengine.api.datastore.Blob。那么 GAEvfs 跟直接使用 com.google.appengine.api.datastore.Blob 存储文件,有什么区别呢?就文件存储方式而言,没什么区别,但是 GAEvfs 有一个很大优点就是能够为文件创建文件夹,就像 Window 的文件系统那样,把文件按照文件夹的形式分类管理。


清单 12. 创建 folder

				
FileObject myFolder = GaeVFS.resolveFile( "gae://myfolder" );
if ( !myFolder.exists() ) {
myFolder.createFolder();
}

 

在下载文件的时候,可以通过 byte 数组从输出字节流中下载文件。


清单 13. 下载文件

				
def down = {
String rootPath = servletContext.getRealPath( "/" );
def rootPathLen = rootPath.length();
if ( System.getProperty( "os.name" ).startsWith( "Windows" ) ) {
rootPathLen++;
}
GaeVFS.setRootPath( rootPath );
try {
FileObject fileObject = GaeVFS.resolveFile("gae://"+params.get("id") );
if ( !fileObject.exists() ) {
……
return
}
def contentType =servletContext.getMimeType(
fileObject.getName().getBaseName() )
response.setContentType(
contentType != null ? contentType :
fileObject.getContent().getContentInfo().getContentType() );
response.getOutputStream()<<fileObject.getContent()
.getInputStream()
response.flushBuffer();
} finally {
GaeVFS.clearFilesCache();
}
}

 

注意事项

之前已经讲过,就文件存储方式而言,Blob 和 GAEvfs 没什么区别。但是如果上传的文件需要分类管理,那么可以选用 GAEvfs,它可以为文件指定文件夹以及创建子文件夹。

如下是使用 GAEvfs 的注意事项:

  1. GAEvfs 对上传文件的类型以及大小没有具体的限制,但同样建议过大的文件不要采用此种方式;
  2. 在 JVM 的同一个线程的同一个时间中,只能允许打开一个文件进行写操作;
  3. 不需要 DataStore 的索引。

BlobStore

在 GAE1.3 中新增了 BlobStore 存储机制,最大可保存 2G 的文件。其中定义了新的数据对象:BlobStore(或称 Blobs),这有别于 datastore 中的 blob 字段。应用程序只有通过上传文件,才能修改 BlobStore 对象。上传保存后的文件被称为 Blobs,应用程序无法直接访问 Blobs,但是可以通过信息实体 -BlobInfo 来操作 Blobs。

BlobStore 中涉及如下对象:

  • BlobstoreServiceFactory:创建 BlobstoreServices;
  • BlobstoreService:用于 Blobs 的创建和获取;
  • BlobInfoFactory:为获取 BlobInfo 的元数据提供了一系列接口;
  • BlobInfo:每个上传的文件都有一个与其相关的 BlobInfo 实体,其中包含了与 Blobs 相关的元数据,比如文件名、文件大小;BlobInfo 对象是只读的,无法通过应用程序进行修改;
  • BlobKey:是 Blobs 的唯一标识,可以通过这个标识获得 BlobInfo 实体。

修改 StreamingMultipartResolver

在本文的前半部分已经将 Grails 的缺省的 MultipartResolver 修改为 StreamingMultipartResolver。如果使用了 BlobStore,这个 StreamingMultipartResolver 就有点问题了,会提示:java.lang.OutOfMemoryError: Java heap space。原因是 StreamingMultipartResolver 预处理后的 Request 以及 StreamingMultipartFile 不是 BlobStore 所需要。BlobStore 所需要的仅仅是原始的 Request。

好,那么就将 StreamingMultipartResolver 中的预处理去掉,给 BlobStore 提供一个原始的 Request。修改 StreamingMultipartResolver 的 resolveMultipart 方法,见如下代码:


清单 13 为 BlobStore 工作的 StreamingMultipartResolver

				
public MultipartHttpServletRequest resolveMultipart(
HttpServletRequest request) throws MultipartException {
Map<String, String[]> multipartParameters =
new HashMap<String, String[]>();
MultiValueMap<String, MultipartFile> multipartFiles =
new LinkedMultiValueMap<String, MultipartFile>();
return new DefaultMultipartHttpServletRequest(
request, multipartFiles, multipartParameters);
}

 

在上述代码中,multipartParameters 和 multipartFiles 都是空的 Map。

这样,StreamingMultipartResolver 已经满足了 BlobSotre 的需求了。下面就开始正式使用 BlobSotre。BlobStore 将上传的路径封装到了服务中,可以通过 BlobstoreService 获得这个路径。见如下代码:


清单 14. 通过 BlobstoreService 获得文件上传的 URL

				
<%@page import="com.google.appengine.api.blobstore
.BlobstoreServiceFactory" %>
<%@page import="com.google.appengine.api.blobstore
.BlobstoreService" %>
<%BlobstoreService blobstoreService =
BlobstoreServiceFactory.getBlobstoreService();
%>
……
<form action="<%=blobstoreService.createUploadUrl("/serve")%>"
method="post" enctype="multipart/form-data">
<input type="file" name="myFile">
<input type="submit" value="Submit">ha
</form>

 

上述代码中,通过 BlobstoreService 获取一个 upload 的 URL,形如:/_ah/upload/agpAYXBwLm5hbWVAchsLEhVfX0Jsb2JVcGxvYWRTZXNzaW9uX18YHww。在这里需要注意如下两点:

  1. createUploadUrl 方法中的参数为回调 Servlet,记住是 Servlet,而不是 Controller 或者 Action;
  2. form 标签的 enctype 属性必须设置成 multipart/form-data。

回调 Servlet

BlobStore 需要一个回调 Servlet,即在文件上传后,程序要继续执行的路径。在文件上传之后,会给每个文件分配一个 BlobKey,作为其唯一标识。可以在回调 Servlet 中通过 BlobstoreService 获得文件的 BlobKey,之后可以将 BlobKey 保存下来或者通过 BlobKey 获得对应得文件。回调 Servlet 见如下示例代码:


清单 14. 回调 Servlet

				
package com.mulan;

import java.io.IOException;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.blobstore.BlobstoreService;
import com.google.appengine.api.blobstore.BlobstoreServiceFactory;

@SuppressWarnings("serial")
public class ServeServlet extends HttpServlet {
private BlobstoreService blobstoreService =
BlobstoreServiceFactory.getBlobstoreService();

public void doPost(
HttpServletRequest req,
HttpServletResponse res)throws IOException {
Map<String, BlobKey> blobs =
blobstoreService.getUploadedBlobs(req);
// 获得 BlobKey 之后,可以对其进行处理,比如保存
BlobKey blobKey = blobs.get("myfile");
if (blobKey == null) {
res.sendRedirect("/"); }
else {
res.sendRedirect("/serve?blob-key="
+ blobKey.getKeyString()); }
}

public void doGet(
HttpServletRequest req,
HttpServletResponse res)throws IOException {
BlobKey blobKey = new BlobKey(
req.getParameter("blob-key"));
// 获得 BlobKey 之后,通过如下方法获得文件
blobstoreService.serve(blobKey, res);
}
}

完成了回调 Servlet 后,需要在 web.xml 中声明一下。由于 Grails 中的 web.xml 是由缺省模板生成的,如果要自定义 web.xml 就要先使用命令:grails install-templates 安装模板,之后在 src/templates/war/web.xml 中添加如下代码,这样应用所使用的 web.xml 就会依据修改后的模板生成:


清单 15. 修改后的 web.xml

 <servlet> 
<servlet-name>serve</servlet-name>
<servlet-class>com.mulan.ServeServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>serve</servlet-name>
<url-pattern>/serve</url-pattern>
</servlet-mapping>

 

获取 BlobInfo

前面已经提到每个上传的文件都有一个 BlobInfo 用来保存其元数据,比如文件名、文件大小、文件类型等。如下是通过 BlobKey 获得 BlobInfo 的示例代码:


清单 16. 获得 BlobInfo

 BlobInfoFactory bf=newBlobInfoFactory(); 
BlobInfo bi=bf.loadBlobInfo(blobKey);
//bi. getFilename() 获得文件名
//bi.getSize() 获得文件大小
//bi.getContentType() 获得文件类型
//bi.getCreation() 获得文件创建时间

 

注意事项

BlobStore 是 GAE 收费的项目,在本地开发环境下,BlobStore 是可以正常工作的。但是如果应用程序部署到 appengine.appspot.com 上,付费后方可使用。文件上传速度较之前两种方式要快些。如下是使用 BlobStore 时的注意事项:

  1. BlobStore 将文件上传的功能封装成了服务,一旦遇到技术问题,没法查看源代码。
  2. BlobStore 目前还是 GAE 的测试功能,所以在使用的时候会有一些不便。所以选择使用 BlobStore 的时候一定要慎重。
  3. 通过 BlobStore 上传的文件可以在 GAE 应用管理控制台的 Data-->Blob Viewer 中查看。
  4. 对上传文件的类型没有限制。
  5. 在数据保存入 datasore 前没法做任何预处理,其次后期处理数据后不能直接存回去。

总结

本文介绍 Grails 下 GAE 应用上传文件的三种方法。由于 GAE 没有明确声明支持文件上传的功能,所以依靠开发者的想象力可以有很多实现方法。本文只是抛砖引玉,介绍了其中的三种方法及使用时需要注意的内容。在实际开 发中,我们可以根据应用的具体需求进行选择。当然,也希望 BlobStore 能够走出实验室,为我们这些个 GAE 的粉丝们提供更强大的功能!最好能够免费 !:)

加载中
返回顶部
顶部