Introdução
O Couchbase Mobile Gateway de sincronização O feed de alterações oferece uma maneira de monitorar eventos em uma implantação móvel. O feed torna viável a criação de lógica comercial sofisticada. Escrevi uma ferramenta para ajudar a examinar e entender o feed. Você pode ler uma introdução e uma descrição em primeira parte desta série de duas partes. O código também serve como exemplo de escuta do feed.
O código
Incluí aqui as principais classes do código do aplicativo. Esta é a primeira versão, portanto, ela pode receber muitos aprimoramentos. Os parâmetros estão todos conectados. Confira o projeto aqui no GitHub para obter atualizações. Você também pode encontrar instruções para criar, executar e empacotar o aplicativo lá.
JavaFX: A classe Controller
O JavaFX divide aplicativos simples em uma classe controladora e uma interface de usuário declarativa. Vamos examinar o controlador.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
pacote com.couchbase.móvel; importação com.couchbase.leve.*; importação com.xml mais rápido.jackson.núcleo.JsonProcessingException; importação javafx.aplicativo.Plataforma; importação javafx.feijões.valor.ChangeListener; importação javafx.feijões.valor.Valor observável; importação javafx.coleções.FXCollections; importação javafx.coleções.Lista de observáveis; importação javafx.evento.ActionEvent; importação javafx.fxml.FXML; importação javafx.cena.controle.*; importação javafx.cena.controle.TextField; importação java.io.IOException; importação java.rede.ExceçãoURLE malformada; importação java.rede.URL; importação java.util.Mapa; importação estático com.couchbase.móvel.Tempo de execução.mapeador; público classe Controlador implementa LiveQuery.ChangeListener, ChangeListener, SGMonitor.Ouvinte do ChangesFeedListener, DBService.Ouvinte de estado de replicação { privado estático final Cordas HOST DO GATEWAY DE SINCRONIZAÇÃO = "http://localhost"; privado estático final Cordas SG_PUBLIC_URL = HOST DO GATEWAY DE SINCRONIZAÇÃO + ":4984/" + DBService.BASE DE DADOS; privado estático final Cordas SG_ADMIN_URL = HOST DO GATEWAY DE SINCRONIZAÇÃO + ":4985/" + DBService.BASE DE DADOS; privado estático final Cordas TOGGLE_INACTIVE = "-fx-background-color: #e6555d;"; privado estático final Cordas TOGGLE_ACTIVE = "-fx-background-color: #ade6a6;"; privado estático final Cordas TOGGLE_DISABLED = "-fx-background-color: #555555;"; @FXML privado Visualização de lista documentList; privado Lista de observáveis documentos = FXCollections.observávelArrayList(); @FXML privado TextArea contentsText; @FXML privado TextArea changesFeed; @FXML privado TextField nome de usuárioTexto; @FXML privado TextField passwordText; @FXML privado Botão de alternância applyCredentialsBtn; @FXML privado Botão de alternância syncBtn; privado DBService serviço = DBService.getInstance(); privado Banco de dados db = serviço.getDatabase(); privado SGMonitor changesMonitor; privado LiveQuery Consulta ao vivo; |
Essa primeira listagem mostra um monte de código padrão. Implemento vários ouvintes para a interface do usuário dentro da própria classe para reduzir o número de arquivos. Isso é para fins ilustrativos.
O @FXML marcam todos os campos que a estrutura associará automaticamente a partes da interface do usuário.
Em seguida, vem a inicialização. O JavaFX chama esse método como parte de seu ciclo de vida padrão.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@FXML privado vazio inicializar() { documentListInitialize(); documentList.setItems(documentos); setState(applyCredentialsBtn, falso); setState(syncBtn, falso); serviço.addReplicationStateListener(este); changesMonitor = novo SGMonitor(SG_ADMIN_URL, "false" (falso), "true" (verdadeiro), "0", "todos_docs", este); changesMonitor.iniciar(); } privado vazio documentListInitialize() { Consulta consulta = db.createAllDocumentsQuery(); consulta.setAllDocsMode(Consulta.AllDocsMode.INCLUDE_DELETED); Consulta ao vivo = consulta.toLiveQuery(); Consulta ao vivo.addChangeListener(este); Consulta ao vivo.iniciar(); documentList.getSelectionModel().selectedItemProperty().addListener(este); } |
Separei a inicialização da lista de documentos em sua própria rotina. A lista de documentos é vinculada ao documentList variável. Por sua vez documentList atualizará a interface do usuário sempre que a lista de itens que passamos for alterada.
Configurei uma consulta em tempo real para monitorar o banco de dados do cliente quanto a quaisquer alterações. Isso acontece por meio de uma consulta de "todos os documentos". Uma consulta de todos os documentos não requer uma visualização associada. Eu defino INCLUDE_DELETED para que a ferramenta possa mostrar a aparência de um documento excluído no banco de dados.
Com os outros vínculos em vigor, só precisamos atualizar o documentos lista. Veremos o ouvinte de consulta ao vivo que faz isso mais adiante.
As próximas linhas definem o estado inicial de alguns botões de alternância. Preciso de um ouvinte extra para manter o Sincronização consistente com o estado real das replicações. Mais informações sobre isso ao longo do artigo.
Escrevi uma classe separada para monitorar o Sync Gateway. O código de inicialização foi concluído com a criação de uma nova instância de monitor e seu início.
A próxima seção contém vários ouvintes.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// LiveQuery.ChangeListener @Override público vazio alterado(LiveQuery.ChangeEvent evento) { se (evento.getSource().iguais(Consulta ao vivo)) { Plataforma.runLater(() -> { QueryEnumerator linhas = evento.getRows(); documentos.claro(); linhas.forEach(queryRow -> documentos.adicionar(queryRow.getDocumentId())); }); } } |
Aqui está o ouvinte de consulta ao vivo que é chamado sempre que o banco de dados local é alterado. Eu não projetei a ferramenta para trabalhar com bancos de dados enormes. Portanto, sempre que os dados mudavam, eu simplesmente adotava a abordagem de força bruta de reler todos os documentos. O getRows retorna um enumerador que indexará e fará exatamente isso. O JavaFX se encarrega de atualizar a interface do usuário quando documentos mudanças.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// ListView ChangeListener @Override público vazio alterado(Valor observável observável, Cordas oldId, Cordas novoId) { se (nulo == novoId) retorno; Mapa propriedades = db.getDocument(novoId).getProperties(); tentar { Cordas json = mapeador.writeValueAsString(propriedades); contentsText.setText(prettyText(json)); } captura (JsonProcessingException ex) { ex.printStackTrace(); Diálogo.exibição(ex); } } |
Esse ouvinte cuida do rastreamento quando um usuário clica em uma entrada na lista de documentos. As entradas são os IDs dos documentos, portanto, podemos usar uma seleção para extrair o documento diretamente do banco de dados.
|
1 2 3 4 5 |
// SGMonitor.ChangesFeedListener @Override público vazio onResponse(Cordas corpo) { changesFeed.anexarTexto(prettyText((Cordas) corpo)); } |
Usei uma abordagem de retorno de chamada para obter os resultados do feed de alterações. A interface é definida na seção SGMonitor classe. Ela tem apenas um método. Nessa implementação, simplesmente pego o corpo da resposta do feed e o coloco sobre o texto existente no painel de texto do feed de alterações. Também foi feita uma pequena formatação para facilitar a leitura.
|
1 2 3 4 5 |
// DBService.ReplicationStateListener @Override público vazio onChange(booleano isActive) { setState(syncBtn, isActive); } |
Por fim, adicionei um ouvinte para a atividade de replicação. A interface vem da classe auxiliar DBService. Escrevi um pouco sobre como detectar o estado de uma replicação aqui. Para este aplicativo, só preciso saber se uma replicação está sendo executada ou não para manter a Sincronização estado do botão consistente. Isso lida com casos em que um usuário tenta iniciar uma sincronização, mas ela falha. Isso pode ocorrer se o usuário precisar fornecer credenciais de autenticação, mas não as tiver fornecido, por exemplo.
Em seguida, temos vários métodos vinculados a elementos da interface do usuário. O JavaFX cuida de grande parte da fiação.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
@FXML privado vazio applyCredentialsToggled(ActionEvent evento) { Cordas nome de usuário = nulo; Cordas senha = nulo; se (applyCredentialsBtn.isSelected()) { nome de usuário = nome de usuárioTexto.getText(); senha = passwordText.getText(); } DBService.getInstance().setCredentials(nome de usuário, senha); applyCredentialsBtn.setStyle(applyCredentialsBtn.isSelected() ? TOGGLE_ACTIVE : TOGGLE_INACTIVE); } |
Aqui eu defino o uso de credenciais de autenticação sempre que o botão correspondente for acionado.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@FXML privado vazio saveContentsClicked(ActionEvent evento) { Mapa propriedades = nulo; Documento documento; tentar { propriedades = mapeador.readValue(contentsText.getText(), Mapa.classe); } captura (IOException ex) { ex.printStackTrace(); Diálogo.exibição(ex); } se (propriedades.containsKey("_id")) { documento = db.getDocument((Cordas) propriedades.obter("_id")); } mais { documento = db.createDocument(); } tentar { documento.putProperties(propriedades); } captura (CouchbaseLiteException ex) { ex.printStackTrace(); Diálogo.exibição(ex); } } |
Esse código mostra alguns itens interessantes. Eu uso um Jackson Mapeador de objetos para converter o texto no painel de conteúdo em um mapa de propriedades.
Em seguida, verifico se há uma entrada _id. O Couchbase Mobile reserva a maioria das propriedades que começam com um "_" para uso do sistema (com exceções especiais). Se o texto que estivermos tentando converter contiver _idSe o documento não for editado, presumo que seja uma edição de um documento existente. Caso contrário, crio um novo documento.
Portanto, em poucas palavras, temos um exemplo de criação e atualização de documentos. Essa não é a maneira preferida de atualizar, embora seja suficiente em muitos casos. Você pode ler mais sobre atualizações aqui.
|
1 2 3 4 5 6 7 8 9 10 11 |
@FXML privado vazio syncToggled(ActionEvent evento) { tentar { syncBtn.setDisable(verdadeiro); syncBtn.setStyle(TOGGLE_DISABLED); serviço.toggleReplication(novo URL(SG_PUBLIC_URL), verdadeiro); } captura (Exceção ex) { ex.printStackTrace(); Diálogo.exibição(ex); syncBtn.setDisable(falso); } } |
Isso reage à alternância do Sincronização botão. Lembre-se, porém, de que usamos um ouvinte para verificar o estado em outro lugar.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@FXML privado vazio exitClicked(ActionEvent evento) { // Tentar encerrar tudo de forma graciosa changesMonitor.parar(); Consulta ao vivo.parar(); serviço.stopReplication(); db.próximo(); db.getManager().próximo(); Plataforma.saída(); } privado vazio setState(Botão de alternância btn, booleano ativo) { btn.setSelected(ativo); btn.setStyle(ativo ? TOGGLE_ACTIVE : TOGGLE_INACTIVE); btn.setDisable(falso); } privado Cordas prettyText(Cordas json) { Cordas fora = nulo; tentar { Objeto objeto = mapeador.readValue(json, Objeto.classe); fora = mapeador.writerWithDefaultPrettyPrinter().writeValueAsString(objeto); } captura (Exceção ex) { ex.printStackTrace(); } retorno fora; } } |
O restante do código aqui são apenas bits auxiliares e uma parte para encerrar tudo antes de sair.
A classe Database Helper
Isso mostra o código de uma classe auxiliar de banco de dados simples. Na maioria das vezes, considero essa classe um bom pacote das operações típicas necessárias para gerenciar um banco de dados e iniciar um conjunto bidirecional padrão de replicações. Estou incluindo-a aqui porque a considero útil e para maior clareza.
Eu implemento o Replicação.ChangeListener interface. Isso talvez seja um pouco incomum. Mencionei o motivo anteriormente. Este link leva você à página postagem no blog sobre isso.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
pacote com.couchbase.móvel; importação com.couchbase.leve.Banco de dados; importação com.couchbase.leve.JavaContext; importação com.couchbase.leve.Gerente; importação com.couchbase.leve.autenticação.Autenticador; importação com.couchbase.leve.autenticação.AuthenticatorFactory; importação com.couchbase.leve.replicador.Replicação; importação com.couchbase.leve.replicador.Estado da Replicação; importação java.rede.URL; importação java.util.ArrayList; importação java.util.Lista; público classe DBService implementa Replicação.ChangeListener { público estático final Cordas BASE DE DADOS = "db"; privado estático final Cordas DB_DIRECTORY = "dados"; privado Gerente gerente; privado Banco de dados banco de dados; privado Replicação pushReplication = nulo; privado Replicação pullReplication = nulo; privado booleano replicationActive = falso; privado Lista Ouvintes de estado = novo ArrayList(); privado Cordas nome de usuário = nulo; privado Cordas senha = nulo; privado DBService() { tentar { gerente = novo Gerente(novo JavaContext(DB_DIRECTORY), Gerente.DEFAULT_OPTIONS); banco de dados = gerente.getDatabase(BASE DE DADOS); } captura (Exceção ex) { ex.printStackTrace(); } } privado estático classe Titular { privado estático DBService INSTÂNCIA = novo DBService(); } público interface Ouvinte de estado de replicação { vazio onChange(booleano isActive); } público estático DBService getInstance() { retorno Titular.INSTÂNCIA; } público Banco de dados getDatabase() { retorno banco de dados; } público vazio setCredentials(Cordas nome de usuário, Cordas senha) { este.nome de usuário = nome de usuário; este.senha = senha; } público vazio toggleReplication(URL portal, booleano contínuo) { se (replicationActive) { stopReplication(); } mais { startReplication(portal, contínuo); } } público vazio startReplication(URL portal, booleano contínuo) { se (replicationActive) { stopReplication(); } pushReplication = banco de dados.createPushReplication(portal); pullReplication = banco de dados.createPullReplication(portal); pushReplication.setContinuous(contínuo); pullReplication.setContinuous(contínuo); se (nome de usuário != nulo) { Autenticador autenticação = AuthenticatorFactory.createBasicAuthenticator(nome de usuário, senha); pushReplication.setAuthenticator(autenticação); pullReplication.setAuthenticator(autenticação); } pushReplication.addChangeListener(este); pullReplication.addChangeListener(este); pushReplication.iniciar(); pullReplication.iniciar(); } público vazio stopReplication() { se (!replicationActive) retorno; pushReplication.parar(); pullReplication.parar(); pushReplication = nulo; pullReplication = nulo; } público vazio addReplicationStateListener(Ouvinte de estado de replicação ouvinte) { Ouvintes de estado.adicionar(ouvinte); } público vazio removeReplicationStateListener(Ouvinte de estado de replicação ouvinte) { Ouvintes de estado.remover(ouvinte); } // Replicação.ChangeListener @Override público vazio alterado(Replicação.ChangeEvent changeEvent) { se (changeEvent.getError() != nulo) { Lançável lastError = changeEvent.getError(); Diálogo.exibição(lastError.getMessage()); retorno; } se (changeEvent.getTransition() == nulo) retorno; Estado da Replicação dest = changeEvent.getTransition().getDestination(); replicationActive = ((dest == Estado da Replicação.PARAR || dest == Estado da Replicação.PARADO) ? falso : verdadeiro); Ouvintes de estado.forEach(ouvinte -> ouvinte.onChange(replicationActive)); } } |
A classe Sync Gateway Monitor
Por fim, vamos dar uma olhada na classe auxiliar para monitorar o Sync Gateway. Também vou analisar isso em partes.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
pacote com.couchbase.móvel; importação com.xml mais rápido.jackson.vinculação de dados.JsonNode; importação okhttp3.*; importação java.io.IOException; importação java.rede.SocketException; importação java.util.concomitante.TimeUnit; importação estático com.couchbase.móvel.Tempo de execução.mapeador; público classe SGMonitor { privado estático final OkHttpClient cliente = novo OkHttpClient.Construtor() .readTimeout(1, TimeUnit.DIAS) .construir(); privado Ouvinte do ChangesFeedListener ouvinte; privado HttpUrl.Construtor Criador de url; privado Tópico monitorThread; privado Cordas desde = "0"; privado Chamada chamada; SGMonitor(Cordas url, Cordas activeOnly, Cordas includeDocs, Cordas desde, Cordas estilo, Ouvinte do ChangesFeedListener ouvinte) { este.desde = desde; Criador de url = HttpUrl.analisar(url).newBuilder() .addPathSegment("_changes") .addQueryParameter("active_only", activeOnly) .addQueryParameter("include_docs", includeDocs) .addQueryParameter("estilo", estilo) .addQueryParameter("desde", desde) .addQueryParameter("feed", "longpoll") .addQueryParameter("timeout", "0"); este.ouvinte = ouvinte; } |
Eu uso o Biblioteca OkHttp da Square. Atualmente, o Couchbase Lite também usa essa biblioteca internamente. O OkHttp usa um padrão de construtor. Preparo uma instância do builder que usarei durante o restante do código no construtor da classe. Você pode ler sobre o significado de todos os parâmetros na seção Documentação do Sync Gateway.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
público interface Ouvinte do ChangesFeedListener { vazio onResponse(Cordas corpo); } público vazio iniciar() { monitorThread = novo Tópico(() -> { enquanto (!Tópico.interrompido()) { Solicitação solicitação = novo Solicitação.Construtor() .url(Criador de url.construir()) .construir(); chamada = cliente.newCall(solicitação); tentar (Resposta resposta = chamada.executar()) { se (!resposta.isSuccessful()) lançar novo IOException("Código inesperado" + resposta); Cordas corpo = resposta.corpo().string(); JsonNode árvore = mapeador.readTree(corpo); desde = árvore.obter("last_seq").asText(); Criador de url.setQueryParameter("desde", desde); ouvinte.onResponse(corpo); } captura (SocketException ex) { retorno; } captura (IOException ex) { ex.printStackTrace(); Diálogo.exibição(ex); } } }); monitorThread.setDaemon(verdadeiro); monitorThread.iniciar(); } |
O iniciar tem a parte mais interessante do código. Ele ativa um thread em segundo plano. Sob a configuração do thread e o código de controle, executo um loop contínuo. O loop faz chamadas de rede síncronas. O tratamento de erros é simples. Basta lançar uma exceção se algo der errado.
O Sync Gateway responde com cadeias de caracteres JSON. Você pode ver que o código separa a resposta e analisa o JSON em um arquivo JsonNode objeto. Isso tudo é para chegar ao última_seq na resposta.
Para rastrear o que deve ser enviado em seguida, o feed de alterações depende de um mecanismo de sequência simples. Você deve tratar isso como um objeto opaco. Pegue o valor de última_seq da resposta anterior e defina o desde para esse mesmo valor na próxima solicitação.
Não há nenhum dano real em não fornecer o desde parâmetro. O Sync Gateway apenas reproduzirá todas as alterações desde o início se ele estiver faltando. É por isso que você verá que, neste exemplo, eu trapaceio um pouco e sempre crio a instância da classe com desde definido como a string "0".
Em um aplicativo do mundo real, talvez você queira ter uma maneira de salvar a última sequência de caracteres que seu aplicativo processou, em vez de ficar percorrendo o histórico de alterações todas as vezes.
O restante do código é apenas um par de métodos curtos.
|
1 2 3 4 5 6 7 8 9 |
público vazio parar() { monitorThread.interromper(); chamada.cancelar(); } público Cordas getSince() { retorno desde; } } |
E isso é tudo para as classes principais. Há outras necessárias para o aplicativo completo.
Confira o Repositório do GitHub para ver todo o código e as instruções para criá-lo.
Leia uma discussão sobre o aplicativo e como usá-lo em primeira parte.
Pós-escrito
Você pode encontrar mais recursos em nosso portal do desenvolvedor e nos siga no Twitter @CouchbaseDev.
Você pode postar perguntas em nosso fóruns. E participamos ativamente de Estouro de pilha.
Entre em contato comigo pelo Twitter com perguntas, comentários, tópicos que você gostaria de ver etc. @HodGreeley