Thrift as a REST API

 
3r3-31. A small article about how we are faced with the problems of synchronization between teams of client and server development. How we connected Thrift in order to simplify the interaction between our teams.
 
Who cares how we did it, and what "side" effects we caught, please look under the cat.
 
 

Prehistory


 
In early 201? when we started a new project, we chose EmberJS as the front end. That almost automatically led us to work on the REST scheme in organizing the interaction of the client and server side of the application. Since 3r314. EmberData
provides a convenient tool for separating the work of backend and frontend teams, and the use of the Adapter allows you to select the "protocol" of interaction.
 
At first, everything is fine - Ember gave us the opportunity to implement emulation of requests to the server. The data for emulation of server models were put into separate fuxtures. If somewhere we started working, I do not use Ember Data, then Ember allows you to write an endpoint handler emulator next to it and return this data. We had an agreement that backend developers should make changes to these files to keep the data up-to-date for the frontend developers to work correctly. But as always, when everything is built on “agreements” (and there is no tool for checking them), a moment comes when “something goes wrong.”
 
New requirements led not only to the appearance of new data on the client, but also to updating the old data model. What ultimately led to the fact that maintaining synchronism of the models on the server and on its emulation in the client's source code was simply expensive. Now the development of the client part, as a rule, begins after the server stub is ready. And the development is on top of the working server, and this complicates the teamwork and increases the release time of the new functionality.
 
Development project 3r3322.
 
Now we are abandoning EmberJS in favor of VueJS. and within the framework of the decision on migration, we began to look for solutions to this problem. The following criteria were developed: 3r3327.  
3r3308.  
3r33333. Compatibility of work with older and newer versions of the protocol 3r-3314.  
3r33333. Maximum convenience for frontend-developers when working “without server”
 
3r33333. Separating the API description from the test data 3r33314.  
3r33333. Easy to synchronize call signatures 3r3308.  
3r33333. clear description of the signature
 
3r33333. easy modification of both frontend and backend developers
 
3r33333. maximum autonomy
 
3r33333.
 
3r33333. A strongly typed API is desirable. Those. the fastest possible detection of a change in protocol
 
3r33333. Ease of testing server logic
 
3r33333. Integration with Spring on the server side without dancing with tambourines.
 
3r33333.
 
Implementation 3r3322.
 
Thinking, it was decided to stop at 3r370. Thrift
. This gave us a simple and understandable API 3r3327 description language.  
3r33180. 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
loadBreeds ()
AdsBreed getAdvBreedById (1: string id)
}
3r33300. 3r33333.
 
For interaction, we use the TMultiplexedProcessor, accessible via TServlet, using the TJSONProtocol. I had to dance a little bit to make Thrift integrate seamlessly with Spring. For this, we had to create and register the Servlet in the ServletContainer programmatically.
 
3r33180. @Component
class ThriftRegister: ApplicationListener
,
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) 3r3333.}
private fun registerServletAndFilter (servletClass: Class
,
filterClass: Class
, 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
}
} 3r33333.
 
What should be noted here. In this code, two service areas are formed. Protected, which is available at /api /v2 /thrift. And open, available at /api /v2 /thrift-ns. For these areas different filters are used. In the first case, when accessing the service by a cookie, an object is created that identifies the user who makes the call. If it is impossible to form such an object, a 401 error is thrown, which is correctly processed on the client side. In the second case, the filter skips all service requests, and if it determines that authorization has occurred, then, after performing the operation, it fills in cookies with the necessary information so that you can make requests to the protected area.
 
To connect a new service, you have to write a little extra code.
 
3r33180. @Component
class DirectoryServiceProcessor @Autowired constructor (handler: DirectoryService.Iface):
DirectoryService.Processor
(handler) 3r33333.
 
And register the processor 3r3327.  
3r33180. @Component
class SecureMultiplexingProcessor @Autowired constructor (dsProcessor: DirectoryServiceProcessor): TMultiplexedProcessor () {3rr3339. init {
this.registerProcessor (DIRECTORY_SERVICE, dsProcessor)
}
} 3r33333.
 
The last part of the code can be simplified by attaching an additional interface to all processors, which will allow you to immediately receive a list of processors with one designer’s parameter, and give responsibility for the processor access key value to the processor itself.
 
 
The work in the mode “without server” has undergone a little change. The developers of the frontend part made an offer that they would work on a stub PHP server. They themselves generate classes for their server that implement the signature for the required protocol version. And implement the server with the necessary data set. All this allows them to work before the server-side developers finish their work.
 
 
The main processing point on the client side is the thrift-plugin, written by us.
 
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, 3r3333339. path: '/api /v2 /thrift',
https: location.protocol === 'https:'
}
let _optionsOpen = {
}
const XHRConnectionerror = (_status) => {
if (_status === 0) {3r3333339.
} else if (_status> = 400) {3r3333339. if (_status === 401) {3r3333339. 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) {3r3333339. self.setRecvBuffer (this.responseText)
} else {
if (this.status === 404 || this.status> = 500) {3r3333339.} else {3r33933.}
}
}
}
xreq.open ('POST', this.url, true)
Object.keys (this.headers) .forEach (function (headerKey) {
Xreq.setRequestHeader (headerKey, self.headers[headerKey]) 3rr3339.}) 3rr3339. 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),
}
}
}
3r33333.
 
For the correct operation of this plugin, you must connect the generated classes.
 
 
The call of server methods on the client looks as follows: 3r3327.  

thriftPlugin.DirectoryService.loadBreeds ()
.then (_response => {
}) 3r3333339. .catch (error => {
}) 3r3333339.})
3r33300. 3r33333.
 
Here I don’t go deep into the features of VueJS itself, where it’s right to keep the code calling the server. This code can be used inside the component, inside the route and inside the Vuex-action.
 
When working with the client side, there are a couple of limitations that need to be taken into account after mental migration with internal thrift integration.
 
3r3308.  
3r33333. jаvascript client does not recognize null values. Therefore, for fields that can be null, you must specify the optional flag. In this case, the client will correctly perceive this value
 
3r33333. jаvascript does not know how to work with long values, so all integer identifiers must be cast to the server-side string
 
3r33333.
 
 
Conclusions 3r3322.
 
The transition to Thrift allowed us to solve those problems that are present in the interaction between server and client development when working on the old version of the interface. Allowed to make possible the handling of global errors in one place.
 
 
At the same time, due to the strict API typing and, consequently, the strict rules of data serialization /deserialization, we received an increase of ~ 30% in the interaction time per client and server for most requests (when comparing the same requests through REST and THRIFT interaction, from the time of sending the request to the server, until the response is received) 3r33335.
3r33333. ! function (e) {function t (t, n) {if (! (n in e)) {for (var r, a = e.document, i = a.scripts, o = i.length; o-- ;) if (-1! == i[o].src.indexOf (t)) {r = i[o]; break} if (! r) {r = a.createElement ("script"), r.type = "text /jаvascript", r.async =! ? r.defer =! ? r.src = t, r.charset = "UTF-8"; var d = function () {var e = a.getElementsByTagName ("script")[0]; e.parentNode.insertBefore (r, e)}; "[object Opera]" == e.opera? a.addEventListener? a.addEventListener ("DOMContentLoaded", d,! 1): e.attachEvent ("onload", d ): d ()}}} t ("//mediator.mail.ru/script/2820404/"""_mediator") () (); 3r33333.
3r33333.
+ 0 -

Add comment