Thrift como uma API REST

Um pequeno artigo sobre como tivemos problemas de sincronização do trabalho entre as equipes de desenvolvimento de clientes e servidores. Como conectamos o Thrift para simplificar a interação entre nossas equipes.

Quem se importa com o modo como fizemos isso e com os efeitos colaterais que capturamos, por favor, olhe embaixo do gato.

Antecedentes


No início de 2017, quando iniciamos um novo projeto, escolhemos o EmberJS como front-end. O que quase automaticamente nos levou a trabalhar no esquema REST ao organizar a interação das partes cliente e servidor do aplicativo. Porque O EmberData fornece uma ferramenta conveniente para separar o trabalho dos comandos de back-end e front-end, e o uso do adaptador permite selecionar o "protocolo" de interação.

No começo, está tudo bem - o Ember nos forneceu a capacidade de implementar a emulação de solicitações ao servidor. Os dados para emular modelos de servidor foram colocados em arquivos de instalação separados. Se, em algum lugar, começarmos a trabalhar sem usar o Ember Data, o Ember permitirá que você escreva um emulador de manipulador de terminal próximo e retorne esses dados. Tínhamos um acordo de que os desenvolvedores de back-end deveriam fazer alterações nesses arquivos para manter os dados atualizados para que os desenvolvedores de front-end funcionassem corretamente. Mas como sempre, quando tudo é construído sobre "acordos" (e não há ferramenta para verificá-los), chega um momento em que "algo está errado".
Novos requisitos levaram não apenas ao aparecimento de novos dados no cliente, mas também à atualização do modelo de dados antigo. O que levou ao fato de que manter a sincronização de modelos no servidor e sua emulação nos arquivos de origem do cliente tornou-se simplesmente caro. Agora, o desenvolvimento da parte do cliente, em regra, começa após o stub do servidor estar pronto. E o desenvolvimento é realizado no servidor de produção, o que complica o trabalho da equipe e aumenta o tempo de liberação de novas funcionalidades.

Desenvolvimento do projeto


Agora estamos abandonando o EmberJS em favor do VueJS. e, como parte da decisão sobre a migração, começamos a procurar soluções para esse problema. Os seguintes critérios foram desenvolvidos:

  • Compatibilidade com versões mais antigas e mais recentes do protocolo
  • Comodidade máxima para desenvolvedores de front-end ao trabalhar "sem um servidor"
  • Separação da descrição da API dos dados de teste
  • Fácil sincronização de assinatura de chamada
    • descrição clara da assinatura
    • facilidade de modificação por desenvolvedores de front-end e back-end
    • autonomia máxima
  • APIs fortemente tipadas são desejáveis. I.e. a detecção mais rápida de um fato de uma alteração de protocolo
  • Facilidade de testar a lógica do servidor
  • Integração com o Spring no lado do servidor sem dançar com pandeiros.

Implementação


Depois de pensar, decidiu-se parar na Thrift . Isso nos deu uma linguagem de descrição de API simples e clara.

namespace java ru.company.api namespace php ru.company.api namespace javascrip ru.company.api const string DIRECTORY_SERVICE= "directoryService" exception ObjectNotFoundException{ } struct AdvBreed { 1: string id, 2: string name, 3: optional string title } service DirectoryService { list<AdvBreed> loadBreeds() AdsBreed getAdvBreedById(1: string id) } 

Para interação, usamos TMultiplexedProcessor, acessível através do TServlet, usando TJSONProtocol. Eu tive que dançar um pouco para fazer esse Thrift se integrar perfeitamente ao Spring. Para fazer isso, tive que criar e registrar um Servlet no ServletContainer programaticamente.

 @Component class ThriftRegister : ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware, ServletContextAware { companion object { private const val unsecureAreaUrlPattern = "/api/v2/thrift-ns" private const val secureAreaUrlPattern = "/api/v2/thrift" } private var inited = false private lateinit var appContext:ApplicationContext private lateinit var servletContext:ServletContext override fun onApplicationEvent(event: ContextRefreshedEvent) { if (!inited) { initServletsAndFilters() inited = true } } private fun initServletsAndFilters() { registerOpenAreaServletAndFilter() registerSecureAreaServletAndFilter() } private fun registerSecureAreaServletAndFilter() { registerServletAndFilter(SecureAreaServlet::class.java, SecureAreaThriftFilter::class.java, secureAreaUrlPattern) } private fun registerOpenAreaServletAndFilter() { registerServletAndFilter(UnsecureAreaServlet::class.java, UnsecureAreaThriftFilter::class.java, unsecureAreaUrlPattern) } private fun registerServletAndFilter(servletClass:Class<out Servlet>, filterClass:Class<out Filter>, pattern:String) { val servletBean = appContext.getBean(servletClass) val addServlet = servletContext.addServlet(servletClass.simpleName, servletBean) addServlet.setLoadOnStartup(1) addServlet.addMapping(pattern) val filterBean = appContext.getBean(filterClass) val addFilter = servletContext.addFilter(filterClass.simpleName, filterBean) addFilter.addMappingForUrlPatterns(null, true, pattern) } override fun setApplicationContext(applicationContext: ApplicationContext) { appContext = applicationContext } override fun setServletContext(context: ServletContext) { this.servletContext = context } } 

O que deve ser observado aqui. Nesse código, duas áreas de serviço são formadas. Protegido, disponível no endereço "/ api / v2 / thrift". E aberto, disponível em "/ api / v2 / thrift-ns". Para essas áreas, diferentes filtros são usados. No primeiro caso, ao acessar o serviço por cookies, é formado um objeto que define o usuário que faz a chamada. Se for impossível formar esse objeto, será gerado um erro 401, processado corretamente no lado do cliente. No segundo caso, o filtro ignora todas as solicitações do serviço e, se determinar que a autorização ocorreu, após a conclusão da operação, ele preenche os cookies com as informações necessárias para poder fazer solicitações à área protegida.

Para conectar um novo serviço, você precisa escrever um pouco de código extra.

 @Component class DirectoryServiceProcessor @Autowired constructor(handler: DirectoryService.Iface): DirectoryService.Processor<DirectoryService.Iface>(handler) 

E registre o processador

 @Component class SecureMultiplexingProcessor @Autowired constructor(dsProcessor: DirectoryServiceProcessor) : TMultiplexedProcessor() { init { this.registerProcessor(DIRECTORY_SERVICE, dsProcessor) ... } } 

A última parte do código pode ser simplificada anexando uma interface adicional a todos os processadores, o que permitirá que você obtenha imediatamente uma lista de processadores com um parâmetro de designer e que tenha sido responsável pelo valor da chave de acesso do processador ao próprio processador.

O trabalho no modo "sem servidor" sofreu uma pequena alteração. Os desenvolvedores da parte frontend fizeram uma proposta de que eles trabalhariam em um servidor stub PHP. Eles mesmos geram classes para o servidor que implementam a assinatura para a versão de protocolo desejada. E eles implementam um servidor com o conjunto de dados necessário. Tudo isso permite que eles trabalhem antes que os desenvolvedores do servidor concluam seu trabalho.

O principal ponto de processamento no lado do cliente é o plugin de economia escrito por nós.

 import store from '../../store' import { UNAUTHORIZED } from '../../store/actions/auth' const thrift = require('thrift') export default { install (Vue, options) { const DirectoryService = require('./gen-nodejs/DirectoryService') let _options = { transport: thrift.TBufferedTransport, protocol: thrift.TJSONProtocol, path: '/api/v2/thrift', https: location.protocol === 'https:' } let _optionsOpen = { ... } const XHRConnectionError = (_status) => { if (_status === 0) { .... } else if (_status >= 400) { if (_status === 401) { store.dispatch(UNAUTHORIZED) } ... } } let bufers = {} thrift.XHRConnection.prototype.flush = function () { var self = this if (this.url === undefined || this.url === '') { return this.send_buf } var xreq = this.getXmlHttpRequestObject() if (xreq.overrideMimeType) { xreq.overrideMimeType('application/json') } xreq.onreadystatechange = function () { if (this.readyState === 4) { if (this.status === 200) { self.setRecvBuffer(this.responseText) } else { if (this.status === 404 || this.status >= 500) {... } else {... } } } } xreq.open('POST', this.url, true) Object.keys(this.headers).forEach(function (headerKey) { xreq.setRequestHeader(headerKey, self.headers[headerKey]) }) if (process.env.NODE_ENV === 'development') { let sendBuf = JSON.parse(this.send_buf) bufers[sendBuf[3]] = this.send_buf xreq.seqid = sendBuf[3] } xreq.send(this.send_buf) } const mp = new thrift.Multiplexer() const connectionHostName = process.env.THRIFT_HOST ? process.env.THRIFT_HOST : location.hostname const connectionPort = process.env.THRIFT_PORT ? process.env.THRIFT_PORT : location.port const connection = thrift.createXHRConnection(connectionHostName, connectionPort, _options) const connectionOpen = thrift.createXHRConnection(connectionHostName, connectionPort, _optionsOpen) Vue.prototype.$ThriftPlugin = { DirectoryService: mp.createClient('directoryService', DirectoryService, connectionOpen), } } } 

Para que este plugin funcione corretamente, você precisa conectar as classes geradas.

A chamada de métodos do servidor no cliente é a seguinte:

 thriftPlugin.DirectoryService.loadBreeds() .then(_response => { ... }) .catch(error => { ... }) }) 

Aqui, não me aprofundo nos recursos do próprio VueJS, onde é correto manter o código que chama o servidor. Esse código pode ser usado tanto dentro do componente quanto na rota interna e dentro da ação Vuex.
Ao trabalhar com o lado do cliente, há algumas limitações que devem ser levadas em consideração após a migração mental da integração de economia interna.

  • O cliente Javascript não reconhece valores nulos. Portanto, para campos que podem ser nulos, você deve especificar o sinalizador opcional. Nesse caso, o cliente aceitará corretamente esse valor.
  • O Javascript não sabe trabalhar com valores longos, portanto, todos os identificadores de número inteiro devem ser convertidos para cadeia no lado do servidor

Conclusões


A transição para o Thrift nos permitiu resolver os problemas presentes na interação entre o desenvolvimento do servidor e do cliente ao trabalhar em uma versão antiga da interface. Permitido possibilitar o processamento de erros globais em um só local.

Ao mesmo tempo, com um bônus adicional, devido à digitação estrita da API e, portanto, regras estritas de serialização / desserialização de dados, recebemos um aumento de ~ 30% no tempo de interação no cliente e no servidor para a maioria das solicitações (ao comparar as mesmas solicitações por meio da interação REST e THRIFT, desde o momento em que a solicitação foi enviada ao servidor até a resposta ser recebida)

Source: https://habr.com/ru/post/pt429376/


All Articles