Mehrinstanzenfähigkeit mit Namespaces implementieren

Mit der Namespaces API können Sie Ihre Anwendung mehrinstanzenfähig machen. Dazu wählen Sie einfach mit dem Paket NamespaceManager in web.xml für jeden Mandanten einen Namespace-String aus.

Aktuellen Namespace festlegen

Sie können Namespaces mit NamespaceManager abrufen, festlegen und validieren. Mit dem Namespace-Manager können Sie einen aktuellen Namespace für Namespace-fähige APIs festlegen. Wenn Sie im Voraus einen aktuellen Namespace in web.xml definieren, verwenden der Datenspeicher und Memcache diesen Namespace automatisch.

Die meisten App Engine-Entwickler verwenden ihre Google Workspace-Domain (ehemals G Suite) als aktuellen Namespace. Da Sie mit Google Workspace Ihre Anwendung in jeder beliebigen eigenen Domain bereitstellen können, können Sie mit diesem Mechanismus für verschiedene Domains jeweils unterschiedliche Namespaces konfigurieren. Dann können Sie diese verschiedenen Namespaces zum Aufteilen von Daten für alle Domains verwenden. Weitere Informationen finden Sie unter Benutzerdefinierte Domains zuordnen.

Im folgenden Codebeispiel wird gezeigt, wie Sie den aktuellen Namespace auf die oogle Workspace-Domain festlegen, die zum Zuordnen der URL verwendet wurde. Beachten Sie, dass dieser String bei allen URLs identisch ist, die über dieselbe Google Workspace-Domain zugeordnet wurden.

Sie können mithilfe der Servlet-Filter-Schnittstelle in Java Namespaces festlegen, bevor Sie Servlet-Methoden aufrufen. Das folgende einfache Beispiel zeigt, wie Sie Ihre Google Workspace-Domain als aktuellen Namespace verwenden:

// 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
  }

Der Namespace-Filter muss in der Datei web.xml konfiguriert sein. Beachten Sie, dass, falls mehrere Filtereinträge vorhanden sind, der Namespace verwendet wird, der zuerst festgelegt wurde.

Das folgende Codebeispiel zeigt, wie der Namespace-Filter in web.xml konfiguriert wird:

<!-- 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>

Weitere allgemeine Informationen zur web.xml-Datei und zur Zuordnung von URL-Pfaden zu Servlets finden Sie unter Deployment-Deskriptor: web.xml.

Sie können auch einen neuen Namespace für einen temporären Vorgang festlegen, indem Sie den ursprünglichen Namespace zurücksetzen, sobald der Vorgang abgeschlossen wurde. Verfahren Sie dabei nach dem unten dargestellten Muster (try/finally):

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

Wenn Sie für namespace keinen Wert festlegen, wird der Namespace als leere Zeichenfolge festgelegt. Der namespace-String kann beliebig sein, ist aber auf maximal 100 alphanumerische Zeichen, Punkte, Unterstriche und Bindestriche beschränkt. Konkret müssen Namespace-Strings dem regulären Ausdruck [0-9A-Za-z._-]{0,100} entsprechen.

Per Konvention sind alle Namespaces, die mit "_" (Unterstrich) beginnen, für die Systemverwendung reserviert. Diese System-Namespace-Regel ist nicht zwingend. Wenn Sie sie jedoch nicht beachten, könnte dies unbestimmte negative Auswirkungen haben.

Datenlecks verhindern

Zu den Risiken, die häufig mit mehrinstanzenfähigen Anwendungen assoziiert werden, gehört die Gefahr von Datenverlusten bei allen Namespaces. Nicht beabsichtigte Datenverluste können verschiedene Ursachen haben, einschließlich:

  • Die Verwendung von Namespaces mit App Engine-APIs, die noch keine Namespaces unterstützen. Zum Beispiel unterstützt Blobstore keine Namespaces. Wenn Sie Namespaces mit Blobstore verwenden, sollten Sie keine Blobstore-Abfragen für Endnutzeranfragen und keine Blobstore-Schlüssel aus nicht vertrauenswürdigen Quellen verwenden.
  • Verwendung eines externen Speichermediums anstelle von Memcache und des Datenspeichers über URL Fetch oder einen anderen Mechanismus, ohne ein Untergliederungsschema für Namespaces bereitzustellen.
  • Festlegen eines Namespace auf der Grundlage der E-Mail-Domain eines Nutzers. In den meisten Fällen sollen nicht alle E-Mail-Adressen einer Domain auf einen Namespace zugreifen können. Die Verwendung der E-Mail-Domain führt außerdem dazu, dass Ihre Anwendung einen Namespace nicht verwendet, bis der Nutzer angemeldet ist.

Namespaces bereitstellen

In den folgenden Abschnitten wird beschrieben, wie Namespaces mit anderen App Engine-Tools und APIs bereitgestellt werden.

Namespaces pro Nutzer erstellen

Bei einigen Anwendungen müssen Namespaces auf nutzerbezogener Basis erstellt werden. Wenn Sie Daten auf Nutzerebene für angemeldete Nutzer gliedern möchten, sollten Sie User.getUserId() verwenden, wobei eine eindeutige permanente ID für den Nutzer zurückgegeben wird. Im folgenden Codebeispiel wird gezeigt, wie die Nutzer-API für diesen Zweck verwendet wird:

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

Normalerweise stellen Anwendungen, die Namespaces nutzerbezogen erstellen, auch spezifische Zielseiten für verschiedene Nutzer bereit. In diesen Fällen muss die Anwendung ein URL-Schema bereitstellen, das festlegt, welche Landingpage einem Nutzer angezeigt wird.

Namespaces mit dem Datenspeicher verwenden

Standardmäßig verwendet der Datenspeicher für Datenspeicheranfragen die aktuelle Namespace-Einstellung im Namespace-Manager. Die API wendet diesen aktuellen Namespace auf Key- oder Query-Objekte an, wenn sie erstellt werden. Sie sollten deshalb mit Bedacht vorgehen, wenn eine Anwendung Key- oder Query-Objekte in serialisierter Form speichert, da der Namespace in diesen Serialisierungen beibehalten wird.

Wenn Sie deserialisierte Key- und Query-Objekte verwenden, ist es wichtig, dass diese sich wie beabsichtigt verhalten. Die meisten einfachen Anwendungen, die den Datenspeicher (put/query/get) ohne andere Speichermechanismen verwenden, funktionieren erwartungsgemäß, wenn der aktuelle Namespace vor dem Aufrufen einer Datastore API festgelegt wird.

Query- und Key-Objekte verhalten sich in Bezug auf Namespaces grundsätzlich folgendermaßen:

  • Query- und Key-Objekte übernehmen bei ihrer Erstellung den aktuellen Namespace, es sei denn, Sie legen explizit einen anderen fest.
  • Wenn eine Anwendung einen neuen Key von einem Ancestor erstellt, übernimmt der neue Key den Namespace des Ancestors.
  • Es gibt keine API für Java, um den Namespace von Key oder Query explizit festzulegen.
Im folgenden Codebeispiel wird der SomeRequest-Anfragehandler dargestellt, mit dem die Anzahl für den aktuellen Namespace und den willkürlich benannten -global--Namespace in einer Counter-Datastore-Entität erhöht werden kann.

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.");
  }

Namespaces mit Memcache verwenden

Standardmäßig verwendet Memcache für Memcache-Anfragen den aktuellen Namespace des Namespace-Managers. In den meisten Fällen ist es nicht notwendig, im Memcache explizit einen Namespace festzulegen. Dies kann außerdem zu unerwarteten Fehlern führen.

Es gibt jedoch einige eindeutige Instanzen, bei denen sich die explizite Festlegung eines Namespace im Memcache empfiehlt. Zum Beispiel verfügt Ihre Anwendung möglicherweise über allgemeine Daten, die für alle Namespaces freigegeben sind, z. B. eine Tabelle mit Ländercodes.

Im folgenden Codeausschnitt wird gezeigt, wie der Namespace im Memcache explizit festgelegt wird:

Standardmäßig fragt die Memcache API für Java den Namespace-Manager für den aktuellen Namespace von MemcacheService ab. Sie können auch ausdrücklich einen Namespace bestimmen, wenn Sie den Memcache mithilfe getMemcacheService(Namespace) von erstellen. Bei den meisten Anwendungen müssen Sie nicht ausdrücklich einen Namespace angeben.

Das folgende Codebeispiel zeigt, wie Sie einen Memcache erstellen, der den aktuellen Namespace im Namespace-Manager verwendet.

// 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);
}

In diesem Codebeispiel wird ein Namespace ausdrücklich bei der Erstellung eines Memcache-Dienstes angegeben:

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

Namespaces mit der Aufgabenwarteschlange verwenden

Push-Warteschlangen verwenden standardmäßig den aktuellen Namespace, der bei der Aufgabenerstellung im Namespace-Manager festgelegt war. In den meisten Fällen ist es nicht erforderlich, in der Aufgabenwarteschlange einen Namespace explizit festzulegen. Dies kann auch zu unerwarteten Fehlern führen.

Aufgabennamen werden für alle Namespaces freigegeben. Die Erstellung von zwei Aufgaben mit demselben Namen ist nicht möglich, auch wenn bei ihnen verschiedene Namespaces verwendet werden. Wenn Sie denselben Aufgabennamen für viele Namespaces verwenden möchten, können Sie jeden Namespace einfach dem Aufgabennamen anfügen.

Wenn eine neue Aufgabe die Methode add() der Aufgabenwarteschlange aufruft, kopiert die Aufgabenwarteschlange den aktuellen Namespace und gegebenenfalls die Google Workspace-Domain aus dem Namespace-Manager. Wenn die Aufgabe ausgeführt wird, werden der aktuelle Namespace und der Google Workspace-Namespace wiederhergestellt.

Wenn der aktuelle Namespace in der ursprünglichen Anfrage nicht festgelegt ist, d. h. wenn get() null zurückgibt, legt die Aufgabenwarteschlange den Namespace in den ausgeführten Aufgaben auf "" fest.

Es gibt einige eindeutige Instanzen, bei denen für Aufgaben, die für alle Namespaces funktionieren, ein Namespace explizit festgelegt werden sollte. Zum Beispiel könnten Sie eine Aufgabe erstellen, bei der für alle Namespaces Nutzungsstatistiken gesammelt werden. Sie könnten dann den Namespace der Aufgabe explizit festlegen. Im folgenden Codebeispiel wird gezeigt, wie mit der Aufgabenwarteschlange Namespaces explizit festgelegt werden.

Erstellen Sie als Erstes einen Aufgabenwarteschlangen-Handler, der die Anzahl in einer Counter-Datenspeicherentität erhöht:

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;
  }

und

// 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]);
}

Erstellen Sie dann Aufgaben mit einem Servlet:

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.");
  }
}

Namespaces mit dem Blobstore verwenden

Der Blobstore ist nicht in Namespaces aufgeteilt. Damit in Blobstore ein Namespace beibehalten wird, ist es notwendig, über ein Namespace-fähiges Speichermedium auf Blobstore zuzugreifen. Hierfür stehen derzeit nur der Memcache, der Datenspeicher und die Aufgabenwarteschlange zur Verfügung. Wenn zum Beispiel der Key eines Blobs in einer Datenspeicherentität gespeichert ist, können Sie mit einem Key oder Query des Datenspeichers, der den Namespace berücksichtigt, darauf zugreifen.

Wenn die Anwendung über Schlüssel, die in einem Namespace-fähigen Speicher gespeichert sind, auf Blobstore zugreift, muss Blobstore selbst nicht nach Namespace aufgeteilt werden. In Anwendungen müssen Blob-Verluste zwischen Namespaces verhindert werden. Dazu muss auf Folgendes beachtet werden:

  • Verwenden Sie für Endnutzer-Anforderungen nicht com.google.appengine.api.blobstore.BlobInfoFactory. Sie können BlobInfo-Abfragen für administrative Anfragen verwenden, z. B. zum Generieren von Berichten über alle Anwendungs-Blobs. Die Verwendung für Endnutzeranfragen kann jedoch zu Datenlecks führen, da die BlobInfo-Einträge nicht nach Namespace aufgeteilt sind.
  • Verwenden Sie keine Blobstore-Schlüssel aus nicht vertrauenswürdigen Quellen.

Namespaces für Datastore-Abfragen festlegen

In der Google Cloud Console können Sie den Namespace für Datastore-Abfragen festlegen.

Wenn Sie die Standardeinstellung nicht verwenden möchten, wählen Sie den gewünschten Namespace aus dem Drop-down aus.

Namespaces mit dem Bulk Loader verwenden

BulkLoader unterstützt ein --namespace=NAMESPACE-Flag, mit dem Sie den zu verwendenden Namespace festlegen können. Jeder Namespace wird separat bearbeitet und wenn Sie auf alle Namespaces zugreifen möchten, müssen diese durchlaufen werden.

Eine neue Instanz von Index übernimmt den Namespace von SearchService, mit dem die Instanz erstellt wurde. Nachdem Sie einen Verweis auf einen Index erstellt haben, kann der Namespace nicht mehr geändert werden. Es gibt zwei Möglichkeiten, den Namespace für einen SearchService festzulegen, bevor Sie ihn zum Erstellen eines Indexes verwenden:

  • Standardmäßig verwendet ein neuer SearchService den aktuellen Namespace. Sie können den aktuellen Namespace vor dem Erstellen des Dienstes festlegen:
// 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);

Sie können beim Erstellen eines Dienstes einen Namespace in SearchServiceConfig angeben:

// 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);