最近换到了新部门,部门所有的开发起点都是依赖raml
这个接口定义规范来书写接口文档,然后利用生成工具生成对应的java接口代码,然后,我就接到一项任务——解析这个文档的定义。
首先要说的是,生成和解析的工具并不是同一个,生成工具直接输出源码文件,而解析工具则读取raml的入口文件封装成一个java的对象,本文主要是讲对解析工具(我对raml的规范和书写也是完全陌生的),具体也是我在使用中的发现;因为网站针对该工具的使用文档资料特别少,以此来记录一下。
一、文档的读取
只要文档编写正确,通过raml-parser-2
提供的RamlModelBuilder.buildApi()
接口读取文件,即可获得一个RamlModelResult
对象;
根据RamlModelResult.hasErrors()
的返回,可以判断读取过程中是否有异常,如果有异常的情况下,可以使用RamlModelResult.getValidationResults()
来获取错误信息的结果集合,再进行遍历得到具体的信息,我使用过程中遇到的异常多是因为文档编写时引用外部文档的路径错误或是大小写敏感(linux下)导致的;
文档成功读取时,从RamlModelResult
对象中就可以获取具体定义的信息了,但是要注意的是文档的实际信息是封装在代理类中的,因此RamlModelResult
对象内部是无法直接看到文档的信息的,只能通过暴露的方法来获取,例如getApiV10()
、getLibrary()
等,因此通过debug模式来查看该对象内部的属性是很不直观的。
二、Api对象的获取
根据查阅得知raml
文档的定义有0.8和1.0这两个版本,现在大多使用1.0版本,因此此次使用时也是调用了getApiV10()
方法来获取对应的Api
对象;
可以看到Api
对象中同样暴露了很多方法来提供文档中的数据,一一对应入口文件中的匹配项,这里不需要赘述,重点看我工作中解析要用到的types()
、uses()
、resources()
三项,具体说明如下:
types()
——返回一个List
类型的结果,TypeDeclaration
就是类型的声明,这个结果主要包含了本文档内定义的类型信息;
uses()
——返回一个List
类型的结果,Library
是库的意思,这个结果主要包含了本文档中对外部文档依赖的引入信息;
resources()
——返回一个List
类型的结果,Resource
是资源的意思,也就是这个文档中定义了哪些接口、url是什么、请求体、响应体等等,因此是本文档中最主要的信息;
这里可以看到types
和uses
获取的多是基础信息,譬如定义了哪些类型、依赖哪些外部文档提供的类型等,而resources
中除了本身资源的定义之外,就是对这些基础信息的使用了,接下来一步一步来讲述我按需解析这个文档的过程。
三、所有类型定义信息的初步获取和组装
根据我对实际文档(并未包含规范要求的全部属性)的观察,发现除了入口文档中对资源,也就是resources
的定义之外,其他诸如types
和uses
多是用来定义类型以及引入外部库定义的类型,引入外部库是为了复用基础的类型定义,这与java中的import和类型的继承十分相似,但也因为这个原因造成了解析工具获取的结果是嵌套的,比如types
中定义的某个类型本身没有太多的属性定义,但是它继承了uses
中引入的某个库文件中定义的类型,而uses
中的库文件内部还可以继续使用uses
引入其他的库,因此解析起来并不是很直观;正因为resources
对类型定义信息的依赖,所以我打算优先读取文档中所有的类型定义信息后,再解析resources
。
uses解析
前面提到uses
是对外面文档库的引用,可以看到这是一个Library
的集合,有暴露的方法可以看到其中包含了types
和uses
,由此可以见这是嵌套的依赖,同时包含了类型定义,因此我们实际要解析的其实就是types;利用递归的方法去读取所有uses
中的types
,并将结果放入平级的映射表中(此处我使用了HashMap的结构存放types
的结果,并利用putAll()
方法来添加递归返回的结果,以此来将嵌套的结果扁平化,当前前提是不能有重名的类型定义,否则会存在key的value被覆盖的情况)。
types解析
这里是读取类型定义的重点,也是容易跳坑的地方,由前文可知types
是一个类型声明的集合,内部都是TypeDeclaration
的对象,其中定义的类型的所有信息都在这里面,但是:
TypeDeclaration
是一个
抽象的接口类型!
抽象的接口类型!
抽象的接口类型!
重要的事情说三遍,是的,这里为了通用,工具使用了抽象,其中暴露一些通用的方法去获取类型定义的部分信息,比如类型名、显示名、类型信息、父类型信息、用例信息等等(其他还有一些我也不甚了解的信息,这里就不说出来误人子弟了),但是有个很关键性的信息没有,那就是类内部的属性定义的没有,换句话说就是我自定义了一个类,但是通过TypeDeclaration
的对象却无法拿到类的成员属性有哪些,真是残忍。
当时我在这里卡了很久不得解,前面说到这些类型都是接口只是定义了方法,真正的实现是运行时动态代理来完成的,根本无法通过debug的方式去观察内部的结构,又没有相关的文档和资料查看,因此我最后只有使用反射的方式,运行时对应实际类型中的方法列表,从中发现了一名为properties
的方法,通过invoke执行得到了自己想要的信息,binggo!这就是我要了的。
但是,怎么调用这个方法呢?不能总是通过反射去完成吧?工具的设计者也不可能这么过分提供了方法却不暴露出来吧?那就只有一种可能——多态,实际运行时的对象也许并不是TypeDeclaration
类型的,因此具有更多的方法,通过反射获取类型名称发现果然如此——拥有properties
的是一个名为ObjectTypeDeclaration
的类型,看着名字就知道跟那个通用类型有关系了,查看源码才知道TypeDeclaration
这货有很多子类接口,ObjectTypeDeclaration
是其中之一,另外还有StringTypeDeclaration
、NumberTypeDeclaration
等,每个都有不同的扩展暴露更多的方法来获取相关的定义。
这里之所以坑,是因为没有相关的资料说明,如果有的话,这应该是最基本的信息了,也因此我了解到raml规范中是有内置类型的,这些子类就对应了这些内置类型,对于raml规范来说,内置类型就是原子单元了,非内置类型最终都会落地到某一个内置类型上,也只有这样才具有实际意义,类似java中的8大基本类型以及某特殊的String类型一样。
分类处理TypeDeclaration
分析到这一步,解析types
的方法就很明确了——按内置类型分类处理,由于实际的需要,我仅做了简单的分类,也就是object
、string
、number
或integer
、其他非自定义类型、自定义类型,如下图:
代码比较简陋,请勿见笑,这里只是提供了一个解析思路,相信大家可以写出更优雅的代码。
细心的同学可能发现了,中间乱入了parentTypes
、parserPropertiesToMapInitial(propertiesList)
,这两个是干什么的呢?
先说parentTypes
吧,顾名思义,这是包含了当前类型继承的父类型的信息,为什么会有它的出现呢?我在分析的过程中发现,raml
文件中自定义类型是可以直接引用已有类型来定义自己的,而且可以引用多个已有类型(这里似乎跟java有冲突,其实raml本身并不是完全对应java的所以没必要遵循java的单继承原则,而且生成的java文件都是interface
类型,接口多继承没毛病),因此,对于自定义类型的解析还需要递归向上的解析其引用的父类型(又是嵌套T.T),如上图2中,我将父类型中解析得到的结果以平行的结构方法本类型中(这与文档的引用目的一致,就是复用引用类型的属性);
再说parserPropertiesToMapInitial(propertiesList)
这个方法,这是我自定义的方法,目的是解析出object
类型定义的所有属性,如图:
由图中可以看到
properties
的本质也是一个
List
,难道又要跳入解析
types
的循环中去?一刀捅死我算了,然而我并未如此做,所以不能捅死我。这里我遍历了这个集合,并将属性名作为key,属性的类型作为value组成了一个HashMap,而不是继续去递归解析,那样真的就没个头了;后续,我将再次遍历我的初次解析结果,然后把这些自定义类的属性按其实际类型替换成对应类型的定义,这样就可以了。
到这里types
的初步解析就完成了。
四、所有类型定义信息的二次解析
通过初次的解析可以得到一个HashMap,里面包含了文档中定义的所有类型的名称(key)以及其定义或引用(value),为什么说是定义或引用呢,原因前面已经说过,因为有些自定义类型的定义就是引用了已有的类型,而且object
的成员属性也可以引用已有的类型,这两处如果引用的不是内置类型的话,前面的初步解析中,我是没有继续递归去解析的,所以就需要这个二次解析来完成了,用我自己的话说就是对自定义类型的属性或引用类进行内置类型的填充,利用HashMap中都是多个引用指向同一对象的原理来完成填充的动作,如图:
这个过程比较简单,唯一要注意的就是文档的定义中,引用某一类型时可能会通过
[]
符号来区分是集合还是单体,这一点在填充的时候需要注意。
到此所有类型的获取就完成了。
五、resources的解析
resources
是资源的集合,对于单个resource
来说也是存在嵌套的,如果其methods()
方法又返回,则说明这一层包含有对应的接口,如果resources()
方法又返回,则说明包含子级别的资源;直观的看文档可以发现resource
的分级其实就是连续uri的分段,某一段可以作为一个层级来定义对应的接口方法和方法内其他信息;
由此可见,resources
的解析又是一个递归的过程,这并没有太多需要讨论的地方,有子级资源就递归进去解析子级资源即可,而resource
中比较重要的信息就只有uri的定义,也就是resourcePath()
方法的返回,因此真正的解析则落到了methods()
方法的返回值上。
methods()
返回值为List
,是接口方法的列表,根据http协议的定义,其中包含了GET
、POST
、DELETE
、PUT
等多个方法,所以这里是一个集合类型。
每一个Method
对象又包含queryParameters
、uriPrameters
、headers
、body
、responses
等几个部分,下面分别解释这几个部分:
queryParameters
——查询参数的定义,多是在GET
方法中使用,跟在url后的参数列表;
uriPrameters
——顾名思义,路径参数,就是通过/router/{xxx}
形式定义的参数列表,这个参数的获取方法没有对应其名称,而是在Method
对象的resource()
方法返回的Resource
对象结果中,调用uriParameters()
,这里的Resource
对象和前面获取方法列表的Resource
对象其实就是一个,路径参数就是定义在uri上的,从Resource
对象获取没毛病;但是需要注意的事parentResource()
方法返回的父级资源,为什么要提到这个呢?因为继承,Resource
的```uriParameters()``方法只能获取本层级的路径参数,但是子级资源是会集成父级资源的路径参数的,因此需要从父级资源中获取,这里算是一个小坑;
headers
、body
——放在一起说明,是因为这两位返回的需要解析的内容比较相似(当然也许是我没见过实际差异比较大的情况),对于headers和body的里面的信息,相信大家都比我熟悉,就是http协议中的;
responses
——相对复杂一点,返回的是List
的集合,很明显这是对返回结果的定义,熟悉http协议的大家肯定都知道,响应的状态码很多,因此这个集合就是对这些状态码的定义的列表,包含了状态码对应的headers
、body
,似乎又回到了前文所述,确实就是这样;
上面的说明中的这些部分,其返回值其实都是一个List
的集合(responses
最终也是解析headers
、body
,因此返回值也可以算是List
),由此可知Method
的解析其实就是对List
的遍历解析,所以为什么要先做types
的解析,答案已经揭晓,到这一步就不需要逐个去分析对应的TypeDeclaration
了,直接根据类型名称,就可以从前面types
的解析结果的HashMap中获取对应的定义进行填充了,这样一来,所有的资源就解析完毕了。下图是一个查询参数的解析实例:
总的来说,虽然有工具类帮我封装文档内容到一个java对象,但是要按业务需求从中取出需要的信息,并重组成新的结构,并不是很简单的事情,加之没有任何官方参考的文档资料,全靠自己瞎胡摸索来获取信息(完全熟悉raml的规范或许能增益,但是时间成本太高了),确实很难短时间得到很完美的结果。
这里我贴图的代码只是针对我这边具体的业务需求所写的解析逻辑,并非通用例子,借此来说明工具类封装对象中的一些信息,大家只要知道内部的结构和组成后,可以按自己的需要去重组。
示例代码地址:https://github.com/avalonyzhily/avalony-materials/tree/master/raml-parser-study