背景
公司成立以来,积累了数以万计的可复用接口。上层的SaaS业务,原则上要复用这些接口开发自己的业务,为了屏蔽调用接口的复杂性,基础服务开发了apisdk组件,定义了一套声明OpenAPI的注解、注解解析器,实例化OpenAPI的工具,封装了签名算法,同时提供了查询域名、查询参与签名的secret、获取Token等基础服务。不同场景的OpenAPI,如App端和Web端,需要调用不同的基础服务。
这套apisdk组件经过了多年的应用,功能上完全hold住业务需求,然而问题来了。
公司在疫情这几年的经济效益实在太差,我做这期分享前,裁了50%(可能有人猜到是哪家公司),当然大多数的互联网公司都有裁员的动作。那裁员之后的业务谁来做?公司战略是交给外包。
但是我们的业务完全依赖基础服务的自研网关、用户体系、接口暴露组件、apisdk组件、基础服务等,外包要承接业务,必须要学会这套东西。但这些东西封装的并不完善,简单来说,都不是傻瓜式的,对外包来说成本太高。别说外包了,业务部门说直白一点,是面向接口编程,对新入职的同学,学习成本高,开发效率低。
于是,为了让一切变得傻瓜式,我承担了网关接口暴露组件、apisdk的二次封装。此次分享内容主要以apisdk为主,涉及的技术/内容:Springboot拓展点的应用、自定义Spring扫描、Spring IoC(FactoryBean)、JDK动态代理、aop源码拓展、javassist字节码技术等。
注:分享的源码全部是自己代码,借鉴了Spring注解、aop等源码,不含公司成分,目的是分享这些技术的实战。
apisdk组件
apisdk组件包括了开放接口声明、签名算法、Http执行器(okhttp3)、响应数据解析等模块,其中接口声明、参与签名的参数需要开发者提供,开发者调用接口方法,触发Http执行器发起接口调用。
二次封装原因:除了学习成本高,它还是java和kotlin混用,原作者早已离职,维护困难,只能在原基础上做封装。
下面截图是原始的apisdk和二次封装后的apisdk使用方式的对比,通过截图,大家会对开放接口的声明、请求上下文构建、接口调用方式有一定的认识,有助于大家理解屏蔽了哪些操作。
接口声明
原接口声明
接口声明必须要有SdkContext固定参数,接口调用前,必须手动调用基础服务构造SdkContext上下文,这样才能正确的发起调用。
二次封装后接口声明
二次封装后,由底层完成了SdkContext参数的自动填充。
接口调用
原调用方式
上面是模拟调用的过程,包括接口实例化、SdkContext封装、接口调用,实际的调用过程比这个复杂的多。
二次封装后接口调用方式
apisdk二次封装介绍
二次封装涉及到的技术:自定义自动装配、自定义Spring扫描、Spring IoC(FactoryBean)、JDK动态代理、aop源码拓展、javassist字节码技术等。下面简单的介绍下二开的思路及相关的技术点。
思路
原始接口声明必须要定义SdkContext参数,必须要手动调用多个服务创建SdkContent,必须要手动声明接口实例。据我了解:
- 公司不允许跨区域(域名)调用开放接口,如不允许中国区调用美国区暴露的接口。
- 调用开放接口一定要有SdkContext参数
- SdkContext构建的步骤是固定的,但涉及的服务较多
综上,域名问题可以通过部署环境动态获取(已有服务);开发者可以不定义SdkContext参数,底层通过字节码技术动态新增SdkContext方法参数;底层拓展aop的advice,方法执行前构建SdkContext参数;实现Spring扫描组件,通过FactoryBean实例化接口,开发者使用注解即可完成Bean对象的依赖。
涉及技术
自定义自动装配
自研boot启动器,主要是@Import的应用,在apisdk中是Spring自定义扫描的入口。
自定义Spring扫描
利用Spring扫描,把开放接口的声明解析成BeanDefinition,随后由Spring进行Bean的实例化。
apisdk自研框架与Spring整合(FactoryBean)
开放接口的BeanDefinition是接口,无法实例化,因此需要替换BeanDefinition的beanClass,由FactoryBean触发Bean的实例化,生成代理对象,通过FactoryBean#getObject方法将代理对象注册到IoC中。
javassist字节码
FactoryBean生成开放接口(A)的代理对象前,使用javassist生成全新的内存类(B),复制A的方法,并在每个方法上添加SdkContext参数,这样形成了A和B的映射关系。
Jdk动态代理
原始接口A和内存接口B,他们的实例化必须由动态代理支持,A和B的代理对象是怎么样的关系?
开发者使用A的代理对象,调用的方法都是没有SdkContext方法的,底层真正执行时,会围绕对象A做一些列的拦截动作,得到SdkContext,随后底层拿到B对象,调用方法并传递SdkContext。
上面对比的截图,读者知道有SdkManager可以生成接口的代理对象,二次封装后,我提供了JdkDynamicAopProxy,A和B对象由这两个工具来生成代理,SdkManager生成B的对象,JdkDynamicAopProxy生成A的对象,并且A对象由一些列的拦截动作。
Aop源码拓展
二次封装最重要的是:在A对象的方法执行过程中前置拦截,生成SdkContext,并调用B对象。拦截的实现是抽取Spring aop的api,形成独立的Api,目的是离开Spring仍然可以运行。
Aop的变动包括:扩展MethodBeforeArgsChangeableAdvice参数可变的前置拦截,动态构建方法参数。
其他
Environment
FactoryBean&property-placeholder
EnvironmentPostProcessor
ApplicationContextInitializer
BeanFactoryPostProcessor
工程结构
工程结构如上图,其中
- access-boot-apisdk:模拟apisdk,包括OpenAPI声明注解、注解解析器,实例化OpenAPI的工具,封装了签名算法等
- access-boot-autoconfigure:apisdk二次封装核心
- access-boot-dependencies:依赖管理
- access-boot-starter-sample:业务开发Demo
- access-boot-starters:启动器依赖
分享内容来自此工程,大家请参考 码云。
注意
之后的blog,我会按照以下规则介绍。
-
开发者定义的接口声明,我称作为原始接口、A接口、A
-
javassist生成的接口声明,我称作为增强接口、内存接口、B接口、B