注意:如果其他某個應用程式使用 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
做為其僅有的引數、不會擲回任何受檢例外狀況,且屬於具有無引數建構函式的類別。當 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
做為其僅有的引數、不會擲回任何受檢例外狀況,且屬於具有無引數建構函式的類別。當 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
做為其僅有的引數、不會擲回任何受檢例外狀況,且屬於具有無引數建構函式的類別。當 PreDelete
回呼擲回例外狀況時,系統不會再執行任何回呼,且在任何 RPC 傳送至資料儲存庫之前,delete()
作業會擲回例外狀況。
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
做為其僅有的引數、不會擲回任何受檢例外狀況,且屬於具有無引數建構函式的類別。當 PostDelete
回呼擲回例外狀況時,系統不會執行其他回呼,但 delete()
作業的結果不會受到影響。請注意,如果 delete()
作業本身失敗,系統就不會叫用 PostDelete
回呼。此外,與交易 delete()
作業相關聯的 PostDelete
回呼,必須等到交易成功提交後才會執行。
PreGet
您可以將
PreGet
回呼註冊為在取得某些特定種類 (或全部種類) 的實體之前呼叫。舉例來說,您可以使用這項功能攔截某些 get 要求,並從快取中擷取資料,而非從資料儲存庫擷取資料。
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) { } }
請勿忘記擷取非同步結果
Post*
回呼會在您呼叫 Future.get()
以擷取作業結果之後執行。如果您忘記在完成 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 的步驟。未來在 Eclipse 適用的 Google 外掛程式,我們預期不再需要執行這些步驟。
- 開啟「專案」的「屬性」對話方塊 (「專案」>「屬性」)
- 開啟「註解處理」對話方塊 (「Java 編譯器」>「註解處理」)
- 勾選 [啟用註解處理]
- 勾選 [在編輯器中啟用處理]
- 開啟「原廠路徑」對話方塊 (「Java 編譯器」>「註解處理」>「原廠路徑」)
- 按一下 [新增外部 JAR]
- 選取 <SDK_ROOT>/lib/impl/appengine-api.jar (其中 SDK_ROOT 是您 sdk 安裝的頂層目錄)
- 按一下 [確定]
您可以採用多個回呼實作一種方法,驗證是否正確設定回呼 (請參閱每種方法一個回呼中的程式碼片段)。這樣應該會產生一個編譯器錯誤。