赞
踩
一直以来,笔者所在的公司对于对外提供的接口这一块都缺乏有效的管理,诸如权限缺失,参数和返回值过于随意等等问题导致请求访问控制,错误快速定位,事后统计分析等等接口管理需求一直无法满足。
鉴于以上情况,公司决定寻求一套完整的接口管理解决方案。最终我们在码云上找到了一个名为 easyopen 的接口管理项目。本文将主要探究下其背后的实现原理,方便以后将该平台集成到公司的既有项目中,或者借鉴其思路实现自己的接口管理平台。
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"); } }
经过以上两步,接口服务就算是编写完成了,启动应用之后:
分析部分大致分为两个部分——框架初始化以及框架如何处理外部请求?
通观以上代码,无疑我们需要看看基类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(); }
上述方法中,最值得关注的应该就是注册部分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("******** 注册操作结束 ********"); }
执行完上述流程之后,框架算是完成了所有的准备工作,正式开始向外提供服务了。这里作者将创建和提供服务进行了泾渭分明的划分,符合《Clean Code》里的思想。相关时序图如下:
我们在进行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; } }
相关时序图如下:
其实整个流程分析下来,easyopen在技术上并没有革新式的创造,但代码中依然处处展示了作者深厚的编程功底,对于包结构的划分,对外扩展接口的提供方式,各种技术的集成等等都无不展示出作者在这代码上投入时间和精力。
另外作者的提供的样例,笔者基本都是一次性成功的,不像曾经经历过的某些开源项目—— 作者寥寥几句话,似乎部署起来非常简单,但总是碰到各种各样稀奇古怪的问题。
最重要的是作者的这份坚持真的让人非常敬佩,这种笔者现在极其敬佩但又非常缺乏的品质,出现在别人身上的时候,实在让笔者万分汗颜和惭愧。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。