当前位置:   article > 正文

关于接口开放平台easyopen

easyopen

1. 概述

一直以来,笔者所在的公司对于对外提供的接口这一块都缺乏有效的管理,诸如权限缺失,参数和返回值过于随意等等问题导致请求访问控制,错误快速定位,事后统计分析等等接口管理需求一直无法满足。

鉴于以上情况,公司决定寻求一套完整的接口管理解决方案。最终我们在码云上找到了一个名为 easyopen 的接口管理项目。本文将主要探究下其背后的实现原理,方便以后将该平台集成到公司的既有项目中,或者借鉴其思路实现自己的接口管理平台。

2. 快速入门

easyopen基于SpringMVC,其快速入门极其简单。读者可以参见 easyopen - 快速开始

// 接口入口响应的配置
@Controller
@RequestMapping("api")
public class IndexController extends ApiController {

    @Override
    protected void initApiConfig(ApiConfig apiConfig) {
   		// 通过这个方法, 用户可以自定义介入框架处理, 根据自身的需求做一些自定义配置
   		// 而且可以看出, 作者在设计的时候考虑得非常不错, 和JFinal类似, 通过提供的自定义类ApiConfig参数, 向外统一暴露了自定义配置设置的入口, 极大降低了用户的学习成本.
        apiConfig.setShowDoc(true); // 显示文档页面
        apiConfig.setDocPassword("doc123"); // 设置文档页面密码
        // 配置国际化消息
        apiConfig.getIsvModules().add("i18n/isv/goods_error");
        // 配置密钥
        Map<String, String> appSecretStore = new HashMap<>();
        appSecretStore.put("test", "123456");
        apiConfig.addAppSecret(appSecretStore);
    }
}

// ===== 接下来就可以编写对外提供的接口了
// 其中:
//	1. 自定义注解 @ApiService 通过继承自Spring中定义的@Service来达到向Spring容器自动注册的效果。
//	2. 自定义注解 @ApiDoc 和 @ApiDocMethod 则是专门用于生成相应的接口说明文档, 类似Swagger2, 避免额外编写接口文档的痛苦, 也最大限度地保证了文档的时效性。
@ApiService
@ApiDoc(value = "Xx模块", order = 1) //
public class OpenApiService {

	@Api(name = "hello", ignoreJWT = true, ignoreValidate = true)
	@ApiDocMethod(description = "第一次测试HelloWord")
	public Map<String, Object> helloworld() {
		return Collections.singletonMap("RT", "hello world");
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

经过以上两步,接口服务就算是编写完成了,启动应用之后:

  1. 访问 http://localhost:8080/api/doc 即可看到相应的接口文档。
  2. 访问 http://localhost:8080/api/monitor 即可看到相应的接口访问监控页面,默认密码是monitor123 。

3. 分析

分析部分大致分为两个部分——框架初始化以及框架如何处理外部请求?

3.1 初始化

通观以上代码,无疑我们需要看看基类ApiController的内部构成了。关于该类,其除了继承自一系列自身定义的接口和抽象类之外,最引人注目的就是其实现了Spring定义的ApplicationListener<ContextRefreshedEvent>接口。

// ApiController.onApplicationEvent()
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
	// 这里作者应用了一个技巧, 内部的注解了 @Component 的私有类Ctx通过实现ApplicationContextAware接口来获取applicationContext实例, 这样便可以通过this.getApplicationContext()取到了.
	ApplicationContext appCtx = this.getApplicationContext();
	if(appCtx == null) {
		appCtx = event.getApplicationContext();
	}
	this.onStartup(appCtx);
}

protected synchronized void onStartup(ApplicationContext applicationContext) {
	// 如果已经配置过了, 则不再进行配置操作
	if(this.apiConfig != null) {
		return;
	}
	// ====ApiContext的这步操作基本可以确定其是作为一个全局容器存在的, 其中存放了各种各样方便操作的信息.
	// 保存ApplicationContext
	ApiContext.setApplicationContext(applicationContext);
	// ==== ApiConfig中存放所有相关的配置信息, 保证了配置入口的一致性.
	// 新建一个ApiConfig
	this.apiConfig = newApiConfig();
	ApiContext.setApiConfig(apiConfig);
	// ==== 加载私钥, 用于可能的数据加密传输 —— https://durcframework.gitee.io/easyopen/#/files/126_数据加密传输
	this.apiConfig.loadPrivateKey();
	// ==== 初始化各类Template, 其中的命名一看就是借鉴自Spring中的JdbcTemplate
	// 初始化Template
	this.initTemplate();
	// ==== 回调用户的自定义配置
	// 初始化配置
	this.initApiConfig(this.apiConfig);
	// ==== 初始化工作, 主要是注册接口, 其中注册工作是委托给了专门的ApiRegister来完成.
	// easyopen初始化工作,注册接口
	this.init(applicationContext, this.apiConfig);
	// ==== 初始化诸如oauth2 Manager和  Interceptor 等等。
	// 初始化其它组件
	// 放在最后
	this.initComponent();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

上述方法中,最值得关注的应该就是注册部分this.init(applicationContext, this.apiConfig);了。

// ApiRegister.regist()
// 回调堆栈:
	ApiController.init()
		ApiRegister.regist()
public void regist(RegistCallback registCallback) {
	logger.info("******** 开始注册操作 ********");
	// ====初始化国际化消息
	this.initMessage();
	// ====注册接口, 将从Spring容器中找到所有被@ApiService注解的Bean, 收集其中所有被@Api注解方法信息, 用于后期提供服务之用. 收集到的信息被存储在名为 DefinitionHolder 类中.
	this.registApi();
	// ====生成doc文档
	this.createDoc();
	// ====回调函数, 默认为空
	this.afterRegist(registCallback);
	logger.info("******** 注册操作结束 ********");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

执行完上述流程之后,框架算是完成了所有的准备工作,正式开始向外提供服务了。这里作者将创建和提供服务进行了泾渭分明的划分,符合《Clean Code》里的思想。相关时序图如下:
启动时序图

3.2 处理请求

我们在进行easyopen配置的时候,并没有与请求处理相关的代码,因此相应处理请求的逻辑应该是在基类ApiController中完成的。

// ApiController.index()
// 这个方法是SpringMVC的默认方法, 对于easyopen接口平台的请求都是统一进的这里
@RequestMapping(method = {RequestMethod.POST, RequestMethod.GET})
public void index(HttpServletRequest request, HttpServletResponse response) {
	// 调用接口方法,即调用被@Api标记的方法
	ApiContext.setRequestMode(RequestMode.SIGNATURE);
	this.invokeTemplate.processInvoke(request, response);
}

// ================================================================================
// InvokeTemplate.processInvoke()
// 关于 InvokeTemplate, 一看就知道其名称借鉴自Spring的JdbcTemplate, 是提供给外界调用API的统一门面入口.
public Object processInvoke(HttpServletRequest request, HttpServletResponse response) {
	try {
		// 再次将处理逻辑委托给专门的Invoker实现类——ApiInvoker
		Object result = this.invoker.invoke(request, response);
		this.afterInvoke(request, response, result);
		return result;
	} catch (Throwable e) {
		return this.processError(request, response, e);
	} finally {
		// 即使不看源码, 只看整个过程里ApiContext的操作, 基本也可以猜到这个类中使用了ThreadLocal特性, 所以这里必须确保存储的资源被释放
		ApiContext.clean();
	}
}

// ================================================================================
// ApiInvoker.invoke()
@Override
public Object invoke(HttpServletRequest request, HttpServletResponse response) throws Throwable {
	// 存储到ThreadLocal, 方便随时随地读取
	ApiContext.setRequest(request);
	ApiContext.setResponse(response);
	try {
		// 解析参数
		ApiParam param = this.apiConfig.getParamParser().parse(request);
		ApiContext.setApiParam(param);
		// 诸如鉴权, 限流, 校验, 回调用户方法都是在以下方法中完成的
		return this.doInvoke(param,request,response);
	} catch (Throwable e) {
		if(e instanceof InvocationTargetException) {
			e = ((InvocationTargetException)e).getTargetException();
		}
		this.logError(e);
		throw e;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

相关时序图如下:
处理请求时序图

4. 最后

其实整个流程分析下来,easyopen在技术上并没有革新式的创造,但代码中依然处处展示了作者深厚的编程功底,对于包结构的划分,对外扩展接口的提供方式,各种技术的集成等等都无不展示出作者在这代码上投入时间和精力。

另外作者的提供的样例,笔者基本都是一次性成功的,不像曾经经历过的某些开源项目—— 作者寥寥几句话,似乎部署起来非常简单,但总是碰到各种各样稀奇古怪的问题。

最重要的是作者的这份坚持真的让人非常敬佩,这种笔者现在极其敬佩但又非常缺乏的品质,出现在别人身上的时候,实在让笔者万分汗颜和惭愧。

5. Links

  1. 一个简单易用的接口开放平台-easyopen
  2. easyopen介绍
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/68945
推荐阅读
相关标签
  

闽ICP备14008679号