Ahorro como API REST

Un breve artículo sobre cómo nos encontramos con problemas de sincronización de trabajo entre los equipos de desarrollo de clientes y servidores. Cómo conectamos Thrift para simplificar la interacción entre nuestros equipos.

A quién le importa cómo lo hicimos, y qué efectos "secundarios" detectamos, mira debajo del gato.

Antecedentes


A principios de 2017, cuando comenzamos un nuevo proyecto, elegimos EmberJS como la interfaz. Lo cual, casi automáticamente nos llevó a trabajar en el esquema REST al organizar la interacción de las partes cliente y servidor de la aplicación. Porque EmberData proporciona una herramienta conveniente para separar el trabajo de los comandos backend y frontend, y el uso del Adaptador le permite seleccionar el "protocolo" de interacción.

Al principio, todo está bien: Ember nos brindó la capacidad de implementar la emulación de las solicitudes del servidor. Los datos para emular modelos de servidor se colocaron en archivos de archivos separados. Si, en algún lugar, comenzamos a trabajar sin usar Ember Data, Ember le permite escribir un emulador de controlador de punto final cercano y devolver estos datos. Acordamos que los desarrolladores de backend deberían realizar cambios en estos archivos para mantener los datos actualizados para que los desarrolladores frontend funcionen correctamente. Pero, como siempre, cuando todo se basa en "acuerdos" (y no hay una herramienta para verificarlos), llega un momento en que "algo va mal".
Los nuevos requisitos condujeron no solo a la aparición de nuevos datos en el cliente, sino también a la actualización del antiguo modelo de datos. Lo que al final condujo al hecho de que mantener la sincronización de modelos en el servidor y su emulación en los archivos fuente del cliente se volvió simplemente costoso. Ahora, el desarrollo de la parte del cliente, como regla, comienza después de que el código auxiliar del servidor esté listo. Y el desarrollo se lleva a cabo sobre el servidor de producción, y esto complica el trabajo en equipo y aumenta el tiempo de lanzamiento de la nueva funcionalidad.

Desarrollo del proyecto


Ahora estamos abandonando EmberJS a favor de VueJS. y como parte de la decisión sobre migración, comenzamos a buscar soluciones a este problema. Se desarrollaron los siguientes criterios:

  • Compatibilidad con versiones anteriores y nuevas del protocolo.
  • Máxima comodidad para los desarrolladores frontend cuando trabajan "sin un servidor"
  • Separación de la descripción de la API de los datos de prueba
  • Fácil sincronización de firma de llamada
    • descripción clara de la firma
    • facilidad de modificación por parte de los desarrolladores frontend y backend
    • máxima autonomía
  • Las API fuertemente tipadas son deseables. Es decir La detección más rápida de un hecho de un cambio de protocolo
  • Facilidad para probar la lógica del servidor
  • Integración con Spring en el lado del servidor sin bailar con panderetas.

Implementación


Después de pensar, se decidió parar en Thrift . Esto nos dio un lenguaje de descripción API simple y claro.

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 la interacción, utilizamos TMultiplexedProcessor, accesible a través de TServlet, utilizando TJSONProtocol. Tuve que bailar un poco para que este Thrift se integrara perfectamente con Spring. Para hacer esto, tuve que crear y registrar un Servlet en ServletContainer mediante programación.

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

Lo que debe notarse aquí. En este código, se forman dos áreas de servicio. Protegido, que está disponible en la dirección "/ api / v2 / thrift". Y abierto, disponible en "/ api / v2 / thrift-ns". Para estas áreas, se utilizan diferentes filtros. En el primer caso, al acceder al servicio mediante cookies, se forma un objeto que define al usuario que realiza la llamada. Si es imposible formar dicho objeto, se produce un error 401, que se procesa correctamente en el lado del cliente. En el segundo caso, el filtro omite todas las solicitudes del servicio, y si determina que se ha producido la autorización, luego de que se completa la operación, llena las cookies con la información necesaria para que pueda realizar solicitudes al área protegida.

Para conectar un nuevo servicio, debe escribir un pequeño código adicional.

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

Y registre el procesador

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

La última parte del código puede simplificarse colgando una interfaz adicional en todos los procesadores, lo que le permitirá obtener de inmediato una lista de procesadores con un parámetro de diseño, y haberle dado la responsabilidad del valor de la clave de acceso al procesador al procesador mismo.

El trabajo en el modo "sin servidor" ha sufrido un pequeño cambio. Los desarrolladores de la parte frontend hicieron una propuesta de que trabajarían en un servidor stub de PHP. Ellos mismos generan clases para su servidor que implementan la firma para la versión de protocolo deseada. E implementan un servidor con el conjunto de datos necesario. Todo esto les permite trabajar antes de que los desarrolladores del lado del servidor terminen su trabajo.

El principal punto de procesamiento en el lado del cliente es el plugin de ahorro escrito por nosotros.

 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 complemento funcione correctamente, debe conectar las clases generadas.

Los métodos de llamada al servidor en el cliente son los siguientes:

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

Aquí no profundizo en las características de VueJS, donde es correcto mantener el código que llama al servidor. Este código se puede usar tanto dentro del componente como dentro de la ruta y dentro de Vuex-action.
Al trabajar con el lado del cliente, hay un par de limitaciones que deben tenerse en cuenta después de la migración mental desde la integración interna de ahorro.

  • El cliente Javascript no reconoce valores nulos. Por lo tanto, para los campos que pueden ser nulos, debe especificar el indicador opcional. En este caso, el cliente aceptará correctamente este valor.
  • Javascript no sabe cómo trabajar con valores largos, por lo que todos los identificadores enteros deben convertirse en una cadena en el lado del servidor

Conclusiones


La transición a Thrift nos permitió resolver los problemas que están presentes en la interacción entre el desarrollo del servidor y el cliente cuando se trabaja en una versión anterior de la interfaz. Se permite hacer posible el procesamiento de errores globales en un solo lugar.

Al mismo tiempo, con una bonificación adicional, debido a la estricta tipificación de la API y, por lo tanto, a las estrictas reglas de serialización / deserialización de datos, recibimos un aumento de ~ 30% en el tiempo de interacción en el cliente y el servidor para la mayoría de las solicitudes (al comparar las mismas solicitudes a través de la interacción REST y THRIFT, desde el momento en que se envió la solicitud al servidor hasta que se recibe la respuesta)

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


All Articles