REST APIとしての節約

クライアントとサーバーの開発チーム間で作業を同期する問題にどのように遭遇したかについての短い記事。 チーム間の相互作用を簡素化するために、Thriftをどのように接続したか。



誰が私たちがこれをどうやってやったか、どのような「副作用」を捕らえたのかは、猫の下で見てください。



背景



新しいプロジェクトを開始した2017年の初めに、フロントエンドとしてEmberJSを選択しました。 これにより、アプリケーションのクライアント部分とサーバー部分の相互作用を整理するときに、ほぼ自動的にRESTスキームに取り組みました。 なぜなら EmberDataは、バックエンドコマンドとフロントエンドコマンドの作業を分離する便利なツールを提供します。アダプターを使用すると、相互作用の「プロトコル」を選択できます。



最初は、すべてが問題ありません-Emberは、サーバー要求のエミュレーションを実装する機能を提供してくれました。 サーバーモデルをエミュレートするためのデータは、個別のfuxturesファイルに格納されていました。 Ember Dataを使用せずに作業を開始した場合、Emberを使用すると、エンドポイントハンドラーエミュレーターを近くに記述して、このデータを返すことができます。 バックエンド開発者がこれらのファイルを変更して、フロントエンド開発者が正しく動作するようにデータを最新の状態に保つ必要があるという合意がありました。 しかし、いつものように、すべてが「合意」に基づいて構築されている場合(およびそれらを検証するツールがない場合)、「何かが間違っている」という時が来ます。

新しい要件により、クライアントに新しいデータが表示されるだけでなく、古いデータモデルが更新されました。 最終的には、サーバー上のモデルの同期を維持し、クライアントのソースコードでのエミュレーションを維持するだけで、コストが高くなるという事実につながりました。 原則として、クライアントスタブの開発は、サーバースタブの準備が整った後に開始されます。 また、開発は運用サーバー上で実行されるため、チームの作業が複雑になり、新しい機能のリリース時間が長くなります。



プロジェクト開発



現在、VueJSを支持してEmberJSを放棄しています。 移行に関する決定の枠組みの中で、この問題の解決策を探し始めました。 次の基準が作成されました。





実装



考えた後、 Thriftに立ち寄ることになりました。 これにより、シンプルで明確なAPI記述言語が提供されました。



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





相互作用には、TServletを介してTJSONProtocolを使用してアクセス可能なTMultiplexedProcessorを使用します。 このThriftをSpringとシームレスに統合するには、少し踊らなければなりませんでした。 これを行うには、ServletContainerでサーブレットをプログラムで作成して登録する必要がありました。



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





ここで注意すべきこと。 このコードでは、2つのサービスエリアが形成されます。 保護されています。アドレス「/ api / v2 / thrift」で利用できます。 そして、「/ api / v2 / thrift-ns」で利用可能です。 これらの領域には、異なるフィルターが使用されます。 最初のケースでは、Cookieによってサービスにアクセスすると、呼び出しを行うユーザーを定義するオブジェクトが形成されます。 そのようなオブジェクトを形成することが不可能な場合、401エラーがスローされ、クライアント側で正しく処理されます。 2番目のケースでは、フィルターはサービスに対するすべてのリクエストをスキップし、承認が行われたと判断した場合、操作が完了した後、保護領域にリクエストを送信できるように必要な情報をCookieに入力します。



新しいサービスを接続するには、少し余分なコードを書く必要があります。



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





プロセッサを登録します



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





コードの最後の部分は、すべてのプロセッサに追加のインターフェイスを追加することで簡素化できます。これにより、1つの設計者パラメータでプロセッサのリストをすぐに取得でき、プロセッサへのアクセスキーの値に責任を与えます。



「サーバーなし」モードでの作業は少し変更されました。 フロントエンド部分の開発者は、PHPスタブサーバーで作業することを提案しました。 プロトコル自体の目的のバージョンの署名を実装するサーバーのクラスを生成します。 そして、必要なデータセットを備えたサーバーを実装します。 これにより、サーバー側の開発者が作業を完了する前に作業を行うことができます。



クライアント側の主な処理ポイントは、私たちが作成したthrift-pluginです。



 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), } } }
      
      





このプラグインが正しく機能するには、生成されたクラスを接続する必要があります。



クライアントでのサーバーメソッドの呼び出しは次のとおりです。



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





ここでは、サーバーを呼び出すコードを保持するのが正しいVueJS自体の機能については詳しく説明しません。 このコードは、コンポーネント内、ルート内およびVuex-action内の両方で使用できます。

クライアント側で作業する場合、内部の節約統合からの精神的な移行後に考慮しなければならないいくつかの制限があります。





結論



Thriftへの移行により、古いバージョンのインターフェイスで作業しているときに、サーバーとクライアントの開発の相互作用に存在する問題を解決できました。 一箇所でグローバルエラーの処理を可能にしました。



同時に、APIの厳密なタイピングと、したがってデータのシリアル化/逆シリアル化の厳格なルールにより、追加のボーナスがあり、ほとんどのリクエスト(RESTとTHRIFTの相互作用を介して同じリクエストを比較する場合、リクエストがサーバーに送信されてから、レスポンスが受信されるまで



All Articles