注意:如果其他应用通过 Remote API 调用 App Engine 应用,则不会触发回调。
回调允许您在持久化流程中的不同点执行代码。您可以使用这些回调轻松实现跨函数逻辑,如日志记录、采样、修饰、审计和验证等等。Datastore API 目前支持在 put()
和 delete()
操作之前和之后执行的回调。
PrePut
当您注册特定种类的 PrePut
回调时,该回调将在放入任意此类实体之前调用(或者,如果未配置任何种类,则该回调将在放入任意实体之前调用):
import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PutContext; import java.util.Date; import java.util.logging.Logger; class PrePutCallbacks { static Logger logger = Logger.getLogger("callbacks"); @PrePut(kinds = {"Customer", "Order"}) void log(PutContext context) { logger.fine("Putting " + context.getCurrentElement().getKey()); } @PrePut // Applies to all kinds void updateTimestamp(PutContext context) { context.getCurrentElement().setProperty("last_updated", new Date()); } }
带 PrePut
注释的方法必须是一种特定的实例方法,即能够返回 void
,接受 PutContext
作为其唯一参数,不引发任何已检查的异常,并且属于具有 no-arg 构造函数的类。当 PrePut
回调引发异常时,不会再进一步回调,并且 put()
操作会在向数据存储区提交任何 RPC 之前引发异常。
PostPut
当您注册特定种类的 PostPut
回调时,该回调将在放入任意此类实体之后调用(或者,如果未配置任何种类,则该回调将在放入任意实体之后调用)。
import com.google.appengine.api.datastore.PostPut; import com.google.appengine.api.datastore.PutContext; import java.util.logging.Logger; class PostPutCallbacks { static Logger logger = Logger.getLogger("logger"); @PostPut(kinds = {"Customer", "Order"}) // Only applies to Customers and Orders void log(PutContext context) { logger.fine("Finished putting " + context.getCurrentElement().getKey()); } @PostPut // Applies to all kinds void collectSample(PutContext context) { Sampler.getSampler().collectSample( "put", context.getCurrentElement().getKey()); } }
带 PostPut
注释的方法必须是一种特定的实例方法,即能够返回 void
,接受 PutContext
作为其唯一参数,不引发任何已检查的异常,并且属于具有 no-arg 构造函数的类。当 PostPut
回调引发异常时,不会再进一步回调,但 put()
操作的结果不受影响。请注意,如果 put()
操作本身失败,则根本不会调用 PostPut
回调。此外,在事务成功提交之前,与事务性 put()
操作关联的 PostPut
回调不会运行。
PreDelete
当您注册特定种类的 PreDelete
回调时,该回调将在删除任意此类实体之前调用(或者,如果未配置任何种类,则该回调将在删除任意实体之前调用):
import com.google.appengine.api.datastore.DeleteContext; import com.google.appengine.api.datastore.PreDelete; import java.util.logging.Logger; class PreDeleteCallbacks { static Logger logger = Logger.getLogger("logger"); @PreDelete(kinds = {"Customer", "Order"}) void checkAccess(DeleteContext context) { if (!Auth.canDelete(context.getCurrentElement()) { throw new SecurityException(); } } @PreDelete // Applies to all kinds void log(DeleteContext context) { logger.fine("Deleting " + context.getCurrentElement().getKey()); } }
带 PreDelete
注释的方法必须是一种特定的实例方法,即能够返回 void
,接受 DeleteContext
作为其唯一参数,不引发任何已检查的异常,并且属于具有 no-arg 构造函数的类。当 PreDelete
回调引发异常时,不会再进一步回调,并且 delete()
操作会在向数据存储区提交任何 RPC 之前引发异常。
PostDelete
当您注册特定种类的 PostDelete
回调时,该回调将在删除任意此类实体之后调用(或者,如果未配置任何种类,则该回调将在删除任意实体之后调用):
import com.google.appengine.api.datastore.DeleteContext; import com.google.appengine.api.datastore.PostDelete; import java.util.logging.Logger; class PostDeleteCallbacks { static Logger logger = Logger.getLogger("logger"); @PostDelete(kinds = {"Customer", "Order"}) void log(DeleteContext context) { logger.fine( "Finished deleting " + context.getCurrentElement().getKey()); } @PostDelete // Applies to all kinds void collectSample(DeleteContext context) { Sampler.getSampler().collectSample( "delete", context.getCurrentElement().getKey()); } }
带 PostDelete
注释的方法必须是一种特定的实例方法,即能够返回 void
,接受 DeleteContext
作为其唯一参数,不引发任何已检查的异常,并且属于具有 no-arg 构造函数的类。当 PostDelete
回调引发异常时,不会再进一步回调,但 delete()
操作的结果不受影响。请注意,如果 delete()
操作本身失败,则根本不会调用 PostDelete
回调。此外,在事务成功提交之前,与事务性 delete()
操作关联的 PostDelete
回调不会运行。
PreGet
您可以注册
PreGet
回调,该回调会在获取特定种类(或所有种类)的实体之前得到调用。例如,您可以使用此方法,拦截一些获取请求,并从缓存而不是数据存储区中提取数据。
import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.PreGetContext; import com.google.appengine.api.datastore.PreGet; class PreGetCallbacks { @PreGet(kinds = {"Customer", "Order"}) public void preGet(PreGetContext context) { Entity e = MyCache.get(context.getCurrentElement()); if (e != null) { context.setResultForCurrentElement(e); } // if not in cache, don't set result; let the normal datastore-fetch happen } }
PreQuery
您可以注册
PreQuery
回调,该回调会在执行特定种类(或所有种类)的查询之前得到调用。例如,您可以使用此方法,根据登录的用户添加相等性过滤条件。查询会传递给回调,而回调可以通过修改该查询来执行一个不同的查询。或者,回调可以引发未经检查的异常,阻止执行该查询。
import com.google.appengine.api.datastore.PreQueryContext; import com.google.appengine.api.datastore.PreQuery; import com.google.appengine.api.datastore.Query; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; class PreQueryCallbacks { @PreQuery(kinds = {"Customer"}) // Queries should only return data owned by the logged in user. public void preQuery(PreQueryContext context) { UserService users = UserServiceFactory.getUserService(); context.getCurrentElement().setFilter( new FilterPredicate("owner", Query.FilterOperator.EQUAL, users.getCurrentUser())); } }
PostLoad
您可以注册
PostLoad
回调,该回调会在加载特定种类(或所有种类)的实体之后得到调用。在这里,“加载”可能意味着获得或查询请求的结果。这对于修饰加载的实体很有用。
import com.google.appengine.api.datastore.PostLoadContext; import com.google.appengine.api.datastore.PostLoad; class PostLoadCallbacks { @PostLoad(kinds = {"Order"}) public void postLoad(PostLoadContext context) { context.getCurrentElement().setProperty("read_timestamp", System.currentTimeMillis()); } }
批处理操作
当您执行批处理操作(例如,带有多个实体的 put()
)时,每个实体都会调用一次回调。通过回调方法的参数调用 CallbackContext.getElements()
,您可以访问整批对象。这样,您就可以回调整个批次,而不是一次回调一个元素。
import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PutContext; class Validation { @PrePut(kinds = "TicketOrder") void checkBatchSize(PutContext context) { if (context.getElements().size() > 5) { throw new IllegalArgumentException( "Cannot purchase more than 5 tickets at once."); } } }
如果您只需要对每个批次执行一次回调,请使用 CallbackContext.getCurrentIndex()
来确定您是否正在查看该批次的第一个元素。
import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PutContext; import java.util.logging.Logger; class Validation { static Logger logger = Logger.getLogger("logger"); @PrePut void log(PutContext context) { if (context.getCurrentIndex() == 0) { logger.fine("Putting batch of size " + getElements().size()); } } }
异步操作
下面介绍了回调如何与异步数据存储操作交互的几个要点。当您使用 async datastore API 执行 put()
或 delete()
时,您已注册的所有 Pre*
回调都会同步执行。您的 Post*
回调也将同步执行,但在您调用任何 Future.get()
方法以检索操作结果之前。
在 App Engine 服务中使用回调
与应用中的任何其他代码一样,回调可以访问所有 App Engine 后端服务。您可以在单个类中定义任意数量需要的回调。
import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.DeleteContext; import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PostPut; import com.google.appengine.api.datastore.PostDelete; import com.google.appengine.api.datastore.PutContext; import com.google.appengine.api.memcache.MemcacheService; import com.google.appengine.api.memcache.MemcacheServiceFactory; import com.google.appengine.api.taskqueue.Queue; import com.google.appengine.api.taskqueue.QueueFactory; import com.google.appengine.api.urlfetch.URLFetchService; import com.google.appengine.api.urlfetch.URLFetchServiceFactory; class ManyCallbacks { @PrePut(kinds = {"kind1", "kind2"}) void foo(PutContext context) { MemcacheService ms = MemcacheServiceFactory.getMemcacheService(); // ... } @PrePut void bar(PutContext context) { DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); // ... } @PostPut(kinds = {"kind1", "kind2"}) void baz(PutContext context) { Queue q = QueueFactory.getDefaultQueue(); // ... } @PostDelete(kinds = {"kind2"}) void yam(DeleteContext context) { URLFetchService us = URLFetchServiceFactory.getURLFetchService(); // ... } }
应避免的常见错误
实现回调时,您需要注意避免一些常见错误。
不要保持非静态状态
import java.util.logging.Logger; import com.google.appengine.api.datastore.PrePut; class MaintainsNonStaticState { static Logger logger = Logger.getLogger("logger"); // ERROR! // should be static to avoid assumptions about lifecycle of the instance boolean alreadyLogged = false; @PrePut void log(PutContext context) { if (!alreadyLogged) { alreadyLogged = true; logger.fine( "Finished deleting " + context.getCurrentElement().getKey()); } } }
不要假定回调执行的顺序
Pre*
回调将始终在 Post*
回调之前执行,但对 Pre*
回调相对于其他 Pre*
回调的执行顺序不做任何假设,也不保证对 Post*
回调相对于其他 Post*
回调的执行顺序做出任何假设。
import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.PrePut; import java.util.HashSet; import java.util.Set; class MakesAssumptionsAboutOrderOfCallbackExecution { static Set<Key> paymentKeys = new HashSet<Key>(); @PrePut(kinds = "Order") void prePutOrder(PutContext context) { Entity order = context.getCurrentElement(); paymentKeys.addAll((Collection<Key>) order.getProperty("payment_keys")); } @PrePut(kinds = "Payment") void prePutPayment(PutContext context) { // ERROR! // Not safe to assume prePutOrder() has already run! if (!paymentKeys.contains(context.getCurrentElement().getKey()) { throw new IllegalArgumentException("Unattached payment!"); } } }
每个方法对应一个回调
虽然一个类可以拥有不限数量的回调方法,单个方法只能关联一个回调。
import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PostPut; class MultipleCallbacksOnAMethod { @PrePut @PostPut // Compiler error! void foo(PutContext context) { } }
不要忘记检索异步结果
除非您调用 Future.get()
检索操作结果,否则 Post*
回调不会运行。如果您在处理完 HTTP 请求之前忘记调用 Future.get()
,则您的 Post*
回调不会运行。
import com.google.appengine.api.datastore.AsyncDatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.PostPut; import com.google.appengine.api.datastore.PutContext; import java.util.concurrent.Future; class IgnoresAsyncResult { AsyncDatastoreService ds = DatastoreServiceFactory.getAsyncDatastoreService(); @PostPut void collectSample(PutContext context) { Sampler.getSampler().collectSample( "put", context.getCurrentElement().getKey()); } void addOrder(Entity order) { Futureresult = ds.put(order); // ERROR! Never calls result.get() so collectSample() will not run. } }
避免无限循环
在回调中执行数据存储操作时,请注意不要陷入无限循环。
import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.PrePut; import com.google.appengine.api.datastore.PutContext; class InfiniteLoop { DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); @PrePut void audit(PutContext context) { Entity original = ds.get(context.getCurrentElement().getKey()); Entity auditEntity = new Entity(original.getKind() + "_audit"); auditEntity.setPropertiesFrom(original); // INFINITE LOOP! // Restricting the callback to specific kinds would solve this. ds.put(auditEntity); } }
在 Eclipse 中使用回调
如果使用 Eclipse 开发应用,那么您需要执行一些配置步骤,才能使用数据存储回调。这些步骤适用于 Eclipse 3.7。我们希望在未来的 Google Plugin For Eclipse 版本中不再需要这些步骤。
- 打开项目的“属性”对话框(“项目”>“属性”)
- 打开“注释处理”对话框(“Java 编译器”>“注释处理”)
- 选中“启用注释处理”
- 选中“启用在编辑器中处理”
- 打开“工厂路径”对话框(“Java 编译器”>“注释处理”>“工厂路径”)
- 点击“添加外部 JAR”
- 选择 <SDK_ROOT> /lib/impl/appengine-api.jar(其中,SDK_ROOT 是 sdk 安装程序的顶级目录)
- 点击“确定”。
通过实现一个有多个回调的方法,您可以验证回调是否配置正确(请参阅每个方法对应一个回调下的代码段)。这应该会产生一个编译器错误。