bigtable和gfs

    技术2024-03-26  94

    由于它们一直在转储字节,因此很容易理会磁盘驱动器和它们上的文件系统。 编写文件时,除了位置,权限和空间要求之外,您无需考虑太多其他内容。 您只需构造一个java.io.File即可开始工作; 无论您是台式计算机,Web服务器还是移动设备, java.io.File工作原理都相同。 但是,当您开始使用Google App Engine(GAE)时,这种透明性或缺乏透明性很快就会变得很明显。 在GAE中,您无法将文件写入磁盘,因为没有可用的文件系统。 实际上,声明java.io.FileInputStream会引发编译错误,因为该类已从GAE SDK列入黑名单。

    幸运的是,生活有很多选择,而且GAE提供了一些特别强大的存储选项。 由于GAE是从头开始设计的,因此考虑了可伸缩性,因此它提供了两个键值存储:数据存储区(又名Bigtable)保存通常在数据库中抛出的常规数据,而Blobstore保存巨大的二进制Blob。 两者都具有固定时间的访问权限,并且两者完全不同于您过去使用过的文件系统。

    除了这两个之外,还有一个新来的东西:Google Storage for Developers。 它的工作方式类似于Amazon S3,它也与传统文件系统明显不同。 在本文中,我们将构建一个示例应用程序,该应用程序依次实现每个GAE存储选项。 您将获得使用Bigtable,Blobstore和Google Storage for Developers的动手经验,并且将了解每种实现的优缺点。

    你需要什么

    您需要一个GAE帐户和几个免费的开源工具来完成本文中的示例。 对于您的开发环境,您将需要JDK 5或JDK 6以及用于Java™开发人员的Eclipse IDE 。 您还需要:

    Google Eclipse插件 Apache Commons FileUpload Objectify-Appengine

    目前,开发人员专用的Google存储空间仅在美国有限的开发人员中可用。 如果您无法立即获得对Google Storage的访问权限,则仍然可以按照Bigtable和Blobstore的示例进行操作,从而可以很好地了解Google Storage的工作方式。

    初步设置:示例应用程序

    在开始探索GAE存储系统之前,我们需要创建示例应用程序所需的三个类:

    代表照片的豆 。 Photo包含标题和标题等字段,以及一些用于存储二进制图像数据的字段。 将Photo持久保存到GAE数据存储( 又称为 Bigtable)的DAO 。 DAO包含一种用于插入Photo的方法,另一种用于通过ID将其拉回的方法。 它使用名为Objectify-Appengine的开源库来实现持久性。 一个使用Template Method模式封装三步工作流的servlet 。 我们将使用工作流探索每个GAE存储选项。

    申请流程

    我们将按照相同的步骤来了解每个GAE数据存储选项; 这将使您有机会专注于技术,并比较每种存储方法的优缺点。 每次应用程序工作流程都相同:

    显示上载表格。 将图像上传到存储设备,然后将记录保存到数据存储中。 整理图像。

    图1是应用程序工作流程的示意图:

    图1.用于演示每个存储选项的三步工作流

    另外一个好处是,该示例应用程序还使您可以练习对于写出并提供二进制文件的任何GAE项目都至关重要的任务。 现在,让我们开始创建这些类!

    GAE的简单应用

    如果没有,请下载Eclipse ,然后安装Eclipse的Google插件并创建一个不使用GWT的新Google Web Application项目。 请参阅本文随附的示例代码,以获取有关构建项目文件的指导。 设置好Google Web应用程序之后,添加应用程序的第一类Photo ,如清单1所示。(请注意,我省略了getter和setters。)

    清单1.照片
    import javax.persistence.Id; public class Photo { @Id private Long id; private String title; private String caption; private String contentType; private byte[] photoData; private String photoPath; public Photo() { } public Photo(String title, String caption) { this.title = title; this.caption = caption; } // getters and setters omitted }

    @Id注释指定哪个字段是主键,当我们开始使用Objectify时,这将很重要。 保存在数据存储区中的每个记录(也称为实体)都需要一个主键。 上载图像时,一种选择是将其直接存储在photoData ,该数据是一个字节数组。 它会与其他Photo域一起作为Blob属性写入数据存储区。 换句话说,图像被保存并直接在bean旁边获取。 如果改为将图像上传到Blobstore或Google Storage,则字节将存储在该系统的外部,并且photoPath指向其位置。 两种情况下仅使用photoData或photoPath 。 图2阐明了每个人的功能:

    图2. photoData和photoPath的工作方式

    接下来,我们将处理bean的持久性。

    基于对象的持久性

    如前所述,我们将使用Objectify为Photo bean创建一个DAO。 尽管JDO和JPA可能是更流行和普遍存在的持久性API,但它们的学习曲线更为陡峭。 另一个选择是使用低级GAE数据存储区API,但这涉及到往返于数据存储区实体的Bean的繁琐工作。 Objectify通过Java反射为我们解决了这一问题。 (请参阅相关主题 ,以了解更多关于GAE持久性替代品,包括物化-的AppEngine)。

    首先创建一个名为PhotoDao的类并对其进行编码,如清单2所示:

    清单2. PhotoDao
    import com.googlecode.objectify.*; import com.googlecode.objectify.helper.DAOBase; public class PhotoDao extends DAOBase { static { ObjectifyService.register(Photo.class); } public Photo save(Photo photo) { ofy().put(photo); return photo; } public Photo findById(Long id) { Key<Photo> key = new Key<Photo>(Photo.class, id); return ofy().get(key); } }

    PhotoDao扩展了DAOBase ,该类是延迟加载Objectify实例的便利类。 Objectify是我们与API的主要接口,并通过ofy方法公开。 但是,在使用ofy之前,我们需要在静态初始化程序中注册持久性类,如清单2中的 Photo 。

    DAO包含两种用于插入和查找Photo的方法。 在每种情况下,使用Objectify就像处理哈希表一样简单。 你可能会注意到, Photo s为获取与Key在findById ,但不要担心:对于本文的目的,只是觉得Key是围绕着一个包装id领域。

    现在,我们有一个Photo Bean和一个PhotoDao来管理持久性。 接下来,我们将充实应用程序的工作流程。

    应用程序工作流程,通过模板方法模式

    如果您曾经玩过疯癫狂,那么模板方法模式对您来说很有意义。 每个Mad Lib都会通过一个空白点来介绍一个故事,以供读者填写。 读者的输入-空白点的完成方式-极大地改变了故事。 同样,使用“模板方法”模式的类包含一系列步骤,有些则留为空白。

    我们将构建一个使用Template Method模式的servlet,以执行示例应用程序的工作流程。 首先,存根一个抽象servlet并将其命名为AbstractUploadServlet 。 您可以使用清单3中的代码作为参考:

    清单3. AbstractUploadServlet
    import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.*; @SuppressWarnings("serial") public abstract class AbstractUploadServlet extends HttpServlet { }

    接下来,添加清单4中的三个抽象方法。每个方法代表工作流程中的一个步骤。

    清单4.三种抽象方法
    protected abstract void showForm(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException; protected abstract void handleSubmit(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException; protected abstract void showRecord(long id, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException;

    现在,假设我们使用的是Template Method模式,那么将清单4中的方法视为空白,并将清单5中的代码视为组装它们的故事:

    清单5.工作流出现
    @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("display".equals(action)) { // don't know why GAE appends underscores to the query string long id = Long.parseLong(req.getParameter("id").replace("_", "")); showRecord(id, req, resp); } else { showForm(req, resp); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { handleSubmit(req, resp); }

    关于Servlet的提醒

    以防万一,因为您使用普通的旧servlet已经有一段时间了,所以doGet和doPost是用于处理HTTP GET和POST的标准方法。 通常,使用GET来获取Web资源并使用POST来发送数据。 本着这种精神,我们的doGet实现要么显示上传表单,要么显示存储中的照片,而doPost处理上传表单的提交。 由扩展AbstractUploadServlet类来定义每个行为。 图3中的图显示了发生的事件的顺序。 可能需要花费几分钟才能清楚了解正在发生的事情。

    图3.序列图中的工作流程

    构建了三个类之后,我们的示例应用程序已准备就绪。 现在,我们可以集中精力查看从Bigtable开始的每个GAE存储选项如何与应用程序工作流程交互。

    GAE存储选项1:Bigtable

    Google的GAE文档将Bigtable描述为分片的,排序的数组,但我发现将其视为在数十亿台服务器中分出的巨型哈希表比较容易。 像关系数据库一样,Bigtable具有数据类型。 实际上,Bigtable数据库和关系数据库都使用blob类型来存储二进制文件。

    不要将Blob类型与Blobstore混淆,这是GAE的另一个键值存储,我们将在下面进行探讨。

    在Bigtable中使用Blob最方便,因为它们与其他字段一起加载,因此可以立即使用。 一个最大的警告是,blob不能大于1MB,尽管将来可能会放宽该限制。 如今,您很难找到比它小的照片的数码相机,因此对于涉及图像的任何用例,使用Bigtable都会带来一个缺点(就像我们的示例应用程序那样)。 如果您现在对1MB的规则还可以,或者如果您要存储小于映像的文件,那么Bigtable可能是一个不错的选择:在三种GAE存储替代方案中,使用最简单。

    在将数据上传到Bigtable之前,我们需要创建一个上传表单。 然后,我们将完成servlet的实现,该实现包括为Bigtable定制的三种抽象方法。 最后,我们将实现错误处理,因为人们很容易突破1MB的限制。

    创建上传表单

    图4显示了Bigtable的上传表单:

    图4. Bigtable的上传表单

    要创建此表单,请从一个名为datastore.jsp的文件开始,然后插入清单6中的代码块:

    清单6.自定义上传表单
    <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <form method="POST" enctype="multipart/form-data"> <table> <tr> <td>Title</td> <td><input type="text" name="title" /></td> </tr> <tr> <td>Caption</td> <td><input type="text" name="caption" /></td> </tr> <tr> <td>Upload</td> <td><input type="file" name="file" /></td> </tr> <tr> <td colspan="2"><input type="submit" /></td> </tr> </table> </form> </body> </html>

    表单必须将其方法属性设置为POST ,并且附件类型为multipart / form-data。 由于未指定action属性,因此表单将提交给自己。 通过POST ,我们最终到达AbstractUploadServlet的doPost ,后者依次调用handleSubmit 。

    我们已经有了表单,因此让我们继续后面的servlet。

    与Bigtable进行上传

    在这里,我们依次实现这三种方法。 一个显示我们刚创建的表单,另一个显示上载的表单。 最后一种方法将上传的内容提供给我们,以便您可以了解如何完成。

    该servlet使用Apache Commons FileUpload库 。 下载它及其依赖项,并将其包括在您的项目中。 完成之后,敲出清单7中的存根:

    清单7. DatastoreUploadServlet
    import info.johnwheeler.gaestorage.core.*; import java.io.*; import javax.servlet.ServletException; import javax.servlet.http.*; import org.apache.commons.fileupload.*; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.util.Streams; @SuppressWarnings("serial") public class DatastoreUploadServlet extends AbstractUploadServlet { private PhotoDao dao = new PhotoDao(); }

    这里没有什么太令人兴奋的事情了。 我们导入所需的类,并构造一个PhotoDao供以后使用。 在实现抽象方法之前, DatastoreUploadServlet不会编译。 让我们从清单8中的showForm开始逐步介绍每个步骤:

    清单8. showForm
    @Override protected void showForm(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getRequestDispatcher("datastore.jsp").forward(req, resp); }

    如您所见, showForm只是转发到我们的上传表单。 清单9中所示的handleSubmit涉及更多:

    清单9. handleSubmit
    @Override protected void handleSubmit(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ServletFileUpload upload = new ServletFileUpload(); try { FileItemIterator it = upload.getItemIterator(req); Photo photo = new Photo(); while (it.hasNext()) { FileItemStream item = it.next(); String fieldName = item.getFieldName(); InputStream fieldValue = item.openStream(); if ("title".equals(fieldName)) { photo.setTitle(Streams.asString(fieldValue)); continue; } if ("caption".equals(fieldName)) { photo.setCaption(Streams.asString(fieldValue)); continue; } if ("file".equals(fieldName)) { photo.setContentType(item.getContentType()); ByteArrayOutputStream out = new ByteArrayOutputStream(); Streams.copy(fieldValue, out, true); photo.setPhotoData(out.toByteArray()); continue; } } dao.save(photo); resp.sendRedirect("datastore?action=display&id=" + photo.getId()); } catch (FileUploadException e) { throw new ServletException(e); } }

    这是一长行代码,但是它的作用很简单。 handleSubmit方法以流形式上传表单的请求主体,将每个表单值提取到FileItemStream 。 同时,一次设置一张Photo 。 遍历每个字段并检查是什么有点笨拙,但这就是通过流数据和流API完成的。

    回到代码,当我们进入文件字段时, ByteArrayOutputStream协助将上传的字节添加到photoData 。 最后,我们使用PhotoDao保存Photo并发送重定向,这使我们进入最终的抽象类清单10中的showRecord :

    清单10. showRecord
    @Override protected void showRecord(long id, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Photo photo = dao.findById(id); resp.setContentType(photo.getContentType()); resp.getOutputStream().write(photo.getPhotoData()); resp.flushBuffer(); }

    showRecord在直接将photoData字节数组写入HTTP响应之前,查找Photo并设置内容类型标头。 flushBuffer将所有剩余内容强制发送到浏览器。

    我们需要做的最后一件事是为大于1MB的上载添加一些错误处理代码。

    显示错误信息

    如前所述,Bigtable施加了1MB的限制,这是在大多数涉及图像的用例中都无法打破的挑战。 充其量,我们可以告诉用户调整图像大小并重试。 出于演示目的,清单11中的代码仅在引发GAE异常时显示一条异常消息。 (请注意,这是标准的servlet规范错误处理,并非特定于GAE。)

    清单11.发生了一个错误
    import java.io.*; import javax.servlet.ServletException; import javax.servlet.http.*; @SuppressWarnings("serial") public class ErrorServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String message = (String) req.getAttribute("javax.servlet.error.message"); PrintWriter out = res.getWriter(); out.write("<html>"); out.write("<body>"); out.write("<h1>An error has occurred</h1>"); out.write("<br />" + message); out.write("</body>"); out.write("</html>"); } }

    不要忘记在web.xml中注册ErrorServlet以及我们将在本文中创建的其他Servlet。 清单12中的代码注册了一个错误页面,该页面指向ErrorServlet :

    清单12.注册错误
    <servlet> <servlet-name>errorServlet</servlet-name> <servlet-class> info.johnwheeler.gaestorage.servlet.ErrorServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>errorServlet</servlet-name> <url-pattern>/error</url-pattern> </servlet-mapping> <error-page> <error-code>500</error-code> <location>/error</location> </error-page>

    到此结束对Bigtable(也称为GAE数据存储)的快速介绍。 Bigtable可能是GAE存储选项中最直观​​的选项,但是它的缺点是文件大小:每个文件只有1MB,您可能不想将其用于任何大于缩略图的文件(如果有的话)。 接下来是Blobstore,这是另一个键值存储选项,可以保存和提供最大2GB的文件。

    GAE存储选项2:Blobstore

    Blobstore具有比Bigtable更大的大小优势,但它并非没有其自身的问题:即,它迫使使用一次性上传URL的事实,而该URL难以构建Web服务。 这是一个看起来像的例子:

    /_ah/upload/aglub19hcHBfaWRyGwsSFV9fQmxvYlVwbG9hZFNlc3Npb25fXxh9DA

    Web服务客户端必须要求之前的URL POST荷兰国际集团给它,这样不仅可以使导线的额外调用。 在许多应用程序中,这可能没什么大不了的,但是还不够完美。 如果客户端在GAE上运行且CPU时间是可计费的,则它也可能是禁止的。 如果您认为可以通过构建一个通过URLFetch将上载转发到一次性URL的servlet来解决这些问题,请三思。 URLFetch的传输限制为1MB,因此,如果您朝着这个方向前进,不妨使用Bigtable。 作为参考,图5中的图形显示了一个和两个分支的Web服务调用之间的区别:

    图5.一站式和二站式的Web服务调用之间的区别

    Blobstore有其优点和缺点,在接下来的部分中,您将自己了解更多内容。 我们将再次构建一个上传表单,并实现AbstractUploadServlet提供的三种抽象方法-但这次我们将针对Blobstore调整代码。

    Blobstore的上传表单

    重新使用Blobstore的上载表单没有什么用:只需将datastore.jsp复制到一个名为blobstore.jsp的文件,然后使用清单13中所示的粗体代码行进行扩充:

    清单13. blobstore.jsp
    <% String uploadUrl = (String) request.getAttribute("uploadUrl"); %><html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <form method="POST" action="<%= uploadUrl %>" enctype="multipart/form-data"> <!-- labels and fields omitted --> </form> </body> </html>

    一次性上传网址是在Servlet中生成的,接下来我们将对其进行编码。 在这里,该URL是从请求中解析出来的,并放置在表单的action属性中。 我们无法控制要上传到的Blobstore servlet,那么如何获取其他表单值? 答案是Blobstore API具有回调机制。 生成一次性URL时,我们会将回调URL传递给API。 上载后,Blobstore会调用回调,并将原始请求与所有上载的Blob一起传递。 接下来,当我们实现AbstractUploadServlet您将看到所有这些操作。

    上载到Blobstore

    首先使用清单14作为参考,以存根名为BlobstoreUploadServlet的类,该类扩展了AbstractUploadServlet :

    清单14. BlobstoreUploadServlet
    import info.johnwheeler.gaestorage.core.*; import java.io.IOException; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.*; import com.google.appengine.api.blobstore.*; @SuppressWarnings("serial") public class BlobstoreUploadServlet extends AbstractUploadServlet { private BlobstoreService blobService = BlobstoreServiceFactory.getBlobstoreService(); private PhotoDao dao = new PhotoDao(); }

    最初的类定义与我们对DatastoreUploadServlet所做的定义相似,但是增加了BlobstoreService变量。 这就是清单15中的showForm生成一次性URL的showForm :

    清单15. blobstore的showForm
    @Override protected void showForm(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String uploadUrl = blobService.createUploadUrl("/blobstore"); req.setAttribute("uploadUrl", uploadUrl); req.getRequestDispatcher("blobstore.jsp").forward(req, resp); }

    清单15中的代码创建一个上传URL并根据请求进行设置。 然后,代码将转发到清单13中创建的表单,在该表单中应有上载URL。 回调URL设置为在web.xml中定义的该servlet的上下文。 这样,当Blobstore POST返回时,我们进入handleSubmit ,如清单16所示:

    清单16. Blobstore的handleSubmit
    @Override protected void handleSubmit(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Map<String, BlobKey> blobs = blobService.getUploadedBlobs(req); BlobKey blobKey = blobs.get(blobs.keySet().iterator().next()); String photoPath = blobKey.getKeyString(); String title = req.getParameter("title"); String caption = req.getParameter("caption"); Photo photo = new Photo(title, caption); photo.setPhotoPath(photoPath); dao.save(photo); resp.sendRedirect("blobstore?action=display&id=" + photo.getId()); }

    getUploadedBlobs返回Map的BlobKeys 。 因为我们的上传表单支持单个上传,所以我们得到了我们期望的唯一BlobKey ,并将其字符串表示形式填充到photoPath变量中。 然后,将其余字段解析为变量,并在新的Photo实例上进行设置。 然后将该实例保存到数据存储中,然后重定向到清单17中的showRecord :。

    清单17. blobstore的showRecord
    @Override protected void showRecord(long id, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Photo photo = dao.findById(id); String photoPath = photo.getPhotoPath(); blobService.serve(new BlobKey(photoPath), resp); }

    在showRecord ,我们刚刚保存在handleSubmit的Photo从Blobstore重新加载。 上载内容的实际字节不会像存储在Bigtable中那样存储在Bean中。 而是使用photoPath重建了BlobKey ,并将其用于向浏览器提供图像。

    Blobstore使处理基于表单的上载变得很轻松,但是基于Web服务的上载则完全不同。 接下来,我们将介绍Google Storage for Developers,它给我们带来了完全相反的难题:基于表单的上传需要一点技巧,而基于服务的上传则很容易。

    GAE存储选项3:Google存储

    Google Storage for Developers是三个GAE存储选项中功能最强大的,一旦您清除了一些杂物,就很容易使用。 Google存储与Amazon S3有很多共同点。 实际上,两者都使用相同的协议并具有相同的RESTful接口,因此与S3兼容的库(如JetS3t)也与Google Storage兼容。 不幸的是,在撰写本文时,这些库无法在Google App Engine上可靠地工作,因为它们执行不允许的操作,例如生成线程。 因此,目前,我们只需要使用RESTful接口,并完成这些API否则需要做的一些繁重工作。

    Google Storage值得为此烦恼,主要是因为它通过访问控制列表(ACL)支持强大的访问控制。 使用ACL,可以授予对对象的只读和读写访问权限,因此您可以轻松地将照片公开或私有化,就像Facebook和Flickr上的照片一样。 ACL不在本文讨论范围之内,因此我们将上载的所有内容都将被授予公共的只读访问权限。 看到谷歌在线存储文档( 相关信息 ),以了解更多关于ACL。

    关于Google存储

    Google Storage for Developers于2010年5月作为预览版发布,目前仅在美国有限的开发人员中可用,并且有一个等待预览的清单。 Google Storage仍处于起步阶段,也带来了一些实施方面的挑战,我将在本节中解决这一问题。 在Google Storage和GAE之间没有明确的集成路径意味着需要额外的编码,但是对于某些用例(例如需要访问控制的用例)来说,这是值得的。 我希望我们会在不久的将来看到集成库。

    与Blobstore不同,默认情况下,Google存储与Web服务和浏览器客户端兼容。 数据通过RESTful PUT或POST 。 第一个选项适用于可控制请求的结构和标头编写方式的Web服务客户端。 我们将在这里探讨的第二个选项是基于浏览器的上传。 我们需要一个JavaScript hack来处理上传表单,这会带来一些复杂性,如您所见。

    入侵Google存储空间上传表单

    与Blobstore不同,Google存储空间在发布到POST后不会转发到回调URL。 相反,它将发出重定向到我们指定的URL。 这就带来了一个问题,因为表单值没有通过重定向传递。 解决此问题的方法是在同一网页中创建两个表单-一个包含标题和标题文本字段,另一个包含文件上载字段和必需的Google Storage参数。 然后,我们将使用Ajax提交第一个表单。 调用Ajax回调后,我们将提交第二个上传表单。

    由于这种形式比后两种形式更为复杂,因此我们将逐步构建它。 首先,我们提取由尚未构建的转发Servlet设置的一些值,如清单18所示:

    清单18.提取表单值
    <% String uploadUrl = (String) request.getAttribute("uploadUrl"); String key = (String) request.getAttribute("key"); String successActionRedirect = (String) request.getAttribute("successActionRedirect"); String accessId = (String) request.getAttribute("GoogleAccessId"); String policy = (String) request.getAttribute("policy"); String signature = (String) request.getAttribute("signature"); String acl = (String) request.getAttribute("acl"); %>

    uploadUrl包含Google Storage的REST端点。 API提供了如下所示的两个。 任一种都可以接受,但是我们有责任用我们自己的值替换斜体中的组件:

    bucket .commondatastorage.googleapis.com/ object commondatastorage.googleapis.com/ bucket / object

    其余变量是必需的Google Storage参数:

    key :在Google存储空间上上传的数据的名称。 success_action_redirect :上传完成后将重定向到的位置。 GoogleAccessId :由Google分配的API密钥。 policy :一个基本的64位编码的JSON字符串,用于约束如何上传数据。 signature :使用哈希算法签名并以64为基数编码的策略。 用于身份验证。 acl :访问控制列表规范。

    两种形式和提交按钮

    清单19中的第一个表单仅包含title和caption字段。 周围的<html>和<body>标记已被省略。

    清单19.第一个上传表单
    <form id="fieldsForm" method="POST"> <table> <tr> <td>Title</td> <td><input type="text" name="title" /></td> </tr> <tr> <td>Caption</td> <td> <input type="hidden" name="key" value="<%= key %>" /> <input type="text" name="caption" /> </td> </tr> </table> </form>

    关于这种形式,除了它本身是POST之外,没有什么要说的。 让我们继续清单20中的表单,该表单更大,因为它包含了六个隐藏的输入字段:

    清单20.具有隐藏字段的第二种形式
    <form id="uploadForm" method="POST" action="<%= uploadUrl %>" enctype="multipart/form-data"> <table> <tr> <td>Upload</td> <td> <input type="hidden" name="key" value="<%= key %>" /> <input type="hidden" name="GoogleAccessId" value="<%= accessId %>" /> <input type="hidden" name="policy" value="<%= policy %>" /> <input type="hidden" name="acl" value="<%= acl %>" /> <input type="hidden" id="success_action_redirect" name="success_action_redirect" value="<%= successActionRedirect %>" /> <input type="hidden" name="signature" value="<%= signature %>" /> <input type="file" name="file" /> </td> </tr> <tr> <td colspan="2"> <input type="button" value="Submit" id="button"/> </td> </tr> </table> </form>

    在JSP脚本中( 清单18中 )提取的值被放置在隐藏字段中。 文件输入在底部。 提交按钮是一个普通的旧按钮,在我们使用JavaScript进行绑定之前,它不会做任何事情,如清单21所示:

    清单21.提交上传表单
    <script type="text/javascript" src="https://Ajax.googleapis.com/Ajax/libs/jquery/1.4.3/jquery.min.js"> </script> <script type="text/javascript"> $(document).ready(function() { $('#button').click(function() { var formData = $('#fieldsForm').serialize(); var callback = function(photoId) { var redir = $('#success_action_redirect').val() + photoId; $('#success_action_redirect').val(redir) $('#uploadForm').submit(); }; $.post("gstorage", formData, callback); }); }); </script>

    清单21中JavaScript用JQuery编写。 即使您没有使用过该库,代码也不难理解。 代码要做的第一件事是导入JQuery。 然后,在按钮上安装一个单击侦听器,以便单击该按钮时,将通过Ajax提交第一个表单。 从那里开始,我们进入Servlet的handleSubmit方法(稍后将进行构建),在其中构造Photo并将其保存到数据存储中。 最后,在提交上传表单之前,新的Photo ID将返回到回调并附加到success_action_redirect的URL。 这样,当我们从重定向返回时,我们可以查找Photo并显示其图像。 图6显示了整个事件序列:

    图6.显示JavaScript调用路径的序列图

    处理完表格后,我们需要一个实用程序类来创建和签署策略文档。 然后我们可以继承AbstractUploadServlet 。

    创建并签署政策文件

    政策文件会限制上传。 例如,我们可以指定可以上传的文件数量上限或可以接受的文件类型,或者甚至可以限制文件名。 公用存储桶不需要策略文件,而专用存储桶(例如Google Storage)则需要。 为了使事情动起来, GSUtils根据清单22中的代码对名为GSUtils的实用程序类进行存根:

    清单22. GSUtils
    import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.TimeZone; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import com.google.appengine.repackaged.com.google.common.util.Base64; private class GSUtils { }

    鉴于实用程序类通常仅由静态方法组成,因此最好私有化其默认构造函数以防止实例化。 在课程结束后,我们可以将注意力转移到创建策略文档上。

    策略文档为JSON格式,但是JSON非常简单,因此我们不必求助于任何精美的库。 相反,我们可以使用简单的StringBuilder手工制作东西。 首先,我们必须构造一个ISO8601日期并设置策略文档以使其过期。 政策文件过期后,上传将不会成功。 然后,我们必须放入前面讨论的约束,这些约束在策略文档中称为条件 。 最后,文档是基于base-64编码的,并返回给调用方。

    将清单23中的方法添加到GSUtils :

    清单23.创建策略文档
    public static String createPolicyDocument(String acl) { GregorianCalendar gc = new GregorianCalendar(); gc.add(Calendar.MINUTE, 20); DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); df.setTimeZone(TimeZone.getTimeZone("GMT")); String expiration = df.format(gc.getTime()); StringBuilder buf = new StringBuilder(); buf.append("{\"expiration\": \""); buf.append(expiration); buf.append("\""); buf.append(",\"conditions\": ["); buf.append(",{\"acl\": \""); buf.append(acl); buf.append("\"}"); buf.append("[\"starts-with\", \"$key\", \"\"]"); buf.append(",[\"starts-with\", \"$success_action_redirect\", \"\"]"); buf.append("]}"); return Base64.encode(buf.toString().replaceAll("\n", "").getBytes()); }

    我们使用已设置为未来20分钟的GregorianCalendar构造失效日期。 该代码是缺憾,这将通过它打印到控制台,复制它,并通过像JSONLint工具机来帮助(参见相关主题 )。 接下来,我们将acl传递到策略文档中,以避免对其进行硬编码。 变量的任何内容都应作为acl类的方法参数传入。 最后,文档在返回给调用者之前已进行base-64编码。 有关政策文件中允许使用的内容的更多信息,请参见Google Storage文档。

    Google的安全数据连接器

    本文不会使用Google的Secure Data Connector,但如果您打算使用Google Storage,则值得一试。 SDC使访问自己系统上的数据变得更加容易,即使这些系统位于防火墙之后。

    Google存储空间中的身份验证

    政策文件有两个功能。 除了执行策略外,它们还是我们生成的用于验证上传内容的签名的基础。 当我们注册Google Storage时,会得到一个只有我们和Google知道的密钥。 我们使用私钥在我们这边签署文档,而Google则使用相同的密钥签名。 如果签名匹配,则允许上传。 图7提供了这个周期的更好的图片:

    图7.如何将上传的内容认证到Google存储

    为了生成签名,我们在存根GSUtils时使用导入的javax.crypto和java.security包。 清单24显示了这些方法:

    清单24.签署策略文档
    public static String signPolicyDocument(String policyDocument, String secret) { try { Mac mac = Mac.getInstance("HmacSHA1"); byte[] secretBytes = secret.getBytes("UTF8"); SecretKeySpec signingKey = new SecretKeySpec(secretBytes, "HmacSHA1"); mac.init(signingKey); byte[] signedSecretBytes = mac.doFinal(policyDocument.getBytes("UTF8")); String signature = Base64.encode(signedSecretBytes); return signature; } catch (InvalidKeyException e) { throw new RuntimeException(e); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } }

    Java代码中的安全散列涉及一些技巧 ,我希望在本文中跳过这些技巧 。 重要的是清单24展示了它是如何正确完成的,并且散列在返回之前必须经过base-64编码。

    满足了这些先决条件之后,我们回到了熟悉的领域:实现三种抽象方法来从Google Storage上传和检索文件。

    上载到Google存储空间

    首先根据清单25中的代码对名为GStorageUploadServlet的类进行存根:

    清单25. GStorageUploadServlet
    import info.johnwheeler.gaestorage.core.GSUtils; import info.johnwheeler.gaestorage.core.Photo; import info.johnwheeler.gaestorage.core.PhotoDao; import java.io.IOException; import java.io.PrintWriter; import java.util.UUID; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @SuppressWarnings("serial") public class GStorageUploadServlet extends AbstractUploadServlet { private PhotoDao dao = new PhotoDao(); }

    清单26中所示的showForm方法设置了我们需要通过上传表单传递给Google Storage的参数:

    清单26. Google Storage的showForm
    @Override protected void showForm(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String acl = "public-read"; String secret = getServletConfig().getInitParameter("secret"); String accessKey = getServletConfig().getInitParameter("accessKey"); String endpoint = getServletConfig().getInitParameter("endpoint"); String successActionRedirect = getBaseUrl(req) + "gstorage?action=display&id="; String key = UUID.randomUUID().toString(); String policy = GSUtils.createPolicyDocument(acl); String signature = GSUtils.signPolicyDocument(policy, secret); req.setAttribute("uploadUrl", endpoint); req.setAttribute("acl", acl); req.setAttribute("GoogleAccessId", accessKey); req.setAttribute("key", key); req.setAttribute("policy", policy); req.setAttribute("signature", signature); req.setAttribute("successActionRedirect", successActionRedirect); req.getRequestDispatcher("gstorage.jsp").forward(req, resp); }

    请注意, acl设置为公开读取,因此任何人都可以查看上传的任何内容。 接下来的三个变量secret , accessKey和endpoint用来访问Google Storage并通过Google Storage进行身份验证。 它们已从web.xml中声明的init-params中退出; 有关详细信息,请参见示例代码 。 回想一下,与Blobstore不同,后者转发到将我们放置在showRecord的URL,而Google Storage发出重定向。 重定向URL存储在successActionRedirect 。 successActionRedirect依赖于清单27中的helper方法来构造重定向URL。

    清单27. getBaseUrl()
    private static String getBaseUrl(HttpServletRequest req) { String base = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + "/"; return base; }

    在将控制权交还给showForm之前,helper方法将轮询传入的请求以构造基本URL。 返回时,将使用通用唯一标识符或UUID创建密钥,UUID是保证唯一的String 。 接下来,使用我们构建的实用程序类生成策略和签名。 最后,我们在转发给JSP之前为其设置请求属性。

    清单28显示了handleSubmit :

    清单28. Google Storage的handleSubmit
    @Override protected void handleSubmit(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String endpoint = getServletConfig().getInitParameter("endpoint"); String title = req.getParameter("title"); String caption = req.getParameter("caption"); String key = req.getParameter("key"); Photo photo = new Photo(title, caption); photo.setPhotoPath(endpoint + key); dao.save(photo); PrintWriter out = resp.getWriter(); out.println(Long.toString(photo.getId())); out.close(); }

    记住,提交第一个表单时,我们将通过Ajax POST放入handleSubmit 。 上传本身不在此处处理,而是在Ajax回调中单独处理。 handleSubmit只是解析第一种形式,构造一个Photo ,然后将其保存到数据存储中。 然后,通过将Photo的ID写出到响应正文中,将其返回给Ajax回调。

    在回调中,上传表单将提交到Google Storage端点。 Google Storage处理完上传后,将其设置为发出重定向回showRecord ,如清单29所示:

    清单29. Google Storage的showRecord
    @Override protected void showRecord(long id, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Photo photo = dao.findById(id); String photoPath = photo.getPhotoPath(); resp.sendRedirect(photoPath); }

    showRecord查找Photo并将其重定向到其photoPath 。 photoPath指向我们托管在Google服务器上的图片。

    结论

    我们研究了三个以Google为中心的存储选项,并评估了它们的优缺点。 Bigtable易于使用,但文件大小限制为1MB。 Blobstore中的Blob最多可以达到2GB,但是一次性URL很难在Web服务中解决。 最后,Google Storage for Developers是最可靠的选择。 我们只为使用的存储付费,而天空是限制单个文件中可以存储多少数据的限制。 但是,由于它的库目前不支持GAE,因此Google Storage也是最复杂的解决方案。 支持基于浏览器的上传也不是世界上最简单的事情。

    随着Google App Engine成为Java开发人员越来越流行的开发平台,了解其各种存储选项至关重要。 在本文中,您浏览了Bigtable,Blobstore和Google Storage for Developers的简单实现示例。 无论您是选择一个存储选项并坚持使用它,还是将每个选项用于不同的用例,现在都应该拥有在GAE上存储大量数据所需的工具。


    翻译自: https://www.ibm.com/developerworks/java/library/j-gaestorage/index.html

    相关资源:BigTable简介
    Processed: 0.019, SQL: 9