使用命名空間實作多租戶架構

Namespaces API 讓您可以在應用程式中輕鬆啟用多租戶架構,只要利用 NamespaceManager 套件,在 web.xml 為每個用戶群選擇一個命名空間字串即可。

設定目前的命名空間

您可以使用 NamespaceManager 取得、設定並驗證命名空間。命名空間管理員可讓您針對啟用命名空間的 API 設定目前的命名空間。您可以透過 web.xml 先設定目前的命名空間,資料儲存庫與 Memcache 就會自動使用這個命名空間。

大多數 App Engine 開發人員使用其 G Suite 網域作為目前的命名空間。您可以使用 G Suite 將應用程式部署至自己擁有的任何網域,因此可以輕鬆使用這個機制為不同的網域設定不同的命名空間。接著您可以使用這些各自獨立的命名空間,區隔各網域的資料。如要進一步瞭解如何透過 G Suite 資訊主頁設定多個網域,請參閱在 G Suite 網址上部署應用程式

以下程式碼範例顯示如何將目前的命名空間設到用於對應網址的 G Suite 網域。很明顯地,這個字串會與透過相同 G Suite 網域對應到的所有網址相同。

您可以在叫用 servlet 方法之前,使用 servlet Filter 介面來設定 Java 命名空間。以下範例簡單示範如何使用 G Suite 網域做為目前的命名空間:

Java 8

// Filter to set the Google Apps domain as the namespace.
public class NamespaceFilter implements Filter {

  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    // Make sure set() is only called if the current namespace is not already set.
    if (NamespaceManager.get() == null) {
      // If your app is hosted on appspot, this will be empty. Otherwise it will be the domain
      // the app is hosted on.
      NamespaceManager.set(NamespaceManager.getGoogleAppsNamespace());
    }
    chain.doFilter(req, res); // Pass request back down the filter chain
  }

Java 7

// Filter to set the Google Apps domain as the namespace.
public class NamespaceFilter implements Filter {
  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    // Make sure set() is only called if the current namespace is not already set.
    if (NamespaceManager.get() == null) {
      // If your app is hosted on appspot, this will be empty. Otherwise it will be the domain
      // the app is hosted on.
      NamespaceManager.set(NamespaceManager.getGoogleAppsNamespace());
    }
    chain.doFilter(req, res); // Pass request back down the filter chain
  }

必須在 web.xml 檔案中設定命名空間篩選器。請注意,如果有多個篩選器項目,則會使用第一個設定的命名空間。

下列程式碼範例示範如何在 web.xml 中設定命名空間篩選器:

Java 8

<!-- Configure the namespace filter. -->
<filter>
    <filter-name>NamespaceFilter</filter-name>
    <filter-class>com.example.appengine.NamespaceFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>NamespaceFilter</filter-name>
    <url-pattern>/sign</url-pattern>
</filter-mapping>

Java 7

<!-- Configure the namespace filter. -->
<filter>
    <filter-name>NamespaceFilter</filter-name>
    <filter-class>com.example.appengine.NamespaceFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>NamespaceFilter</filter-name>
    <url-pattern>/sign</url-pattern>
</filter-mapping>

如要進一步瞭解 web.xml 檔案和對應至 Servlet 的網址路徑,請參閱部署作業描述元:web.xml

您也可以針對暫時作業設定新的命名空間,在作業完成之後,利用以下的 try/finally 模式重設原始命名空間:

Java 8

// Set the namepace temporarily to "abc"
String oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  //      ... perform operation using current namespace ...
} finally {
  NamespaceManager.set(oldNamespace);
}

Java 7

// Set the namepace temporarily to "abc"
String oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
//    ... perform operation using current namespace ...
} finally {
  NamespaceManager.set(oldNamespace);
}

如果沒有為 namespace 指定任何值,命名空間會設為空白字串。namespace 字串可為任意字串,不過長度上限為 100 個英數字元,且只能使用半形句號、底線和連字號。更明確地說,命名空間字串必須與規則運算式 [0-9A-Za-z._-]{0,100} 相符。

按照慣例,所有開頭為「_」(底線) 的命名空間字串都會保留供系統使用。此系統命名空間規則並未強制執行,但若您的命名空間字串不符合這項規則,很可能會發生不明的負面後果。

避免資料外洩

多組織用戶共享應用程式經常發生的一種風險,就是命名空間之間的資料外洩。許多原因可能意外導致資料外洩,包括:

  • 某些 App Engine API 目前還不支援命名空間,卻搭配使用命名空間。舉例來說,Blobstore 就不支援命名空間。如果您將命名空間與 Blobstore 搭配使用,就必須避免針對使用者要求而使用 Blobstore 查詢,或使用不受信任來源的 Blobstore 金鑰。
  • 在沒有為命名空間提供分隔配置的情況下,透過 URL Fetch 或其他機制使用外部儲存媒體 (而非 Memcache 與資料儲存庫)。
  • 根據使用者的電子郵件網域,設定命名空間。在大部分情況下,您不會想讓同一網域的所有電子郵件地址存取相同的命名空間。使用電子郵件網域後,使用者必須先行登入,應用程式才能使用命名空間。

部署命名空間

以下各節說明如何使用其他 App Engine 工具和 API 部署命名空間。

為個別使用者建立命名空間

有些應用程式需要為個別使用者建立命名空間。如果您想要為登入的使用者區隔使用者層級的資料,請考慮使用 User.getUserId() 來傳回使用者的唯一永久 ID。以下程式碼範例示範如何使用 Users API 進行這項作業:

Java 8

if (com.google.appengine.api.NamespaceManager.get() == null) {
  // Assuming there is a logged in user.
  namespace = UserServiceFactory.getUserService().getCurrentUser().getUserId();
  NamespaceManager.set(namespace);
}

Java 7

if (com.google.appengine.api.NamespaceManager.get() == null) {
  // Assuming there is a logged in user.
  namespace = UserServiceFactory.getUserService().getCurrentUser().getUserId();
  NamespaceManager.set(namespace);
}

一般來說,為個別使用者建立命名空間的應用程式,也會為個別使用者提供專屬的到達網頁。在這些情況下,應用程式必須提供網址通訊協定,指出要向使用者顯示哪個到達網頁。

將命名空間與 Datastore 搭配使用

根據預設,資料儲存庫將針對資料儲存庫要求,使用目前在命名空間管理員中的命名空間設定。API 在 KeyQuery 物件建立時會為其套用目前的命名空間。因此,當應用程式以序列化形式儲存 KeyQuery 物件時,請特別小心,因為命名空間將以這些序列化格式保存。

如果您使用的是還原序列化的 KeyQuery 物件,請確定這些物件能如預期執行。針對使用資料儲存庫 (put/query/get) 而沒有使用其他儲存機制的簡易應用程式,如果先設定目前的命名空間,然後再呼叫任何資料儲存庫 API,則這些應用程式大多能夠如正常運作。

以下是 QueryKey 物件在命名空間方面展示的獨特行為:

  • 除非您已設定明確的命名空間,否則 QueryKey 物件會在建構時繼承目前的命名空間。
  • 當應用程式從祖系建立新的 Key 時,這個新的 Key 將繼承祖系的命名空間。
  • 沒有建立 Java 適用的 API,以明確設定 KeyQuery 的命名空間。
以下程式碼範例為 SomeRequest 要求處理常式,這個處理常式可針對目前的命名空間以及在 Counter 資料儲存庫實體中任意指定的 -global- 命名空間來增加計數器。

Java 8

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws java.io.IOException {

    // Update the count for the current namespace.
    updateCount("request");

    // Update the count for the "-global-" namespace.
    String namespace = NamespaceManager.get();
    try {
      // "-global-" is namespace reserved by the application.
      NamespaceManager.set("-global-");
      updateCount("request");
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are now updated.");
  }

Java 7

public class UpdateCountsServlet extends HttpServlet {
  private static final int NUM_RETRIES = 10;

  @Entity public class CounterPojo {
    @Id public Long id;
    @Index public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  // Increment the count in a Counter datastore entity.
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws java.io.IOException {

    // Update the count for the current namespace.
    updateCount("request");

    // Update the count for the "-global-" namespace.
    String namespace = NamespaceManager.get();
    try {
      // "-global-" is namespace reserved by the application.
      NamespaceManager.set("-global-");
      updateCount("request");
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are now updated.");
  }

將命名空間與 Memcache 搭配使用

根據預設,Memcache 將針對 Memcache 要求,使用命名空間管理員目前的命名空間。在大部分情況下,您不需要在 Memcache 中明確設定命名空間,否則可能造成未預期的錯誤。

不過於某些特殊案例中,在 Memcache 明確設定命名空間會是適當的做法。舉例來說,應用程式可能有在所有命名空間之間共用的常用資料 (例如包含國碼的表格)。

以下程式碼片段示範如何在 Memcache 中明確設定命名空間:

根據預設,Java 適用的 Memcache API 會從 MemcacheService 向命名空間管理員查詢目前的命名空間。您也可以在建構 Memcache 時利用 getMemcacheService(Namespace) 明確指出命名空間。大部分的應用程式都不需要特別指定命名空間。

下列程式碼範例將示範如何在命名空間管理員中建立使用目前命名空間的 Memcache。

Java 8

// Create a MemcacheService that uses the current namespace by
// calling NamespaceManager.get() for every access.
MemcacheService current = MemcacheServiceFactory.getMemcacheService();

// stores value in namespace "abc"
oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  current.put("key", value); // stores value in namespace “abc”
} finally {
  NamespaceManager.set(oldNamespace);
}

Java 7

// Create a MemcacheService that uses the current namespace by
// calling NamespaceManager.get() for every access.
MemcacheService current = MemcacheServiceFactory.getMemcacheService();

// stores value in namespace "abc"
oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  current.put("key", value);  // stores value in namespace “abc”
} finally {
  NamespaceManager.set(oldNamespace);
}

此程式碼範例在建立 Memcache 服務時直接指定命名空間:

Java 8

// Create a MemcacheService that uses the namespace "abc".
MemcacheService explicit = MemcacheServiceFactory.getMemcacheService("abc");
explicit.put("key", value); // stores value in namespace "abc"

Java 7

// Create a MemcacheService that uses the namespace "abc".
MemcacheService explicit = MemcacheServiceFactory.getMemcacheService("abc");
explicit.put("key", value);  // stores value in namespace "abc"

將命名空間與工作佇列搭配使用

根據預設,發送佇列會使用工作建立時在命名空間管理員中設定的目前命名空間。在大部分情況下,您不需要在工作佇列明確設定命名空間,否則可能將造成未預期的錯誤。

所有命名空間都將共用工作名稱。您不能建立兩個具有相同名稱的工作,即使這些工作使用不同的命名空間亦同。如果您要讓多個命名空間使用相同的工作名稱,可以將各個命名空間附加到工作名稱。

當新的工作呼叫工作佇列 add() 方法時,工作佇列會從命名空間管理員複製目前的命名空間及 (如果適用) G Suite 網域。執行工作時,則會復原目前的命名空間與 G Suite 命名空間。

如果來源要求中並未設定目前的命名空間 (即如果 get() 傳回 null),則工作佇列會將命名空間設定於已執行工作中的 ""

在某些特殊情況下,建議針對在所有命名空間中運作的工作明確設定命名空間。舉例來說,您可能建立一個工作,匯總所有命名空間的使用統計資料。接著,您可以明確設定這個工作的命名空間。下列的程式碼範例說明如何為工作佇列明確設定命名空間。

首先,建立工作佇列處理常式以累加 Counter Datastore 實體中的數量:

Java 8

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

Java 7

public class UpdateCountsServlet extends HttpServlet {
  private static final int NUM_RETRIES = 10;

  @Entity public class CounterPojo {
    @Id public Long id;
    @Index public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  // Increment the count in a Counter datastore entity.
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

以及

Java 8

// called from Task Queue
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
  String[] countName = req.getParameterValues("countName");
  if (countName.length != 1) {
    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    return;
  }
  updateCount(countName[0]);
}

Java 7

// called from Task Queue
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
  String[] countName = req.getParameterValues("countName");
  if (countName.length != 1) {
    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    return;
  }
  updateCount(countName[0]);
}

接著,使用 servlet 建立工作:

Java 8

public class SomeRequestServlet extends HttpServlet {

  // Handler for URL get requests.
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

    // Increment the count for the current namespace asynchronously.
    QueueFactory.getDefaultQueue()
        .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    // Increment the global count and set the
    // namespace locally.  The namespace is
    // transferred to the invoked request and
    // executed asynchronously.
    String namespace = NamespaceManager.get();
    try {
      NamespaceManager.set("-global-");
      QueueFactory.getDefaultQueue()
          .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are being updated.");
  }
}

Java 7

public class SomeRequestServlet extends HttpServlet {
  // Handler for URL get requests.
  @Override
  protected void doGet(HttpServletRequest req,
                       HttpServletResponse resp)
      throws IOException {

    // Increment the count for the current namespace asynchronously.
    QueueFactory.getDefaultQueue().add(
        TaskOptions.Builder.withUrl("/_ah/update_count")
            .param("countName", "SomeRequest"));
    // Increment the global count and set the
    // namespace locally.  The namespace is
    // transferred to the invoked request and
    // executed asynchronously.
    String namespace = NamespaceManager.get();
    try {
      NamespaceManager.set("-global-");
      QueueFactory.getDefaultQueue().add(
          TaskOptions.Builder.withUrl("/_ah/update_count")
              .param("countName", "SomeRequest"));
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are being updated.");
  }
}

將命名空間與 Blobstore 搭配使用

命名空間不會區隔 Blobstore。若要在 Blobstore 保留命名空間,您需要使用能感知命名空間的儲存媒體 (目前只有 Memcache、資料儲存庫和工作佇列) 來存取 Blobstore。舉例來說,如果 Blob 的 Key 儲存於資料儲存庫實體中,您可以使用可感知命名空間的資料儲存庫 KeyQuery 加以存取。

如果應用程式存取 Blobstore 時使用的金鑰儲存在可感知命名空間的儲存空間中,就無需透過命名空間區隔 Blobstore。應用程式必須藉由以下方式,避免命名空間之間的 Blob 外洩:

  • 不使用 com.google.appengine.api.blobstore.BlobInfoFactory 處理使用者要求。您可以針對管理要求 (例如產生所有應用程式 Blob 的相關報表) 使用 BlobInfo 查詢,但是對使用者要求使用 BlobInfo 查詢可能造成資料外洩,因為所有 BlobInfo 記錄皆尚未透過命名空間加以區隔。
  • 不使用不受信任來源的 Blobstore 金鑰。

為 Datastore 查詢設定命名空間

在 Google Cloud Platform 主控台中,您可以為 Datastore 查詢設定命名空間

如果您不想要使用預設值,請從下拉式選單中選取您要使用的命名空間。

將命名空間與 Bulk Loader 搭配使用

Bulk Loader 支援 --namespace=NAMESPACE 標記,可讓您指定要使用的命名空間。系統會分別處理每個命名空間,如果您要存取所有命名空間,則須逐一查看。

Index 的新執行個體繼承用於建立此執行個體的 SearchService 命名空間。建立索引的參照之後,即無法變更其命名空間。在利用 SearchService 建立索引之前為其設定命名空間的方法有兩種:

  • 根據預設,全新的 SearchService 會取用目前的命名空間。您可以在建立服務之前設定目前的命名空間:

Java 8

// Set the current namespace to "aSpace"
NamespaceManager.set("aSpace");
// Create a SearchService with the namespace "aSpace"
SearchService searchService = SearchServiceFactory.getSearchService();
// Create an IndexSpec
IndexSpec indexSpec = IndexSpec.newBuilder().setName("myIndex").build();
// Create an Index with the namespace "aSpace"
Index index = searchService.getIndex(indexSpec);

Java 7

// Set the current namespace to "aSpace"
NamespaceManager.set("aSpace");
// Create a SearchService with the namespace "aSpace"
SearchService searchService = SearchServiceFactory.getSearchService();
// Create an IndexSpec
IndexSpec indexSpec = IndexSpec.newBuilder().setName("myIndex").build();
// Create an Index with the namespace "aSpace"
Index index = searchService.getIndex(indexSpec);

您可以在建立服務時在 SearchServiceConfig 指定命名空間:

Java 8

// Create a SearchServiceConfig, specifying the namespace "anotherSpace"
SearchServiceConfig config =
    SearchServiceConfig.newBuilder().setNamespace("anotherSpace").build();
// Create a SearchService with the namespace "anotherSpace"
searchService = SearchServiceFactory.getSearchService(config);
// Create an IndexSpec
indexSpec = IndexSpec.newBuilder().setName("myindex").build();
// Create an Index with the namespace "anotherSpace"
index = searchService.getIndex(indexSpec);

Java 7

// Create a SearchServiceConfig, specifying the namespace "anotherSpace"
SearchServiceConfig config =  SearchServiceConfig.newBuilder()
    .setNamespace("anotherSpace").build();
// Create a SearchService with the namespace "anotherSpace"
searchService = SearchServiceFactory.getSearchService(config);
// Create an IndexSpec
indexSpec = IndexSpec.newBuilder().setName("myindex").build();
// Create an Index with the namespace "anotherSpace"
index = searchService.getIndex(indexSpec);
本頁內容對您是否有任何幫助?請提供意見:

傳送您對下列選項的寶貴意見...

這個網頁
Java 8 適用的 App Engine 標準環境