本文主要是介绍天穹-Api接口自动化管理系列2:MiApi- 多协议接口扫描器详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
开源地址
https://github.com/XiaoMi/mone/tree/master/miapi-all/mi-api 欢迎对云原生技术/研发效能感兴趣的小伙伴加入(Fork、Star)我们。
概述
在上一篇对平台的整体介绍中我们介绍了平台提供的核心能力与流程,并且在服务接口信息的加载生成中大致描述了我们获取生成接口数据信息的主要逻辑。本文将深入到源码层面为大家介绍我们是如何实现接口文档数据的扫描解析与生成的。
业务依赖包
在前文中我们提到了获取业务项目数据的关键组件/模块,即注解、扫描器、缓存。
为了获取上述我们所需的业务项目接口的基本数据,我们需要业务方引入我们提供的一套依赖包,针对不同协议接口的服务我们提供了针对性的依赖包,例如对提供 Http 接口、Dubbo接口的项目,我们分别设计提供了适配 Http、适配 Dubbo 的依赖包。这些包将用于不同协议接口数据的解析、数据推送、服务注册等等。
注解定义
作用
注解用于在项目中做一些特定的标记,例如模块层标记、接口层标记、字段层标记等,用于后续的扫描器扫描解析过程。
实现
在设计之初我们首先基于我们定义的接口交付流程,明确我们需要哪些数据。我们希望业务项目在运行之后,研发人员即可在平台中搜索自身项目模块的信息,并根据选择的模块信息加载生成相应的接口数据。这里的模块我们希望是研发人员习惯的、在代码中明确定义的类。
例如对于一个Http接口,基于传统的 mvc 架构,通常该接口入口将实现于 xxxController 类下,例如:
@RestController
@RequestMapping()
public class HelloController {@RequestMapping(path = "/hello",method = RequestMethod.POST)
public Response<String> getToken() {Response<String> r = new Response<>();r.setData("hello");return r;
}
}
那么我们希望用户可以在搜索框直接搜索关键字 HelloController,即可看到该项目下所有模块、接口信息,并针对性选择加载生成文档。
而对于一个Dubbo接口如下:
public interface DubboHealthService {Result<Health> health2(AaReq aaReq);
}@DubboService(timeout = 1000, group = "staging",version = "1.0")
@Slf4j
public class DubboHealthServiceImpl implements DubboHealthService {@Overridepublic Result<Health> health(AaReq aaReq) {Health health = new Health();health.setVersion("1.0");return Result.success(health);}
我们希望可搜索的模块为接口定义,即 DubboHealthService ,用户只需要搜索该接口,即可获取到该项目下所有 interface及其方法列表,并基于这两者进行筛选加载生成文档。
综上针对我们需要获取的接口数据,在业务依赖包中我们提供了几项基础注解:@EnableApiDocs、@ApiModule、@ApiDoc、@ParamDefine。(对于不同协议接口,注解命名上略有不同)
@EnableApiDocs
@EnableApiDocs用于启动类 Bootstrap上,用作开关,用户可以根据填不添加该开关,决定是否启用数据扫描推送功能。
@EnableAutoConfiguration
@ComponentScan(basePackages = {"com.xxx.xxx.hello", "com.xxx.xxx"})
@DubboComponentScan(basePackages = "com.xxx.xxx.hello")
@ServletComponentScan
@EnableDubboApiDocspublic class HelloBootstrap {private static final Logger logger = LoggerFactory.getLogger(HelloBootstrap.class);public static void main(String... args) {try {SpringApplication.run(HelloBootstrap.class, args);} catch (Throwable throwable) {logger.error(throwable.getMessage(), throwable);System.exit(-1);}}
}
例如,如上一个提供 dubbo 接口的项目只需要在启动类上添加 @EnableDubboApiDocs 注解,即可启用该功能。
这个启动类注解的实现也很简单:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Inherited
@Import({DubboApiDocsAnnotationScanner.class})
public @interface EnableDubboApiDocs {
}
实际上我们只是在这个注解中 @Import 入依赖包中的扫描器类,那么只要添加该注解,spring便会帮我们把扫描器类初始化进容器中,后续的行为都将由扫描器执行。
@ApiModule
该注解用于标注模块类,其实现如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
public @interface ApiModule {/*** 用于自定义模块名*/ String value();/*** 用于定位模块类的类型* dubbo api interface class* 若为http接口,则该选项为 apiController*/ Class<?> apiInterface();}
该注解将在扫描器的扫描过程中用于从spring容器中筛选需要解析的类信息,同时也提供关于这些类的基本信息。
对于Http接口通常用法如下:
@RestController
@RequestMapping()
@HttpApiModule(value = "这是一个controller类HelloController", apiController = HelloController.class)
public class HelloController {
}
对于Dubbo接口通常用法如下:
@DubboService(timeout = 1000, group = "staging",version = "1.0")
@Slf4j
@ApiModule(value = "健康检测服务", apiInterface = DubboHealthService.class)public class DubboHealthServiceImpl implements DubboHealthService {
}
@ApiDoc
该注解用于标注具体需要生成文档的接口,实现如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ApiDoc {/*** api name.用于自定义接口名*/ String value();/*** api description.自定义接口描述文档*/ String description() default "";}
通常用法如下:
@Override
@ApiDoc(value = "健康监测方法",description = "这是一个用于健康监测的方法")
public Result<Health> health(AaReq aaReq) {Health health = new Health();health.setVersion("1.0");return Result.success(health);
}
@ParamDefine
该注解用于具体参数字段的定义,实现如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Documented
@Inherited
public @interface ParamDefine {/*** 参数名*/String value();/*** 该参数是否必填*/boolean required() default false;/*** 是否忽略该字段,若是,在生成文档时该字段将被忽略*/boolean ignore() default false;/*** 字段说明描述*/String description() default "";/*** 默认值,若有默认值,将以该值生成mock数据*/String defaultValue() default "";}
通常用法如下:
@Data
public class AaReq implements Serializable {@ApiDocClassDefine(value = "用户名a",required = true,description = "这里是用户名参数",defaultValue = "dongzhenixng")private String username;@ApiDocClassDefine(value ="年龄",required = false,description = "用户年龄",defaultValue = "23")private Integer age;/**
* 也可以不使用该字段,平台将默认提取该字段基本信息,如参数名、类型等*/ private double age21;}
扫描器
作用
扫描器(scanner)为本项目的核心,我们基于jdk的反射能力,在运行时获取项目的服务、接口信息数据。
实现
扫描器由 @EnableApiDocs 开关导入 spring容器,这里由于我们需要基于 spring 容器初始化后的bean数据作为解析目标,因此扫描解析的动作必须发生在spring完成基本初始化操作后,因此这里实现了 spring 开放的 ApplicationListener 接口,该接口能够接收 spring 的项目触发的一系列事件。这里我们接收 ApplicationReadyEvent,即在 spring 框架初始化完成项目基本信息后触发。
public class ApiDocsScanner implements ApplicationListener<ApplicationReadyEvent> {@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {//扫描解析逻辑......
}
}
spring 初始化完成后,项目的 bean 信息等都在spring的上下文 ApplicationContext 中维护,那么在扫描器中即可从中获取标识了 @ApiModule 的模块集合,之后通过反射逐一获取标识了该注解的某块类信息,构造模块数据结构,再通过反射获取每个模块下的方法列表,筛选带有 @ApiDoc 注解的方法,构造接口方法层的数据结构,再对标识了该注解的方法进行进一步处理。
在方法层级的处理中,同样使用反射的方式获取具体参数字段信息,这里根据字段的类型递归构造字段级的数据结构。当然,这里的解析操作较为复杂繁琐,我们需要区分针对不同的参数类型做针对性的解析,例如对于基本类型的参数怎么处理?对于对象object、List、Map、Set、Queue甚至是嵌套的泛型参数怎么处理?哪些类型不能循环递归?哪些需要针对性忽略......这些细节我们在开发以及测试的过程中做了大量的斟酌与兼容处理,这里我们不做详细介绍,感兴趣的朋友可以翻看我们开源的代码。
完成以上几个步骤的数据扫描与解析后,根据一定的规则聚合所有数据,再获取项目运行时本地 ip及使用的端口,调用平台提供的开放接口,将以上数据统一推送到平台,平台将以 ip:port 为唯一索引将数据存入平台数据库中。这里之所以使用 ip:port 作为唯一索引,是由于一般微服务业务项目不管是在开发过程中或者发布到测试环境、生成环境,它们大概率都将拥有多个实例,即同一个项目在多方、多处运行,不同实例的代码版本、开发进度可能不完全一致,因此我们希望用户在平台上可以选择指定的实例,针对性加载生成该实例的接口数据。
扫描器执行流程图
这篇关于天穹-Api接口自动化管理系列2:MiApi- 多协议接口扫描器详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!