bigtable和gfs_具有Bigtable,Blobstore和Google Storage的GAE存储

由于它们一直在转储字节,因此很容易理会磁盘驱动器和它们上的文件系统。 编写文件时,除了位置,权限和空间要求之外,您无需考虑太多其他内容。 您只需构造一个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存储系统之前,我们需要创建示例应用程序所需的三个类:

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

申请流程

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

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

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

图1.用于演示每个存储选项的三步工作流
GAE数据存储示例应用程序的图。

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

GAE的简单应用

如果没有,请下载Eclipse ,然后安装EclipseGoogle插件并创建一个不使用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指向其位置。 两种情况下仅使用photoDataphotoPath 。 图2阐明了每个人的功能:

图2. photoData和photoPath的工作方式
该图显示了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为获取与KeyfindById ,但不要担心:对于本文的目的,只是觉得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已经有一段时间了,所以doGetdoPost是用于处理HTTP GETPOST的标准方法。 通常,使用GET来获取Web资源并使用POST来发送数据。 本着这种精神,我们的doGet实现要么显示上传表单,要么显示存储中的照片,而doPost处理上传表单的提交。 由扩展AbstractUploadServlet类来定义每个行为。 图3中的图显示了发生的事件的顺序。 可能需要花费几分钟才能清楚了解正在发生的事情。

图3.序列图中的工作流程
示例应用程序工作流程的序列图。

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

GAE存储选项1:Bigtable

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

在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 ,我们最终到达AbstractUploadServletdoPost ,后者依次调用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服务调用之间的区别
该图显示了在Web服务客户端和Blobstore(两个分支)与Bigtable(一个分支)之间移动的图像文件。

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返回MapBlobKeys 。 因为我们的上传表单支持单个上传,所以我们得到了我们期望的唯一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 ,我们刚刚保存在handleSubmitPhoto从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。

与Blobstore不同,默认情况下,Google存储与Web服务和浏览器客户端兼容。 数据通过RESTful PUTPOST 。 第一个选项适用于可控制请求的结构和标头编写方式的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调用路径的序列图
显示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 Storage时,会得到一个只有我们和Google知道的密钥。 我们使用私钥在我们这边签署文档,而Google则使用相同的密钥签名。 如果签名匹配,则允许上传。 图7提供了这个周期的更好的图片:

图7.如何将上传的内容认证到Google存储
GAE身份验证周期图。

为了生成签名,我们在存根GSUtils时使用导入的javax.cryptojava.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设置为公开读取,因此任何人都可以查看上传的任何内容。 接下来的三个变量secretaccessKeyendpoint用来访问Google Storage并通过Google Storage进行身份验证。 它们已从web.xml中声明的init-params中退出; 有关详细信息,请参见示例代码 。 回想一下,与Blobstore不同,后者转发到将我们放置在showRecord的URL,而Google Storage发出重定向。 重定向URL存储在successActionRedirectsuccessActionRedirect依赖于清单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并将其重定向到其photoPathphotoPath指向我们托管在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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值