Implementar a multilocação através de espaços de nomes

A API Namespaces permite-lhe ativar facilmente a multitenancy na sua aplicação, bastando selecionar uma string de espaço de nomes para cada inquilino em web.xml usando o pacote NamespaceManager.

Definir o espaço de nomes atual

Pode obter, definir e validar espaços de nomes através do pacote NamespaceManager. O gestor de espaços de nomes permite-lhe definir um espaço de nomes atual para APIs ativadas para espaços de nomes. Configura um espaço de nomes atual antecipadamente em web.xml e o Datastore e o memcache usam automaticamente esse espaço de nomes.

A maioria dos programadores do App Engine usa o respetivo domínio do Google Workspace (anteriormente denominado G Suite) como o espaço de nomes atual. O Google Workspace permite-lhe implementar a sua app em qualquer domínio que possua, para que possa usar facilmente este mecanismo para configurar diferentes espaços de nomes para diferentes domínios. Em seguida, pode usar esses espaços de nomes separados para segregar os dados nos domínios. Para mais informações, consulte o artigo Mapear domínios personalizados.

O exemplo de código seguinte mostra como definir o espaço de nomes atual para o domínio do Google Workspace que foi usado para mapear o URL. Em particular, esta string é igual para todos os URLs mapeados através do mesmo domínio do Google Workspace.

Pode definir espaços de nomes em Java através da interface de filtro de servlet antes de invocar métodos de servlet. O seguinte exemplo de código demonstra como usar o seu domínio do Google Workspace como o espaço de nomes atual:

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

O filtro de espaço de nomes tem de ser configurado no ficheiro web.xml. Tenha em atenção que, se existirem várias entradas de filtros, é usado o primeiro espaço de nomes a ser definido.

O seguinte exemplo de código demonstra como configurar o filtro de espaço de nomes em web.xml:

<!-- 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>
Para informações mais gerais sobre o ficheiro web.xml e o mapeamento de caminhos de URL para servlets, consulte O descritor de implementação: web.xml.

Também pode definir um novo espaço de nomes para uma operação temporária, repor o espaço de nomes original assim que a operação estiver concluída, usando o padrão try/finally apresentado abaixo:

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

Se não especificar um valor para namespace, o espaço de nomes é definido como uma string vazia. A string namespace é arbitrária, mas também está limitada a um máximo de 100 carateres alfanuméricos, pontos, sublinhados e hífenes. Mais explicitamente, as strings de espaço de nomes têm de corresponder à expressão regular [0-9A-Za-z._-]{0,100}.

Por convenção, todos os espaços de nomes que começam por "_" (sublinhado) estão reservados para utilização do sistema. Esta regra do espaço de nomes do sistema não é aplicada, mas pode facilmente deparar-se com consequências negativas indefinidas se não a seguir.

Evitar fugas de dados

Um dos riscos frequentemente associados a apps multi-inquilinos é o perigo de fuga de dados entre espaços de nomes. As fugas de dados não intencionais podem surgir de várias origens, incluindo:

  • Usar espaços de nomes com APIs do App Engine que ainda não suportam espaços de nomes. Por exemplo, o Blobstore não suporta espaços de nomes. Se usar namespaces com o Blobstore, tem de evitar usar consultas do Blobstore para pedidos de utilizadores finais ou chaves do Blobstore de origens não fidedignas.
  • Usar um meio de armazenamento externo (em vez de memcache e Datastore), através de URL Fetch ou algum outro mecanismo, sem fornecer um esquema de compartimentalização para espaços de nomes.
  • Definir um espaço de nomes com base no domínio de email de um utilizador. Na maioria dos casos, não quer que todos os endereços de email de um domínio acedam a um espaço de nomes. A utilização do domínio de email também impede que a sua aplicação utilize um espaço de nomes até o utilizador iniciar sessão.

Implementar espaços de nomes

As secções seguintes descrevem como implementar espaços de nomes com outras ferramentas e APIs do App Engine.

Criar espaços de nomes por utilizador

Algumas aplicações têm de criar espaços de nomes por utilizador. Se quiser compartimentar os dados ao nível do utilizador para utilizadores com sessão iniciada, considere usar User.getUserId() , que devolve um ID exclusivo e permanente para o utilizador. O seguinte exemplo de código demonstra como usar a API Users para este fim:

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

Normalmente, as apps que criam espaços de nomes por utilizador também fornecem páginas de destino específicas a diferentes utilizadores. Nestes casos, a aplicação tem de fornecer um esquema de URL que determine a página de destino a apresentar a um utilizador.

Usar espaços de nomes com o Datastore

Por predefinição, o Datastore usa a definição do espaço de nomes atual no gestor de espaços de nomes para pedidos do Datastore. A API aplica este espaço de nomes atual a objetos Key ou Query quando são criados. Por conseguinte, tem de ter cuidado se uma aplicação armazenar objetos Key ou Query em formas serializadas, uma vez que o espaço de nomes é preservado nessas serializações.

Se estiver a usar objetos Key e Query desserializados, certifique-se de que têm o comportamento pretendido. A maioria das aplicações simples que usam o Datastore (put/query/get) sem usar outros mecanismos de armazenamento funciona como esperado definindo o espaço de nomes atual antes de chamar qualquer API Datastore.

Os objetos Query e Key demonstram os seguintes comportamentos únicos relativamente aos espaços de nomes:

  • Os objetos Query e Key herdam o espaço de nomes atual quando são construídos, a menos que defina um espaço de nomes explícito.
  • Quando uma aplicação cria um novo Key a partir de um elemento principal, o novo Key herda o espaço de nomes do elemento principal.
  • Não existe uma API para Java para definir explicitamente o espaço de nomes de um Key ou Query.
O exemplo de código seguinte mostra o controlador de pedidos SomeRequest para incrementar a contagem do espaço de nomes atual e o espaço de nomes -global- com nome arbitrário numa entidade de arquivo de dados Counter.

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

Usar espaços de nomes com o Memcache

Por predefinição, o memcache usa o espaço de nomes atual do gestor de espaços de nomes para pedidos de memcache. Na maioria dos casos, não precisa de definir explicitamente um espaço de nomes na memcache, e fazê-lo pode introduzir erros inesperados.

No entanto, existem algumas instâncias únicas em que é adequado definir explicitamente um espaço de nomes na memcache. Por exemplo, a sua aplicação pode ter dados comuns partilhados em todos os espaços de nomes (como uma tabela que contenha códigos de países).

O seguinte fragmento do código demonstra como definir explicitamente o espaço de nomes na cache de memória:

Por predefinição, a API memcache para Java consulta o gestor do espaço de nomes para o espaço de nomes atual a partir de MemcacheService. Também pode indicar explicitamente um espaço de nomes quando constrói a cache de memória com getMemcacheService(Namespace). Para a maioria das aplicações, não precisa de especificar explicitamente um espaço de nomes.

O exemplo de código seguinte demonstra como criar uma memcache que usa o espaço de nomes atual no gestor de espaços de nomes.

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

Este exemplo de código especifica explicitamente um espaço de nomes quando cria um serviço de memcache:

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

Usar espaços de nomes com a fila de tarefas

Por predefinição, as filas de envio usam o espaço de nomes atual, conforme definido no gestor de espaços de nomes no momento em que a tarefa foi criada. Na maioria dos casos, não precisa de definir explicitamente um espaço de nomes na fila de tarefas. Se o fizer, pode introduzir erros inesperados.

Os nomes das tarefas são partilhados em todos os namespaces. Não pode criar duas tarefas com o mesmo nome, mesmo que usem espaços de nomes diferentes. Se quiser usar o mesmo nome de tarefa para muitos namespaces, pode simplesmente anexar cada namespace ao nome da tarefa.

Quando uma nova tarefa chama o método da fila de tarefas add() , a fila de tarefas copia o espaço de nomes atual e (se aplicável) o domínio do Google Workspace do gestor de espaços de nomes. Quando a tarefa é executada, o espaço de nomes atual e o espaço de nomes do Google Workspace são restaurados.

Se o espaço de nomes atual não estiver definido no pedido de origem (por outras palavras, se get() devolver null), a fila de tarefas define o espaço de nomes como "" nas tarefas executadas.

Existem algumas instâncias únicas em que é adequado definir explicitamente um espaço de nomes para uma tarefa que funciona em todos os espaços de nomes. Por exemplo, pode criar uma tarefa que agregue estatísticas de utilização em todos os espaços de nomes. Em seguida, pode definir explicitamente o espaço de nomes da tarefa.

Primeiro, crie um controlador de fila de tarefas que incrementa a contagem numa entidade de Counterdatastore:

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

e,

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

Em seguida, crie tarefas com um 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.");
  }
}

Usar espaços de nomes com o Blobstore

O Blobstore não está segmentado por espaço de nomes. Para preservar um espaço de nomes no Blobstore, tem de aceder ao Blobstore através de um suporte de armazenamento que tenha conhecimento do espaço de nomes (atualmente, apenas memcache, Datastore e fila de tarefas). Por exemplo, se o Key de um objeto binário grande (blob) estiver armazenado numa entidade do Datastore, pode aceder ao mesmo com um Key ou Query que tenha conhecimento do espaço de nomes.

Se a aplicação estiver a aceder ao Blobstore através de chaves armazenadas no armazenamento com reconhecimento de espaço de nomes, o próprio Blobstore não precisa de ser segmentado por espaço de nomes. As aplicações têm de evitar fugas de blobs entre espaços de nomes:

  • Não usar com.google.appengine.api.blobstore.BlobInfoFactory para pedidos de utilizadores finais. Pode usar consultas BlobInfo para pedidos administrativos (como gerar relatórios sobre todos os blobs de aplicações), mas usá-las para pedidos de utilizadores finais pode resultar em fugas de dados, porque todos os registos BlobInfo não estão compartimentados por espaço de nomes.
  • Não usar chaves do Blobstore de origens não fidedignas.

Definir espaços de nomes para consultas do Datastore

Na Google Cloud consola, pode definir o espaço de nomes para consultas do Datastore.

Se não quiser usar o espaço de nomes predefinido, selecione o espaço de nomes que quer usar no menu pendente.

Usar espaços de nomes com o carregador em massa

O carregador em massa suporta uma flag --namespace=NAMESPACE que lhe permite especificar o espaço de nomes a usar. Cada espaço de nomes é processado separadamente e, se quiser aceder a todos os espaços de nomes, tem de iterar através deles.

Uma nova instância de Index herda o espaço de nomes do SearchService usado para a criar. Depois de criar uma referência a um índice, o respetivo espaço de nomes não pode ser alterado. Existem duas formas de definir o espaço de nomes de um SearchService antes de o usar para criar um índice:

  • Por predefinição, um novo SearchService assume o espaço de nomes atual. Pode definir o espaço de nomes atual antes de criar o serviço:
// 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);
  • Pode especificar um espaço de nomes no SearchServiceConfig quando cria um serviço:
// 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);